From 7f5355be43d58702d8b75f5546f530dd71821917 Mon Sep 17 00:00:00 2001 From: Andrew Coutts Date: Sun, 28 Feb 2021 12:30:30 -0500 Subject: [PATCH 001/114] More refactoring --- .../flutter/plugins/camera/AspectRatio.java | 175 +++ .../io/flutter/plugins/camera/Camera.java | 1233 ++++++++++------- .../plugins/camera/CameraConstants.java | 26 + .../plugins/camera/CaptureSessionState.java | 11 + .../io/flutter/plugins/camera/ImageSaver.java | 78 ++ .../plugins/camera/PictureCaptureRequest.java | 36 +- .../camera/example/android/gradle.properties | 17 +- 7 files changed, 1022 insertions(+), 554 deletions(-) create mode 100644 packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/AspectRatio.java create mode 100644 packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/CameraConstants.java create mode 100644 packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/CaptureSessionState.java create mode 100644 packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/ImageSaver.java diff --git a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/AspectRatio.java b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/AspectRatio.java new file mode 100644 index 000000000000..455dd7ceaf62 --- /dev/null +++ b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/AspectRatio.java @@ -0,0 +1,175 @@ +package io.flutter.plugins.camera; + +import android.os.Parcel; +import android.os.Parcelable; +import android.util.Size; + +import androidx.annotation.NonNull; +import androidx.collection.SparseArrayCompat; + +/** + * Immutable class for describing proportional relationship between width and height. + */ +public class AspectRatio implements Comparable, Parcelable { + + private final static SparseArrayCompat> sCache + = new SparseArrayCompat<>(16); + + private final int mX; + private final int mY; + + /** + * Returns an instance of {@link AspectRatio} specified by {@code x} and {@code y} values. + * The values {@code x} and {@code} will be reduced by their greatest common divider. + * + * @param x The width + * @param y The height + * @return An instance of {@link AspectRatio} + */ + public static AspectRatio of(int x, int y) { + int gcd = gcd(x, y); + x /= gcd; + y /= gcd; + SparseArrayCompat arrayX = sCache.get(x); + if (arrayX == null) { + AspectRatio ratio = new AspectRatio(x, y); + arrayX = new SparseArrayCompat<>(); + arrayX.put(y, ratio); + sCache.put(x, arrayX); + return ratio; + } else { + AspectRatio ratio = arrayX.get(y); + if (ratio == null) { + ratio = new AspectRatio(x, y); + arrayX.put(y, ratio); + } + return ratio; + } + } + + /** + * Parse an {@link AspectRatio} from a {@link String} formatted like "4:3". + * + * @param s The string representation of the aspect ratio + * @return The aspect ratio + * @throws IllegalArgumentException when the format is incorrect. + */ + public static AspectRatio parse(String s) { + int position = s.indexOf(':'); + if (position == -1) { + throw new IllegalArgumentException("Malformed aspect ratio: " + s); + } + try { + int x = Integer.parseInt(s.substring(0, position)); + int y = Integer.parseInt(s.substring(position + 1)); + return AspectRatio.of(x, y); + } catch (NumberFormatException e) { + throw new IllegalArgumentException("Malformed aspect ratio: " + s, e); + } + } + + private AspectRatio(int x, int y) { + mX = x; + mY = y; + } + + public int getX() { + return mX; + } + + public int getY() { + return mY; + } + + public boolean matches(Size size) { + int gcd = gcd(size.getWidth(), size.getHeight()); + int x = size.getWidth() / gcd; + int y = size.getHeight() / gcd; + return mX == x && mY == y; + } + + @Override + public boolean equals(Object o) { + if (o == null) { + return false; + } + if (this == o) { + return true; + } + if (o instanceof AspectRatio) { + AspectRatio ratio = (AspectRatio) o; + return mX == ratio.mX && mY == ratio.mY; + } + return false; + } + + @Override + public String toString() { + return mX + ":" + mY; + } + + public float toFloat() { + return (float) mX / mY; + } + + @Override + public int hashCode() { + // assuming most sizes are <2^16, doing a rotate will give us perfect hashing + return mY ^ ((mX << (Integer.SIZE / 2)) | (mX >>> (Integer.SIZE / 2))); + } + + @Override + public int compareTo(@NonNull AspectRatio another) { + if (equals(another)) { + return 0; + } else if (toFloat() - another.toFloat() > 0) { + return 1; + } + return -1; + } + + /** + * @return The inverse of this {@link AspectRatio}. + */ + public AspectRatio inverse() { + //noinspection SuspiciousNameCombination + return AspectRatio.of(mY, mX); + } + + private static int gcd(int a, int b) { + while (b != 0) { + int c = b; + b = a % b; + a = c; + } + return a; + } + + @Override + public int describeContents() { + return 0; + } + + @Override + public void writeToParcel(Parcel dest, int flags) { + dest.writeInt(mX); + dest.writeInt(mY); + } + + public static final Creator CREATOR + = new Creator() { + + @Override + public AspectRatio createFromParcel(Parcel source) { + int x = source.readInt(); + int y = source.readInt(); + return AspectRatio.of(x, y); + } + + @Override + public AspectRatio[] newArray(int size) { + return new AspectRatio[size]; + } + }; + +} 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 5169a3babb74..ea879f4e2bf1 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 @@ -4,8 +4,6 @@ package io.flutter.plugins.camera; -import static io.flutter.plugins.camera.CameraUtils.computeBestPreviewSize; - import android.annotation.SuppressLint; import android.annotation.TargetApi; import android.app.Activity; @@ -35,27 +33,19 @@ import android.os.Build.VERSION; import android.os.Build.VERSION_CODES; import android.os.Handler; -import android.os.Looper; +import android.os.HandlerThread; import android.os.SystemClock; import android.util.Log; import android.util.Range; import android.util.Rational; import android.util.Size; +import android.util.SparseIntArray; import android.view.Surface; + import androidx.annotation.NonNull; import androidx.annotation.Nullable; -import io.flutter.embedding.engine.systemchannels.PlatformChannel; -import io.flutter.plugin.common.EventChannel; -import io.flutter.plugin.common.MethodChannel.Result; -import io.flutter.plugins.camera.PictureCaptureRequest.State; -import io.flutter.plugins.camera.media.MediaRecorderBuilder; -import io.flutter.plugins.camera.types.ExposureMode; -import io.flutter.plugins.camera.types.FlashMode; -import io.flutter.plugins.camera.types.FocusMode; -import io.flutter.plugins.camera.types.ResolutionPreset; -import io.flutter.view.TextureRegistry.SurfaceTextureEntry; + import java.io.File; -import java.io.FileOutputStream; import java.io.IOException; import java.nio.ByteBuffer; import java.util.ArrayList; @@ -66,6 +56,18 @@ import java.util.Map; import java.util.concurrent.Executors; +import io.flutter.embedding.engine.systemchannels.PlatformChannel; +import io.flutter.plugin.common.EventChannel; +import io.flutter.plugin.common.MethodChannel.Result; +import io.flutter.plugins.camera.media.MediaRecorderBuilder; +import io.flutter.plugins.camera.types.ExposureMode; +import io.flutter.plugins.camera.types.FlashMode; +import io.flutter.plugins.camera.types.FocusMode; +import io.flutter.plugins.camera.types.ResolutionPreset; +import io.flutter.view.TextureRegistry.SurfaceTextureEntry; + +import static io.flutter.plugins.camera.CameraUtils.computeBestPreviewSize; + @FunctionalInterface interface ErrorCallback { void onError(String errorCode, String errorMessage); @@ -74,8 +76,29 @@ interface ErrorCallback { public class Camera { private static final String TAG = "Camera"; - /** Timeout for the pre-capture sequence. */ + /** + * Timeout for the pre-capture sequence. + */ private static final long PRECAPTURE_TIMEOUT_MS = 1000; + /** + * Conversion from screen rotation to JPEG orientation. + */ + private static final SparseIntArray ORIENTATIONS = new SparseIntArray(); + private static final HashMap supportedImageFormats; + + static { + ORIENTATIONS.append(Surface.ROTATION_0, 90); + ORIENTATIONS.append(Surface.ROTATION_90, 0); + ORIENTATIONS.append(Surface.ROTATION_180, 270); + ORIENTATIONS.append(Surface.ROTATION_270, 180); + } + + // Current supported outputs + static { + supportedImageFormats = new HashMap<>(); + supportedImageFormats.put("yuv420", 35); + supportedImageFormats.put("jpeg", 256); + } private final SurfaceTextureEntry flutterTexture; private final CameraManager cameraManager; @@ -91,19 +114,78 @@ public class Camera { private final DartMessenger dartMessenger; private final CameraZoom cameraZoom; private final CameraCharacteristics cameraCharacteristics; + private final Activity activity; + /** + * Whether the current camera device supports Flash or not. + */ + private final boolean mFlashSupported; + /** + * A {@link Handler} for running tasks in the background. + */ + private Handler mBackgroundHandler; + /** + * An additional thread for running tasks that shouldn't block the UI. + */ + private HandlerThread mBackgroundThread; private CameraDevice cameraDevice; - private CameraCaptureSession cameraCaptureSession; + private CameraCaptureSession mPreviewSession; private ImageReader pictureImageReader; private ImageReader imageStreamReader; - private CaptureRequest.Builder captureRequestBuilder; + + /** + * {@link CaptureRequest.Builder} for the camera preview + */ + private CaptureRequest.Builder mPreviewRequestBuilder; + + /** + * {@link CaptureRequest} generated by {@link #mPreviewRequestBuilder} + */ + private CaptureRequest mPreviewRequest; + + /** + * The current camera auto focus mode + */ + private boolean mAutoFocus = true; + + /** + * Whether the current camera device supports auto focus or not. + */ + private boolean mAutoFocusSupported = true; + private MediaRecorder mediaRecorder; private boolean recordingVideo; private File videoRecordingFile; - private FlashMode flashMode; + + /** + * Flash mode setting of the current camera + */ + private FlashMode currentFlashMode; + + /** + * Exposure mode setting of the current camera. + */ private ExposureMode exposureMode; - private FocusMode focusMode; + + /** + * Focus mode setting of the current camera. + */ + private FocusMode currentFocusMode; private PictureCaptureRequest pictureCaptureRequest; + /** + * This a callback object for the {@link ImageReader}. "onImageAvailable" will be called when a + * still image is ready to be saved. + */ + private final ImageReader.OnImageAvailableListener mOnImageAvailableListener + = new ImageReader.OnImageAvailableListener() { + + @Override + public void onImageAvailable(ImageReader reader) { + Log.i(TAG, "onImageAvailable"); + mBackgroundHandler.post(new ImageSaver(reader.acquireNextImage(), pictureCaptureRequest.mFile, pictureCaptureRequest)); + } + + }; private CameraRegions cameraRegions; private int exposureOffset; private boolean useAutoFocus = true; @@ -111,61 +193,64 @@ public class Camera { private PlatformChannel.DeviceOrientation lockedCaptureOrientation; private long preCaptureStartTime; - private static final HashMap supportedImageFormats; - // Current supported outputs - static { - supportedImageFormats = new HashMap<>(); - supportedImageFormats.put("yuv420", 35); - supportedImageFormats.put("jpeg", 256); - } public Camera( - final Activity activity, - final SurfaceTextureEntry flutterTexture, - final DartMessenger dartMessenger, - final String cameraName, - final String resolutionPreset, - final boolean enableAudio) - throws CameraAccessException { + final Activity activity, + final SurfaceTextureEntry flutterTexture, + final DartMessenger dartMessenger, + final String cameraName, + final String resolutionPreset, + final boolean enableAudio) + throws CameraAccessException { if (activity == null) { throw new IllegalStateException("No activity available!"); } + this.activity = activity; this.cameraName = cameraName; this.enableAudio = enableAudio; this.flutterTexture = flutterTexture; this.dartMessenger = dartMessenger; this.cameraManager = (CameraManager) activity.getSystemService(Context.CAMERA_SERVICE); this.applicationContext = activity.getApplicationContext(); - this.flashMode = FlashMode.auto; + this.currentFlashMode = FlashMode.off; this.exposureMode = ExposureMode.auto; - this.focusMode = FocusMode.auto; + this.currentFocusMode = FocusMode.auto; this.exposureOffset = 0; cameraCharacteristics = cameraManager.getCameraCharacteristics(cameraName); - initFps(cameraCharacteristics); + getAvailableFpsRange(cameraCharacteristics); sensorOrientation = cameraCharacteristics.get(CameraCharacteristics.SENSOR_ORIENTATION); isFrontFacing = - cameraCharacteristics.get(CameraCharacteristics.LENS_FACING) - == CameraMetadata.LENS_FACING_FRONT; + cameraCharacteristics.get(CameraCharacteristics.LENS_FACING) + == CameraMetadata.LENS_FACING_FRONT; ResolutionPreset preset = ResolutionPreset.valueOf(resolutionPreset); recordingProfile = - CameraUtils.getBestAvailableCamcorderProfileForResolutionPreset(cameraName, preset); + CameraUtils.getBestAvailableCamcorderProfileForResolutionPreset(cameraName, preset); captureSize = new Size(recordingProfile.videoFrameWidth, recordingProfile.videoFrameHeight); previewSize = computeBestPreviewSize(cameraName, preset); cameraZoom = - new CameraZoom( - cameraCharacteristics.get(CameraCharacteristics.SENSOR_INFO_ACTIVE_ARRAY_SIZE), - cameraCharacteristics.get(CameraCharacteristics.SCALER_AVAILABLE_MAX_DIGITAL_ZOOM)); + new CameraZoom( + cameraCharacteristics.get(CameraCharacteristics.SENSOR_INFO_ACTIVE_ARRAY_SIZE), + cameraCharacteristics.get(CameraCharacteristics.SCALER_AVAILABLE_MAX_DIGITAL_ZOOM)); deviceOrientationListener = - new DeviceOrientationManager(activity, dartMessenger, isFrontFacing, sensorOrientation); + new DeviceOrientationManager(activity, dartMessenger, isFrontFacing, sensorOrientation); deviceOrientationListener.start(); + + // Check if the flash is supported. + Boolean flashAvailable = cameraCharacteristics.get(CameraCharacteristics.FLASH_INFO_AVAILABLE); + mFlashSupported = flashAvailable == null ? false : flashAvailable; + startBackgroundThread(); } - private void initFps(CameraCharacteristics cameraCharacteristics) { + /** + * Load available FPS range for the current camera and update the available fps range with it. + * @param cameraCharacteristics + */ + private void getAvailableFpsRange(CameraCharacteristics cameraCharacteristics) { try { Range[] ranges = - cameraCharacteristics.get(CameraCharacteristics.CONTROL_AE_AVAILABLE_TARGET_FPS_RANGES); + cameraCharacteristics.get(CameraCharacteristics.CONTROL_AE_AVAILABLE_TARGET_FPS_RANGES); if (ranges != null) { for (Range range : ranges) { int upper = range.getUpper(); @@ -178,7 +263,7 @@ private void initFps(CameraCharacteristics cameraCharacteristics) { } } } catch (Exception e) { - e.printStackTrace(); + pictureCaptureRequest.error("cameraAccess", e.getMessage(), null); } Log.i("Camera", "[FPS Range] is:" + fpsRange); } @@ -189,20 +274,20 @@ private void prepareMediaRecorder(String outputFilePath) throws IOException { } mediaRecorder = - new MediaRecorderBuilder(recordingProfile, outputFilePath) - .setEnableAudio(enableAudio) - .setMediaOrientation( - lockedCaptureOrientation == null - ? deviceOrientationListener.getMediaOrientation() - : deviceOrientationListener.getMediaOrientation(lockedCaptureOrientation)) - .build(); + new MediaRecorderBuilder(recordingProfile, outputFilePath) + .setEnableAudio(enableAudio) + .setMediaOrientation( + lockedCaptureOrientation == null + ? deviceOrientationListener.getMediaOrientation() + : deviceOrientationListener.getMediaOrientation(lockedCaptureOrientation)) + .build(); } @SuppressLint("MissingPermission") public void open(String imageFormatGroup) throws CameraAccessException { pictureImageReader = - ImageReader.newInstance( - captureSize.getWidth(), captureSize.getHeight(), ImageFormat.JPEG, 2); + ImageReader.newInstance( + captureSize.getWidth(), captureSize.getHeight(), ImageFormat.JPEG, 2); Integer imageFormat = supportedImageFormats.get(imageFormatGroup); if (imageFormat == null) { @@ -212,95 +297,95 @@ public void open(String imageFormatGroup) throws CameraAccessException { // Used to steam image byte data to dart side. imageStreamReader = - ImageReader.newInstance(previewSize.getWidth(), previewSize.getHeight(), imageFormat, 2); + ImageReader.newInstance(previewSize.getWidth(), previewSize.getHeight(), imageFormat, 2); cameraManager.openCamera( - cameraName, - new CameraDevice.StateCallback() { - @Override - public void onOpened(@NonNull CameraDevice device) { - cameraDevice = device; - try { - startPreview(); - dartMessenger.sendCameraInitializedEvent( - previewSize.getWidth(), - previewSize.getHeight(), - exposureMode, - focusMode, - isExposurePointSupported(), - isFocusPointSupported()); - } catch (CameraAccessException e) { - dartMessenger.sendCameraErrorEvent(e.getMessage()); - close(); - } - } + cameraName, + new CameraDevice.StateCallback() { + @Override + public void onOpened(@NonNull CameraDevice device) { + cameraDevice = device; + try { + startPreview(); + dartMessenger.sendCameraInitializedEvent( + previewSize.getWidth(), + previewSize.getHeight(), + exposureMode, + currentFocusMode, + isExposurePointSupported(), + isFocusPointSupported()); + } catch (CameraAccessException e) { + dartMessenger.sendCameraErrorEvent(e.getMessage()); + close(); + } + } - @Override - public void onClosed(@NonNull CameraDevice camera) { - dartMessenger.sendCameraClosingEvent(); - super.onClosed(camera); - } + @Override + public void onClosed(@NonNull CameraDevice camera) { + dartMessenger.sendCameraClosingEvent(); + super.onClosed(camera); + } - @Override - public void onDisconnected(@NonNull CameraDevice cameraDevice) { - close(); - dartMessenger.sendCameraErrorEvent("The camera was disconnected."); - } + @Override + public void onDisconnected(@NonNull CameraDevice cameraDevice) { + close(); + dartMessenger.sendCameraErrorEvent("The camera was disconnected."); + } - @Override - public void onError(@NonNull CameraDevice cameraDevice, int errorCode) { - close(); - String errorDescription; - switch (errorCode) { - case ERROR_CAMERA_IN_USE: - errorDescription = "The camera device is in use already."; - break; - case ERROR_MAX_CAMERAS_IN_USE: - errorDescription = "Max cameras in use"; - break; - case ERROR_CAMERA_DISABLED: - errorDescription = "The camera device could not be opened due to a device policy."; - break; - case ERROR_CAMERA_DEVICE: - errorDescription = "The camera device has encountered a fatal error"; - break; - case ERROR_CAMERA_SERVICE: - errorDescription = "The camera service has encountered a fatal error."; - break; - default: - errorDescription = "Unknown camera error"; - } - dartMessenger.sendCameraErrorEvent(errorDescription); - } - }, - null); + @Override + public void onError(@NonNull CameraDevice cameraDevice, int errorCode) { + close(); + String errorDescription; + switch (errorCode) { + case ERROR_CAMERA_IN_USE: + errorDescription = "The camera device is in use already."; + break; + case ERROR_MAX_CAMERAS_IN_USE: + errorDescription = "Max cameras in use"; + break; + case ERROR_CAMERA_DISABLED: + errorDescription = "The camera device could not be opened due to a device policy."; + break; + case ERROR_CAMERA_DEVICE: + errorDescription = "The camera device has encountered a fatal error"; + break; + case ERROR_CAMERA_SERVICE: + errorDescription = "The camera service has encountered a fatal error."; + break; + default: + errorDescription = "Unknown camera error"; + } + dartMessenger.sendCameraErrorEvent(errorDescription); + } + }, + null); } private void createCaptureSession(int templateType, Surface... surfaces) - throws CameraAccessException { + throws CameraAccessException { createCaptureSession(templateType, null, surfaces); } private void createCaptureSession( - int templateType, Runnable onSuccessCallback, Surface... surfaces) - throws CameraAccessException { + int templateType, Runnable onSuccessCallback, Surface... surfaces) + throws CameraAccessException { // Close any existing capture session. closeCaptureSession(); // Create a new capture builder. - captureRequestBuilder = cameraDevice.createCaptureRequest(templateType); + mPreviewRequestBuilder = cameraDevice.createCaptureRequest(templateType); // Build Flutter surface to render to SurfaceTexture surfaceTexture = flutterTexture.surfaceTexture(); surfaceTexture.setDefaultBufferSize(previewSize.getWidth(), previewSize.getHeight()); Surface flutterSurface = new Surface(surfaceTexture); - captureRequestBuilder.addTarget(flutterSurface); + mPreviewRequestBuilder.addTarget(flutterSurface); List remainingSurfaces = Arrays.asList(surfaces); if (templateType != CameraDevice.TEMPLATE_PREVIEW) { // If it is not preview mode, add all surfaces as targets. for (Surface surface : remainingSurfaces) { - captureRequestBuilder.addTarget(surface); + mPreviewRequestBuilder.addTarget(surface); } } @@ -308,29 +393,30 @@ private void createCaptureSession( // Prepare the callback CameraCaptureSession.StateCallback callback = - new CameraCaptureSession.StateCallback() { - @Override - public void onConfigured(@NonNull CameraCaptureSession session) { - if (cameraDevice == null) { - dartMessenger.sendCameraErrorEvent("The camera was closed during configuration."); - return; - } - cameraCaptureSession = session; + new CameraCaptureSession.StateCallback() { + @Override + public void onConfigured(@NonNull CameraCaptureSession session) { + // Camera was already closed. + if (cameraDevice == null) { + dartMessenger.sendCameraErrorEvent("The camera was closed during configuration."); + return; + } + mPreviewSession = session; - updateFpsRange(); - updateFocus(focusMode); - updateFlash(flashMode); - updateExposure(exposureMode); + updateFpsRange(); + updateFocus(mPreviewRequestBuilder); + updateFlash(mPreviewRequestBuilder); + updateExposure(exposureMode); - refreshPreviewCaptureSession( - onSuccessCallback, (code, message) -> dartMessenger.sendCameraErrorEvent(message)); - } + refreshPreviewCaptureSession( + onSuccessCallback, (code, message) -> dartMessenger.sendCameraErrorEvent(message)); + } - @Override - public void onConfigureFailed(@NonNull CameraCaptureSession cameraCaptureSession) { - dartMessenger.sendCameraErrorEvent("Failed to configure camera session."); - } - }; + @Override + public void onConfigureFailed(@NonNull CameraCaptureSession cameraCaptureSession) { + dartMessenger.sendCameraErrorEvent("Failed to configure camera session."); + } + }; // Start the session if (VERSION.SDK_INT >= VERSION_CODES.P) { @@ -352,35 +438,36 @@ public void onConfigureFailed(@NonNull CameraCaptureSession cameraCaptureSession @TargetApi(VERSION_CODES.P) private void createCaptureSessionWithSessionConfig( - List outputConfigs, CameraCaptureSession.StateCallback callback) - throws CameraAccessException { + List outputConfigs, CameraCaptureSession.StateCallback callback) + throws CameraAccessException { cameraDevice.createCaptureSession( - new SessionConfiguration( - SessionConfiguration.SESSION_REGULAR, - outputConfigs, - Executors.newSingleThreadExecutor(), - callback)); + new SessionConfiguration( + SessionConfiguration.SESSION_REGULAR, + outputConfigs, + Executors.newSingleThreadExecutor(), + callback)); } @TargetApi(VERSION_CODES.LOLLIPOP) @SuppressWarnings("deprecation") private void createCaptureSession( - List surfaces, CameraCaptureSession.StateCallback callback) - throws CameraAccessException { + List surfaces, CameraCaptureSession.StateCallback callback) + throws CameraAccessException { cameraDevice.createCaptureSession(surfaces, callback, null); } private void refreshPreviewCaptureSession( - @Nullable Runnable onSuccessCallback, @NonNull ErrorCallback onErrorCallback) { - if (cameraCaptureSession == null) { + @Nullable Runnable onSuccessCallback, @NonNull ErrorCallback onErrorCallback) { + Log.i(TAG, "refreshPreviewCaptureSession"); + if (mPreviewSession == null) { + Log.i(TAG, "[refreshPreviewCaptureSession] mPreviewSession null, returning"); return; } try { - cameraCaptureSession.setRepeatingRequest( - captureRequestBuilder.build(), - pictureCaptureCallback, - new Handler(Looper.getMainLooper())); + mPreviewRequest = mPreviewRequestBuilder.build(); + mPreviewSession.setRepeatingRequest(mPreviewRequest, + mCaptureCallback, mBackgroundHandler); if (onSuccessCallback != null) { onSuccessCallback.run(); @@ -390,233 +477,330 @@ private void refreshPreviewCaptureSession( } } - private void writeToFile(ByteBuffer buffer, File file) throws IOException { - try (FileOutputStream outputStream = new FileOutputStream(file)) { - while (0 < buffer.remaining()) { - outputStream.getChannel().write(buffer); - } - } - } - public void takePicture(@NonNull final Result result) { + Log.i(TAG, "takePicture | useAutoFocus: " + useAutoFocus); + // Only take 1 picture at a time if (pictureCaptureRequest != null && !pictureCaptureRequest.isFinished()) { result.error("captureAlreadyActive", "Picture is currently already being captured", null); return; } // Store the result - this.pictureCaptureRequest = new PictureCaptureRequest(result); + pictureCaptureRequest = new PictureCaptureRequest(result); // Create temporary file final File outputDir = applicationContext.getCacheDir(); - final File file; try { - file = File.createTempFile("CAP", ".jpg", outputDir); + pictureCaptureRequest.mFile = File.createTempFile("CAP", ".jpg", outputDir); } catch (IOException | SecurityException e) { pictureCaptureRequest.error("cannotCreateFile", e.getMessage(), null); return; } // Listen for picture being taken - pictureImageReader.setOnImageAvailableListener( - reader -> { - try (Image image = reader.acquireLatestImage()) { - ByteBuffer buffer = image.getPlanes()[0].getBuffer(); - writeToFile(buffer, file); - pictureCaptureRequest.finish(file.getAbsolutePath()); - } catch (IOException e) { - pictureCaptureRequest.error("IOError", "Failed saving image", null); - } - }, - null); + pictureImageReader.setOnImageAvailableListener(mOnImageAvailableListener, mBackgroundHandler); if (useAutoFocus) { runPictureAutoFocus(); } else { - runPicturePreCapture(); + runPrecaptureSequence(); } } - private final CameraCaptureSession.CaptureCallback pictureCaptureCallback = - new CameraCaptureSession.CaptureCallback() { - @Override - public void onCaptureCompleted( - @NonNull CameraCaptureSession session, - @NonNull CaptureRequest request, - @NonNull TotalCaptureResult result) { - processCapture(result); - } + /** + * Run the precapture sequence for capturing a still image. This method should be called when + * we get a response in {@link #mCaptureCallback} from lockFocus(). + */ + private void runPrecaptureSequence() { + Log.i(TAG, "runPrecaptureSequence"); + try { + // This is how to tell the camera to trigger. + mPreviewRequestBuilder.set(CaptureRequest.CONTROL_AE_PRECAPTURE_TRIGGER, + CaptureRequest.CONTROL_AE_PRECAPTURE_TRIGGER_START); + // Tell #mCaptureCallback to wait for the precapture sequence to be set. + pictureCaptureRequest.setState(CaptureSessionState.STATE_WAITING_PRECAPTURE); + mPreviewSession.capture(mPreviewRequestBuilder.build(), mCaptureCallback, + mBackgroundHandler); + } catch (CameraAccessException e) { + pictureCaptureRequest.error("cameraAccess", e.getMessage(), null); + } + } - @Override - public void onCaptureProgressed( - @NonNull CameraCaptureSession session, - @NonNull CaptureRequest request, - @NonNull CaptureResult partialResult) { - processCapture(partialResult); - } + /** + * Capture a still picture. This method should be called when we get a response in + * {@link #mCaptureCallback} from both lockFocus(). + */ + private void captureStillPicture() { + Log.i(TAG, "captureStillPicture"); + try { + if (null == cameraDevice) { + return; + } + // This is the CaptureRequest.Builder that we use to take a picture. + final CaptureRequest.Builder captureBuilder = + cameraDevice.createCaptureRequest(CameraDevice.TEMPLATE_STILL_CAPTURE); + captureBuilder.addTarget(pictureImageReader.getSurface()); - @Override - public void onCaptureFailed( - @NonNull CameraCaptureSession session, - @NonNull CaptureRequest request, - @NonNull CaptureFailure failure) { - if (pictureCaptureRequest == null || pictureCaptureRequest.isFinished()) { - return; - } - String reason; - boolean fatalFailure = false; - switch (failure.getReason()) { - case CaptureFailure.REASON_ERROR: - reason = "An error happened in the framework"; - break; - case CaptureFailure.REASON_FLUSHED: - reason = "The capture has failed due to an abortCaptures() call"; - fatalFailure = true; - break; - default: - reason = "Unknown reason"; - } - Log.w("Camera", "pictureCaptureCallback.onCaptureFailed(): " + reason); - if (fatalFailure) pictureCaptureRequest.error("captureFailure", reason, null); - } + // Zoom + captureBuilder.set( + CaptureRequest.SCALER_CROP_REGION, + mPreviewRequestBuilder.get(CaptureRequest.SCALER_CROP_REGION)); - private void processCapture(CaptureResult result) { - if (pictureCaptureRequest == null) { - return; - } + // Set focus / flash from preview mode + updateFlash(captureBuilder); + updateFocus(captureBuilder); - Integer aeState = result.get(CaptureResult.CONTROL_AE_STATE); - Integer afState = result.get(CaptureResult.CONTROL_AF_STATE); - switch (pictureCaptureRequest.getState()) { - case focusing: - if (afState == null) { - return; - } else if (afState == CaptureResult.CONTROL_AF_STATE_FOCUSED_LOCKED - || afState == CaptureResult.CONTROL_AF_STATE_NOT_FOCUSED_LOCKED) { - // Some devices might return null here, in which case we will also continue. - if (aeState == null || aeState == CaptureResult.CONTROL_AE_STATE_CONVERGED) { - runPictureCapture(); - } else { - runPicturePreCapture(); - } - } - break; - case preCapture: - // Some devices might return null here, in which case we will also continue. - if (aeState == null - || aeState == CaptureRequest.CONTROL_AE_STATE_PRECAPTURE - || aeState == CaptureRequest.CONTROL_AE_STATE_FLASH_REQUIRED - || aeState == CaptureRequest.CONTROL_AE_STATE_CONVERGED) { - pictureCaptureRequest.setState(State.waitingPreCaptureReady); - setPreCaptureStartTime(); - } - break; - case waitingPreCaptureReady: - if (aeState == null || aeState != CaptureRequest.CONTROL_AE_STATE_PRECAPTURE) { - runPictureCapture(); - } else { - if (hitPreCaptureTimeout()) { - unlockAutoFocus(); - } - } - } + // Orientation + int rotation = activity.getWindowManager().getDefaultDisplay().getRotation(); + captureBuilder.set(CaptureRequest.JPEG_ORIENTATION, getOrientation(rotation)); + + CameraCaptureSession.CaptureCallback CaptureCallback + = new CameraCaptureSession.CaptureCallback() { + + @Override + public void onCaptureCompleted(@NonNull CameraCaptureSession session, + @NonNull CaptureRequest request, + @NonNull TotalCaptureResult result) { + unlockAutoFocus(); } }; - private void runPictureAutoFocus() { - assert (pictureCaptureRequest != null); + mPreviewSession.stopRepeating(); + mPreviewSession.abortCaptures(); + mPreviewSession.capture(captureBuilder.build(), CaptureCallback, null); + } catch (CameraAccessException e) { + pictureCaptureRequest.error("cameraAccess", e.getMessage(), null); + } + } - pictureCaptureRequest.setState(PictureCaptureRequest.State.focusing); - lockAutoFocus(pictureCaptureCallback); + /** + * Starts a background thread and its {@link Handler}. + * TODO: call when activity resumed + */ + private void startBackgroundThread() { + mBackgroundThread = new HandlerThread("CameraBackground"); + mBackgroundThread.start(); + mBackgroundHandler = new Handler(mBackgroundThread.getLooper()); } - private void runPicturePreCapture() { - assert (pictureCaptureRequest != null); - pictureCaptureRequest.setState(PictureCaptureRequest.State.preCapture); + /** + * Stops the background thread and its {@link Handler}. + * TODO: call when activity paused + */ + private void stopBackgroundThread() { + mBackgroundThread.quitSafely(); + try { + mBackgroundThread.join(); + mBackgroundThread = null; + mBackgroundHandler = null; + } catch (InterruptedException e) { + pictureCaptureRequest.error("cameraAccess", e.getMessage(), null); + } + } - captureRequestBuilder.set( - CaptureRequest.CONTROL_AE_PRECAPTURE_TRIGGER, - CaptureRequest.CONTROL_AE_PRECAPTURE_TRIGGER_START); - refreshPreviewCaptureSession( - () -> - captureRequestBuilder.set( - CaptureRequest.CONTROL_AE_PRECAPTURE_TRIGGER, - CaptureRequest.CONTROL_AE_PRECAPTURE_TRIGGER_IDLE), - (code, message) -> pictureCaptureRequest.error(code, message, null)); + + /** + * Sync the requestBuilder flash setting to the current flash mode setting of the camera. + */ + void updateFlash(CaptureRequest.Builder requestBuilder) { + if (!mFlashSupported) { + return; + } + + switch (currentFlashMode) { + case off: + requestBuilder.set(CaptureRequest.CONTROL_AE_MODE, + CaptureRequest.CONTROL_AE_MODE_ON); + requestBuilder.set(CaptureRequest.FLASH_MODE, + CaptureRequest.FLASH_MODE_OFF); + break; + + case always: + requestBuilder.set(CaptureRequest.CONTROL_AE_MODE, + CaptureRequest.CONTROL_AE_MODE_ON_ALWAYS_FLASH); + requestBuilder.set(CaptureRequest.FLASH_MODE, + CaptureRequest.FLASH_MODE_OFF); + break; + + case torch: + requestBuilder.set(CaptureRequest.CONTROL_AE_MODE, + CaptureRequest.CONTROL_AE_MODE_ON); + requestBuilder.set(CaptureRequest.FLASH_MODE, + CaptureRequest.FLASH_MODE_TORCH); + break; + + case auto: + requestBuilder.set(CaptureRequest.CONTROL_AE_MODE, + CaptureRequest.CONTROL_AE_MODE_ON_AUTO_FLASH); + requestBuilder.set(CaptureRequest.FLASH_MODE, + CaptureRequest.FLASH_MODE_OFF); + break; + + // TODO: to be implemented someday. Need to add it to dart and iOS. +// case autoRedEye: +// requestBuilder.set(CaptureRequest.CONTROL_AE_MODE, +// CaptureRequest.CONTROL_AE_MODE_ON_AUTO_FLASH_REDEYE); +// requestBuilder.set(CaptureRequest.FLASH_MODE, +// CaptureRequest.FLASH_MODE_OFF); +// break; + } } - private void runPictureCapture() { - assert (pictureCaptureRequest != null); - pictureCaptureRequest.setState(PictureCaptureRequest.State.capturing); - try { - final CaptureRequest.Builder captureBuilder = - cameraDevice.createCaptureRequest(CameraDevice.TEMPLATE_STILL_CAPTURE); - captureBuilder.addTarget(pictureImageReader.getSurface()); - captureBuilder.set( - CaptureRequest.SCALER_CROP_REGION, - captureRequestBuilder.get(CaptureRequest.SCALER_CROP_REGION)); - captureBuilder.set( - CaptureRequest.JPEG_ORIENTATION, - lockedCaptureOrientation == null - ? deviceOrientationListener.getMediaOrientation() - : deviceOrientationListener.getMediaOrientation(lockedCaptureOrientation)); - - switch (flashMode) { - case off: - captureBuilder.set(CaptureRequest.CONTROL_AE_MODE, CaptureRequest.CONTROL_AE_MODE_ON); - captureBuilder.set(CaptureRequest.FLASH_MODE, CaptureRequest.FLASH_MODE_OFF); + /** + * Retrieves the JPEG orientation from the specified screen rotation. + * + * @param rotation The screen rotation. + * @return The JPEG orientation (one of 0, 90, 270, and 360) + */ + private int getOrientation(int rotation) { + // Sensor orientation is 90 for most devices, or 270 for some devices (eg. Nexus 5X) + // We have to take that into account and rotate JPEG properly. + // For devices with orientation of 90, we simply return our mapping from ORIENTATIONS. + // For devices with orientation of 270, we need to rotate the JPEG 180 degrees. + return (ORIENTATIONS.get(rotation) + sensorOrientation + 270) % 360; + } + + /** + * A {@link CameraCaptureSession.CaptureCallback} that handles events related to JPEG capture. + */ + private final CameraCaptureSession.CaptureCallback mCaptureCallback + = new CameraCaptureSession.CaptureCallback() { + + private void process(CaptureResult result) { + if (pictureCaptureRequest == null) { + return; + } + + Integer aeState = result.get(CaptureResult.CONTROL_AE_STATE); + Integer afState = result.get(CaptureResult.CONTROL_AF_STATE); + + if (pictureCaptureRequest.getState() != CaptureSessionState.IDLE) { + Log.i(TAG, "mCaptureCallback | state: " + pictureCaptureRequest.getState() + " | afState: " + afState + " | aeState: " + aeState); + } + + switch (pictureCaptureRequest.getState()) { + case IDLE: { + // We have nothing to do when the camera preview is working normally. break; - case auto: - captureBuilder.set( - CaptureRequest.CONTROL_AE_MODE, CaptureRequest.CONTROL_AE_MODE_ON_AUTO_FLASH); + } + case FOCUSING: { + if (afState == null) { + captureStillPicture(); + } else if (afState == CaptureResult.CONTROL_AF_STATE_FOCUSED_LOCKED || + afState == CaptureResult.CONTROL_AF_STATE_NOT_FOCUSED_LOCKED) { + // CONTROL_AE_STATE can be null on some devices + + if (aeState == null || + aeState == CaptureResult.CONTROL_AE_STATE_CONVERGED) { + pictureCaptureRequest.setState(CaptureSessionState.CAPTURING); + + captureStillPicture(); + } else { + runPrecaptureSequence(); + } + } break; - case always: - default: - captureBuilder.set( - CaptureRequest.CONTROL_AE_MODE, CaptureRequest.CONTROL_AE_MODE_ON_ALWAYS_FLASH); + } + case STATE_WAITING_PRECAPTURE: { + // CONTROL_AE_STATE can be null on some devices + if (aeState == null || + aeState == CaptureResult.CONTROL_AE_STATE_PRECAPTURE || + aeState == CaptureRequest.CONTROL_AE_STATE_FLASH_REQUIRED) { + pictureCaptureRequest.setState(CaptureSessionState.STATE_WAITING_NON_PRECAPTURE); + setPreCaptureStartTime(); + } break; - } - cameraCaptureSession.stopRepeating(); - cameraCaptureSession.capture( - captureBuilder.build(), - new CameraCaptureSession.CaptureCallback() { - @Override - public void onCaptureCompleted( - @NonNull CameraCaptureSession session, - @NonNull CaptureRequest request, - @NonNull TotalCaptureResult result) { + } + case STATE_WAITING_NON_PRECAPTURE: { + // CONTROL_AE_STATE can be null on some devices + if (aeState == null || aeState != CaptureResult.CONTROL_AE_STATE_PRECAPTURE) { + pictureCaptureRequest.setState(CaptureSessionState.CAPTURING); + captureStillPicture(); + } else { + if (hitPreCaptureTimeout()) { unlockAutoFocus(); } - }, - null); + } + break; + } + } + } + + @Override + public void onCaptureProgressed(@NonNull CameraCaptureSession session, + @NonNull CaptureRequest request, + @NonNull CaptureResult partialResult) { + process(partialResult); + } + + @Override + public void onCaptureCompleted(@NonNull CameraCaptureSession session, + @NonNull CaptureRequest request, + @NonNull TotalCaptureResult result) { + process(result); + } + }; + + /** + * Trigger auto focus to start and refresh preview capture session. + */ + private void startAutoFocus() { + Log.i(TAG, "startAutoFocus"); + mPreviewRequestBuilder.set( + CaptureRequest.CONTROL_AF_TRIGGER, CaptureRequest.CONTROL_AF_TRIGGER_START); + + try { + mPreviewSession.capture(mPreviewRequestBuilder.build(), mCaptureCallback, + mBackgroundHandler); } catch (CameraAccessException e) { - pictureCaptureRequest.error("cameraAccess", e.getMessage(), null); + pictureCaptureRequest.error("cannotStartAutofocus", e.getMessage(), null); } } - private void lockAutoFocus(CaptureCallback callback) { - captureRequestBuilder.set( - CaptureRequest.CONTROL_AF_TRIGGER, CaptureRequest.CONTROL_AF_TRIGGER_START); + /** + * Start capturing a picture, doing autofocus first. + */ + private void runPictureAutoFocus() { + Log.i(TAG, "runPictureAutoFocus"); + assert (pictureCaptureRequest != null); + + pictureCaptureRequest.setState(CaptureSessionState.FOCUSING); + lockAutoFocus(); + } + + /** + * Start the autofocus routine. + */ + private void lockAutoFocus() { + Log.i(TAG, "lockAutoFocus"); + mPreviewRequestBuilder.set( + CaptureRequest.CONTROL_AF_TRIGGER, CaptureRequest.CONTROL_AF_TRIGGER_START); refreshPreviewCaptureSession( - null, (code, message) -> pictureCaptureRequest.error(code, message, null)); + null, (code, message) -> pictureCaptureRequest.error(code, message, null)); } + /** + * Cancel and reset auto focus state and refresh the preview session. + */ private void unlockAutoFocus() { - captureRequestBuilder.set( - CaptureRequest.CONTROL_AF_TRIGGER, CameraMetadata.CONTROL_AF_TRIGGER_CANCEL); - updateFocus(focusMode); + Log.i(TAG, "unlockAutoFocus"); + + mPreviewRequestBuilder.set( + CaptureRequest.CONTROL_AF_TRIGGER, CameraMetadata.CONTROL_AF_TRIGGER_CANCEL); + + updateFocus(mPreviewRequestBuilder); + updateFlash(mPreviewRequestBuilder); + try { - cameraCaptureSession.capture(captureRequestBuilder.build(), null, null); + mPreviewSession.capture(mPreviewRequestBuilder.build(), null, null); } catch (CameraAccessException ignored) { } - captureRequestBuilder.set( - CaptureRequest.CONTROL_AF_TRIGGER, CaptureRequest.CONTROL_AF_TRIGGER_IDLE); refreshPreviewCaptureSession( - null, - (errorCode, errorMessage) -> pictureCaptureRequest.error(errorCode, errorMessage, null)); + null, + (errorCode, errorMessage) -> pictureCaptureRequest.error(errorCode, errorMessage, null)); } public void startVideoRecording(Result result) { @@ -632,7 +816,7 @@ public void startVideoRecording(Result result) { prepareMediaRecorder(videoRecordingFile.getAbsolutePath()); recordingVideo = true; createCaptureSession( - CameraDevice.TEMPLATE_RECORD, () -> mediaRecorder.start(), mediaRecorder.getSurface()); + CameraDevice.TEMPLATE_RECORD, () -> mediaRecorder.start(), mediaRecorder.getSurface()); result.success(null); } catch (CameraAccessException | IOException e) { recordingVideo = false; @@ -651,7 +835,7 @@ public void stopVideoRecording(@NonNull final Result result) { recordingVideo = false; try { - cameraCaptureSession.abortCaptures(); + mPreviewSession.abortCaptures(); mediaRecorder.stop(); } catch (CameraAccessException | IllegalStateException e) { // Ignore exceptions and try to continue (changes are camera session already aborted capture) @@ -698,7 +882,7 @@ public void resumeVideoRecording(@NonNull final Result result) { mediaRecorder.resume(); } else { result.error( - "videoRecordingFailed", "resumeVideoRecording requires Android API +24.", null); + "videoRecordingFailed", "resumeVideoRecording requires Android API +24.", null); return; } } catch (IllegalStateException e) { @@ -709,13 +893,20 @@ public void resumeVideoRecording(@NonNull final Result result) { result.success(null); } - public void setFlashMode(@NonNull final Result result, FlashMode mode) - throws CameraAccessException { - // Get the flash availability + /** + * Dart handler when it's time to set a new flash mode. This will try to set a new + * flash mode to the current camera. + * @param result + * @param newMode + * @throws CameraAccessException + */ + public void setFlashMode(@NonNull final Result result, FlashMode newMode) + throws CameraAccessException { + // Check if the current camera can modify the flash mode. Boolean flashAvailable = - cameraManager - .getCameraCharacteristics(cameraDevice.getId()) - .get(CameraCharacteristics.FLASH_INFO_AVAILABLE); + cameraManager + .getCameraCharacteristics(cameraDevice.getId()) + .get(CameraCharacteristics.FLASH_INFO_AVAILABLE); // Check if flash is available. if (flashAvailable == null || !flashAvailable) { @@ -723,70 +914,72 @@ public void setFlashMode(@NonNull final Result result, FlashMode mode) return; } - // If switching directly from torch to auto or on, make sure we turn off the torch. - if (flashMode == FlashMode.torch && mode != FlashMode.torch && mode != FlashMode.off) { - updateFlash(FlashMode.off); - - this.cameraCaptureSession.setRepeatingRequest( - captureRequestBuilder.build(), - new CaptureCallback() { - private boolean isFinished = false; - - @Override - public void onCaptureCompleted( - @NonNull CameraCaptureSession session, - @NonNull CaptureRequest request, - @NonNull TotalCaptureResult captureResult) { - if (isFinished) { - return; - } - - updateFlash(mode); - refreshPreviewCaptureSession( - () -> { - result.success(null); - isFinished = true; - }, - (code, message) -> - result.error("setFlashModeFailed", "Could not set flash mode.", null)); - } + // Save the new flash mode setting + final FlashMode oldFlashMode = currentFlashMode; + currentFlashMode = newMode; + updateFlash(mPreviewRequestBuilder); - @Override - public void onCaptureFailed( - @NonNull CameraCaptureSession session, - @NonNull CaptureRequest request, - @NonNull CaptureFailure failure) { - if (isFinished) { - return; - } + // If switching directly from torch to auto or on, make sure we turn off the torch. + if (oldFlashMode == FlashMode.torch && newMode != FlashMode.torch && newMode != FlashMode.off) { + // TODO: why cant we just call refresh preview here? + mPreviewSession.setRepeatingRequest( + mPreviewRequestBuilder.build(), + new CaptureCallback() { + private boolean isFinished = false; + + @Override + public void onCaptureCompleted( + @NonNull CameraCaptureSession session, + @NonNull CaptureRequest request, + @NonNull TotalCaptureResult captureResult) { + if (isFinished) { + return; + } + + updateFlash(mPreviewRequestBuilder); + refreshPreviewCaptureSession( + () -> { + result.success(null); + isFinished = true; + }, + (code, message) -> + result.error("setFlashModeFailed", "Could not set flash mode.", null)); + } - result.error("setFlashModeFailed", "Could not set flash mode.", null); - isFinished = true; - } - }, - null); + @Override + public void onCaptureFailed( + @NonNull CameraCaptureSession session, + @NonNull CaptureRequest request, + @NonNull CaptureFailure failure) { + if (isFinished) { + return; + } + + result.error("setFlashModeFailed", "Could not set flash mode.", null); + isFinished = true; + } + }, + null); } else { - updateFlash(mode); - refreshPreviewCaptureSession( - () -> result.success(null), - (code, message) -> result.error("setFlashModeFailed", "Could not set flash mode.", null)); + () -> result.success(null), + (code, message) -> result.error("setFlashModeFailed", "Could not set flash mode.", null)); } } public void setExposureMode(@NonNull final Result result, ExposureMode mode) - throws CameraAccessException { + throws CameraAccessException { updateExposure(mode); - cameraCaptureSession.setRepeatingRequest(captureRequestBuilder.build(), null, null); + mPreviewSession.setRepeatingRequest(mPreviewRequestBuilder.build(), null, null); result.success(null); } public void setExposurePoint(@NonNull final Result result, Double x, Double y) - throws CameraAccessException { + throws CameraAccessException { // Check if exposure point functionality is available. if (!isExposurePointSupported()) { result.error( - "setExposurePointFailed", "Device does not have exposure point capabilities", null); + "setExposurePointFailed", "Device does not have exposure point capabilities", null); return; } // Check if the current region boundaries are known @@ -800,38 +993,34 @@ public void setExposurePoint(@NonNull final Result result, Double x, Double y) // Apply it updateExposure(exposureMode); refreshPreviewCaptureSession( - () -> result.success(null), (code, message) -> result.error("CameraAccess", message, null)); + () -> result.success(null), (code, message) -> result.error("CameraAccess", message, null)); } - public void setFocusMode(@NonNull final Result result, FocusMode mode) - throws CameraAccessException { - this.focusMode = mode; - - updateFocus(mode); + /** + * Set new focus mode from dart. + * @param result + * @param newMode + * @throws CameraAccessException + */ + public void setFocusMode(@NonNull final Result result, FocusMode newMode) + throws CameraAccessException { + currentFocusMode = newMode; + updateFocus(mPreviewRequestBuilder); - switch (mode) { + switch (newMode) { case auto: refreshPreviewCaptureSession( - null, (code, message) -> result.error("setFocusMode", message, null)); + null, (code, message) -> result.error("setFocusMode", message, null)); break; case locked: - lockAutoFocus( - new CaptureCallback() { - @Override - public void onCaptureCompleted( - @NonNull CameraCaptureSession session, - @NonNull CaptureRequest request, - @NonNull TotalCaptureResult result) { - unlockAutoFocus(); - } - }); + startAutoFocus(); break; } result.success(null); } public void setFocusPoint(@NonNull final Result result, Double x, Double y) - throws CameraAccessException { + throws CameraAccessException { // Check if focus point functionality is available. if (!isFocusPointSupported()) { result.error("setFocusPointFailed", "Device does not have focus point capabilities", null); @@ -852,20 +1041,21 @@ public void setFocusPoint(@NonNull final Result result, Double x, Double y) } // Apply the new metering rectangle - setFocusMode(result, focusMode); + setFocusMode(result, currentFocusMode); } @TargetApi(VERSION_CODES.P) private boolean supportsDistortionCorrection() throws CameraAccessException { int[] availableDistortionCorrectionModes = - cameraManager - .getCameraCharacteristics(cameraDevice.getId()) - .get(CameraCharacteristics.DISTORTION_CORRECTION_AVAILABLE_MODES); - if (availableDistortionCorrectionModes == null) availableDistortionCorrectionModes = new int[0]; + cameraManager + .getCameraCharacteristics(cameraDevice.getId()) + .get(CameraCharacteristics.DISTORTION_CORRECTION_AVAILABLE_MODES); + if (availableDistortionCorrectionModes == null) + availableDistortionCorrectionModes = new int[0]; long nonOffModesSupported = - Arrays.stream(availableDistortionCorrectionModes) - .filter((value) -> value != CaptureRequest.DISTORTION_CORRECTION_MODE_OFF) - .count(); + Arrays.stream(availableDistortionCorrectionModes) + .filter((value) -> value != CaptureRequest.DISTORTION_CORRECTION_MODE_OFF) + .count(); return nonOffModesSupported > 0; } @@ -873,50 +1063,50 @@ private Size getRegionBoundaries() throws CameraAccessException { // No distortion correction support if (android.os.Build.VERSION.SDK_INT < VERSION_CODES.P || !supportsDistortionCorrection()) { return cameraManager - .getCameraCharacteristics(cameraDevice.getId()) - .get(CameraCharacteristics.SENSOR_INFO_PIXEL_ARRAY_SIZE); + .getCameraCharacteristics(cameraDevice.getId()) + .get(CameraCharacteristics.SENSOR_INFO_PIXEL_ARRAY_SIZE); } // Get the current distortion correction mode Integer distortionCorrectionMode = - captureRequestBuilder.get(CaptureRequest.DISTORTION_CORRECTION_MODE); + mPreviewRequestBuilder.get(CaptureRequest.DISTORTION_CORRECTION_MODE); // Return the correct boundaries depending on the mode android.graphics.Rect rect; if (distortionCorrectionMode == null - || distortionCorrectionMode == CaptureRequest.DISTORTION_CORRECTION_MODE_OFF) { + || distortionCorrectionMode == CaptureRequest.DISTORTION_CORRECTION_MODE_OFF) { rect = - cameraManager - .getCameraCharacteristics(cameraDevice.getId()) - .get(CameraCharacteristics.SENSOR_INFO_PRE_CORRECTION_ACTIVE_ARRAY_SIZE); + cameraManager + .getCameraCharacteristics(cameraDevice.getId()) + .get(CameraCharacteristics.SENSOR_INFO_PRE_CORRECTION_ACTIVE_ARRAY_SIZE); } else { rect = - cameraManager - .getCameraCharacteristics(cameraDevice.getId()) - .get(CameraCharacteristics.SENSOR_INFO_ACTIVE_ARRAY_SIZE); + cameraManager + .getCameraCharacteristics(cameraDevice.getId()) + .get(CameraCharacteristics.SENSOR_INFO_ACTIVE_ARRAY_SIZE); } return rect == null ? null : new Size(rect.width(), rect.height()); } private boolean isExposurePointSupported() throws CameraAccessException { Integer supportedRegions = - cameraManager - .getCameraCharacteristics(cameraDevice.getId()) - .get(CameraCharacteristics.CONTROL_MAX_REGIONS_AE); + cameraManager + .getCameraCharacteristics(cameraDevice.getId()) + .get(CameraCharacteristics.CONTROL_MAX_REGIONS_AE); return supportedRegions != null && supportedRegions > 0; } private boolean isFocusPointSupported() throws CameraAccessException { Integer supportedRegions = - cameraManager - .getCameraCharacteristics(cameraDevice.getId()) - .get(CameraCharacteristics.CONTROL_MAX_REGIONS_AF); + cameraManager + .getCameraCharacteristics(cameraDevice.getId()) + .get(CameraCharacteristics.CONTROL_MAX_REGIONS_AF); return supportedRegions != null && supportedRegions > 0; } public double getMinExposureOffset() throws CameraAccessException { Range range = - cameraManager - .getCameraCharacteristics(cameraDevice.getId()) - .get(CameraCharacteristics.CONTROL_AE_COMPENSATION_RANGE); + cameraManager + .getCameraCharacteristics(cameraDevice.getId()) + .get(CameraCharacteristics.CONTROL_AE_COMPENSATION_RANGE); double minStepped = range == null ? 0 : range.getLower(); double stepSize = getExposureOffsetStepSize(); return minStepped * stepSize; @@ -924,9 +1114,9 @@ public double getMinExposureOffset() throws CameraAccessException { public double getMaxExposureOffset() throws CameraAccessException { Range range = - cameraManager - .getCameraCharacteristics(cameraDevice.getId()) - .get(CameraCharacteristics.CONTROL_AE_COMPENSATION_RANGE); + cameraManager + .getCameraCharacteristics(cameraDevice.getId()) + .get(CameraCharacteristics.CONTROL_AE_COMPENSATION_RANGE); double maxStepped = range == null ? 0 : range.getUpper(); double stepSize = getExposureOffsetStepSize(); return maxStepped * stepSize; @@ -934,20 +1124,20 @@ public double getMaxExposureOffset() throws CameraAccessException { public double getExposureOffsetStepSize() throws CameraAccessException { Rational stepSize = - cameraManager - .getCameraCharacteristics(cameraDevice.getId()) - .get(CameraCharacteristics.CONTROL_AE_COMPENSATION_STEP); + cameraManager + .getCameraCharacteristics(cameraDevice.getId()) + .get(CameraCharacteristics.CONTROL_AE_COMPENSATION_STEP); return stepSize == null ? 0.0 : stepSize.doubleValue(); } public void setExposureOffset(@NonNull final Result result, double offset) - throws CameraAccessException { + throws CameraAccessException { // Set the exposure offset double stepSize = getExposureOffsetStepSize(); exposureOffset = (int) (offset / stepSize); // Apply it updateExposure(exposureMode); - this.cameraCaptureSession.setRepeatingRequest(captureRequestBuilder.build(), null, null); + this.mPreviewSession.setRepeatingRequest(mPreviewRequestBuilder.build(), null, null); result.success(offset); } @@ -965,20 +1155,20 @@ public void setZoomLevel(@NonNull final Result result, float zoom) throws Camera if (zoom > maxZoom || zoom < minZoom) { String errorMessage = - String.format( - Locale.ENGLISH, - "Zoom level out of bounds (zoom level should be between %f and %f).", - minZoom, - maxZoom); + String.format( + Locale.ENGLISH, + "Zoom level out of bounds (zoom level should be between %f and %f).", + minZoom, + maxZoom); result.error("ZOOM_ERROR", errorMessage, null); return; } //Zoom area is calculated relative to sensor area (activeRect) - if (captureRequestBuilder != null) { + if (mPreviewRequestBuilder != null) { final Rect computedZoom = cameraZoom.computeZoom(zoom); - captureRequestBuilder.set(CaptureRequest.SCALER_CROP_REGION, computedZoom); - cameraCaptureSession.setRepeatingRequest(captureRequestBuilder.build(), null, null); + mPreviewRequestBuilder.set(CaptureRequest.SCALER_CROP_REGION, computedZoom); + mPreviewSession.setRepeatingRequest(mPreviewRequestBuilder.build(), null, null); } result.success(null); @@ -992,47 +1182,50 @@ public void unlockCaptureOrientation() { this.lockedCaptureOrientation = null; } + /** + * Set current fps range setting to the current preview request builder + */ private void updateFpsRange() { if (fpsRange == null) { return; } - captureRequestBuilder.set(CaptureRequest.CONTROL_AE_TARGET_FPS_RANGE, fpsRange); + mPreviewRequestBuilder.set(CaptureRequest.CONTROL_AE_TARGET_FPS_RANGE, fpsRange); } - private void updateFocus(FocusMode mode) { + private void updateFocus(CaptureRequest.Builder requestBuilder) { if (useAutoFocus) { int[] modes = cameraCharacteristics.get(CameraCharacteristics.CONTROL_AF_AVAILABLE_MODES); // Auto focus is not supported if (modes == null - || modes.length == 0 - || (modes.length == 1 && modes[0] == CameraCharacteristics.CONTROL_AF_MODE_OFF)) { + || modes.length == 0 + || (modes.length == 1 && modes[0] == CameraCharacteristics.CONTROL_AF_MODE_OFF)) { useAutoFocus = false; - captureRequestBuilder.set( - CaptureRequest.CONTROL_AF_MODE, CaptureRequest.CONTROL_AF_MODE_OFF); + mPreviewRequestBuilder.set( + CaptureRequest.CONTROL_AF_MODE, CaptureRequest.CONTROL_AF_MODE_OFF); } else { // Applying auto focus - switch (mode) { + switch (currentFocusMode) { case locked: - captureRequestBuilder.set( - CaptureRequest.CONTROL_AF_MODE, CaptureRequest.CONTROL_AF_MODE_AUTO); + requestBuilder.set( + CaptureRequest.CONTROL_AF_MODE, CaptureRequest.CONTROL_AF_MODE_AUTO); break; case auto: - captureRequestBuilder.set( - CaptureRequest.CONTROL_AF_MODE, - recordingVideo - ? CaptureRequest.CONTROL_AF_MODE_CONTINUOUS_VIDEO - : CaptureRequest.CONTROL_AF_MODE_CONTINUOUS_PICTURE); + mPreviewRequestBuilder.set( + CaptureRequest.CONTROL_AF_MODE, + recordingVideo + ? CaptureRequest.CONTROL_AF_MODE_CONTINUOUS_VIDEO + : CaptureRequest.CONTROL_AF_MODE_CONTINUOUS_PICTURE); default: break; } MeteringRectangle afRect = cameraRegions.getAFMeteringRectangle(); - captureRequestBuilder.set( - CaptureRequest.CONTROL_AF_REGIONS, - afRect == null ? null : new MeteringRectangle[] {afRect}); + mPreviewRequestBuilder.set( + CaptureRequest.CONTROL_AF_REGIONS, + afRect == null ? null : new MeteringRectangle[]{afRect}); } } else { - captureRequestBuilder.set(CaptureRequest.CONTROL_AF_MODE, CaptureRequest.CONTROL_AF_MODE_OFF); + mPreviewRequestBuilder.set(CaptureRequest.CONTROL_AF_MODE, CaptureRequest.CONTROL_AF_MODE_OFF); } } @@ -1041,51 +1234,21 @@ private void updateExposure(ExposureMode mode) { // Applying auto exposure MeteringRectangle aeRect = cameraRegions.getAEMeteringRectangle(); - captureRequestBuilder.set( - CaptureRequest.CONTROL_AE_REGIONS, - aeRect == null ? null : new MeteringRectangle[] {cameraRegions.getAEMeteringRectangle()}); + mPreviewRequestBuilder.set( + CaptureRequest.CONTROL_AE_REGIONS, + aeRect == null ? null : new MeteringRectangle[]{cameraRegions.getAEMeteringRectangle()}); switch (mode) { case locked: - captureRequestBuilder.set(CaptureRequest.CONTROL_AE_LOCK, true); + mPreviewRequestBuilder.set(CaptureRequest.CONTROL_AE_LOCK, true); break; case auto: default: - captureRequestBuilder.set(CaptureRequest.CONTROL_AE_LOCK, false); + mPreviewRequestBuilder.set(CaptureRequest.CONTROL_AE_LOCK, false); break; } - captureRequestBuilder.set(CaptureRequest.CONTROL_AE_EXPOSURE_COMPENSATION, exposureOffset); - } - - private void updateFlash(FlashMode mode) { - // Get flash - flashMode = mode; - - // Applying flash modes - switch (flashMode) { - case off: - captureRequestBuilder.set( - CaptureRequest.CONTROL_AE_MODE, CaptureRequest.CONTROL_AE_MODE_ON); - captureRequestBuilder.set(CaptureRequest.FLASH_MODE, CaptureRequest.FLASH_MODE_OFF); - break; - case auto: - captureRequestBuilder.set( - CaptureRequest.CONTROL_AE_MODE, CaptureRequest.CONTROL_AE_MODE_ON_AUTO_FLASH); - captureRequestBuilder.set(CaptureRequest.FLASH_MODE, CaptureRequest.FLASH_MODE_OFF); - break; - case always: - captureRequestBuilder.set( - CaptureRequest.CONTROL_AE_MODE, CaptureRequest.CONTROL_AE_MODE_ON_ALWAYS_FLASH); - captureRequestBuilder.set(CaptureRequest.FLASH_MODE, CaptureRequest.FLASH_MODE_OFF); - break; - case torch: - default: - captureRequestBuilder.set( - CaptureRequest.CONTROL_AE_MODE, CaptureRequest.CONTROL_AE_MODE_ON); - captureRequestBuilder.set(CaptureRequest.FLASH_MODE, CaptureRequest.FLASH_MODE_TORCH); - break; - } + mPreviewRequestBuilder.set(CaptureRequest.CONTROL_AE_EXPOSURE_COMPENSATION, exposureOffset); } public void startPreview() throws CameraAccessException { @@ -1095,54 +1258,54 @@ public void startPreview() throws CameraAccessException { } public void startPreviewWithImageStream(EventChannel imageStreamChannel) - throws CameraAccessException { + throws CameraAccessException { createCaptureSession(CameraDevice.TEMPLATE_RECORD, imageStreamReader.getSurface()); imageStreamChannel.setStreamHandler( - new EventChannel.StreamHandler() { - @Override - public void onListen(Object o, EventChannel.EventSink imageStreamSink) { - setImageStreamImageAvailableListener(imageStreamSink); - } + new EventChannel.StreamHandler() { + @Override + public void onListen(Object o, EventChannel.EventSink imageStreamSink) { + setImageStreamImageAvailableListener(imageStreamSink); + } - @Override - public void onCancel(Object o) { - imageStreamReader.setOnImageAvailableListener(null, null); - } - }); + @Override + public void onCancel(Object o) { + imageStreamReader.setOnImageAvailableListener(null, null); + } + }); } private void setImageStreamImageAvailableListener(final EventChannel.EventSink imageStreamSink) { imageStreamReader.setOnImageAvailableListener( - reader -> { - Image img = reader.acquireLatestImage(); - if (img == null) return; + reader -> { + Image img = reader.acquireLatestImage(); + if (img == null) return; - List> planes = new ArrayList<>(); - for (Image.Plane plane : img.getPlanes()) { - ByteBuffer buffer = plane.getBuffer(); + List> planes = new ArrayList<>(); + for (Image.Plane plane : img.getPlanes()) { + ByteBuffer buffer = plane.getBuffer(); - byte[] bytes = new byte[buffer.remaining()]; - buffer.get(bytes, 0, bytes.length); + byte[] bytes = new byte[buffer.remaining()]; + buffer.get(bytes, 0, bytes.length); - Map planeBuffer = new HashMap<>(); - planeBuffer.put("bytesPerRow", plane.getRowStride()); - planeBuffer.put("bytesPerPixel", plane.getPixelStride()); - planeBuffer.put("bytes", bytes); + Map planeBuffer = new HashMap<>(); + planeBuffer.put("bytesPerRow", plane.getRowStride()); + planeBuffer.put("bytesPerPixel", plane.getPixelStride()); + planeBuffer.put("bytes", bytes); - planes.add(planeBuffer); - } + planes.add(planeBuffer); + } - Map imageBuffer = new HashMap<>(); - imageBuffer.put("width", img.getWidth()); - imageBuffer.put("height", img.getHeight()); - imageBuffer.put("format", img.getFormat()); - imageBuffer.put("planes", planes); + Map imageBuffer = new HashMap<>(); + imageBuffer.put("width", img.getWidth()); + imageBuffer.put("height", img.getHeight()); + imageBuffer.put("format", img.getFormat()); + imageBuffer.put("planes", planes); - imageStreamSink.success(imageBuffer); - img.close(); - }, - null); + imageStreamSink.success(imageBuffer); + img.close(); + }, + null); } public void stopImageStream() throws CameraAccessException { @@ -1152,7 +1315,9 @@ public void stopImageStream() throws CameraAccessException { startPreview(); } - /** Sets the time the pre-capture sequence started. */ + /** + * Sets the time the pre-capture sequence started. + */ private void setPreCaptureStartTime() { preCaptureStartTime = SystemClock.elapsedRealtime(); } @@ -1167,9 +1332,9 @@ private boolean hitPreCaptureTimeout() { } private void closeCaptureSession() { - if (cameraCaptureSession != null) { - cameraCaptureSession.close(); - cameraCaptureSession = null; + if (mPreviewSession != null) { + mPreviewSession.close(); + mPreviewSession = null; } } diff --git a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/CameraConstants.java b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/CameraConstants.java new file mode 100644 index 000000000000..ad71203da028 --- /dev/null +++ b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/CameraConstants.java @@ -0,0 +1,26 @@ +package io.flutter.plugins.camera; + +public class CameraConstants { + + public static final AspectRatio DEFAULT_ASPECT_RATIO = AspectRatio.of(16, 9); + + public static final long AUTO_FOCUS_TIMEOUT_MS = 800; //800ms timeout, Under normal circumstances need to a few hundred milliseconds + + public static final long OPEN_CAMERA_TIMEOUT_MS = 2500; //2.5s + + public static final int FOCUS_HOLD_MILLIS = 3000; + + public static final float METERING_REGION_FRACTION = 0.1225f; + + public static final int ZOOM_REGION_DEFAULT = 1; + + public static final int FLASH_OFF = 0; + public static final int FLASH_ON = 1; + public static final int FLASH_TORCH = 2; + public static final int FLASH_AUTO = 3; + public static final int FLASH_RED_EYE = 4; + + public static final int FACING_BACK = 0; + public static final int FACING_FRONT = 1; + +} diff --git a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/CaptureSessionState.java b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/CaptureSessionState.java new file mode 100644 index 000000000000..9853c706ca34 --- /dev/null +++ b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/CaptureSessionState.java @@ -0,0 +1,11 @@ +package io.flutter.plugins.camera; + +public enum CaptureSessionState { + IDLE, // Idle + FOCUSING, // Focusing + STATE_WAITING_PRECAPTURE, // Precapture + STATE_WAITING_NON_PRECAPTURE, // Waiting precapture ready + CAPTURING, // Capturing + FINISHED, // Finished + ERROR, // Error +} diff --git a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/ImageSaver.java b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/ImageSaver.java new file mode 100644 index 000000000000..12d2accce199 --- /dev/null +++ b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/ImageSaver.java @@ -0,0 +1,78 @@ +package io.flutter.plugins.camera; + +import android.media.Image; +import android.media.ImageReader; +import android.os.Handler; +import android.os.Looper; + +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.nio.ByteBuffer; + +/** + * Saves a JPEG {@link Image} into the specified {@link File}. + */ +public class ImageSaver implements Runnable { + + /** + * The JPEG image + */ + private final Image mImage; + /** + * The file we save the image into. + */ + private final File mFile; + + /** + * + * Used to finish the picture capture request + */ + private PictureCaptureRequest mPictureCaptureRequest; + + ImageSaver(Image image, File file, PictureCaptureRequest pictureCaptureRequest) { + mImage = image; + mFile = file; + mPictureCaptureRequest = pictureCaptureRequest; + } + + @Override + public void run() { + final Handler handler = new Handler(Looper.getMainLooper()); + ByteBuffer buffer = mImage.getPlanes()[0].getBuffer(); + byte[] bytes = new byte[buffer.remaining()]; + buffer.get(bytes); + FileOutputStream output = null; + try { + output = new FileOutputStream(mFile); + output.write(bytes); + + handler.post(new Runnable() { + @Override + public void run() { + mPictureCaptureRequest.finish(mFile.getAbsolutePath()); + } + }); + + } catch (IOException e) { + mPictureCaptureRequest.error("IOError", "Failed saving image", null); + } finally { + mImage.close(); + if (null != output) { + try { + output.close(); + } catch (IOException e) { + handler.post(new Runnable() { + @Override + public void run() { + mPictureCaptureRequest.error("cameraAccess", e.getMessage(), null); + } + }); + + + } + } + } + } + +} \ No newline at end of file diff --git a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/PictureCaptureRequest.java b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/PictureCaptureRequest.java index 396f782a2a08..99301f961a6b 100644 --- a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/PictureCaptureRequest.java +++ b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/PictureCaptureRequest.java @@ -6,21 +6,16 @@ import android.os.Handler; import android.os.Looper; + import androidx.annotation.Nullable; + +import java.io.File; + +import io.flutter.Log; import io.flutter.plugin.common.MethodChannel; class PictureCaptureRequest { - enum State { - idle, - focusing, - preCapture, - waitingPreCaptureReady, - capturing, - finished, - error, - } - private final Runnable timeoutCallback = new Runnable() { @Override @@ -31,7 +26,12 @@ public void run() { private final MethodChannel.Result result; private final TimeoutHandler timeoutHandler; - private State state; + private CaptureSessionState state = CaptureSessionState.IDLE; + + /** + * This is the output file for our picture. + */ + File mFile; public PictureCaptureRequest(MethodChannel.Result result) { this(result, new TimeoutHandler()); @@ -39,33 +39,33 @@ public PictureCaptureRequest(MethodChannel.Result result) { public PictureCaptureRequest(MethodChannel.Result result, TimeoutHandler timeoutHandler) { this.result = result; - this.state = State.idle; this.timeoutHandler = timeoutHandler; } - public void setState(State state) { + public void setState(CaptureSessionState state) { if (isFinished()) throw new IllegalStateException("Request has already been finished"); this.state = state; - if (state != State.idle && state != State.finished && state != State.error) { + if (state != CaptureSessionState.IDLE && state != CaptureSessionState.FINISHED && state != CaptureSessionState.ERROR) { this.timeoutHandler.resetTimeout(timeoutCallback); } else { this.timeoutHandler.clearTimeout(timeoutCallback); } } - public State getState() { + public CaptureSessionState getState() { return state; } public boolean isFinished() { - return state == State.finished || state == State.error; + return state == CaptureSessionState.FINISHED || state == CaptureSessionState.ERROR; } public void finish(String absolutePath) { if (isFinished()) throw new IllegalStateException("Request has already been finished"); + Log.i("Camera", "PictureCaptureRequest finish"); this.timeoutHandler.clearTimeout(timeoutCallback); result.success(absolutePath); - state = State.finished; + setState(CaptureSessionState.FINISHED); } public void error( @@ -73,7 +73,7 @@ public void error( if (isFinished()) throw new IllegalStateException("Request has already been finished"); this.timeoutHandler.clearTimeout(timeoutCallback); result.error(errorCode, errorMessage, errorDetails); - state = State.error; + setState(CaptureSessionState.ERROR); } static class TimeoutHandler { diff --git a/packages/camera/camera/example/android/gradle.properties b/packages/camera/camera/example/android/gradle.properties index a6738207fd15..7ab5b529f76c 100644 --- a/packages/camera/camera/example/android/gradle.properties +++ b/packages/camera/camera/example/android/gradle.properties @@ -1,4 +1,17 @@ -org.gradle.jvmargs=-Xmx1536M -android.useAndroidX=true +## For more details on how to configure your build environment visit +# http://www.gradle.org/docs/current/userguide/build_environment.html +# +# Specifies the JVM arguments used for the daemon process. +# The setting is particularly useful for tweaking memory settings. +# Default value: -Xmx1024m -XX:MaxPermSize=256m +# org.gradle.jvmargs=-Xmx2048m -XX:MaxPermSize=512m -XX:+HeapDumpOnOutOfMemoryError -Dfile.encoding=UTF-8 +# +# When configured, Gradle will run in incubating parallel mode. +# This option should only be used with decoupled projects. More details, visit +# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects +# org.gradle.parallel=true +#Sun Feb 28 11:56:11 EST 2021 android.enableJetifier=true android.enableR8=true +android.useAndroidX=true +org.gradle.jvmargs=-Xmx2048M -Dkotlin.daemon.jvm.options\="-Xmx2048M" From 6348e23a0e1fa4acec89283227bff40265e1112a Mon Sep 17 00:00:00 2001 From: Andrew Coutts Date: Mon, 1 Mar 2021 05:09:47 -0500 Subject: [PATCH 002/114] Tweaks --- .../io/flutter/plugins/camera/Camera.java | 149 ++++++----- .../flutter/plugins/camera/CameraState.java | 34 +++ .../plugins/camera/CaptureSessionState.java | 11 - .../plugins/camera/PictureCaptureRequest.java | 231 ++++++++++++------ .../camera/PictureCaptureRequestState.java | 33 +++ .../plugins/camera/types/FlashMode.java | 12 +- 6 files changed, 331 insertions(+), 139 deletions(-) create mode 100644 packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/CameraState.java delete mode 100644 packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/CaptureSessionState.java create mode 100644 packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/PictureCaptureRequestState.java 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 ea879f4e2bf1..debd0d77fff8 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 @@ -47,6 +47,7 @@ import java.io.File; import java.io.IOException; +import java.lang.reflect.Field; import java.nio.ByteBuffer; import java.util.ArrayList; import java.util.Arrays; @@ -76,10 +77,7 @@ interface ErrorCallback { public class Camera { private static final String TAG = "Camera"; - /** - * Timeout for the pre-capture sequence. - */ - private static final long PRECAPTURE_TIMEOUT_MS = 1000; + /** * Conversion from screen rotation to JPEG orientation. */ @@ -116,18 +114,21 @@ public class Camera { private final CameraCharacteristics cameraCharacteristics; private final Activity activity; - /** - * Whether the current camera device supports Flash or not. - */ - private final boolean mFlashSupported; +/** + * The state of the camera. By default we are in the preview state. + */ + private CameraState cameraState = CameraState.STATE_PREVIEW; + /** * A {@link Handler} for running tasks in the background. */ private Handler mBackgroundHandler; + /** * An additional thread for running tasks that shouldn't block the UI. */ private HandlerThread mBackgroundThread; + private CameraDevice cameraDevice; private CameraCaptureSession mPreviewSession; private ImageReader pictureImageReader; @@ -143,35 +144,48 @@ public class Camera { */ private CaptureRequest mPreviewRequest; + private MediaRecorder mediaRecorder; + private boolean recordingVideo; + private File videoRecordingFile; + /** - * The current camera auto focus mode + * Flash mode setting of the current camera. Initialize to off because + * we don't know if the current camera supports flash yet. */ - private boolean mAutoFocus = true; + private FlashMode currentFlashMode = FlashMode.off; /** - * Whether the current camera device supports auto focus or not. + * Exposure mode setting of the current camera. Initialize to auto + * because all cameras support autoexposure by default. */ - private boolean mAutoFocusSupported = true; + private ExposureMode exposureMode = ExposureMode.auto; - private MediaRecorder mediaRecorder; - private boolean recordingVideo; - private File videoRecordingFile; + /** + * Focus mode setting of the current camera. Initialize to locked because + * we don't know if the current camera supports autofocus yet. + */ + private FocusMode currentFocusMode = FocusMode.locked; /** - * Flash mode setting of the current camera + * Whether the current camera device supports auto focus or not. */ - private FlashMode currentFlashMode; + private boolean mAutoFocusSupported = false; /** - * Exposure mode setting of the current camera. + * Whether or not to use autofocus. */ - private ExposureMode exposureMode; + private boolean useAutoFocus = false; /** - * Focus mode setting of the current camera. + * Whether the current camera device supports Flash or not. */ - private FocusMode currentFocusMode; - private PictureCaptureRequest pictureCaptureRequest; + private boolean mFlashSupported = false; + + /** + * This manages the state of the camera and the current capture request. + */ + PictureCaptureRequest pictureCaptureRequest; + /** * This a callback object for the {@link ImageReader}. "onImageAvailable" will be called when a * still image is ready to be saved. @@ -188,10 +202,10 @@ public void onImageAvailable(ImageReader reader) { }; private CameraRegions cameraRegions; private int exposureOffset; - private boolean useAutoFocus = true; + private Range fpsRange; private PlatformChannel.DeviceOrientation lockedCaptureOrientation; - private long preCaptureStartTime; + public Camera( @@ -202,6 +216,8 @@ public Camera( final String resolutionPreset, final boolean enableAudio) throws CameraAccessException { + Log.i(TAG, "Camear constructor"); + if (activity == null) { throw new IllegalStateException("No activity available!"); } @@ -248,6 +264,8 @@ public Camera( * @param cameraCharacteristics */ private void getAvailableFpsRange(CameraCharacteristics cameraCharacteristics) { + Log.i(TAG, "getAvailableFpsRange"); + try { Range[] ranges = cameraCharacteristics.get(CameraCharacteristics.CONTROL_AE_AVAILABLE_TARGET_FPS_RANGES); @@ -269,6 +287,8 @@ private void getAvailableFpsRange(CameraCharacteristics cameraCharacteristics) { } private void prepareMediaRecorder(String outputFilePath) throws IOException { + Log.i(TAG, "prepareMediaRecorder"); + if (mediaRecorder != null) { mediaRecorder.release(); } @@ -304,6 +324,9 @@ public void open(String imageFormatGroup) throws CameraAccessException { new CameraDevice.StateCallback() { @Override public void onOpened(@NonNull CameraDevice device) { + Log.i(TAG, "open | onOpened"); + + cameraDevice = device; try { startPreview(); @@ -322,18 +345,24 @@ public void onOpened(@NonNull CameraDevice device) { @Override public void onClosed(@NonNull CameraDevice camera) { + Log.i(TAG, "open | onClosed"); + dartMessenger.sendCameraClosingEvent(); super.onClosed(camera); } @Override public void onDisconnected(@NonNull CameraDevice cameraDevice) { + Log.i(TAG, "open | onDisconnected"); + close(); dartMessenger.sendCameraErrorEvent("The camera was disconnected."); } @Override public void onError(@NonNull CameraDevice cameraDevice, int errorCode) { + Log.i(TAG, "open | onError"); + close(); String errorDescription; switch (errorCode) { @@ -369,6 +398,8 @@ private void createCaptureSession(int templateType, Surface... surfaces) private void createCaptureSession( int templateType, Runnable onSuccessCallback, Surface... surfaces) throws CameraAccessException { + Log.i(TAG, "createCaptureSession"); + // Close any existing capture session. closeCaptureSession(); @@ -469,6 +500,9 @@ private void refreshPreviewCaptureSession( mPreviewSession.setRepeatingRequest(mPreviewRequest, mCaptureCallback, mBackgroundHandler); + // Put camera back to preview mode + cameraState = CameraState.STATE_PREVIEW; + if (onSuccessCallback != null) { onSuccessCallback.run(); } @@ -480,18 +514,19 @@ private void refreshPreviewCaptureSession( public void takePicture(@NonNull final Result result) { Log.i(TAG, "takePicture | useAutoFocus: " + useAutoFocus); - // Only take 1 picture at a time + // Only take one 1 picture at a time. if (pictureCaptureRequest != null && !pictureCaptureRequest.isFinished()) { result.error("captureAlreadyActive", "Picture is currently already being captured", null); return; } - // Store the result - pictureCaptureRequest = new PictureCaptureRequest(result); // Create temporary file final File outputDir = applicationContext.getCacheDir(); try { - pictureCaptureRequest.mFile = File.createTempFile("CAP", ".jpg", outputDir); + final File file = File.createTempFile("CAP", ".jpg", outputDir); + + // Start a new capture + pictureCaptureRequest = new PictureCaptureRequest(result, file); } catch (IOException | SecurityException e) { pictureCaptureRequest.error("cannotCreateFile", e.getMessage(), null); return; @@ -518,7 +553,7 @@ private void runPrecaptureSequence() { mPreviewRequestBuilder.set(CaptureRequest.CONTROL_AE_PRECAPTURE_TRIGGER, CaptureRequest.CONTROL_AE_PRECAPTURE_TRIGGER_START); // Tell #mCaptureCallback to wait for the precapture sequence to be set. - pictureCaptureRequest.setState(CaptureSessionState.STATE_WAITING_PRECAPTURE); + cameraState = CameraState.STATE_WAITING_PRECAPTURE; mPreviewSession.capture(mPreviewRequestBuilder.build(), mCaptureCallback, mBackgroundHandler); } catch (CameraAccessException e) { @@ -675,17 +710,19 @@ private void process(CaptureResult result) { Integer aeState = result.get(CaptureResult.CONTROL_AE_STATE); Integer afState = result.get(CaptureResult.CONTROL_AF_STATE); - if (pictureCaptureRequest.getState() != CaptureSessionState.IDLE) { - Log.i(TAG, "mCaptureCallback | state: " + pictureCaptureRequest.getState() + " | afState: " + afState + " | aeState: " + aeState); + if (cameraState != CameraState.STATE_PREVIEW) { + Log.i(TAG, "mCaptureCallback | state: " + cameraState + " | afState: " + afState + " | aeState: " + aeState); } - switch (pictureCaptureRequest.getState()) { - case IDLE: { + switch (cameraState) { + case STATE_PREVIEW: { // We have nothing to do when the camera preview is working normally. break; } - case FOCUSING: { + + case STATE_WAITING_FOCUS: { if (afState == null) { + cameraState = CameraState.STATE_CAPTURING; captureStillPicture(); } else if (afState == CaptureResult.CONTROL_AF_STATE_FOCUSED_LOCKED || afState == CaptureResult.CONTROL_AF_STATE_NOT_FOCUSED_LOCKED) { @@ -693,8 +730,7 @@ private void process(CaptureResult result) { if (aeState == null || aeState == CaptureResult.CONTROL_AE_STATE_CONVERGED) { - pictureCaptureRequest.setState(CaptureSessionState.CAPTURING); - + cameraState = CameraState.STATE_CAPTURING; captureStillPicture(); } else { runPrecaptureSequence(); @@ -702,23 +738,26 @@ private void process(CaptureResult result) { } break; } + case STATE_WAITING_PRECAPTURE: { // CONTROL_AE_STATE can be null on some devices if (aeState == null || + aeState == CaptureResult.CONTROL_AE_STATE_CONVERGED || aeState == CaptureResult.CONTROL_AE_STATE_PRECAPTURE || - aeState == CaptureRequest.CONTROL_AE_STATE_FLASH_REQUIRED) { - pictureCaptureRequest.setState(CaptureSessionState.STATE_WAITING_NON_PRECAPTURE); - setPreCaptureStartTime(); + aeState == CaptureResult.CONTROL_AE_STATE_FLASH_REQUIRED) { + cameraState = CameraState.STATE_WAITING_PRECAPTURE_READY; + pictureCaptureRequest.setPreCaptureStartTime(); } break; } - case STATE_WAITING_NON_PRECAPTURE: { + + case STATE_WAITING_PRECAPTURE_READY: { // CONTROL_AE_STATE can be null on some devices if (aeState == null || aeState != CaptureResult.CONTROL_AE_STATE_PRECAPTURE) { - pictureCaptureRequest.setState(CaptureSessionState.CAPTURING); + cameraState = CameraState.STATE_CAPTURING; captureStillPicture(); } else { - if (hitPreCaptureTimeout()) { + if (pictureCaptureRequest.hitPreCaptureTimeout()) { unlockAutoFocus(); } } @@ -765,7 +804,7 @@ private void runPictureAutoFocus() { Log.i(TAG, "runPictureAutoFocus"); assert (pictureCaptureRequest != null); - pictureCaptureRequest.setState(CaptureSessionState.FOCUSING); + cameraState = CameraState.STATE_WAITING_FOCUS; lockAutoFocus(); } @@ -1253,6 +1292,7 @@ private void updateExposure(ExposureMode mode) { public void startPreview() throws CameraAccessException { if (pictureImageReader == null || pictureImageReader.getSurface() == null) return; + Log.i(TAG, "startPreview"); createCaptureSession(CameraDevice.TEMPLATE_PREVIEW, pictureImageReader.getSurface()); } @@ -1260,6 +1300,7 @@ public void startPreview() throws CameraAccessException { public void startPreviewWithImageStream(EventChannel imageStreamChannel) throws CameraAccessException { createCaptureSession(CameraDevice.TEMPLATE_RECORD, imageStreamReader.getSurface()); + Log.i(TAG, "startPreviewWithImageStream"); imageStreamChannel.setStreamHandler( new EventChannel.StreamHandler() { @@ -1315,30 +1356,22 @@ public void stopImageStream() throws CameraAccessException { startPreview(); } - /** - * Sets the time the pre-capture sequence started. - */ - private void setPreCaptureStartTime() { - preCaptureStartTime = SystemClock.elapsedRealtime(); - } - /** - * Check if the timeout for the pre-capture sequence has been reached. - * - * @return true if the timeout is reached; otherwise false is returned. - */ - private boolean hitPreCaptureTimeout() { - return (SystemClock.elapsedRealtime() - preCaptureStartTime) > PRECAPTURE_TIMEOUT_MS; - } + + private void closeCaptureSession() { if (mPreviewSession != null) { + Log.i(TAG, "closeCaptureSession"); + mPreviewSession.close(); mPreviewSession = null; } } public void close() { + Log.i(TAG, "close"); + closeCaptureSession(); if (cameraDevice != null) { @@ -1361,6 +1394,8 @@ public void close() { } public void dispose() { + Log.i(TAG, "dispose"); + close(); flutterTexture.release(); deviceOrientationListener.stop(); diff --git a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/CameraState.java b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/CameraState.java new file mode 100644 index 000000000000..fca1e0099e40 --- /dev/null +++ b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/CameraState.java @@ -0,0 +1,34 @@ +package io.flutter.plugins.camera; + + +/** + * These are the states that the camera can be in. The camera can only take one photo at a time + * so this state describes the state of the camera itself. The camera works like a pipeline where + * we feed it requests through. It can only process one tasks at a time. + */ +public enum CameraState { + /** + * Idle, showing preview and not capturing anything. + */ + STATE_PREVIEW, + + /** + * Starting and waiting for autofocus to complete. + */ + STATE_WAITING_FOCUS, + + /** + * Start performing autoexposure. + */ + STATE_WAITING_PRECAPTURE, + + /** + * waiting for autoexposure to complete. + */ + STATE_WAITING_PRECAPTURE_READY, + + /** + * Capturing an image. + */ + STATE_CAPTURING, +} diff --git a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/CaptureSessionState.java b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/CaptureSessionState.java deleted file mode 100644 index 9853c706ca34..000000000000 --- a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/CaptureSessionState.java +++ /dev/null @@ -1,11 +0,0 @@ -package io.flutter.plugins.camera; - -public enum CaptureSessionState { - IDLE, // Idle - FOCUSING, // Focusing - STATE_WAITING_PRECAPTURE, // Precapture - STATE_WAITING_NON_PRECAPTURE, // Waiting precapture ready - CAPTURING, // Capturing - FINISHED, // Finished - ERROR, // Error -} diff --git a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/PictureCaptureRequest.java b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/PictureCaptureRequest.java index 99301f961a6b..de60c68d37eb 100644 --- a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/PictureCaptureRequest.java +++ b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/PictureCaptureRequest.java @@ -6,6 +6,7 @@ import android.os.Handler; import android.os.Looper; +import android.os.SystemClock; import androidx.annotation.Nullable; @@ -14,83 +15,173 @@ import io.flutter.Log; import io.flutter.plugin.common.MethodChannel; +/** + * This is where we store the state of the camera. This conveniently + * allows us to handle capture results and send results back to flutter + * so we can handle errors. + *

+ * It also handles a capture timeout so if a capture doesn't happen within + * 5 seconds it will return an error to dart. + */ class PictureCaptureRequest { - private final Runnable timeoutCallback = - new Runnable() { - @Override - public void run() { - error("captureTimeout", "Picture capture request timed out", state.toString()); + /** + * Timeout for the pre-capture sequence. + */ + private static final long PRECAPTURE_TIMEOUT_MS = 1000; + + /** + * This is the output file for the curent capture. The file is created + * in Camera and passed here as reference to it. + */ + final File mFile; + + /** + * Dart method chanel result. + */ + private final MethodChannel.Result result; + + /** + * Timeout handler. + */ + private final TimeoutHandler timeoutHandler; + + /** + * The time that the most recent capture started at. Used to check if + * the current capture request has timed out. + */ + private long preCaptureStartTime; + + /** + * The state of this picture capture request. + */ + private PictureCaptureRequestState state = PictureCaptureRequestState.STATE_IDLE; + private final Runnable timeoutCallback = + () -> error("captureTimeout", "Picture capture request timed out", state.toString()); + + + /** + * Constructor to create a picture capture request. + * + * @param result + * @param mFile + */ + public PictureCaptureRequest(MethodChannel.Result result, File mFile) { + this.result = result; + this.timeoutHandler = new TimeoutHandler(); + this.mFile = mFile; + } + + /** + * Return the current state of this picture capture request. + * + * @return + */ + public PictureCaptureRequestState getState() { + return state; + } + + /** + * Set the picture capture request to a new state. + * + * @param newState + */ + public void setState(PictureCaptureRequestState newState) { + Log.i("Camera", "PictureCaptureRequest setState: " + newState); + + // Once a request is finished, that's it for its lifecycle. + if (state == PictureCaptureRequestState.STATE_FINISHED) { + throw new IllegalStateException("Request has already been finished"); } - }; - - private final MethodChannel.Result result; - private final TimeoutHandler timeoutHandler; - private CaptureSessionState state = CaptureSessionState.IDLE; - - /** - * This is the output file for our picture. - */ - File mFile; - - public PictureCaptureRequest(MethodChannel.Result result) { - this(result, new TimeoutHandler()); - } - - public PictureCaptureRequest(MethodChannel.Result result, TimeoutHandler timeoutHandler) { - this.result = result; - this.timeoutHandler = timeoutHandler; - } - - public void setState(CaptureSessionState state) { - if (isFinished()) throw new IllegalStateException("Request has already been finished"); - this.state = state; - if (state != CaptureSessionState.IDLE && state != CaptureSessionState.FINISHED && state != CaptureSessionState.ERROR) { - this.timeoutHandler.resetTimeout(timeoutCallback); - } else { - this.timeoutHandler.clearTimeout(timeoutCallback); + + final PictureCaptureRequestState oldState = state; + state = newState; + onStateChange(oldState); + } + + public boolean isFinished() { + return state == PictureCaptureRequestState.STATE_FINISHED; + } + + /** + * Send the picture result back to Flutter. Returns the image path. + * + * @param absolutePath + */ + public void finish(String absolutePath) { + setState(PictureCaptureRequestState.STATE_FINISHED); + Log.i("Camera", "PictureCaptureRequest finish"); + result.success(absolutePath); + } + + public void error( + String errorCode, @Nullable String errorMessage, + @Nullable Object errorDetails) { + setState(PictureCaptureRequestState.STATE_ERROR); + result.error(errorCode, errorMessage, errorDetails); } - } - - public CaptureSessionState getState() { - return state; - } - - public boolean isFinished() { - return state == CaptureSessionState.FINISHED || state == CaptureSessionState.ERROR; - } - - public void finish(String absolutePath) { - if (isFinished()) throw new IllegalStateException("Request has already been finished"); - Log.i("Camera", "PictureCaptureRequest finish"); - this.timeoutHandler.clearTimeout(timeoutCallback); - result.success(absolutePath); - setState(CaptureSessionState.FINISHED); - } - - public void error( - String errorCode, @Nullable String errorMessage, @Nullable Object errorDetails) { - if (isFinished()) throw new IllegalStateException("Request has already been finished"); - this.timeoutHandler.clearTimeout(timeoutCallback); - result.error(errorCode, errorMessage, errorDetails); - setState(CaptureSessionState.ERROR); - } - - static class TimeoutHandler { - private static final int REQUEST_TIMEOUT = 5000; - private final Handler handler; - - TimeoutHandler() { - this.handler = new Handler(Looper.getMainLooper()); + + /** + * Check if the timeout for the pre-capture sequence has been reached. + * + * @return true if the timeout is reached; otherwise false is returned. + */ + public boolean hitPreCaptureTimeout() { + return (SystemClock.elapsedRealtime() - preCaptureStartTime) > PRECAPTURE_TIMEOUT_MS; + } + + /** + * Sets the time the pre-capture sequence started. + */ + public void setPreCaptureStartTime() { + preCaptureStartTime = SystemClock.elapsedRealtime(); } - public void resetTimeout(Runnable runnable) { - clearTimeout(runnable); - handler.postDelayed(runnable, REQUEST_TIMEOUT); + /** + * Handle new state changes. + */ + private void onStateChange(PictureCaptureRequestState oldState) { + switch (state) { + case STATE_IDLE: + // Nothing to do in idle state. + break; + + case STATE_CAPTURING: + // Started an image capture. + timeoutHandler.resetTimeout(timeoutCallback); + break; + + case STATE_FINISHED: + timeoutHandler.clearTimeout(timeoutCallback); + break; + + case STATE_ERROR: + timeoutHandler.clearTimeout(timeoutCallback); + setState(PictureCaptureRequestState.STATE_FINISHED); + break; + + } } - public void clearTimeout(Runnable runnable) { - handler.removeCallbacks(runnable); + /** + * This handles the timeout for capture requests so they return within a + * reasonable amount of time. + */ + static class TimeoutHandler { + private static final int REQUEST_TIMEOUT = 5000; + private final Handler handler; + + TimeoutHandler() { + this.handler = new Handler(Looper.getMainLooper()); + } + + public void resetTimeout(Runnable runnable) { + clearTimeout(runnable); + handler.postDelayed(runnable, REQUEST_TIMEOUT); + } + + public void clearTimeout(Runnable runnable) { + handler.removeCallbacks(runnable); + } } - } } diff --git a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/PictureCaptureRequestState.java b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/PictureCaptureRequestState.java new file mode 100644 index 000000000000..fb8c2c7503bd --- /dev/null +++ b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/PictureCaptureRequestState.java @@ -0,0 +1,33 @@ +package io.flutter.plugins.camera; + +/** + * This describes the state of the current picture capture request. + * This is different from the camera state because this simply says + * whether or not the current capture is finished and if there was + * an error. + * + * We have to separate this state because a picture capture request + * only exists in the context of a dart call where we have a result + * to return. + */ +public enum PictureCaptureRequestState { + /** + * Not doing anything yet. + */ + STATE_IDLE, + + /** + * Picture is being captured. + */ + STATE_CAPTURING, + + /** + * Picture capture is finished. + */ + STATE_FINISHED, + + /** + * An error occurred. + */ + STATE_ERROR, +} diff --git a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/types/FlashMode.java b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/types/FlashMode.java index c4f0998c418a..69db79859b80 100644 --- a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/types/FlashMode.java +++ b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/types/FlashMode.java @@ -4,6 +4,16 @@ package io.flutter.plugins.camera.types; +import android.hardware.camera2.CameraCaptureSession; +import android.hardware.camera2.CameraCharacteristics; +import android.hardware.camera2.CaptureFailure; +import android.hardware.camera2.CaptureRequest; +import android.hardware.camera2.TotalCaptureResult; + +import androidx.annotation.NonNull; + +import io.flutter.plugin.common.MethodChannel; + // Mirrors flash_mode.dart public enum FlashMode { off("off"), @@ -28,4 +38,4 @@ public static FlashMode getValueForString(String modeStr) { public String toString() { return strValue; } -} +} \ No newline at end of file From a698fe8e2c48a7f60fb38668dfac1968b7e863fe Mon Sep 17 00:00:00 2001 From: Andrew Coutts Date: Mon, 1 Mar 2021 06:36:00 -0500 Subject: [PATCH 003/114] Can now capture repeated images with no AF but with AE working --- .../io/flutter/plugins/camera/Camera.java | 2417 +++++++++-------- .../flutter/plugins/camera/CameraState.java | 4 +- .../plugins/camera/PictureCaptureRequest.java | 1 + 3 files changed, 1225 insertions(+), 1197 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 debd0d77fff8..9058c1112235 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 @@ -34,7 +34,6 @@ import android.os.Build.VERSION_CODES; import android.os.Handler; import android.os.HandlerThread; -import android.os.SystemClock; import android.util.Log; import android.util.Range; import android.util.Rational; @@ -47,7 +46,6 @@ import java.io.File; import java.io.IOException; -import java.lang.reflect.Field; import java.nio.ByteBuffer; import java.util.ArrayList; import java.util.Arrays; @@ -71,1333 +69,1362 @@ @FunctionalInterface interface ErrorCallback { - void onError(String errorCode, String errorMessage); + void onError(String errorCode, String errorMessage); } public class Camera { - private static final String TAG = "Camera"; - - - /** - * Conversion from screen rotation to JPEG orientation. - */ - private static final SparseIntArray ORIENTATIONS = new SparseIntArray(); - private static final HashMap supportedImageFormats; - - static { - ORIENTATIONS.append(Surface.ROTATION_0, 90); - ORIENTATIONS.append(Surface.ROTATION_90, 0); - ORIENTATIONS.append(Surface.ROTATION_180, 270); - ORIENTATIONS.append(Surface.ROTATION_270, 180); - } - - // Current supported outputs - static { - supportedImageFormats = new HashMap<>(); - supportedImageFormats.put("yuv420", 35); - supportedImageFormats.put("jpeg", 256); - } - - private final SurfaceTextureEntry flutterTexture; - private final CameraManager cameraManager; - private final DeviceOrientationManager deviceOrientationListener; - private final boolean isFrontFacing; - private final int sensorOrientation; - private final String cameraName; - private final Size captureSize; - private final Size previewSize; - private final boolean enableAudio; - private final Context applicationContext; - private final CamcorderProfile recordingProfile; - private final DartMessenger dartMessenger; - private final CameraZoom cameraZoom; - private final CameraCharacteristics cameraCharacteristics; - private final Activity activity; - -/** - * The state of the camera. By default we are in the preview state. - */ - private CameraState cameraState = CameraState.STATE_PREVIEW; - - /** - * A {@link Handler} for running tasks in the background. - */ - private Handler mBackgroundHandler; - - /** - * An additional thread for running tasks that shouldn't block the UI. - */ - private HandlerThread mBackgroundThread; - - private CameraDevice cameraDevice; - private CameraCaptureSession mPreviewSession; - private ImageReader pictureImageReader; - private ImageReader imageStreamReader; - - /** - * {@link CaptureRequest.Builder} for the camera preview - */ - private CaptureRequest.Builder mPreviewRequestBuilder; - - /** - * {@link CaptureRequest} generated by {@link #mPreviewRequestBuilder} - */ - private CaptureRequest mPreviewRequest; - - private MediaRecorder mediaRecorder; - private boolean recordingVideo; - private File videoRecordingFile; - - /** - * Flash mode setting of the current camera. Initialize to off because - * we don't know if the current camera supports flash yet. - */ - private FlashMode currentFlashMode = FlashMode.off; - - /** - * Exposure mode setting of the current camera. Initialize to auto - * because all cameras support autoexposure by default. - */ - private ExposureMode exposureMode = ExposureMode.auto; - - /** - * Focus mode setting of the current camera. Initialize to locked because - * we don't know if the current camera supports autofocus yet. - */ - private FocusMode currentFocusMode = FocusMode.locked; - - /** - * Whether the current camera device supports auto focus or not. - */ - private boolean mAutoFocusSupported = false; - - /** - * Whether or not to use autofocus. - */ - private boolean useAutoFocus = false; - - /** - * Whether the current camera device supports Flash or not. - */ - private boolean mFlashSupported = false; - - /** - * This manages the state of the camera and the current capture request. - */ - PictureCaptureRequest pictureCaptureRequest; - - /** - * This a callback object for the {@link ImageReader}. "onImageAvailable" will be called when a - * still image is ready to be saved. - */ - private final ImageReader.OnImageAvailableListener mOnImageAvailableListener - = new ImageReader.OnImageAvailableListener() { - - @Override - public void onImageAvailable(ImageReader reader) { - Log.i(TAG, "onImageAvailable"); - mBackgroundHandler.post(new ImageSaver(reader.acquireNextImage(), pictureCaptureRequest.mFile, pictureCaptureRequest)); + private static final String TAG = "Camera"; + + + /** + * Conversion from screen rotation to JPEG orientation. + */ + private static final SparseIntArray ORIENTATIONS = new SparseIntArray(); + private static final HashMap supportedImageFormats; + + static { + ORIENTATIONS.append(Surface.ROTATION_0, 90); + ORIENTATIONS.append(Surface.ROTATION_90, 0); + ORIENTATIONS.append(Surface.ROTATION_180, 270); + ORIENTATIONS.append(Surface.ROTATION_270, 180); } - }; - private CameraRegions cameraRegions; - private int exposureOffset; + // Current supported outputs + static { + supportedImageFormats = new HashMap<>(); + supportedImageFormats.put("yuv420", 35); + supportedImageFormats.put("jpeg", 256); + } - private Range fpsRange; - private PlatformChannel.DeviceOrientation lockedCaptureOrientation; + private final SurfaceTextureEntry flutterTexture; + private final CameraManager cameraManager; + private final DeviceOrientationManager deviceOrientationListener; + private final boolean isFrontFacing; + private final int sensorOrientation; + private final String cameraName; + private final Size captureSize; + private final Size previewSize; + private final boolean enableAudio; + private final Context applicationContext; + private final CamcorderProfile recordingProfile; + private final DartMessenger dartMessenger; + private final CameraZoom cameraZoom; + private final CameraCharacteristics cameraCharacteristics; + private final Activity activity; + + /** + * The state of the camera. By default we are in the preview state. + */ + private CameraState cameraState = CameraState.STATE_PREVIEW; + + /** + * A {@link Handler} for running tasks in the background. + */ + private Handler mBackgroundHandler; + + /** + * An additional thread for running tasks that shouldn't block the UI. + */ + private HandlerThread mBackgroundThread; + + private CameraDevice cameraDevice; + private CameraCaptureSession mPreviewSession; + private ImageReader pictureImageReader; + private ImageReader imageStreamReader; + + /** + * {@link CaptureRequest.Builder} for the camera preview + */ + private CaptureRequest.Builder mPreviewRequestBuilder; + + /** + * {@link CaptureRequest} generated by {@link #mPreviewRequestBuilder} + */ + private CaptureRequest mPreviewRequest; + + private MediaRecorder mediaRecorder; + private boolean recordingVideo; + private File videoRecordingFile; + + /** + * Flash mode setting of the current camera. Initialize to off because + * we don't know if the current camera supports flash yet. + */ + private FlashMode currentFlashMode = FlashMode.off; + + /** + * Exposure mode setting of the current camera. Initialize to auto + * because all cameras support autoexposure by default. + */ + private ExposureMode exposureMode = ExposureMode.auto; + + /** + * Focus mode setting of the current camera. Initialize to locked because + * we don't know if the current camera supports autofocus yet. + */ + private FocusMode currentFocusMode = FocusMode.locked; + + /** + * Whether the current camera device supports auto focus or not. + */ + private boolean mAutoFocusSupported = false; + + /** + * Whether or not to use autofocus. + */ + private boolean useAutoFocus = false; + + /** + * Whether the current camera device supports Flash or not. + */ + private boolean mFlashSupported = false; + + /** + * This manages the state of the camera and the current capture request. + */ + PictureCaptureRequest pictureCaptureRequest; + + /** + * This a callback object for the {@link ImageReader}. "onImageAvailable" will be called when a + * still image is ready to be saved. + */ + private final ImageReader.OnImageAvailableListener mOnImageAvailableListener + = new ImageReader.OnImageAvailableListener() { + + @Override + public void onImageAvailable(ImageReader reader) { + Log.i(TAG, "onImageAvailable"); + mBackgroundHandler.post(new ImageSaver(reader.acquireNextImage(), pictureCaptureRequest.mFile, pictureCaptureRequest)); + cameraState = CameraState.STATE_PREVIEW; + } + }; + private CameraRegions cameraRegions; + private int exposureOffset; + private Range fpsRange; + private PlatformChannel.DeviceOrientation lockedCaptureOrientation; - public Camera( - final Activity activity, - final SurfaceTextureEntry flutterTexture, - final DartMessenger dartMessenger, - final String cameraName, - final String resolutionPreset, - final boolean enableAudio) - throws CameraAccessException { - Log.i(TAG, "Camear constructor"); - if (activity == null) { - throw new IllegalStateException("No activity available!"); + public Camera( + final Activity activity, + final SurfaceTextureEntry flutterTexture, + final DartMessenger dartMessenger, + final String cameraName, + final String resolutionPreset, + final boolean enableAudio) + throws CameraAccessException { + Log.i(TAG, "Camear constructor"); + + if (activity == null) { + throw new IllegalStateException("No activity available!"); + } + this.activity = activity; + this.cameraName = cameraName; + this.enableAudio = enableAudio; + this.flutterTexture = flutterTexture; + this.dartMessenger = dartMessenger; + this.cameraManager = (CameraManager) activity.getSystemService(Context.CAMERA_SERVICE); + this.applicationContext = activity.getApplicationContext(); + this.currentFlashMode = FlashMode.off; + this.exposureMode = ExposureMode.auto; + this.currentFocusMode = FocusMode.auto; + this.exposureOffset = 0; + + cameraCharacteristics = cameraManager.getCameraCharacteristics(cameraName); + getAvailableFpsRange(cameraCharacteristics); + sensorOrientation = cameraCharacteristics.get(CameraCharacteristics.SENSOR_ORIENTATION); + isFrontFacing = + cameraCharacteristics.get(CameraCharacteristics.LENS_FACING) + == CameraMetadata.LENS_FACING_FRONT; + ResolutionPreset preset = ResolutionPreset.valueOf(resolutionPreset); + recordingProfile = + CameraUtils.getBestAvailableCamcorderProfileForResolutionPreset(cameraName, preset); + captureSize = new Size(recordingProfile.videoFrameWidth, recordingProfile.videoFrameHeight); + previewSize = computeBestPreviewSize(cameraName, preset); + cameraZoom = + new CameraZoom( + cameraCharacteristics.get(CameraCharacteristics.SENSOR_INFO_ACTIVE_ARRAY_SIZE), + cameraCharacteristics.get(CameraCharacteristics.SCALER_AVAILABLE_MAX_DIGITAL_ZOOM)); + + deviceOrientationListener = + new DeviceOrientationManager(activity, dartMessenger, isFrontFacing, sensorOrientation); + deviceOrientationListener.start(); + + // Check if the flash is supported. + Boolean flashAvailable = cameraCharacteristics.get(CameraCharacteristics.FLASH_INFO_AVAILABLE); + mFlashSupported = flashAvailable == null ? false : flashAvailable; + startBackgroundThread(); } - this.activity = activity; - this.cameraName = cameraName; - this.enableAudio = enableAudio; - this.flutterTexture = flutterTexture; - this.dartMessenger = dartMessenger; - this.cameraManager = (CameraManager) activity.getSystemService(Context.CAMERA_SERVICE); - this.applicationContext = activity.getApplicationContext(); - this.currentFlashMode = FlashMode.off; - this.exposureMode = ExposureMode.auto; - this.currentFocusMode = FocusMode.auto; - this.exposureOffset = 0; - - cameraCharacteristics = cameraManager.getCameraCharacteristics(cameraName); - getAvailableFpsRange(cameraCharacteristics); - sensorOrientation = cameraCharacteristics.get(CameraCharacteristics.SENSOR_ORIENTATION); - isFrontFacing = - cameraCharacteristics.get(CameraCharacteristics.LENS_FACING) - == CameraMetadata.LENS_FACING_FRONT; - ResolutionPreset preset = ResolutionPreset.valueOf(resolutionPreset); - recordingProfile = - CameraUtils.getBestAvailableCamcorderProfileForResolutionPreset(cameraName, preset); - captureSize = new Size(recordingProfile.videoFrameWidth, recordingProfile.videoFrameHeight); - previewSize = computeBestPreviewSize(cameraName, preset); - cameraZoom = - new CameraZoom( - cameraCharacteristics.get(CameraCharacteristics.SENSOR_INFO_ACTIVE_ARRAY_SIZE), - cameraCharacteristics.get(CameraCharacteristics.SCALER_AVAILABLE_MAX_DIGITAL_ZOOM)); - - deviceOrientationListener = - new DeviceOrientationManager(activity, dartMessenger, isFrontFacing, sensorOrientation); - deviceOrientationListener.start(); - - // Check if the flash is supported. - Boolean flashAvailable = cameraCharacteristics.get(CameraCharacteristics.FLASH_INFO_AVAILABLE); - mFlashSupported = flashAvailable == null ? false : flashAvailable; - startBackgroundThread(); - } - - /** - * Load available FPS range for the current camera and update the available fps range with it. - * @param cameraCharacteristics - */ - private void getAvailableFpsRange(CameraCharacteristics cameraCharacteristics) { - Log.i(TAG, "getAvailableFpsRange"); - - try { - Range[] ranges = - cameraCharacteristics.get(CameraCharacteristics.CONTROL_AE_AVAILABLE_TARGET_FPS_RANGES); - if (ranges != null) { - for (Range range : ranges) { - int upper = range.getUpper(); - Log.i("Camera", "[FPS Range Available] is:" + range); - if (upper >= 10) { - if (fpsRange == null || upper > fpsRange.getUpper()) { - fpsRange = range; + + /** + * Load available FPS range for the current camera and update the available fps range with it. + * + * @param cameraCharacteristics + */ + private void getAvailableFpsRange(CameraCharacteristics cameraCharacteristics) { + Log.i(TAG, "getAvailableFpsRange"); + + try { + Range[] ranges = + cameraCharacteristics.get(CameraCharacteristics.CONTROL_AE_AVAILABLE_TARGET_FPS_RANGES); + if (ranges != null) { + for (Range range : ranges) { + int upper = range.getUpper(); + Log.i("Camera", "[FPS Range Available] is:" + range); + if (upper >= 10) { + if (fpsRange == null || upper > fpsRange.getUpper()) { + fpsRange = range; + } + } + } } - } + } catch (Exception e) { + pictureCaptureRequest.error("cameraAccess", e.getMessage(), null); } - } - } catch (Exception e) { - pictureCaptureRequest.error("cameraAccess", e.getMessage(), null); + Log.i("Camera", "[FPS Range] is:" + fpsRange); } - Log.i("Camera", "[FPS Range] is:" + fpsRange); - } - private void prepareMediaRecorder(String outputFilePath) throws IOException { - Log.i(TAG, "prepareMediaRecorder"); + private void prepareMediaRecorder(String outputFilePath) throws IOException { + Log.i(TAG, "prepareMediaRecorder"); - if (mediaRecorder != null) { - mediaRecorder.release(); - } + if (mediaRecorder != null) { + mediaRecorder.release(); + } - mediaRecorder = - new MediaRecorderBuilder(recordingProfile, outputFilePath) - .setEnableAudio(enableAudio) - .setMediaOrientation( - lockedCaptureOrientation == null - ? deviceOrientationListener.getMediaOrientation() - : deviceOrientationListener.getMediaOrientation(lockedCaptureOrientation)) - .build(); - } - - @SuppressLint("MissingPermission") - public void open(String imageFormatGroup) throws CameraAccessException { - pictureImageReader = - ImageReader.newInstance( - captureSize.getWidth(), captureSize.getHeight(), ImageFormat.JPEG, 2); - - Integer imageFormat = supportedImageFormats.get(imageFormatGroup); - if (imageFormat == null) { - Log.w(TAG, "The selected imageFormatGroup is not supported by Android. Defaulting to yuv420"); - imageFormat = ImageFormat.YUV_420_888; + mediaRecorder = + new MediaRecorderBuilder(recordingProfile, outputFilePath) + .setEnableAudio(enableAudio) + .setMediaOrientation( + lockedCaptureOrientation == null + ? deviceOrientationListener.getMediaOrientation() + : deviceOrientationListener.getMediaOrientation(lockedCaptureOrientation)) + .build(); } - // Used to steam image byte data to dart side. - imageStreamReader = - ImageReader.newInstance(previewSize.getWidth(), previewSize.getHeight(), imageFormat, 2); - - cameraManager.openCamera( - cameraName, - new CameraDevice.StateCallback() { - @Override - public void onOpened(@NonNull CameraDevice device) { - Log.i(TAG, "open | onOpened"); - - - cameraDevice = device; - try { - startPreview(); - dartMessenger.sendCameraInitializedEvent( - previewSize.getWidth(), - previewSize.getHeight(), - exposureMode, - currentFocusMode, - isExposurePointSupported(), - isFocusPointSupported()); - } catch (CameraAccessException e) { - dartMessenger.sendCameraErrorEvent(e.getMessage()); - close(); - } - } + @SuppressLint("MissingPermission") + public void open(String imageFormatGroup) throws CameraAccessException { + pictureImageReader = + ImageReader.newInstance( + captureSize.getWidth(), captureSize.getHeight(), ImageFormat.JPEG, 2); - @Override - public void onClosed(@NonNull CameraDevice camera) { - Log.i(TAG, "open | onClosed"); + Integer imageFormat = supportedImageFormats.get(imageFormatGroup); + if (imageFormat == null) { + Log.w(TAG, "The selected imageFormatGroup is not supported by Android. Defaulting to yuv420"); + imageFormat = ImageFormat.YUV_420_888; + } - dartMessenger.sendCameraClosingEvent(); - super.onClosed(camera); - } + // Used to steam image byte data to dart side. + imageStreamReader = + ImageReader.newInstance(previewSize.getWidth(), previewSize.getHeight(), imageFormat, 2); + + cameraManager.openCamera( + cameraName, + new CameraDevice.StateCallback() { + @Override + public void onOpened(@NonNull CameraDevice device) { + Log.i(TAG, "open | onOpened"); + + + cameraDevice = device; + try { + startPreview(); + dartMessenger.sendCameraInitializedEvent( + previewSize.getWidth(), + previewSize.getHeight(), + exposureMode, + currentFocusMode, + isExposurePointSupported(), + isFocusPointSupported()); + } catch (CameraAccessException e) { + dartMessenger.sendCameraErrorEvent(e.getMessage()); + close(); + } + } + + @Override + public void onClosed(@NonNull CameraDevice camera) { + Log.i(TAG, "open | onClosed"); + + dartMessenger.sendCameraClosingEvent(); + super.onClosed(camera); + } + + @Override + public void onDisconnected(@NonNull CameraDevice cameraDevice) { + Log.i(TAG, "open | onDisconnected"); + + close(); + dartMessenger.sendCameraErrorEvent("The camera was disconnected."); + } + + @Override + public void onError(@NonNull CameraDevice cameraDevice, int errorCode) { + Log.i(TAG, "open | onError"); + + close(); + String errorDescription; + switch (errorCode) { + case ERROR_CAMERA_IN_USE: + errorDescription = "The camera device is in use already."; + break; + case ERROR_MAX_CAMERAS_IN_USE: + errorDescription = "Max cameras in use"; + break; + case ERROR_CAMERA_DISABLED: + errorDescription = "The camera device could not be opened due to a device policy."; + break; + case ERROR_CAMERA_DEVICE: + errorDescription = "The camera device has encountered a fatal error"; + break; + case ERROR_CAMERA_SERVICE: + errorDescription = "The camera service has encountered a fatal error."; + break; + default: + errorDescription = "Unknown camera error"; + } + dartMessenger.sendCameraErrorEvent(errorDescription); + } + }, + null); + } - @Override - public void onDisconnected(@NonNull CameraDevice cameraDevice) { - Log.i(TAG, "open | onDisconnected"); + private void createCaptureSession(int templateType, Surface... surfaces) + throws CameraAccessException { + createCaptureSession(templateType, null, surfaces); + } - close(); - dartMessenger.sendCameraErrorEvent("The camera was disconnected."); - } + private void createCaptureSession( + int templateType, Runnable onSuccessCallback, Surface... surfaces) + throws CameraAccessException { + Log.i(TAG, "createCaptureSession"); - @Override - public void onError(@NonNull CameraDevice cameraDevice, int errorCode) { - Log.i(TAG, "open | onError"); + // Close any existing capture session. + closeCaptureSession(); - close(); - String errorDescription; - switch (errorCode) { - case ERROR_CAMERA_IN_USE: - errorDescription = "The camera device is in use already."; - break; - case ERROR_MAX_CAMERAS_IN_USE: - errorDescription = "Max cameras in use"; - break; - case ERROR_CAMERA_DISABLED: - errorDescription = "The camera device could not be opened due to a device policy."; - break; - case ERROR_CAMERA_DEVICE: - errorDescription = "The camera device has encountered a fatal error"; - break; - case ERROR_CAMERA_SERVICE: - errorDescription = "The camera service has encountered a fatal error."; - break; - default: - errorDescription = "Unknown camera error"; - } - dartMessenger.sendCameraErrorEvent(errorDescription); - } - }, - null); - } - - private void createCaptureSession(int templateType, Surface... surfaces) - throws CameraAccessException { - createCaptureSession(templateType, null, surfaces); - } - - private void createCaptureSession( - int templateType, Runnable onSuccessCallback, Surface... surfaces) - throws CameraAccessException { - Log.i(TAG, "createCaptureSession"); - - // Close any existing capture session. - closeCaptureSession(); - - // Create a new capture builder. - mPreviewRequestBuilder = cameraDevice.createCaptureRequest(templateType); - - // Build Flutter surface to render to - SurfaceTexture surfaceTexture = flutterTexture.surfaceTexture(); - surfaceTexture.setDefaultBufferSize(previewSize.getWidth(), previewSize.getHeight()); - Surface flutterSurface = new Surface(surfaceTexture); - mPreviewRequestBuilder.addTarget(flutterSurface); - - List remainingSurfaces = Arrays.asList(surfaces); - if (templateType != CameraDevice.TEMPLATE_PREVIEW) { - // If it is not preview mode, add all surfaces as targets. - for (Surface surface : remainingSurfaces) { - mPreviewRequestBuilder.addTarget(surface); - } - } + // Create a new capture builder. + mPreviewRequestBuilder = cameraDevice.createCaptureRequest(templateType); - cameraRegions = new CameraRegions(getRegionBoundaries()); - - // Prepare the callback - CameraCaptureSession.StateCallback callback = - new CameraCaptureSession.StateCallback() { - @Override - public void onConfigured(@NonNull CameraCaptureSession session) { - // Camera was already closed. - if (cameraDevice == null) { - dartMessenger.sendCameraErrorEvent("The camera was closed during configuration."); - return; - } - mPreviewSession = session; + // Build Flutter surface to render to + SurfaceTexture surfaceTexture = flutterTexture.surfaceTexture(); + surfaceTexture.setDefaultBufferSize(previewSize.getWidth(), previewSize.getHeight()); + Surface flutterSurface = new Surface(surfaceTexture); + mPreviewRequestBuilder.addTarget(flutterSurface); - updateFpsRange(); - updateFocus(mPreviewRequestBuilder); - updateFlash(mPreviewRequestBuilder); - updateExposure(exposureMode); - - refreshPreviewCaptureSession( - onSuccessCallback, (code, message) -> dartMessenger.sendCameraErrorEvent(message)); - } + List remainingSurfaces = Arrays.asList(surfaces); + if (templateType != CameraDevice.TEMPLATE_PREVIEW) { + // If it is not preview mode, add all surfaces as targets. + for (Surface surface : remainingSurfaces) { + mPreviewRequestBuilder.addTarget(surface); + } + } - @Override - public void onConfigureFailed(@NonNull CameraCaptureSession cameraCaptureSession) { - dartMessenger.sendCameraErrorEvent("Failed to configure camera session."); - } - }; + cameraRegions = new CameraRegions(getRegionBoundaries()); + + // Prepare the callback + CameraCaptureSession.StateCallback callback = + new CameraCaptureSession.StateCallback() { + @Override + public void onConfigured(@NonNull CameraCaptureSession session) { + // Camera was already closed. + if (cameraDevice == null) { + dartMessenger.sendCameraErrorEvent("The camera was closed during configuration."); + return; + } + mPreviewSession = session; + + updateFpsRange(); + updateFocus(mPreviewRequestBuilder); + updateFlash(mPreviewRequestBuilder); + updateExposureMode(mPreviewRequestBuilder); + + refreshPreviewCaptureSession( + onSuccessCallback, (code, message) -> dartMessenger.sendCameraErrorEvent(message)); + } + + @Override + public void onConfigureFailed(@NonNull CameraCaptureSession cameraCaptureSession) { + dartMessenger.sendCameraErrorEvent("Failed to configure camera session."); + } + }; + + // Start the session + if (VERSION.SDK_INT >= VERSION_CODES.P) { + // Collect all surfaces we want to render to. + List configs = new ArrayList<>(); + configs.add(new OutputConfiguration(flutterSurface)); + for (Surface surface : remainingSurfaces) { + configs.add(new OutputConfiguration(surface)); + } + createCaptureSessionWithSessionConfig(configs, callback); + } else { + // Collect all surfaces we want to render to. + List surfaceList = new ArrayList<>(); + surfaceList.add(flutterSurface); + surfaceList.addAll(remainingSurfaces); + createCaptureSession(surfaceList, callback); + } + } - // Start the session - if (VERSION.SDK_INT >= VERSION_CODES.P) { - // Collect all surfaces we want to render to. - List configs = new ArrayList<>(); - configs.add(new OutputConfiguration(flutterSurface)); - for (Surface surface : remainingSurfaces) { - configs.add(new OutputConfiguration(surface)); - } - createCaptureSessionWithSessionConfig(configs, callback); - } else { - // Collect all surfaces we want to render to. - List surfaceList = new ArrayList<>(); - surfaceList.add(flutterSurface); - surfaceList.addAll(remainingSurfaces); - createCaptureSession(surfaceList, callback); + @TargetApi(VERSION_CODES.P) + private void createCaptureSessionWithSessionConfig( + List outputConfigs, CameraCaptureSession.StateCallback callback) + throws CameraAccessException { + cameraDevice.createCaptureSession( + new SessionConfiguration( + SessionConfiguration.SESSION_REGULAR, + outputConfigs, + Executors.newSingleThreadExecutor(), + callback)); } - } - - @TargetApi(VERSION_CODES.P) - private void createCaptureSessionWithSessionConfig( - List outputConfigs, CameraCaptureSession.StateCallback callback) - throws CameraAccessException { - cameraDevice.createCaptureSession( - new SessionConfiguration( - SessionConfiguration.SESSION_REGULAR, - outputConfigs, - Executors.newSingleThreadExecutor(), - callback)); - } - - @TargetApi(VERSION_CODES.LOLLIPOP) - @SuppressWarnings("deprecation") - private void createCaptureSession( - List surfaces, CameraCaptureSession.StateCallback callback) - throws CameraAccessException { - cameraDevice.createCaptureSession(surfaces, callback, null); - } - - private void refreshPreviewCaptureSession( - @Nullable Runnable onSuccessCallback, @NonNull ErrorCallback onErrorCallback) { - Log.i(TAG, "refreshPreviewCaptureSession"); - if (mPreviewSession == null) { - Log.i(TAG, "[refreshPreviewCaptureSession] mPreviewSession null, returning"); - return; + + @TargetApi(VERSION_CODES.LOLLIPOP) + @SuppressWarnings("deprecation") + private void createCaptureSession( + List surfaces, CameraCaptureSession.StateCallback callback) + throws CameraAccessException { + cameraDevice.createCaptureSession(surfaces, callback, null); } - try { - mPreviewRequest = mPreviewRequestBuilder.build(); - mPreviewSession.setRepeatingRequest(mPreviewRequest, - mCaptureCallback, mBackgroundHandler); + private void refreshPreviewCaptureSession( + @Nullable Runnable onSuccessCallback, @NonNull ErrorCallback onErrorCallback) { + Log.i(TAG, "refreshPreviewCaptureSession"); + if (mPreviewSession == null) { + Log.i(TAG, "[refreshPreviewCaptureSession] mPreviewSession null, returning"); + return; + } - // Put camera back to preview mode - cameraState = CameraState.STATE_PREVIEW; + try { +// mPreviewRequestBuilder.set(CaptureRequest.CONTROL_AE_PRECAPTURE_TRIGGER, +// CaptureRequest.CONTROL_AE_PRECAPTURE_TRIGGER_START); + mPreviewSession.setRepeatingRequest(mPreviewRequestBuilder.build(), + mCaptureCallback, mBackgroundHandler); - if (onSuccessCallback != null) { - onSuccessCallback.run(); - } - } catch (CameraAccessException | IllegalStateException | IllegalArgumentException e) { - onErrorCallback.onError("cameraAccess", e.getMessage()); + if (onSuccessCallback != null) { + onSuccessCallback.run(); + } + } catch (CameraAccessException | IllegalStateException | IllegalArgumentException e) { + onErrorCallback.onError("cameraAccess", e.getMessage()); + } } - } - public void takePicture(@NonNull final Result result) { - Log.i(TAG, "takePicture | useAutoFocus: " + useAutoFocus); + public void takePicture(@NonNull final Result result) { + Log.i(TAG, "takePicture | useAutoFocus: " + useAutoFocus); - // Only take one 1 picture at a time. - if (pictureCaptureRequest != null && !pictureCaptureRequest.isFinished()) { - result.error("captureAlreadyActive", "Picture is currently already being captured", null); - return; - } + // Only take one 1 picture at a time. + if (pictureCaptureRequest != null && !pictureCaptureRequest.isFinished()) { + result.error("captureAlreadyActive", "Picture is currently already being captured", null); + return; + } - // Create temporary file - final File outputDir = applicationContext.getCacheDir(); - try { - final File file = File.createTempFile("CAP", ".jpg", outputDir); + // Create temporary file + final File outputDir = applicationContext.getCacheDir(); + try { + final File file = File.createTempFile("CAP", ".jpg", outputDir); - // Start a new capture - pictureCaptureRequest = new PictureCaptureRequest(result, file); - } catch (IOException | SecurityException e) { - pictureCaptureRequest.error("cannotCreateFile", e.getMessage(), null); - return; - } + // Start a new capture + pictureCaptureRequest = new PictureCaptureRequest(result, file); + } catch (IOException | SecurityException e) { + pictureCaptureRequest.error("cannotCreateFile", e.getMessage(), null); + return; + } - // Listen for picture being taken - pictureImageReader.setOnImageAvailableListener(mOnImageAvailableListener, mBackgroundHandler); + // Listen for picture being taken + pictureImageReader.setOnImageAvailableListener(mOnImageAvailableListener, mBackgroundHandler); - if (useAutoFocus) { - runPictureAutoFocus(); - } else { - runPrecaptureSequence(); + if (useAutoFocus) { + runPictureAutoFocus(); + } else { + runPrecaptureSequence(); + } } - } - - /** - * Run the precapture sequence for capturing a still image. This method should be called when - * we get a response in {@link #mCaptureCallback} from lockFocus(). - */ - private void runPrecaptureSequence() { - Log.i(TAG, "runPrecaptureSequence"); - try { - // This is how to tell the camera to trigger. - mPreviewRequestBuilder.set(CaptureRequest.CONTROL_AE_PRECAPTURE_TRIGGER, - CaptureRequest.CONTROL_AE_PRECAPTURE_TRIGGER_START); - // Tell #mCaptureCallback to wait for the precapture sequence to be set. - cameraState = CameraState.STATE_WAITING_PRECAPTURE; - mPreviewSession.capture(mPreviewRequestBuilder.build(), mCaptureCallback, - mBackgroundHandler); - } catch (CameraAccessException e) { - pictureCaptureRequest.error("cameraAccess", e.getMessage(), null); + + /** + * Run the precapture sequence for capturing a still image. This method should be called when + * we get a response in {@link #mCaptureCallback} from lockFocus(). + */ + private void runPrecaptureSequence() { + Log.i(TAG, "runPrecaptureSequence"); + try { + +// // First set precapture state to idle or else it can hang in STATE_WAITING_PRECAPTURE +// mPreviewRequestBuilder.set(CaptureRequest.CONTROL_AE_PRECAPTURE_TRIGGER, +// CaptureRequest.CONTROL_AE_PRECAPTURE_TRIGGER_IDLE); +// mPreviewSession.capture(mPreviewRequestBuilder.build(), mCaptureCallback, +// mBackgroundHandler); +// refreshPreviewCaptureSession( +// null, (code, message) -> pictureCaptureRequest.error(code, message, null)); + + // Start precapture now + cameraState = CameraState.STATE_WAITING_PRECAPTURE_START; + + mPreviewRequestBuilder.set(CaptureRequest.CONTROL_AE_PRECAPTURE_TRIGGER, + CaptureRequest.CONTROL_AE_PRECAPTURE_TRIGGER_START); + mPreviewSession.capture(mPreviewRequestBuilder.build(), mCaptureCallback, + mBackgroundHandler); + } catch (CameraAccessException e) { + pictureCaptureRequest.error("cameraAccess", e.getMessage(), null); + } } - } - - /** - * Capture a still picture. This method should be called when we get a response in - * {@link #mCaptureCallback} from both lockFocus(). - */ - private void captureStillPicture() { - Log.i(TAG, "captureStillPicture"); - try { - if (null == cameraDevice) { - return; - } - // This is the CaptureRequest.Builder that we use to take a picture. - final CaptureRequest.Builder captureBuilder = - cameraDevice.createCaptureRequest(CameraDevice.TEMPLATE_STILL_CAPTURE); - captureBuilder.addTarget(pictureImageReader.getSurface()); - - // Zoom - captureBuilder.set( - CaptureRequest.SCALER_CROP_REGION, - mPreviewRequestBuilder.get(CaptureRequest.SCALER_CROP_REGION)); - - // Set focus / flash from preview mode - updateFlash(captureBuilder); - updateFocus(captureBuilder); - - // Orientation - int rotation = activity.getWindowManager().getDefaultDisplay().getRotation(); - captureBuilder.set(CaptureRequest.JPEG_ORIENTATION, getOrientation(rotation)); - - CameraCaptureSession.CaptureCallback CaptureCallback - = new CameraCaptureSession.CaptureCallback() { - @Override - public void onCaptureCompleted(@NonNull CameraCaptureSession session, - @NonNull CaptureRequest request, - @NonNull TotalCaptureResult result) { - unlockAutoFocus(); + /** + * Capture a still picture. This method should be called when we get a response in + * {@link #mCaptureCallback} from both lockFocus(). + */ + private void captureStillPicture() { + Log.i(TAG, "captureStillPicture"); + try { + if (null == cameraDevice) { + return; + } + // This is the CaptureRequest.Builder that we use to take a picture. + final CaptureRequest.Builder captureBuilder = + cameraDevice.createCaptureRequest(CameraDevice.TEMPLATE_STILL_CAPTURE); + captureBuilder.addTarget(pictureImageReader.getSurface()); + + // Zoom + captureBuilder.set( + CaptureRequest.SCALER_CROP_REGION, + mPreviewRequestBuilder.get(CaptureRequest.SCALER_CROP_REGION)); + + // Set focus / flash from preview mode + updateFlash(captureBuilder); + updateFocus(captureBuilder); + updateExposureMode(captureBuilder); + + // Orientation + int rotation = activity.getWindowManager().getDefaultDisplay().getRotation(); + captureBuilder.set(CaptureRequest.JPEG_ORIENTATION, getOrientation(rotation)); + + CameraCaptureSession.CaptureCallback CaptureCallback + = new CameraCaptureSession.CaptureCallback() { + + @Override + public void onCaptureCompleted(@NonNull CameraCaptureSession session, + @NonNull CaptureRequest request, + @NonNull TotalCaptureResult result) { + Log.i(TAG, "onCaptureCompleted"); + unlockAutoFocus(); + } + }; + + mPreviewSession.stopRepeating(); + mPreviewSession.abortCaptures(); + mPreviewSession.capture(captureBuilder.build(), CaptureCallback, null); + } catch (CameraAccessException e) { + pictureCaptureRequest.error("cameraAccess", e.getMessage(), null); } - }; + } - mPreviewSession.stopRepeating(); - mPreviewSession.abortCaptures(); - mPreviewSession.capture(captureBuilder.build(), CaptureCallback, null); - } catch (CameraAccessException e) { - pictureCaptureRequest.error("cameraAccess", e.getMessage(), null); + /** + * Starts a background thread and its {@link Handler}. + * TODO: call when activity resumed + */ + private void startBackgroundThread() { + mBackgroundThread = new HandlerThread("CameraBackground"); + mBackgroundThread.start(); + mBackgroundHandler = new Handler(mBackgroundThread.getLooper()); } - } - - /** - * Starts a background thread and its {@link Handler}. - * TODO: call when activity resumed - */ - private void startBackgroundThread() { - mBackgroundThread = new HandlerThread("CameraBackground"); - mBackgroundThread.start(); - mBackgroundHandler = new Handler(mBackgroundThread.getLooper()); - } - - /** - * Stops the background thread and its {@link Handler}. - * TODO: call when activity paused - */ - private void stopBackgroundThread() { - mBackgroundThread.quitSafely(); - try { - mBackgroundThread.join(); - mBackgroundThread = null; - mBackgroundHandler = null; - } catch (InterruptedException e) { - pictureCaptureRequest.error("cameraAccess", e.getMessage(), null); + + /** + * Stops the background thread and its {@link Handler}. + * TODO: call when activity paused + */ + private void stopBackgroundThread() { + mBackgroundThread.quitSafely(); + try { + mBackgroundThread.join(); + mBackgroundThread = null; + mBackgroundHandler = null; + } catch (InterruptedException e) { + pictureCaptureRequest.error("cameraAccess", e.getMessage(), null); + } } - } + /** + * Sync the requestBuilder exposure mode setting ot the current exposure mode setting of the camera. + */ + void updateExposureMode(CaptureRequest.Builder requestBuilder) { + Log.i(TAG, "updateExposureMode"); + // Applying auto exposure + MeteringRectangle aeRect = cameraRegions.getAEMeteringRectangle(); + mPreviewRequestBuilder.set( + CaptureRequest.CONTROL_AE_REGIONS, + aeRect == null ? null : new MeteringRectangle[]{cameraRegions.getAEMeteringRectangle()}); + + switch (exposureMode) { + case locked: + requestBuilder.set(CaptureRequest.CONTROL_AE_LOCK, true); + break; + case auto: + default: + requestBuilder.set(CaptureRequest.CONTROL_AE_LOCK, false); + break; + } - /** - * Sync the requestBuilder flash setting to the current flash mode setting of the camera. - */ - void updateFlash(CaptureRequest.Builder requestBuilder) { - if (!mFlashSupported) { - return; + // TODO: move this to its own setting (exposure offset) + requestBuilder.set(CaptureRequest.CONTROL_AE_EXPOSURE_COMPENSATION, exposureOffset); } - switch (currentFlashMode) { - case off: - requestBuilder.set(CaptureRequest.CONTROL_AE_MODE, - CaptureRequest.CONTROL_AE_MODE_ON); - requestBuilder.set(CaptureRequest.FLASH_MODE, - CaptureRequest.FLASH_MODE_OFF); - break; - - case always: - requestBuilder.set(CaptureRequest.CONTROL_AE_MODE, - CaptureRequest.CONTROL_AE_MODE_ON_ALWAYS_FLASH); - requestBuilder.set(CaptureRequest.FLASH_MODE, - CaptureRequest.FLASH_MODE_OFF); - break; - - case torch: - requestBuilder.set(CaptureRequest.CONTROL_AE_MODE, - CaptureRequest.CONTROL_AE_MODE_ON); - requestBuilder.set(CaptureRequest.FLASH_MODE, - CaptureRequest.FLASH_MODE_TORCH); - break; - - case auto: - requestBuilder.set(CaptureRequest.CONTROL_AE_MODE, - CaptureRequest.CONTROL_AE_MODE_ON_AUTO_FLASH); - requestBuilder.set(CaptureRequest.FLASH_MODE, - CaptureRequest.FLASH_MODE_OFF); - break; - - // TODO: to be implemented someday. Need to add it to dart and iOS. + /** + * Sync the requestBuilder flash setting to the current flash mode setting of the camera. + */ + void updateFlash(CaptureRequest.Builder requestBuilder) { + Log.i(TAG, "updateFlash"); + + if (!mFlashSupported) { + return; + } + + switch (currentFlashMode) { + case off: + requestBuilder.set(CaptureRequest.CONTROL_AE_MODE, + CaptureRequest.CONTROL_AE_MODE_ON); + requestBuilder.set(CaptureRequest.FLASH_MODE, + CaptureRequest.FLASH_MODE_OFF); + break; + + case always: + requestBuilder.set(CaptureRequest.CONTROL_AE_MODE, + CaptureRequest.CONTROL_AE_MODE_ON_ALWAYS_FLASH); + requestBuilder.set(CaptureRequest.FLASH_MODE, + CaptureRequest.FLASH_MODE_OFF); + break; + + case torch: + requestBuilder.set(CaptureRequest.CONTROL_AE_MODE, + CaptureRequest.CONTROL_AE_MODE_ON); + requestBuilder.set(CaptureRequest.FLASH_MODE, + CaptureRequest.FLASH_MODE_TORCH); + break; + + case auto: + requestBuilder.set(CaptureRequest.CONTROL_AE_MODE, + CaptureRequest.CONTROL_AE_MODE_ON_AUTO_FLASH); + requestBuilder.set(CaptureRequest.FLASH_MODE, + CaptureRequest.FLASH_MODE_OFF); + break; + + // TODO: to be implemented someday. Need to add it to dart and iOS. // case autoRedEye: // requestBuilder.set(CaptureRequest.CONTROL_AE_MODE, // CaptureRequest.CONTROL_AE_MODE_ON_AUTO_FLASH_REDEYE); // requestBuilder.set(CaptureRequest.FLASH_MODE, // CaptureRequest.FLASH_MODE_OFF); // break; - } - } - - /** - * Retrieves the JPEG orientation from the specified screen rotation. - * - * @param rotation The screen rotation. - * @return The JPEG orientation (one of 0, 90, 270, and 360) - */ - private int getOrientation(int rotation) { - // Sensor orientation is 90 for most devices, or 270 for some devices (eg. Nexus 5X) - // We have to take that into account and rotate JPEG properly. - // For devices with orientation of 90, we simply return our mapping from ORIENTATIONS. - // For devices with orientation of 270, we need to rotate the JPEG 180 degrees. - return (ORIENTATIONS.get(rotation) + sensorOrientation + 270) % 360; - } - - /** - * A {@link CameraCaptureSession.CaptureCallback} that handles events related to JPEG capture. - */ - private final CameraCaptureSession.CaptureCallback mCaptureCallback - = new CameraCaptureSession.CaptureCallback() { - - private void process(CaptureResult result) { - if (pictureCaptureRequest == null) { - return; - } - - Integer aeState = result.get(CaptureResult.CONTROL_AE_STATE); - Integer afState = result.get(CaptureResult.CONTROL_AF_STATE); - - if (cameraState != CameraState.STATE_PREVIEW) { - Log.i(TAG, "mCaptureCallback | state: " + cameraState + " | afState: " + afState + " | aeState: " + aeState); - } - - switch (cameraState) { - case STATE_PREVIEW: { - // We have nothing to do when the camera preview is working normally. - break; } + } - case STATE_WAITING_FOCUS: { - if (afState == null) { - cameraState = CameraState.STATE_CAPTURING; - captureStillPicture(); - } else if (afState == CaptureResult.CONTROL_AF_STATE_FOCUSED_LOCKED || - afState == CaptureResult.CONTROL_AF_STATE_NOT_FOCUSED_LOCKED) { - // CONTROL_AE_STATE can be null on some devices - - if (aeState == null || - aeState == CaptureResult.CONTROL_AE_STATE_CONVERGED) { - cameraState = CameraState.STATE_CAPTURING; - captureStillPicture(); - } else { - runPrecaptureSequence(); + /** + * Retrieves the JPEG orientation from the specified screen rotation. + * + * @param rotation The screen rotation. + * @return The JPEG orientation (one of 0, 90, 270, and 360) + */ + private int getOrientation(int rotation) { + // Sensor orientation is 90 for most devices, or 270 for some devices (eg. Nexus 5X) + // We have to take that into account and rotate JPEG properly. + // For devices with orientation of 90, we simply return our mapping from ORIENTATIONS. + // For devices with orientation of 270, we need to rotate the JPEG 180 degrees. + return (ORIENTATIONS.get(rotation) + sensorOrientation + 270) % 360; + } + + /** + * A {@link CameraCaptureSession.CaptureCallback} that handles events related to JPEG capture. + */ + private final CameraCaptureSession.CaptureCallback mCaptureCallback + = new CameraCaptureSession.CaptureCallback() { + + private void process(CaptureResult result) { + if (pictureCaptureRequest == null) { + return; + } + + Integer aeState = result.get(CaptureResult.CONTROL_AE_STATE); + Integer afState = result.get(CaptureResult.CONTROL_AF_STATE); + + if (cameraState != CameraState.STATE_PREVIEW) { + Log.i(TAG, "mCaptureCallback | state: " + cameraState + " | afState: " + afState + " | aeState: " + aeState); + } + + switch (cameraState) { + case STATE_PREVIEW: { + // We have nothing to do when the camera preview is working normally. + break; + } + + case STATE_WAITING_FOCUS: { + if (afState == null) { + cameraState = CameraState.STATE_CAPTURING; + captureStillPicture(); + } else if (afState == CaptureResult.CONTROL_AF_STATE_FOCUSED_LOCKED || + afState == CaptureResult.CONTROL_AF_STATE_NOT_FOCUSED_LOCKED) { + // CONTROL_AE_STATE can be null on some devices + + if (aeState == null || + aeState == CaptureResult.CONTROL_AE_STATE_CONVERGED) { + cameraState = CameraState.STATE_CAPTURING; + captureStillPicture(); + } else { + runPrecaptureSequence(); + } + } + break; + } + + case STATE_WAITING_PRECAPTURE_START: { + // CONTROL_AE_STATE can be null on some devices + if (aeState == null || + aeState == CaptureResult.CONTROL_AE_STATE_CONVERGED || + aeState == CaptureResult.CONTROL_AE_STATE_PRECAPTURE || + aeState == CaptureResult.CONTROL_AE_STATE_FLASH_REQUIRED) { + cameraState = CameraState.STATE_WAITING_PRECAPTURE_DONE; + pictureCaptureRequest.setPreCaptureStartTime(); + } + break; + } + + case STATE_WAITING_PRECAPTURE_DONE: { + // CONTROL_AE_STATE can be null on some devices + if (aeState == null || aeState != CaptureResult.CONTROL_AE_STATE_PRECAPTURE) { + cameraState = CameraState.STATE_CAPTURING; + captureStillPicture(); + } else { + if (pictureCaptureRequest.hitPreCaptureTimeout()) { + unlockAutoFocus(); + } + } + break; + } } - } - break; } - case STATE_WAITING_PRECAPTURE: { - // CONTROL_AE_STATE can be null on some devices - if (aeState == null || - aeState == CaptureResult.CONTROL_AE_STATE_CONVERGED || - aeState == CaptureResult.CONTROL_AE_STATE_PRECAPTURE || - aeState == CaptureResult.CONTROL_AE_STATE_FLASH_REQUIRED) { - cameraState = CameraState.STATE_WAITING_PRECAPTURE_READY; - pictureCaptureRequest.setPreCaptureStartTime(); - } - break; + @Override + public void onCaptureProgressed(@NonNull CameraCaptureSession session, + @NonNull CaptureRequest request, + @NonNull CaptureResult partialResult) { + process(partialResult); } - case STATE_WAITING_PRECAPTURE_READY: { - // CONTROL_AE_STATE can be null on some devices - if (aeState == null || aeState != CaptureResult.CONTROL_AE_STATE_PRECAPTURE) { - cameraState = CameraState.STATE_CAPTURING; - captureStillPicture(); - } else { - if (pictureCaptureRequest.hitPreCaptureTimeout()) { - unlockAutoFocus(); - } - } - break; + @Override + public void onCaptureCompleted(@NonNull CameraCaptureSession session, + @NonNull CaptureRequest request, + @NonNull TotalCaptureResult result) { + process(result); } - } - } + }; - @Override - public void onCaptureProgressed(@NonNull CameraCaptureSession session, - @NonNull CaptureRequest request, - @NonNull CaptureResult partialResult) { - process(partialResult); - } + /** + * Trigger auto focus to start and refresh preview capture session. + */ + private void startAutoFocus() { + Log.i(TAG, "startAutoFocus"); + mPreviewRequestBuilder.set( + CaptureRequest.CONTROL_AF_TRIGGER, CaptureRequest.CONTROL_AF_TRIGGER_START); - @Override - public void onCaptureCompleted(@NonNull CameraCaptureSession session, - @NonNull CaptureRequest request, - @NonNull TotalCaptureResult result) { - process(result); - } - }; - - /** - * Trigger auto focus to start and refresh preview capture session. - */ - private void startAutoFocus() { - Log.i(TAG, "startAutoFocus"); - mPreviewRequestBuilder.set( - CaptureRequest.CONTROL_AF_TRIGGER, CaptureRequest.CONTROL_AF_TRIGGER_START); - - try { - mPreviewSession.capture(mPreviewRequestBuilder.build(), mCaptureCallback, - mBackgroundHandler); - } catch (CameraAccessException e) { - pictureCaptureRequest.error("cannotStartAutofocus", e.getMessage(), null); + try { + mPreviewSession.capture(mPreviewRequestBuilder.build(), mCaptureCallback, + mBackgroundHandler); + } catch (CameraAccessException e) { + pictureCaptureRequest.error("cannotStartAutofocus", e.getMessage(), null); + } } - } - - /** - * Start capturing a picture, doing autofocus first. - */ - private void runPictureAutoFocus() { - Log.i(TAG, "runPictureAutoFocus"); - assert (pictureCaptureRequest != null); - - cameraState = CameraState.STATE_WAITING_FOCUS; - lockAutoFocus(); - } - - /** - * Start the autofocus routine. - */ - private void lockAutoFocus() { - Log.i(TAG, "lockAutoFocus"); - mPreviewRequestBuilder.set( - CaptureRequest.CONTROL_AF_TRIGGER, CaptureRequest.CONTROL_AF_TRIGGER_START); - - refreshPreviewCaptureSession( - null, (code, message) -> pictureCaptureRequest.error(code, message, null)); - } - - /** - * Cancel and reset auto focus state and refresh the preview session. - */ - private void unlockAutoFocus() { - Log.i(TAG, "unlockAutoFocus"); - - mPreviewRequestBuilder.set( - CaptureRequest.CONTROL_AF_TRIGGER, CameraMetadata.CONTROL_AF_TRIGGER_CANCEL); - - updateFocus(mPreviewRequestBuilder); - updateFlash(mPreviewRequestBuilder); - - try { - mPreviewSession.capture(mPreviewRequestBuilder.build(), null, null); - } catch (CameraAccessException ignored) { + + /** + * Start capturing a picture, doing autofocus first. + */ + private void runPictureAutoFocus() { + Log.i(TAG, "runPictureAutoFocus"); + assert (pictureCaptureRequest != null); + + cameraState = CameraState.STATE_WAITING_FOCUS; + lockAutoFocus(); } - refreshPreviewCaptureSession( - null, - (errorCode, errorMessage) -> pictureCaptureRequest.error(errorCode, errorMessage, null)); - } - - public void startVideoRecording(Result result) { - final File outputDir = applicationContext.getCacheDir(); - try { - videoRecordingFile = File.createTempFile("REC", ".mp4", outputDir); - } catch (IOException | SecurityException e) { - result.error("cannotCreateFile", e.getMessage(), null); - return; + /** + * Start the autofocus routine. + */ + private void lockAutoFocus() { + Log.i(TAG, "lockAutoFocus"); + mPreviewRequestBuilder.set( + CaptureRequest.CONTROL_AF_TRIGGER, CaptureRequest.CONTROL_AF_TRIGGER_START); + + refreshPreviewCaptureSession( + null, (code, message) -> pictureCaptureRequest.error(code, message, null)); } - try { - prepareMediaRecorder(videoRecordingFile.getAbsolutePath()); - recordingVideo = true; - createCaptureSession( - CameraDevice.TEMPLATE_RECORD, () -> mediaRecorder.start(), mediaRecorder.getSurface()); - result.success(null); - } catch (CameraAccessException | IOException e) { - recordingVideo = false; - videoRecordingFile = null; - result.error("videoRecordingFailed", e.getMessage(), null); + /** + * Cancel and reset auto focus state and refresh the preview session. + */ + private void unlockAutoFocus() { + Log.i(TAG, "unlockAutoFocus"); + + // Reset AF state + mPreviewRequestBuilder.set( + CaptureRequest.CONTROL_AF_TRIGGER, CameraMetadata.CONTROL_AF_TRIGGER_CANCEL); + + // Reset AE state. If we don't call this then the preview won't show AE again. + mPreviewRequestBuilder.set(CaptureRequest.CONTROL_AE_PRECAPTURE_TRIGGER, + CaptureRequest.CONTROL_AE_PRECAPTURE_TRIGGER_IDLE); + + updateFocus(mPreviewRequestBuilder); + updateFlash(mPreviewRequestBuilder); + updateExposureMode(mPreviewRequestBuilder); + + try { + mPreviewSession.capture(mPreviewRequestBuilder.build(), null, null); + } catch (CameraAccessException ignored) { + } + + refreshPreviewCaptureSession( + null, + (errorCode, errorMessage) -> pictureCaptureRequest.error(errorCode, errorMessage, null)); } - } - public void stopVideoRecording(@NonNull final Result result) { - if (!recordingVideo) { - result.success(null); - return; + public void startVideoRecording(Result result) { + final File outputDir = applicationContext.getCacheDir(); + try { + videoRecordingFile = File.createTempFile("REC", ".mp4", outputDir); + } catch (IOException | SecurityException e) { + result.error("cannotCreateFile", e.getMessage(), null); + return; + } + + try { + prepareMediaRecorder(videoRecordingFile.getAbsolutePath()); + recordingVideo = true; + createCaptureSession( + CameraDevice.TEMPLATE_RECORD, () -> mediaRecorder.start(), mediaRecorder.getSurface()); + result.success(null); + } catch (CameraAccessException | IOException e) { + recordingVideo = false; + videoRecordingFile = null; + result.error("videoRecordingFailed", e.getMessage(), null); + } } - try { - recordingVideo = false; - - try { - mPreviewSession.abortCaptures(); - mediaRecorder.stop(); - } catch (CameraAccessException | IllegalStateException e) { - // Ignore exceptions and try to continue (changes are camera session already aborted capture) - } - - mediaRecorder.reset(); - startPreview(); - result.success(videoRecordingFile.getAbsolutePath()); - videoRecordingFile = null; - } catch (CameraAccessException | IllegalStateException e) { - result.error("videoRecordingFailed", e.getMessage(), null); + public void stopVideoRecording(@NonNull final Result result) { + if (!recordingVideo) { + result.success(null); + return; + } + + try { + recordingVideo = false; + + try { + mPreviewSession.abortCaptures(); + mediaRecorder.stop(); + } catch (CameraAccessException | IllegalStateException e) { + // Ignore exceptions and try to continue (changes are camera session already aborted capture) + } + + mediaRecorder.reset(); + startPreview(); + result.success(videoRecordingFile.getAbsolutePath()); + videoRecordingFile = null; + } catch (CameraAccessException | IllegalStateException e) { + result.error("videoRecordingFailed", e.getMessage(), null); + } } - } - public void pauseVideoRecording(@NonNull final Result result) { - if (!recordingVideo) { - result.success(null); - return; + public void pauseVideoRecording(@NonNull final Result result) { + if (!recordingVideo) { + result.success(null); + return; + } + + try { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { + mediaRecorder.pause(); + } else { + result.error("videoRecordingFailed", "pauseVideoRecording requires Android API +24.", null); + return; + } + } catch (IllegalStateException e) { + result.error("videoRecordingFailed", e.getMessage(), null); + return; + } + + result.success(null); } - try { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { - mediaRecorder.pause(); - } else { - result.error("videoRecordingFailed", "pauseVideoRecording requires Android API +24.", null); - return; - } - } catch (IllegalStateException e) { - result.error("videoRecordingFailed", e.getMessage(), null); - return; + public void resumeVideoRecording(@NonNull final Result result) { + if (!recordingVideo) { + result.success(null); + return; + } + + try { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { + mediaRecorder.resume(); + } else { + result.error( + "videoRecordingFailed", "resumeVideoRecording requires Android API +24.", null); + return; + } + } catch (IllegalStateException e) { + result.error("videoRecordingFailed", e.getMessage(), null); + return; + } + + result.success(null); } - result.success(null); - } + /** + * Dart handler when it's time to set a new flash mode. This will try to set a new + * flash mode to the current camera. + * + * @param result + * @param newMode + * @throws CameraAccessException + */ + public void setFlashMode(@NonNull final Result result, FlashMode newMode) + throws CameraAccessException { + // Check if the current camera can modify the flash mode. + Boolean flashAvailable = + cameraManager + .getCameraCharacteristics(cameraDevice.getId()) + .get(CameraCharacteristics.FLASH_INFO_AVAILABLE); + + // Check if flash is available. + if (flashAvailable == null || !flashAvailable) { + result.error("setFlashModeFailed", "Device does not have flash capabilities", null); + return; + } - public void resumeVideoRecording(@NonNull final Result result) { - if (!recordingVideo) { - result.success(null); - return; + // Save the new flash mode setting + final FlashMode oldFlashMode = currentFlashMode; + currentFlashMode = newMode; + updateFlash(mPreviewRequestBuilder); + + // If switching directly from torch to auto or on, make sure we turn off the torch. + if (oldFlashMode == FlashMode.torch && newMode != FlashMode.torch && newMode != FlashMode.off) { + // TODO: why cant we just call refresh preview here? + mPreviewSession.setRepeatingRequest( + mPreviewRequestBuilder.build(), + new CaptureCallback() { + private boolean isFinished = false; + + @Override + public void onCaptureCompleted( + @NonNull CameraCaptureSession session, + @NonNull CaptureRequest request, + @NonNull TotalCaptureResult captureResult) { + if (isFinished) { + return; + } + + updateFlash(mPreviewRequestBuilder); + refreshPreviewCaptureSession( + () -> { + result.success(null); + isFinished = true; + }, + (code, message) -> + result.error("setFlashModeFailed", "Could not set flash mode.", null)); + } + + @Override + public void onCaptureFailed( + @NonNull CameraCaptureSession session, + @NonNull CaptureRequest request, + @NonNull CaptureFailure failure) { + if (isFinished) { + return; + } + + result.error("setFlashModeFailed", "Could not set flash mode.", null); + isFinished = true; + } + }, + null); + } else { + refreshPreviewCaptureSession( + () -> result.success(null), + (code, message) -> result.error("setFlashModeFailed", "Could not set flash mode.", null)); + } } - try { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { - mediaRecorder.resume(); - } else { - result.error( - "videoRecordingFailed", "resumeVideoRecording requires Android API +24.", null); - return; - } - } catch (IllegalStateException e) { - result.error("videoRecordingFailed", e.getMessage(), null); - return; + /** + * Dart handler for setting new exposure mode setting. + * + * @param result + * @param newMode + * @throws CameraAccessException + */ + public void setExposureMode(@NonNull final Result result, ExposureMode newMode) + throws CameraAccessException { + exposureMode = newMode; + + refreshPreviewCaptureSession( + () -> result.success(null), + (code, message) -> result.error("setExposureModeFailed", "Could not set exposure mode.", null)); } - result.success(null); - } - - /** - * Dart handler when it's time to set a new flash mode. This will try to set a new - * flash mode to the current camera. - * @param result - * @param newMode - * @throws CameraAccessException - */ - public void setFlashMode(@NonNull final Result result, FlashMode newMode) - throws CameraAccessException { - // Check if the current camera can modify the flash mode. - Boolean flashAvailable = - cameraManager - .getCameraCharacteristics(cameraDevice.getId()) - .get(CameraCharacteristics.FLASH_INFO_AVAILABLE); + public void setExposurePoint(@NonNull final Result result, Double x, Double y) + throws CameraAccessException { + // Check if exposure point functionality is available. + if (!isExposurePointSupported()) { + result.error( + "setExposurePointFailed", "Device does not have exposure point capabilities", null); + return; + } + // Check if the current region boundaries are known + if (cameraRegions.getMaxBoundaries() == null) { + result.error("setExposurePointFailed", "Could not determine max region boundaries", null); + return; + } + // Set the metering rectangle + if (x == null || y == null) cameraRegions.resetAutoExposureMeteringRectangle(); + else cameraRegions.setAutoExposureMeteringRectangleFromPoint(y, 1 - x); + // Apply it + updateExposureMode(mPreviewRequestBuilder); + refreshPreviewCaptureSession( + () -> result.success(null), (code, message) -> result.error("CameraAccess", message, null)); + } - // Check if flash is available. - if (flashAvailable == null || !flashAvailable) { - result.error("setFlashModeFailed", "Device does not have flash capabilities", null); - return; + /** + * Set new focus mode from dart. + * + * @param result + * @param newMode + * @throws CameraAccessException + */ + public void setFocusMode(@NonNull final Result result, FocusMode newMode) + throws CameraAccessException { + currentFocusMode = newMode; + updateFocus(mPreviewRequestBuilder); + + switch (newMode) { + case auto: + refreshPreviewCaptureSession( + null, (code, message) -> result.error("setFocusMode", message, null)); + break; + case locked: + startAutoFocus(); + break; + } + result.success(null); } - // Save the new flash mode setting - final FlashMode oldFlashMode = currentFlashMode; - currentFlashMode = newMode; - updateFlash(mPreviewRequestBuilder); + public void setFocusPoint(@NonNull final Result result, Double x, Double y) + throws CameraAccessException { + // Check if focus point functionality is available. + if (!isFocusPointSupported()) { + result.error("setFocusPointFailed", "Device does not have focus point capabilities", null); + return; + } + + // Check if the current region boundaries are known + if (cameraRegions.getMaxBoundaries() == null) { + result.error("setFocusPointFailed", "Could not determine max region boundaries", null); + return; + } - // If switching directly from torch to auto or on, make sure we turn off the torch. - if (oldFlashMode == FlashMode.torch && newMode != FlashMode.torch && newMode != FlashMode.off) { - // TODO: why cant we just call refresh preview here? - mPreviewSession.setRepeatingRequest( - mPreviewRequestBuilder.build(), - new CaptureCallback() { - private boolean isFinished = false; + // Set the metering rectangle + if (x == null || y == null) { + cameraRegions.resetAutoFocusMeteringRectangle(); + } else { + cameraRegions.setAutoFocusMeteringRectangleFromPoint(y, 1 - x); + } - @Override - public void onCaptureCompleted( - @NonNull CameraCaptureSession session, - @NonNull CaptureRequest request, - @NonNull TotalCaptureResult captureResult) { - if (isFinished) { - return; - } - - updateFlash(mPreviewRequestBuilder); - refreshPreviewCaptureSession( - () -> { - result.success(null); - isFinished = true; - }, - (code, message) -> - result.error("setFlashModeFailed", "Could not set flash mode.", null)); - } + // Apply the new metering rectangle + setFocusMode(result, currentFocusMode); + } - @Override - public void onCaptureFailed( - @NonNull CameraCaptureSession session, - @NonNull CaptureRequest request, - @NonNull CaptureFailure failure) { - if (isFinished) { - return; - } - - result.error("setFlashModeFailed", "Could not set flash mode.", null); - isFinished = true; - } - }, - null); - } else { - refreshPreviewCaptureSession( - () -> result.success(null), - (code, message) -> result.error("setFlashModeFailed", "Could not set flash mode.", null)); + @TargetApi(VERSION_CODES.P) + private boolean supportsDistortionCorrection() throws CameraAccessException { + int[] availableDistortionCorrectionModes = + cameraManager + .getCameraCharacteristics(cameraDevice.getId()) + .get(CameraCharacteristics.DISTORTION_CORRECTION_AVAILABLE_MODES); + if (availableDistortionCorrectionModes == null) + availableDistortionCorrectionModes = new int[0]; + long nonOffModesSupported = + Arrays.stream(availableDistortionCorrectionModes) + .filter((value) -> value != CaptureRequest.DISTORTION_CORRECTION_MODE_OFF) + .count(); + return nonOffModesSupported > 0; } - } - - public void setExposureMode(@NonNull final Result result, ExposureMode mode) - throws CameraAccessException { - updateExposure(mode); - mPreviewSession.setRepeatingRequest(mPreviewRequestBuilder.build(), null, null); - result.success(null); - } - - public void setExposurePoint(@NonNull final Result result, Double x, Double y) - throws CameraAccessException { - // Check if exposure point functionality is available. - if (!isExposurePointSupported()) { - result.error( - "setExposurePointFailed", "Device does not have exposure point capabilities", null); - return; + + private Size getRegionBoundaries() throws CameraAccessException { + // No distortion correction support + if (android.os.Build.VERSION.SDK_INT < VERSION_CODES.P || !supportsDistortionCorrection()) { + return cameraManager + .getCameraCharacteristics(cameraDevice.getId()) + .get(CameraCharacteristics.SENSOR_INFO_PIXEL_ARRAY_SIZE); + } + // Get the current distortion correction mode + Integer distortionCorrectionMode = + mPreviewRequestBuilder.get(CaptureRequest.DISTORTION_CORRECTION_MODE); + // Return the correct boundaries depending on the mode + android.graphics.Rect rect; + if (distortionCorrectionMode == null + || distortionCorrectionMode == CaptureRequest.DISTORTION_CORRECTION_MODE_OFF) { + rect = + cameraManager + .getCameraCharacteristics(cameraDevice.getId()) + .get(CameraCharacteristics.SENSOR_INFO_PRE_CORRECTION_ACTIVE_ARRAY_SIZE); + } else { + rect = + cameraManager + .getCameraCharacteristics(cameraDevice.getId()) + .get(CameraCharacteristics.SENSOR_INFO_ACTIVE_ARRAY_SIZE); + } + return rect == null ? null : new Size(rect.width(), rect.height()); } - // Check if the current region boundaries are known - if (cameraRegions.getMaxBoundaries() == null) { - result.error("setExposurePointFailed", "Could not determine max region boundaries", null); - return; + + private boolean isExposurePointSupported() throws CameraAccessException { + Integer supportedRegions = + cameraManager + .getCameraCharacteristics(cameraDevice.getId()) + .get(CameraCharacteristics.CONTROL_MAX_REGIONS_AE); + return supportedRegions != null && supportedRegions > 0; } - // Set the metering rectangle - if (x == null || y == null) cameraRegions.resetAutoExposureMeteringRectangle(); - else cameraRegions.setAutoExposureMeteringRectangleFromPoint(y, 1 - x); - // Apply it - updateExposure(exposureMode); - refreshPreviewCaptureSession( - () -> result.success(null), (code, message) -> result.error("CameraAccess", message, null)); - } - - /** - * Set new focus mode from dart. - * @param result - * @param newMode - * @throws CameraAccessException - */ - public void setFocusMode(@NonNull final Result result, FocusMode newMode) - throws CameraAccessException { - currentFocusMode = newMode; - updateFocus(mPreviewRequestBuilder); - - switch (newMode) { - case auto: - refreshPreviewCaptureSession( - null, (code, message) -> result.error("setFocusMode", message, null)); - break; - case locked: - startAutoFocus(); - break; + + private boolean isFocusPointSupported() throws CameraAccessException { + Integer supportedRegions = + cameraManager + .getCameraCharacteristics(cameraDevice.getId()) + .get(CameraCharacteristics.CONTROL_MAX_REGIONS_AF); + return supportedRegions != null && supportedRegions > 0; } - result.success(null); - } - - public void setFocusPoint(@NonNull final Result result, Double x, Double y) - throws CameraAccessException { - // Check if focus point functionality is available. - if (!isFocusPointSupported()) { - result.error("setFocusPointFailed", "Device does not have focus point capabilities", null); - return; + + public double getMinExposureOffset() throws CameraAccessException { + Range range = + cameraManager + .getCameraCharacteristics(cameraDevice.getId()) + .get(CameraCharacteristics.CONTROL_AE_COMPENSATION_RANGE); + double minStepped = range == null ? 0 : range.getLower(); + double stepSize = getExposureOffsetStepSize(); + return minStepped * stepSize; } - // Check if the current region boundaries are known - if (cameraRegions.getMaxBoundaries() == null) { - result.error("setFocusPointFailed", "Could not determine max region boundaries", null); - return; + public double getMaxExposureOffset() throws CameraAccessException { + Range range = + cameraManager + .getCameraCharacteristics(cameraDevice.getId()) + .get(CameraCharacteristics.CONTROL_AE_COMPENSATION_RANGE); + double maxStepped = range == null ? 0 : range.getUpper(); + double stepSize = getExposureOffsetStepSize(); + return maxStepped * stepSize; } - // Set the metering rectangle - if (x == null || y == null) { - cameraRegions.resetAutoFocusMeteringRectangle(); - } else { - cameraRegions.setAutoFocusMeteringRectangleFromPoint(y, 1 - x); + public double getExposureOffsetStepSize() throws CameraAccessException { + Rational stepSize = + cameraManager + .getCameraCharacteristics(cameraDevice.getId()) + .get(CameraCharacteristics.CONTROL_AE_COMPENSATION_STEP); + return stepSize == null ? 0.0 : stepSize.doubleValue(); } - // Apply the new metering rectangle - setFocusMode(result, currentFocusMode); - } + public void setExposureOffset(@NonNull final Result result, double offset) + throws CameraAccessException { + // Set the exposure offset + double stepSize = getExposureOffsetStepSize(); + exposureOffset = (int) (offset / stepSize); + // Apply it + updateExposureMode(mPreviewRequestBuilder); + // TODO: refresh preview session? + this.mPreviewSession.setRepeatingRequest(mPreviewRequestBuilder.build(), null, null); + result.success(offset); + } - @TargetApi(VERSION_CODES.P) - private boolean supportsDistortionCorrection() throws CameraAccessException { - int[] availableDistortionCorrectionModes = - cameraManager - .getCameraCharacteristics(cameraDevice.getId()) - .get(CameraCharacteristics.DISTORTION_CORRECTION_AVAILABLE_MODES); - if (availableDistortionCorrectionModes == null) - availableDistortionCorrectionModes = new int[0]; - long nonOffModesSupported = - Arrays.stream(availableDistortionCorrectionModes) - .filter((value) -> value != CaptureRequest.DISTORTION_CORRECTION_MODE_OFF) - .count(); - return nonOffModesSupported > 0; - } - - private Size getRegionBoundaries() throws CameraAccessException { - // No distortion correction support - if (android.os.Build.VERSION.SDK_INT < VERSION_CODES.P || !supportsDistortionCorrection()) { - return cameraManager - .getCameraCharacteristics(cameraDevice.getId()) - .get(CameraCharacteristics.SENSOR_INFO_PIXEL_ARRAY_SIZE); + public float getMaxZoomLevel() { + return cameraZoom.maxZoom; } - // Get the current distortion correction mode - Integer distortionCorrectionMode = - mPreviewRequestBuilder.get(CaptureRequest.DISTORTION_CORRECTION_MODE); - // Return the correct boundaries depending on the mode - android.graphics.Rect rect; - if (distortionCorrectionMode == null - || distortionCorrectionMode == CaptureRequest.DISTORTION_CORRECTION_MODE_OFF) { - rect = - cameraManager - .getCameraCharacteristics(cameraDevice.getId()) - .get(CameraCharacteristics.SENSOR_INFO_PRE_CORRECTION_ACTIVE_ARRAY_SIZE); - } else { - rect = - cameraManager - .getCameraCharacteristics(cameraDevice.getId()) - .get(CameraCharacteristics.SENSOR_INFO_ACTIVE_ARRAY_SIZE); + + public float getMinZoomLevel() { + return CameraZoom.DEFAULT_ZOOM_FACTOR; } - return rect == null ? null : new Size(rect.width(), rect.height()); - } - private boolean isExposurePointSupported() throws CameraAccessException { - Integer supportedRegions = - cameraManager - .getCameraCharacteristics(cameraDevice.getId()) - .get(CameraCharacteristics.CONTROL_MAX_REGIONS_AE); - return supportedRegions != null && supportedRegions > 0; - } + public void setZoomLevel(@NonNull final Result result, float zoom) throws CameraAccessException { + float maxZoom = cameraZoom.maxZoom; + float minZoom = CameraZoom.DEFAULT_ZOOM_FACTOR; + + if (zoom > maxZoom || zoom < minZoom) { + String errorMessage = + String.format( + Locale.ENGLISH, + "Zoom level out of bounds (zoom level should be between %f and %f).", + minZoom, + maxZoom); + result.error("ZOOM_ERROR", errorMessage, null); + return; + } - private boolean isFocusPointSupported() throws CameraAccessException { - Integer supportedRegions = - cameraManager - .getCameraCharacteristics(cameraDevice.getId()) - .get(CameraCharacteristics.CONTROL_MAX_REGIONS_AF); - return supportedRegions != null && supportedRegions > 0; - } + //Zoom area is calculated relative to sensor area (activeRect) + if (mPreviewRequestBuilder != null) { + final Rect computedZoom = cameraZoom.computeZoom(zoom); + mPreviewRequestBuilder.set(CaptureRequest.SCALER_CROP_REGION, computedZoom); + mPreviewSession.setRepeatingRequest(mPreviewRequestBuilder.build(), null, null); + } - public double getMinExposureOffset() throws CameraAccessException { - Range range = - cameraManager - .getCameraCharacteristics(cameraDevice.getId()) - .get(CameraCharacteristics.CONTROL_AE_COMPENSATION_RANGE); - double minStepped = range == null ? 0 : range.getLower(); - double stepSize = getExposureOffsetStepSize(); - return minStepped * stepSize; - } - - public double getMaxExposureOffset() throws CameraAccessException { - Range range = - cameraManager - .getCameraCharacteristics(cameraDevice.getId()) - .get(CameraCharacteristics.CONTROL_AE_COMPENSATION_RANGE); - double maxStepped = range == null ? 0 : range.getUpper(); - double stepSize = getExposureOffsetStepSize(); - return maxStepped * stepSize; - } - - public double getExposureOffsetStepSize() throws CameraAccessException { - Rational stepSize = - cameraManager - .getCameraCharacteristics(cameraDevice.getId()) - .get(CameraCharacteristics.CONTROL_AE_COMPENSATION_STEP); - return stepSize == null ? 0.0 : stepSize.doubleValue(); - } - - public void setExposureOffset(@NonNull final Result result, double offset) - throws CameraAccessException { - // Set the exposure offset - double stepSize = getExposureOffsetStepSize(); - exposureOffset = (int) (offset / stepSize); - // Apply it - updateExposure(exposureMode); - this.mPreviewSession.setRepeatingRequest(mPreviewRequestBuilder.build(), null, null); - result.success(offset); - } - - public float getMaxZoomLevel() { - return cameraZoom.maxZoom; - } - - public float getMinZoomLevel() { - return CameraZoom.DEFAULT_ZOOM_FACTOR; - } - - public void setZoomLevel(@NonNull final Result result, float zoom) throws CameraAccessException { - float maxZoom = cameraZoom.maxZoom; - float minZoom = CameraZoom.DEFAULT_ZOOM_FACTOR; - - if (zoom > maxZoom || zoom < minZoom) { - String errorMessage = - String.format( - Locale.ENGLISH, - "Zoom level out of bounds (zoom level should be between %f and %f).", - minZoom, - maxZoom); - result.error("ZOOM_ERROR", errorMessage, null); - return; + result.success(null); } - //Zoom area is calculated relative to sensor area (activeRect) - if (mPreviewRequestBuilder != null) { - final Rect computedZoom = cameraZoom.computeZoom(zoom); - mPreviewRequestBuilder.set(CaptureRequest.SCALER_CROP_REGION, computedZoom); - mPreviewSession.setRepeatingRequest(mPreviewRequestBuilder.build(), null, null); + public void lockCaptureOrientation(PlatformChannel.DeviceOrientation orientation) { + this.lockedCaptureOrientation = orientation; } - result.success(null); - } - - public void lockCaptureOrientation(PlatformChannel.DeviceOrientation orientation) { - this.lockedCaptureOrientation = orientation; - } + public void unlockCaptureOrientation() { + this.lockedCaptureOrientation = null; + } - public void unlockCaptureOrientation() { - this.lockedCaptureOrientation = null; - } + /** + * Set current fps range setting to the current preview request builder + */ + private void updateFpsRange() { + if (fpsRange == null) { + return; + } - /** - * Set current fps range setting to the current preview request builder - */ - private void updateFpsRange() { - if (fpsRange == null) { - return; + mPreviewRequestBuilder.set(CaptureRequest.CONTROL_AE_TARGET_FPS_RANGE, fpsRange); } - mPreviewRequestBuilder.set(CaptureRequest.CONTROL_AE_TARGET_FPS_RANGE, fpsRange); - } - - private void updateFocus(CaptureRequest.Builder requestBuilder) { - if (useAutoFocus) { - int[] modes = cameraCharacteristics.get(CameraCharacteristics.CONTROL_AF_AVAILABLE_MODES); - // Auto focus is not supported - if (modes == null - || modes.length == 0 - || (modes.length == 1 && modes[0] == CameraCharacteristics.CONTROL_AF_MODE_OFF)) { - useAutoFocus = false; - mPreviewRequestBuilder.set( - CaptureRequest.CONTROL_AF_MODE, CaptureRequest.CONTROL_AF_MODE_OFF); - } else { - // Applying auto focus - switch (currentFocusMode) { - case locked: - requestBuilder.set( - CaptureRequest.CONTROL_AF_MODE, CaptureRequest.CONTROL_AF_MODE_AUTO); - break; - case auto: - mPreviewRequestBuilder.set( - CaptureRequest.CONTROL_AF_MODE, - recordingVideo - ? CaptureRequest.CONTROL_AF_MODE_CONTINUOUS_VIDEO - : CaptureRequest.CONTROL_AF_MODE_CONTINUOUS_PICTURE); - default: - break; + private void updateFocus(CaptureRequest.Builder requestBuilder) { + if (useAutoFocus) { + int[] modes = cameraCharacteristics.get(CameraCharacteristics.CONTROL_AF_AVAILABLE_MODES); + // Auto focus is not supported + if (modes == null + || modes.length == 0 + || (modes.length == 1 && modes[0] == CameraCharacteristics.CONTROL_AF_MODE_OFF)) { + useAutoFocus = false; + mPreviewRequestBuilder.set( + CaptureRequest.CONTROL_AF_MODE, CaptureRequest.CONTROL_AF_MODE_OFF); + } else { + // Applying auto focus + switch (currentFocusMode) { + case locked: + requestBuilder.set( + CaptureRequest.CONTROL_AF_MODE, CaptureRequest.CONTROL_AF_MODE_AUTO); + break; + case auto: + mPreviewRequestBuilder.set( + CaptureRequest.CONTROL_AF_MODE, + recordingVideo + ? CaptureRequest.CONTROL_AF_MODE_CONTINUOUS_VIDEO + : CaptureRequest.CONTROL_AF_MODE_CONTINUOUS_PICTURE); + default: + break; + } + MeteringRectangle afRect = cameraRegions.getAFMeteringRectangle(); + mPreviewRequestBuilder.set( + CaptureRequest.CONTROL_AF_REGIONS, + afRect == null ? null : new MeteringRectangle[]{afRect}); + } + } else { + mPreviewRequestBuilder.set(CaptureRequest.CONTROL_AF_MODE, CaptureRequest.CONTROL_AF_MODE_OFF); } - MeteringRectangle afRect = cameraRegions.getAFMeteringRectangle(); - mPreviewRequestBuilder.set( - CaptureRequest.CONTROL_AF_REGIONS, - afRect == null ? null : new MeteringRectangle[]{afRect}); - } - } else { - mPreviewRequestBuilder.set(CaptureRequest.CONTROL_AF_MODE, CaptureRequest.CONTROL_AF_MODE_OFF); - } - } - - private void updateExposure(ExposureMode mode) { - exposureMode = mode; - - // Applying auto exposure - MeteringRectangle aeRect = cameraRegions.getAEMeteringRectangle(); - mPreviewRequestBuilder.set( - CaptureRequest.CONTROL_AE_REGIONS, - aeRect == null ? null : new MeteringRectangle[]{cameraRegions.getAEMeteringRectangle()}); - - switch (mode) { - case locked: - mPreviewRequestBuilder.set(CaptureRequest.CONTROL_AE_LOCK, true); - break; - case auto: - default: - mPreviewRequestBuilder.set(CaptureRequest.CONTROL_AE_LOCK, false); - break; } - mPreviewRequestBuilder.set(CaptureRequest.CONTROL_AE_EXPOSURE_COMPENSATION, exposureOffset); - } - - public void startPreview() throws CameraAccessException { - if (pictureImageReader == null || pictureImageReader.getSurface() == null) return; - Log.i(TAG, "startPreview"); - - createCaptureSession(CameraDevice.TEMPLATE_PREVIEW, pictureImageReader.getSurface()); - } - - public void startPreviewWithImageStream(EventChannel imageStreamChannel) - throws CameraAccessException { - createCaptureSession(CameraDevice.TEMPLATE_RECORD, imageStreamReader.getSurface()); - Log.i(TAG, "startPreviewWithImageStream"); - - imageStreamChannel.setStreamHandler( - new EventChannel.StreamHandler() { - @Override - public void onListen(Object o, EventChannel.EventSink imageStreamSink) { - setImageStreamImageAvailableListener(imageStreamSink); - } - - @Override - public void onCancel(Object o) { - imageStreamReader.setOnImageAvailableListener(null, null); - } - }); - } - - private void setImageStreamImageAvailableListener(final EventChannel.EventSink imageStreamSink) { - imageStreamReader.setOnImageAvailableListener( - reader -> { - Image img = reader.acquireLatestImage(); - if (img == null) return; - - List> planes = new ArrayList<>(); - for (Image.Plane plane : img.getPlanes()) { - ByteBuffer buffer = plane.getBuffer(); - - byte[] bytes = new byte[buffer.remaining()]; - buffer.get(bytes, 0, bytes.length); - - Map planeBuffer = new HashMap<>(); - planeBuffer.put("bytesPerRow", plane.getRowStride()); - planeBuffer.put("bytesPerPixel", plane.getPixelStride()); - planeBuffer.put("bytes", bytes); - - planes.add(planeBuffer); - } - - Map imageBuffer = new HashMap<>(); - imageBuffer.put("width", img.getWidth()); - imageBuffer.put("height", img.getHeight()); - imageBuffer.put("format", img.getFormat()); - imageBuffer.put("planes", planes); - - imageStreamSink.success(imageBuffer); - img.close(); - }, - null); - } - - public void stopImageStream() throws CameraAccessException { - if (imageStreamReader != null) { - imageStreamReader.setOnImageAvailableListener(null, null); + public void startPreview() throws CameraAccessException { + if (pictureImageReader == null || pictureImageReader.getSurface() == null) return; + Log.i(TAG, "startPreview"); + + createCaptureSession(CameraDevice.TEMPLATE_PREVIEW, pictureImageReader.getSurface()); } - startPreview(); - } + public void startPreviewWithImageStream(EventChannel imageStreamChannel) + throws CameraAccessException { + createCaptureSession(CameraDevice.TEMPLATE_RECORD, imageStreamReader.getSurface()); + Log.i(TAG, "startPreviewWithImageStream"); + + imageStreamChannel.setStreamHandler( + new EventChannel.StreamHandler() { + @Override + public void onListen(Object o, EventChannel.EventSink imageStreamSink) { + setImageStreamImageAvailableListener(imageStreamSink); + } + + @Override + public void onCancel(Object o) { + imageStreamReader.setOnImageAvailableListener(null, null); + } + }); + } + private void setImageStreamImageAvailableListener(final EventChannel.EventSink imageStreamSink) { + imageStreamReader.setOnImageAvailableListener( + reader -> { + Image img = reader.acquireLatestImage(); + if (img == null) return; + + List> planes = new ArrayList<>(); + for (Image.Plane plane : img.getPlanes()) { + ByteBuffer buffer = plane.getBuffer(); + + byte[] bytes = new byte[buffer.remaining()]; + buffer.get(bytes, 0, bytes.length); + + Map planeBuffer = new HashMap<>(); + planeBuffer.put("bytesPerRow", plane.getRowStride()); + planeBuffer.put("bytesPerPixel", plane.getPixelStride()); + planeBuffer.put("bytes", bytes); + + planes.add(planeBuffer); + } + + Map imageBuffer = new HashMap<>(); + imageBuffer.put("width", img.getWidth()); + imageBuffer.put("height", img.getHeight()); + imageBuffer.put("format", img.getFormat()); + imageBuffer.put("planes", planes); + + imageStreamSink.success(imageBuffer); + img.close(); + }, + null); + } + public void stopImageStream() throws CameraAccessException { + if (imageStreamReader != null) { + imageStreamReader.setOnImageAvailableListener(null, null); + } + startPreview(); + } - private void closeCaptureSession() { - if (mPreviewSession != null) { - Log.i(TAG, "closeCaptureSession"); + private void closeCaptureSession() { + if (mPreviewSession != null) { + Log.i(TAG, "closeCaptureSession"); - mPreviewSession.close(); - mPreviewSession = null; + mPreviewSession.close(); + mPreviewSession = null; + } } - } - public void close() { - Log.i(TAG, "close"); + public void close() { + Log.i(TAG, "close"); - closeCaptureSession(); + closeCaptureSession(); - if (cameraDevice != null) { - cameraDevice.close(); - cameraDevice = null; - } - if (pictureImageReader != null) { - pictureImageReader.close(); - pictureImageReader = null; - } - if (imageStreamReader != null) { - imageStreamReader.close(); - imageStreamReader = null; - } - if (mediaRecorder != null) { - mediaRecorder.reset(); - mediaRecorder.release(); - mediaRecorder = null; + if (cameraDevice != null) { + cameraDevice.close(); + cameraDevice = null; + } + if (pictureImageReader != null) { + pictureImageReader.close(); + pictureImageReader = null; + } + if (imageStreamReader != null) { + imageStreamReader.close(); + imageStreamReader = null; + } + if (mediaRecorder != null) { + mediaRecorder.reset(); + mediaRecorder.release(); + mediaRecorder = null; + } } - } - public void dispose() { - Log.i(TAG, "dispose"); + public void dispose() { + Log.i(TAG, "dispose"); - close(); - flutterTexture.release(); - deviceOrientationListener.stop(); - } + close(); + flutterTexture.release(); + deviceOrientationListener.stop(); + } } diff --git a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/CameraState.java b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/CameraState.java index fca1e0099e40..0bb9c0c2cee0 100644 --- a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/CameraState.java +++ b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/CameraState.java @@ -20,12 +20,12 @@ public enum CameraState { /** * Start performing autoexposure. */ - STATE_WAITING_PRECAPTURE, + STATE_WAITING_PRECAPTURE_START, /** * waiting for autoexposure to complete. */ - STATE_WAITING_PRECAPTURE_READY, + STATE_WAITING_PRECAPTURE_DONE, /** * Capturing an image. diff --git a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/PictureCaptureRequest.java b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/PictureCaptureRequest.java index de60c68d37eb..d6f3ee40b546 100644 --- a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/PictureCaptureRequest.java +++ b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/PictureCaptureRequest.java @@ -67,6 +67,7 @@ class PictureCaptureRequest { * @param mFile */ public PictureCaptureRequest(MethodChannel.Result result, File mFile) { + Log.i("Camera", "PictureCaptureRequest constructor"); this.result = result; this.timeoutHandler = new TimeoutHandler(); this.mFile = mFile; From 22887fe5c0d792766c85531ff44708b0357a47ee Mon Sep 17 00:00:00 2001 From: Andrew Coutts Date: Mon, 1 Mar 2021 07:27:09 -0500 Subject: [PATCH 004/114] Reduce capture delay and preview lag while capturing by providing preview texture to capture request --- .../io/flutter/plugins/camera/Camera.java | 320 ++++++++++-------- .../io/flutter/plugins/camera/ImageSaver.java | 7 + 2 files changed, 177 insertions(+), 150 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 9058c1112235..55d38e5dd9e2 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 @@ -111,101 +111,175 @@ public class Camera { private final CameraZoom cameraZoom; private final CameraCharacteristics cameraCharacteristics; private final Activity activity; - + /** + * Whether the current camera device supports auto focus or not. + */ + private final boolean mAutoFocusSupported = false; + /** + * This manages the state of the camera and the current capture request. + */ + PictureCaptureRequest pictureCaptureRequest; /** * The state of the camera. By default we are in the preview state. */ private CameraState cameraState = CameraState.STATE_PREVIEW; - /** * A {@link Handler} for running tasks in the background. */ private Handler mBackgroundHandler; + /** + * The preview surface which will be provided to still capture requests to keep the + * preview going during capture. + */ + private Surface previewSurface; + + /** + * This a callback object for the {@link ImageReader}. "onImageAvailable" will be called when a + * still image is ready to be saved. + */ + private final ImageReader.OnImageAvailableListener mOnImageAvailableListener + = new ImageReader.OnImageAvailableListener() { + + @Override + public void onImageAvailable(ImageReader reader) { + Log.i(TAG, "onImageAvailable"); + mBackgroundHandler.post(new ImageSaver(reader.acquireNextImage(), pictureCaptureRequest.mFile, pictureCaptureRequest)); + cameraState = CameraState.STATE_PREVIEW; + } + + }; /** * An additional thread for running tasks that shouldn't block the UI. */ private HandlerThread mBackgroundThread; - private CameraDevice cameraDevice; - private CameraCaptureSession mPreviewSession; + private CameraCaptureSession captureSession; private ImageReader pictureImageReader; private ImageReader imageStreamReader; - /** * {@link CaptureRequest.Builder} for the camera preview */ private CaptureRequest.Builder mPreviewRequestBuilder; - /** * {@link CaptureRequest} generated by {@link #mPreviewRequestBuilder} */ private CaptureRequest mPreviewRequest; - private MediaRecorder mediaRecorder; private boolean recordingVideo; private File videoRecordingFile; - /** * Flash mode setting of the current camera. Initialize to off because * we don't know if the current camera supports flash yet. */ private FlashMode currentFlashMode = FlashMode.off; - /** * Exposure mode setting of the current camera. Initialize to auto * because all cameras support autoexposure by default. */ private ExposureMode exposureMode = ExposureMode.auto; - /** * Focus mode setting of the current camera. Initialize to locked because * we don't know if the current camera supports autofocus yet. */ private FocusMode currentFocusMode = FocusMode.locked; - - /** - * Whether the current camera device supports auto focus or not. - */ - private boolean mAutoFocusSupported = false; - /** * Whether or not to use autofocus. */ private boolean useAutoFocus = false; - /** * Whether the current camera device supports Flash or not. */ private boolean mFlashSupported = false; - + private CameraRegions cameraRegions; + private int exposureOffset; /** - * This manages the state of the camera and the current capture request. + * A {@link CameraCaptureSession.CaptureCallback} that handles events related to JPEG capture. */ - PictureCaptureRequest pictureCaptureRequest; + private final CameraCaptureSession.CaptureCallback mCaptureCallback + = new CameraCaptureSession.CaptureCallback() { - /** - * This a callback object for the {@link ImageReader}. "onImageAvailable" will be called when a - * still image is ready to be saved. - */ - private final ImageReader.OnImageAvailableListener mOnImageAvailableListener - = new ImageReader.OnImageAvailableListener() { + private void process(CaptureResult result) { + if (pictureCaptureRequest == null) { + return; + } + + Integer aeState = result.get(CaptureResult.CONTROL_AE_STATE); + Integer afState = result.get(CaptureResult.CONTROL_AF_STATE); + + if (cameraState != CameraState.STATE_PREVIEW) { + Log.i(TAG, "mCaptureCallback | state: " + cameraState + " | afState: " + afState + " | aeState: " + aeState); + } + + switch (cameraState) { + case STATE_PREVIEW: { + // We have nothing to do when the camera preview is working normally. + break; + } + + case STATE_WAITING_FOCUS: { + if (afState == null) { + cameraState = CameraState.STATE_CAPTURING; + takePictureAfterPrecapture(); + } else if (afState == CaptureResult.CONTROL_AF_STATE_FOCUSED_LOCKED || + afState == CaptureResult.CONTROL_AF_STATE_NOT_FOCUSED_LOCKED) { + // CONTROL_AE_STATE can be null on some devices + + if (aeState == null || + aeState == CaptureResult.CONTROL_AE_STATE_CONVERGED) { + cameraState = CameraState.STATE_CAPTURING; + takePictureAfterPrecapture(); + } else { + runPrecaptureSequence(); + } + } + break; + } + + case STATE_WAITING_PRECAPTURE_START: { + // CONTROL_AE_STATE can be null on some devices + if (aeState == null || + aeState == CaptureResult.CONTROL_AE_STATE_CONVERGED || + aeState == CaptureResult.CONTROL_AE_STATE_PRECAPTURE || + aeState == CaptureResult.CONTROL_AE_STATE_FLASH_REQUIRED) { + cameraState = CameraState.STATE_WAITING_PRECAPTURE_DONE; + pictureCaptureRequest.setPreCaptureStartTime(); + } + break; + } + + case STATE_WAITING_PRECAPTURE_DONE: { + // CONTROL_AE_STATE can be null on some devices + if (aeState == null || aeState != CaptureResult.CONTROL_AE_STATE_PRECAPTURE) { + cameraState = CameraState.STATE_CAPTURING; + takePictureAfterPrecapture(); + } else { + if (pictureCaptureRequest.hitPreCaptureTimeout()) { + unlockAutoFocus(); + } + } + break; + } + } + } @Override - public void onImageAvailable(ImageReader reader) { - Log.i(TAG, "onImageAvailable"); - mBackgroundHandler.post(new ImageSaver(reader.acquireNextImage(), pictureCaptureRequest.mFile, pictureCaptureRequest)); - cameraState = CameraState.STATE_PREVIEW; + public void onCaptureProgressed(@NonNull CameraCaptureSession session, + @NonNull CaptureRequest request, + @NonNull CaptureResult partialResult) { + process(partialResult); } + @Override + public void onCaptureCompleted(@NonNull CameraCaptureSession session, + @NonNull CaptureRequest request, + @NonNull TotalCaptureResult result) { + process(result); + } }; - private CameraRegions cameraRegions; - private int exposureOffset; - private Range fpsRange; private PlatformChannel.DeviceOrientation lockedCaptureOrientation; - public Camera( final Activity activity, final SurfaceTextureEntry flutterTexture, @@ -386,7 +460,8 @@ public void onError(@NonNull CameraDevice cameraDevice, int errorCode) { dartMessenger.sendCameraErrorEvent(errorDescription); } }, - null); + mBackgroundHandler + ); } private void createCaptureSession(int templateType, Surface... surfaces) @@ -411,6 +486,11 @@ private void createCaptureSession( Surface flutterSurface = new Surface(surfaceTexture); mPreviewRequestBuilder.addTarget(flutterSurface); + // Save surface if it's preview + if (templateType == CameraDevice.TEMPLATE_PREVIEW) { + previewSurface =flutterSurface; + } + List remainingSurfaces = Arrays.asList(surfaces); if (templateType != CameraDevice.TEMPLATE_PREVIEW) { // If it is not preview mode, add all surfaces as targets. @@ -431,7 +511,7 @@ public void onConfigured(@NonNull CameraCaptureSession session) { dartMessenger.sendCameraErrorEvent("The camera was closed during configuration."); return; } - mPreviewSession = session; + captureSession = session; updateFpsRange(); updateFocus(mPreviewRequestBuilder); @@ -483,13 +563,13 @@ private void createCaptureSessionWithSessionConfig( private void createCaptureSession( List surfaces, CameraCaptureSession.StateCallback callback) throws CameraAccessException { - cameraDevice.createCaptureSession(surfaces, callback, null); + cameraDevice.createCaptureSession(surfaces, callback, mBackgroundHandler); } private void refreshPreviewCaptureSession( @Nullable Runnable onSuccessCallback, @NonNull ErrorCallback onErrorCallback) { Log.i(TAG, "refreshPreviewCaptureSession"); - if (mPreviewSession == null) { + if (captureSession == null) { Log.i(TAG, "[refreshPreviewCaptureSession] mPreviewSession null, returning"); return; } @@ -497,7 +577,7 @@ private void refreshPreviewCaptureSession( try { // mPreviewRequestBuilder.set(CaptureRequest.CONTROL_AE_PRECAPTURE_TRIGGER, // CaptureRequest.CONTROL_AE_PRECAPTURE_TRIGGER_START); - mPreviewSession.setRepeatingRequest(mPreviewRequestBuilder.build(), + captureSession.setRepeatingRequest(mPreviewRequestBuilder.build(), mCaptureCallback, mBackgroundHandler); if (onSuccessCallback != null) { @@ -560,7 +640,7 @@ private void runPrecaptureSequence() { mPreviewRequestBuilder.set(CaptureRequest.CONTROL_AE_PRECAPTURE_TRIGGER, CaptureRequest.CONTROL_AE_PRECAPTURE_TRIGGER_START); - mPreviewSession.capture(mPreviewRequestBuilder.build(), mCaptureCallback, + captureSession.capture(mPreviewRequestBuilder.build(), mCaptureCallback, mBackgroundHandler); } catch (CameraAccessException e) { pictureCaptureRequest.error("cameraAccess", e.getMessage(), null); @@ -571,34 +651,52 @@ private void runPrecaptureSequence() { * Capture a still picture. This method should be called when we get a response in * {@link #mCaptureCallback} from both lockFocus(). */ - private void captureStillPicture() { + private void takePictureAfterPrecapture() { Log.i(TAG, "captureStillPicture"); try { if (null == cameraDevice) { return; } // This is the CaptureRequest.Builder that we use to take a picture. - final CaptureRequest.Builder captureBuilder = + final CaptureRequest.Builder stillBuilder = cameraDevice.createCaptureRequest(CameraDevice.TEMPLATE_STILL_CAPTURE); - captureBuilder.addTarget(pictureImageReader.getSurface()); + stillBuilder.addTarget(pictureImageReader.getSurface()); + + // Add the preview surface to reduce delay when capturing + stillBuilder.addTarget(previewSurface); // Zoom - captureBuilder.set( + stillBuilder.set( CaptureRequest.SCALER_CROP_REGION, mPreviewRequestBuilder.get(CaptureRequest.SCALER_CROP_REGION)); // Set focus / flash from preview mode - updateFlash(captureBuilder); - updateFocus(captureBuilder); - updateExposureMode(captureBuilder); + updateFlash(stillBuilder); + updateFocus(stillBuilder); + updateExposureMode(stillBuilder); // Orientation int rotation = activity.getWindowManager().getDefaultDisplay().getRotation(); - captureBuilder.set(CaptureRequest.JPEG_ORIENTATION, getOrientation(rotation)); + stillBuilder.set(CaptureRequest.JPEG_ORIENTATION, getOrientation(rotation)); CameraCaptureSession.CaptureCallback CaptureCallback = new CameraCaptureSession.CaptureCallback() { + @Override + public void onCaptureStarted(@NonNull CameraCaptureSession session, + @NonNull CaptureRequest request, + long timestamp, + long frameNumber) { + Log.i(TAG, "onCaptureStarted"); + } + + @Override + public void onCaptureProgressed(@NonNull CameraCaptureSession session, + @NonNull CaptureRequest request, + @NonNull CaptureResult partialResult) { + Log.i(TAG, "onCaptureProgressed"); + } + @Override public void onCaptureCompleted(@NonNull CameraCaptureSession session, @NonNull CaptureRequest request, @@ -608,9 +706,15 @@ public void onCaptureCompleted(@NonNull CameraCaptureSession session, } }; - mPreviewSession.stopRepeating(); - mPreviewSession.abortCaptures(); - mPreviewSession.capture(captureBuilder.build(), CaptureCallback, null); + Log.i(TAG, "stopRepeating"); + captureSession.stopRepeating(); + + Log.i(TAG, "abortCaptures"); + captureSession.abortCaptures(); + + Log.i(TAG, "sending capture request"); + captureSession.capture(stillBuilder.build(), CaptureCallback, mBackgroundHandler); +// captureSession.setRepeatingRequest(mPreviewRequestBuilder.build(), null, mBackgroundHandler); } catch (CameraAccessException e) { pictureCaptureRequest.error("cameraAccess", e.getMessage(), null); } @@ -730,91 +834,6 @@ private int getOrientation(int rotation) { return (ORIENTATIONS.get(rotation) + sensorOrientation + 270) % 360; } - /** - * A {@link CameraCaptureSession.CaptureCallback} that handles events related to JPEG capture. - */ - private final CameraCaptureSession.CaptureCallback mCaptureCallback - = new CameraCaptureSession.CaptureCallback() { - - private void process(CaptureResult result) { - if (pictureCaptureRequest == null) { - return; - } - - Integer aeState = result.get(CaptureResult.CONTROL_AE_STATE); - Integer afState = result.get(CaptureResult.CONTROL_AF_STATE); - - if (cameraState != CameraState.STATE_PREVIEW) { - Log.i(TAG, "mCaptureCallback | state: " + cameraState + " | afState: " + afState + " | aeState: " + aeState); - } - - switch (cameraState) { - case STATE_PREVIEW: { - // We have nothing to do when the camera preview is working normally. - break; - } - - case STATE_WAITING_FOCUS: { - if (afState == null) { - cameraState = CameraState.STATE_CAPTURING; - captureStillPicture(); - } else if (afState == CaptureResult.CONTROL_AF_STATE_FOCUSED_LOCKED || - afState == CaptureResult.CONTROL_AF_STATE_NOT_FOCUSED_LOCKED) { - // CONTROL_AE_STATE can be null on some devices - - if (aeState == null || - aeState == CaptureResult.CONTROL_AE_STATE_CONVERGED) { - cameraState = CameraState.STATE_CAPTURING; - captureStillPicture(); - } else { - runPrecaptureSequence(); - } - } - break; - } - - case STATE_WAITING_PRECAPTURE_START: { - // CONTROL_AE_STATE can be null on some devices - if (aeState == null || - aeState == CaptureResult.CONTROL_AE_STATE_CONVERGED || - aeState == CaptureResult.CONTROL_AE_STATE_PRECAPTURE || - aeState == CaptureResult.CONTROL_AE_STATE_FLASH_REQUIRED) { - cameraState = CameraState.STATE_WAITING_PRECAPTURE_DONE; - pictureCaptureRequest.setPreCaptureStartTime(); - } - break; - } - - case STATE_WAITING_PRECAPTURE_DONE: { - // CONTROL_AE_STATE can be null on some devices - if (aeState == null || aeState != CaptureResult.CONTROL_AE_STATE_PRECAPTURE) { - cameraState = CameraState.STATE_CAPTURING; - captureStillPicture(); - } else { - if (pictureCaptureRequest.hitPreCaptureTimeout()) { - unlockAutoFocus(); - } - } - break; - } - } - } - - @Override - public void onCaptureProgressed(@NonNull CameraCaptureSession session, - @NonNull CaptureRequest request, - @NonNull CaptureResult partialResult) { - process(partialResult); - } - - @Override - public void onCaptureCompleted(@NonNull CameraCaptureSession session, - @NonNull CaptureRequest request, - @NonNull TotalCaptureResult result) { - process(result); - } - }; - /** * Trigger auto focus to start and refresh preview capture session. */ @@ -824,7 +843,7 @@ private void startAutoFocus() { CaptureRequest.CONTROL_AF_TRIGGER, CaptureRequest.CONTROL_AF_TRIGGER_START); try { - mPreviewSession.capture(mPreviewRequestBuilder.build(), mCaptureCallback, + captureSession.capture(mPreviewRequestBuilder.build(), mCaptureCallback, mBackgroundHandler); } catch (CameraAccessException e) { pictureCaptureRequest.error("cannotStartAutofocus", e.getMessage(), null); @@ -865,15 +884,15 @@ private void unlockAutoFocus() { CaptureRequest.CONTROL_AF_TRIGGER, CameraMetadata.CONTROL_AF_TRIGGER_CANCEL); // Reset AE state. If we don't call this then the preview won't show AE again. - mPreviewRequestBuilder.set(CaptureRequest.CONTROL_AE_PRECAPTURE_TRIGGER, - CaptureRequest.CONTROL_AE_PRECAPTURE_TRIGGER_IDLE); + mPreviewRequestBuilder.set(CaptureRequest.CONTROL_AE_PRECAPTURE_TRIGGER, + CaptureRequest.CONTROL_AE_PRECAPTURE_TRIGGER_IDLE); updateFocus(mPreviewRequestBuilder); updateFlash(mPreviewRequestBuilder); updateExposureMode(mPreviewRequestBuilder); try { - mPreviewSession.capture(mPreviewRequestBuilder.build(), null, null); + captureSession.capture(mPreviewRequestBuilder.build(), null, mBackgroundHandler); } catch (CameraAccessException ignored) { } @@ -914,7 +933,7 @@ public void stopVideoRecording(@NonNull final Result result) { recordingVideo = false; try { - mPreviewSession.abortCaptures(); + captureSession.abortCaptures(); mediaRecorder.stop(); } catch (CameraAccessException | IllegalStateException e) { // Ignore exceptions and try to continue (changes are camera session already aborted capture) @@ -1002,7 +1021,7 @@ public void setFlashMode(@NonNull final Result result, FlashMode newMode) // If switching directly from torch to auto or on, make sure we turn off the torch. if (oldFlashMode == FlashMode.torch && newMode != FlashMode.torch && newMode != FlashMode.off) { // TODO: why cant we just call refresh preview here? - mPreviewSession.setRepeatingRequest( + captureSession.setRepeatingRequest( mPreviewRequestBuilder.build(), new CaptureCallback() { private boolean isFinished = false; @@ -1228,7 +1247,7 @@ public void setExposureOffset(@NonNull final Result result, double offset) // Apply it updateExposureMode(mPreviewRequestBuilder); // TODO: refresh preview session? - this.mPreviewSession.setRepeatingRequest(mPreviewRequestBuilder.build(), null, null); + this.captureSession.setRepeatingRequest(mPreviewRequestBuilder.build(), null, mBackgroundHandler); result.success(offset); } @@ -1259,7 +1278,7 @@ public void setZoomLevel(@NonNull final Result result, float zoom) throws Camera if (mPreviewRequestBuilder != null) { final Rect computedZoom = cameraZoom.computeZoom(zoom); mPreviewRequestBuilder.set(CaptureRequest.SCALER_CROP_REGION, computedZoom); - mPreviewSession.setRepeatingRequest(mPreviewRequestBuilder.build(), null, null); + captureSession.setRepeatingRequest(mPreviewRequestBuilder.build(), null, mBackgroundHandler); } result.success(null); @@ -1341,7 +1360,7 @@ public void onListen(Object o, EventChannel.EventSink imageStreamSink) { @Override public void onCancel(Object o) { - imageStreamReader.setOnImageAvailableListener(null, null); + imageStreamReader.setOnImageAvailableListener(null, mBackgroundHandler); } }); } @@ -1376,23 +1395,24 @@ private void setImageStreamImageAvailableListener(final EventChannel.EventSink i imageStreamSink.success(imageBuffer); img.close(); }, - null); + mBackgroundHandler + ); } public void stopImageStream() throws CameraAccessException { if (imageStreamReader != null) { - imageStreamReader.setOnImageAvailableListener(null, null); + imageStreamReader.setOnImageAvailableListener(null, mBackgroundHandler); } startPreview(); } private void closeCaptureSession() { - if (mPreviewSession != null) { + if (captureSession != null) { Log.i(TAG, "closeCaptureSession"); - mPreviewSession.close(); - mPreviewSession = null; + captureSession.close(); + captureSession = null; } } diff --git a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/ImageSaver.java b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/ImageSaver.java index 12d2accce199..954818606db9 100644 --- a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/ImageSaver.java +++ b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/ImageSaver.java @@ -24,6 +24,12 @@ public class ImageSaver implements Runnable { */ private final File mFile; + /** + * For running background tasks + */ + private Handler mBackgroundHandler; + + /** * * Used to finish the picture capture request @@ -34,6 +40,7 @@ public class ImageSaver implements Runnable { mImage = image; mFile = file; mPictureCaptureRequest = pictureCaptureRequest; + mBackgroundHandler = mBackgroundHandler; } @Override From 9b74bc4ec7f743b9e936ba17453fd1f95e274b4a Mon Sep 17 00:00:00 2001 From: Andrew Coutts Date: Mon, 1 Mar 2021 08:06:46 -0500 Subject: [PATCH 005/114] Can't fix delay on pixel yet --- .../io/flutter/plugins/camera/Camera.java | 25 ++++++++++--------- .../example/ios/Flutter/Flutter.podspec | 18 +++++++++++++ .../xcshareddata/IDEWorkspaceChecks.plist | 8 ++++++ 3 files changed, 39 insertions(+), 12 deletions(-) create mode 100644 packages/camera/camera/example/ios/Flutter/Flutter.podspec create mode 100644 packages/camera/camera/example/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist 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 55d38e5dd9e2..e9a0d4857f85 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 @@ -127,14 +127,7 @@ public class Camera { * A {@link Handler} for running tasks in the background. */ private Handler mBackgroundHandler; - - /** - * The preview surface which will be provided to still capture requests to keep the - * preview going during capture. - */ - private Surface previewSurface; - - /** + /** * This a callback object for the {@link ImageReader}. "onImageAvailable" will be called when a * still image is ready to be saved. */ @@ -149,6 +142,11 @@ public void onImageAvailable(ImageReader reader) { } }; + /** + * The preview surface which will be provided to still capture requests to keep the + * preview going during capture. + */ + private Surface previewSurface; /** * An additional thread for running tasks that shouldn't block the UI. */ @@ -380,7 +378,9 @@ private void prepareMediaRecorder(String outputFilePath) throws IOException { public void open(String imageFormatGroup) throws CameraAccessException { pictureImageReader = ImageReader.newInstance( - captureSize.getWidth(), captureSize.getHeight(), ImageFormat.JPEG, 2); + captureSize.getWidth(), + captureSize.getHeight(), + ImageFormat.JPEG, 2); Integer imageFormat = supportedImageFormats.get(imageFormatGroup); if (imageFormat == null) { @@ -488,7 +488,7 @@ private void createCaptureSession( // Save surface if it's preview if (templateType == CameraDevice.TEMPLATE_PREVIEW) { - previewSurface =flutterSurface; + previewSurface = flutterSurface; } List remainingSurfaces = Arrays.asList(surfaces); @@ -662,8 +662,8 @@ private void takePictureAfterPrecapture() { cameraDevice.createCaptureRequest(CameraDevice.TEMPLATE_STILL_CAPTURE); stillBuilder.addTarget(pictureImageReader.getSurface()); - // Add the preview surface to reduce delay when capturing - stillBuilder.addTarget(previewSurface); + // Add the preview surface to show image after it's captured +// stillBuilder.addTarget(previewSurface); // Zoom stillBuilder.set( @@ -715,6 +715,7 @@ public void onCaptureCompleted(@NonNull CameraCaptureSession session, Log.i(TAG, "sending capture request"); captureSession.capture(stillBuilder.build(), CaptureCallback, mBackgroundHandler); // captureSession.setRepeatingRequest(mPreviewRequestBuilder.build(), null, mBackgroundHandler); +// pictureCaptureRequest.finish("a"); } catch (CameraAccessException e) { pictureCaptureRequest.error("cameraAccess", e.getMessage(), null); } diff --git a/packages/camera/camera/example/ios/Flutter/Flutter.podspec b/packages/camera/camera/example/ios/Flutter/Flutter.podspec new file mode 100644 index 000000000000..2c4421cfe51e --- /dev/null +++ b/packages/camera/camera/example/ios/Flutter/Flutter.podspec @@ -0,0 +1,18 @@ +# +# NOTE: This podspec is NOT to be published. It is only used as a local source! +# This is a generated file; do not edit or check into version control. +# + +Pod::Spec.new do |s| + s.name = 'Flutter' + s.version = '1.0.0' + s.summary = 'High-performance, high-fidelity mobile apps.' + s.homepage = 'https://flutter.io' + s.license = { :type => 'MIT' } + s.author = { 'Flutter Dev Team' => 'flutter-dev@googlegroups.com' } + s.source = { :git => 'https://github.com/flutter/engine', :tag => s.version.to_s } + s.ios.deployment_target = '8.0' + # Framework linking is handled by Flutter tooling, not CocoaPods. + # Add a placeholder to satisfy `s.dependency 'Flutter'` plugin podspecs. + s.vendored_frameworks = 'path/to/nothing' +end diff --git a/packages/camera/camera/example/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/packages/camera/camera/example/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 000000000000..18d981003d68 --- /dev/null +++ b/packages/camera/camera/example/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + From ac72eb80d0024ea1fc924b23a4f64b2973ad388e Mon Sep 17 00:00:00 2001 From: Andrew Coutts Date: Mon, 1 Mar 2021 11:04:23 -0500 Subject: [PATCH 006/114] Focus lock works now for image capture --- .../io/flutter/plugins/camera/Camera.java | 399 ++++++++++-------- packages/camera/camera/example/lib/main.dart | 2 +- 2 files changed, 233 insertions(+), 168 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 e9a0d4857f85..15a04d446250 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 @@ -13,12 +13,10 @@ import android.graphics.SurfaceTexture; import android.hardware.camera2.CameraAccessException; import android.hardware.camera2.CameraCaptureSession; -import android.hardware.camera2.CameraCaptureSession.CaptureCallback; import android.hardware.camera2.CameraCharacteristics; import android.hardware.camera2.CameraDevice; import android.hardware.camera2.CameraManager; import android.hardware.camera2.CameraMetadata; -import android.hardware.camera2.CaptureFailure; import android.hardware.camera2.CaptureRequest; import android.hardware.camera2.CaptureResult; import android.hardware.camera2.TotalCaptureResult; @@ -111,14 +109,14 @@ public class Camera { private final CameraZoom cameraZoom; private final CameraCharacteristics cameraCharacteristics; private final Activity activity; - /** - * Whether the current camera device supports auto focus or not. - */ - private final boolean mAutoFocusSupported = false; /** * This manages the state of the camera and the current capture request. */ PictureCaptureRequest pictureCaptureRequest; + /** + * Whether the current camera device supports auto focus or not. + */ + private boolean mAutoFocusSupported = true; /** * The state of the camera. By default we are in the preview state. */ @@ -142,11 +140,7 @@ public void onImageAvailable(ImageReader reader) { } }; - /** - * The preview surface which will be provided to still capture requests to keep the - * preview going during capture. - */ - private Surface previewSurface; + /** * An additional thread for running tasks that shouldn't block the UI. */ @@ -162,7 +156,6 @@ public void onImageAvailable(ImageReader reader) { /** * {@link CaptureRequest} generated by {@link #mPreviewRequestBuilder} */ - private CaptureRequest mPreviewRequest; private MediaRecorder mediaRecorder; private boolean recordingVideo; private File videoRecordingFile; @@ -170,17 +163,17 @@ public void onImageAvailable(ImageReader reader) { * Flash mode setting of the current camera. Initialize to off because * we don't know if the current camera supports flash yet. */ - private FlashMode currentFlashMode = FlashMode.off; + private FlashMode currentFlashMode; /** * Exposure mode setting of the current camera. Initialize to auto * because all cameras support autoexposure by default. */ - private ExposureMode exposureMode = ExposureMode.auto; + private ExposureMode exposureMode; /** * Focus mode setting of the current camera. Initialize to locked because * we don't know if the current camera supports autofocus yet. */ - private FocusMode currentFocusMode = FocusMode.locked; + private FocusMode currentFocusMode; /** * Whether or not to use autofocus. */ @@ -198,10 +191,6 @@ public void onImageAvailable(ImageReader reader) { = new CameraCaptureSession.CaptureCallback() { private void process(CaptureResult result) { - if (pictureCaptureRequest == null) { - return; - } - Integer aeState = result.get(CaptureResult.CONTROL_AE_STATE); Integer afState = result.get(CaptureResult.CONTROL_AF_STATE); @@ -219,12 +208,14 @@ private void process(CaptureResult result) { if (afState == null) { cameraState = CameraState.STATE_CAPTURING; takePictureAfterPrecapture(); - } else if (afState == CaptureResult.CONTROL_AF_STATE_FOCUSED_LOCKED || - afState == CaptureResult.CONTROL_AF_STATE_NOT_FOCUSED_LOCKED) { + } else if ( + afState == CaptureRequest.CONTROL_AF_STATE_PASSIVE_SCAN || + afState == CaptureRequest.CONTROL_AF_STATE_FOCUSED_LOCKED || + afState == CaptureRequest.CONTROL_AF_STATE_NOT_FOCUSED_LOCKED) { // CONTROL_AE_STATE can be null on some devices if (aeState == null || - aeState == CaptureResult.CONTROL_AE_STATE_CONVERGED) { + aeState == CaptureRequest.CONTROL_AE_STATE_CONVERGED) { cameraState = CameraState.STATE_CAPTURING; takePictureAfterPrecapture(); } else { @@ -303,32 +294,69 @@ public Camera( this.currentFocusMode = FocusMode.auto; this.exposureOffset = 0; + // Get camera characteristics and check for supported features cameraCharacteristics = cameraManager.getCameraCharacteristics(cameraName); getAvailableFpsRange(cameraCharacteristics); + checkAutoFocusSupported(); + checkFlashSupported(); + + // Setup orientation sensorOrientation = cameraCharacteristics.get(CameraCharacteristics.SENSOR_ORIENTATION); isFrontFacing = cameraCharacteristics.get(CameraCharacteristics.LENS_FACING) == CameraMetadata.LENS_FACING_FRONT; + deviceOrientationListener = + new DeviceOrientationManager(activity, dartMessenger, isFrontFacing, sensorOrientation); + deviceOrientationListener.start(); + + // Resolution configuration ResolutionPreset preset = ResolutionPreset.valueOf(resolutionPreset); recordingProfile = CameraUtils.getBestAvailableCamcorderProfileForResolutionPreset(cameraName, preset); captureSize = new Size(recordingProfile.videoFrameWidth, recordingProfile.videoFrameHeight); previewSize = computeBestPreviewSize(cameraName, preset); + + // Zoom setup cameraZoom = new CameraZoom( cameraCharacteristics.get(CameraCharacteristics.SENSOR_INFO_ACTIVE_ARRAY_SIZE), cameraCharacteristics.get(CameraCharacteristics.SCALER_AVAILABLE_MAX_DIGITAL_ZOOM)); - deviceOrientationListener = - new DeviceOrientationManager(activity, dartMessenger, isFrontFacing, sensorOrientation); - deviceOrientationListener.start(); - - // Check if the flash is supported. - Boolean flashAvailable = cameraCharacteristics.get(CameraCharacteristics.FLASH_INFO_AVAILABLE); - mFlashSupported = flashAvailable == null ? false : flashAvailable; + // Start background thread. startBackgroundThread(); } + /** + * Check if the auto focus is supported. + */ + private void checkAutoFocusSupported() { + int[] modes = cameraCharacteristics.get(CameraCharacteristics.CONTROL_AF_AVAILABLE_MODES); + Log.i(TAG, "checkAutoFocusSupported | modes:"); + for (int mode : modes) { + Log.i(TAG, "checkAutoFocusSupported | ==> " + mode); + } + + // Check if fixed focal length lens. If LENS_INFO_MINIMUM_FOCUS_DISTANCE=0, then this is fixed. + // Can be null on some devices. + float minFocus = cameraCharacteristics.get(CameraCharacteristics.LENS_INFO_MINIMUM_FOCUS_DISTANCE); + float maxFocus = cameraCharacteristics.get(CameraCharacteristics.LENS_INFO_HYPERFOCAL_DISTANCE); + final boolean isFixedLength = minFocus == 0; + Log.i(TAG, "checkAutoFocusSupported | minFocus " + minFocus + " | maxFocus: " + maxFocus); + + + mAutoFocusSupported = !(modes == null || modes.length == 0 || + (modes.length == 1 && modes[0] == CameraCharacteristics.CONTROL_AF_MODE_OFF)); + Log.i(TAG, "checkAutoFocusSupported: " + mAutoFocusSupported); + } + + /** + * Check if the flash is supported. + */ + private void checkFlashSupported() { + Boolean available = cameraCharacteristics.get(CameraCharacteristics.FLASH_INFO_AVAILABLE); + mFlashSupported = available == null ? false : available; + } + /** * Load available FPS range for the current camera and update the available fps range with it. * @@ -486,11 +514,6 @@ private void createCaptureSession( Surface flutterSurface = new Surface(surfaceTexture); mPreviewRequestBuilder.addTarget(flutterSurface); - // Save surface if it's preview - if (templateType == CameraDevice.TEMPLATE_PREVIEW) { - previewSurface = flutterSurface; - } - List remainingSurfaces = Arrays.asList(surfaces); if (templateType != CameraDevice.TEMPLATE_PREVIEW) { // If it is not preview mode, add all surfaces as targets. @@ -514,7 +537,7 @@ public void onConfigured(@NonNull CameraCaptureSession session) { captureSession = session; updateFpsRange(); - updateFocus(mPreviewRequestBuilder); + updateFocusMode(mPreviewRequestBuilder); updateFlash(mPreviewRequestBuilder); updateExposureMode(mPreviewRequestBuilder); @@ -566,6 +589,7 @@ private void createCaptureSession( cameraDevice.createCaptureSession(surfaces, callback, mBackgroundHandler); } + // Send a repeating request to refresh our capture session. private void refreshPreviewCaptureSession( @Nullable Runnable onSuccessCallback, @NonNull ErrorCallback onErrorCallback) { Log.i(TAG, "refreshPreviewCaptureSession"); @@ -575,14 +599,19 @@ private void refreshPreviewCaptureSession( } try { -// mPreviewRequestBuilder.set(CaptureRequest.CONTROL_AE_PRECAPTURE_TRIGGER, -// CaptureRequest.CONTROL_AE_PRECAPTURE_TRIGGER_START); captureSession.setRepeatingRequest(mPreviewRequestBuilder.build(), - mCaptureCallback, mBackgroundHandler); + new CameraCaptureSession.CaptureCallback() { + @Override + public void onCaptureCompleted(@NonNull CameraCaptureSession session, + @NonNull CaptureRequest request, + @NonNull TotalCaptureResult result) { + if (onSuccessCallback != null) { + onSuccessCallback.run(); + } + } + }, mBackgroundHandler); + - if (onSuccessCallback != null) { - onSuccessCallback.run(); - } } catch (CameraAccessException | IllegalStateException | IllegalArgumentException e) { onErrorCallback.onError("cameraAccess", e.getMessage()); } @@ -662,9 +691,6 @@ private void takePictureAfterPrecapture() { cameraDevice.createCaptureRequest(CameraDevice.TEMPLATE_STILL_CAPTURE); stillBuilder.addTarget(pictureImageReader.getSurface()); - // Add the preview surface to show image after it's captured -// stillBuilder.addTarget(previewSurface); - // Zoom stillBuilder.set( CaptureRequest.SCALER_CROP_REGION, @@ -672,7 +698,7 @@ private void takePictureAfterPrecapture() { // Set focus / flash from preview mode updateFlash(stillBuilder); - updateFocus(stillBuilder); + updateFocusMode(stillBuilder); updateExposureMode(stillBuilder); // Orientation @@ -811,7 +837,7 @@ void updateFlash(CaptureRequest.Builder requestBuilder) { CaptureRequest.FLASH_MODE_OFF); break; - // TODO: to be implemented someday. Need to add it to dart and iOS. + // TODO: to be implemented someday. Need to add it to dart/iOS as another flash mode setting. // case autoRedEye: // requestBuilder.set(CaptureRequest.CONTROL_AE_MODE, // CaptureRequest.CONTROL_AE_MODE_ON_AUTO_FLASH_REDEYE); @@ -835,22 +861,6 @@ private int getOrientation(int rotation) { return (ORIENTATIONS.get(rotation) + sensorOrientation + 270) % 360; } - /** - * Trigger auto focus to start and refresh preview capture session. - */ - private void startAutoFocus() { - Log.i(TAG, "startAutoFocus"); - mPreviewRequestBuilder.set( - CaptureRequest.CONTROL_AF_TRIGGER, CaptureRequest.CONTROL_AF_TRIGGER_START); - - try { - captureSession.capture(mPreviewRequestBuilder.build(), mCaptureCallback, - mBackgroundHandler); - } catch (CameraAccessException e) { - pictureCaptureRequest.error("cannotStartAutofocus", e.getMessage(), null); - } - } - /** * Start capturing a picture, doing autofocus first. */ @@ -859,19 +869,22 @@ private void runPictureAutoFocus() { assert (pictureCaptureRequest != null); cameraState = CameraState.STATE_WAITING_FOCUS; - lockAutoFocus(); + lockAutoFocus(null); } /** * Start the autofocus routine. */ - private void lockAutoFocus() { + private void lockAutoFocus(Runnable callback) { Log.i(TAG, "lockAutoFocus"); mPreviewRequestBuilder.set( CaptureRequest.CONTROL_AF_TRIGGER, CaptureRequest.CONTROL_AF_TRIGGER_START); - refreshPreviewCaptureSession( - null, (code, message) -> pictureCaptureRequest.error(code, message, null)); + try { + captureSession.capture(mPreviewRequestBuilder.build(), mCaptureCallback, + mBackgroundHandler); + } catch (CameraAccessException e) { + } } /** @@ -879,20 +892,25 @@ private void lockAutoFocus() { */ private void unlockAutoFocus() { Log.i(TAG, "unlockAutoFocus"); + try { + // Cancel existing AF state + mPreviewRequestBuilder.set( + CaptureRequest.CONTROL_AF_TRIGGER, CameraMetadata.CONTROL_AF_TRIGGER_CANCEL); + captureSession.capture(mPreviewRequestBuilder.build(), null, + mBackgroundHandler); - // Reset AF state - mPreviewRequestBuilder.set( - CaptureRequest.CONTROL_AF_TRIGGER, CameraMetadata.CONTROL_AF_TRIGGER_CANCEL); + // Set AE state to idle again + mPreviewRequestBuilder.set(CaptureRequest.CONTROL_AE_PRECAPTURE_TRIGGER, + CaptureRequest.CONTROL_AE_PRECAPTURE_TRIGGER_IDLE); - // Reset AE state. If we don't call this then the preview won't show AE again. - mPreviewRequestBuilder.set(CaptureRequest.CONTROL_AE_PRECAPTURE_TRIGGER, - CaptureRequest.CONTROL_AE_PRECAPTURE_TRIGGER_IDLE); + // Set AF state to idle again + mPreviewRequestBuilder.set( + CaptureRequest.CONTROL_AF_TRIGGER, CameraMetadata.CONTROL_AF_TRIGGER_CANCEL); - updateFocus(mPreviewRequestBuilder); - updateFlash(mPreviewRequestBuilder); - updateExposureMode(mPreviewRequestBuilder); + updateFocusMode(mPreviewRequestBuilder); + updateFlash(mPreviewRequestBuilder); + updateExposureMode(mPreviewRequestBuilder); - try { captureSession.capture(mPreviewRequestBuilder.build(), null, mBackgroundHandler); } catch (CameraAccessException ignored) { } @@ -1000,17 +1018,8 @@ public void resumeVideoRecording(@NonNull final Result result) { * @param newMode * @throws CameraAccessException */ - public void setFlashMode(@NonNull final Result result, FlashMode newMode) - throws CameraAccessException { - // Check if the current camera can modify the flash mode. - Boolean flashAvailable = - cameraManager - .getCameraCharacteristics(cameraDevice.getId()) - .get(CameraCharacteristics.FLASH_INFO_AVAILABLE); - - // Check if flash is available. - if (flashAvailable == null || !flashAvailable) { - result.error("setFlashModeFailed", "Device does not have flash capabilities", null); + public void setFlashMode(@NonNull final Result result, FlashMode newMode) { + if (currentFlashMode == newMode) { return; } @@ -1019,52 +1028,56 @@ public void setFlashMode(@NonNull final Result result, FlashMode newMode) currentFlashMode = newMode; updateFlash(mPreviewRequestBuilder); - // If switching directly from torch to auto or on, make sure we turn off the torch. - if (oldFlashMode == FlashMode.torch && newMode != FlashMode.torch && newMode != FlashMode.off) { - // TODO: why cant we just call refresh preview here? - captureSession.setRepeatingRequest( - mPreviewRequestBuilder.build(), - new CaptureCallback() { - private boolean isFinished = false; - - @Override - public void onCaptureCompleted( - @NonNull CameraCaptureSession session, - @NonNull CaptureRequest request, - @NonNull TotalCaptureResult captureResult) { - if (isFinished) { - return; - } - - updateFlash(mPreviewRequestBuilder); - refreshPreviewCaptureSession( - () -> { - result.success(null); - isFinished = true; - }, - (code, message) -> - result.error("setFlashModeFailed", "Could not set flash mode.", null)); - } - - @Override - public void onCaptureFailed( - @NonNull CameraCaptureSession session, - @NonNull CaptureRequest request, - @NonNull CaptureFailure failure) { - if (isFinished) { - return; - } - - result.error("setFlashModeFailed", "Could not set flash mode.", null); - isFinished = true; - } - }, - null); - } else { - refreshPreviewCaptureSession( - () -> result.success(null), - (code, message) -> result.error("setFlashModeFailed", "Could not set flash mode.", null)); - } + refreshPreviewCaptureSession( + () -> result.success(null), + (code, message) -> result.error("setFlashModeFailed", "Could not set flash mode.", null)); + +// // If switching directly from torch to auto or on, make sure we turn off the torch. +// if (oldFlashMode == FlashMode.torch && newMode != FlashMode.torch && newMode != FlashMode.off) { +// // TODO: why cant we just call refresh preview here? +// captureSession.setRepeatingRequest( +// mPreviewRequestBuilder.build(), +// new CaptureCallback() { +// private boolean isFinished = false; +// +// @Override +// public void onCaptureCompleted( +// @NonNull CameraCaptureSession session, +// @NonNull CaptureRequest request, +// @NonNull TotalCaptureResult captureResult) { +// if (isFinished) { +// return; +// } +// +// updateFlash(mPreviewRequestBuilder); +// refreshPreviewCaptureSession( +// () -> { +// result.success(null); +// isFinished = true; +// }, +// (code, message) -> +// result.error("setFlashModeFailed", "Could not set flash mode.", null)); +// } +// +// @Override +// public void onCaptureFailed( +// @NonNull CameraCaptureSession session, +// @NonNull CaptureRequest request, +// @NonNull CaptureFailure failure) { +// if (isFinished) { +// return; +// } +// +// result.error("setFlashModeFailed", "Could not set flash mode.", null); +// isFinished = true; +// } +// }, +// null); +// } else { +// refreshPreviewCaptureSession( +// () -> result.success(null), +// (code, message) -> result.error("setFlashModeFailed", "Could not set flash mode.", null)); +// } } /** @@ -1114,21 +1127,65 @@ public void setExposurePoint(@NonNull final Result result, Double x, Double y) */ public void setFocusMode(@NonNull final Result result, FocusMode newMode) throws CameraAccessException { + if (currentFocusMode == newMode) { + return; + } + + Log.i(TAG, "setFocusMode: " + newMode); + + // Set new focus mode currentFocusMode = newMode; - updateFocus(mPreviewRequestBuilder); + // Sync new focus mode to the current capture request builder + updateFocusMode(mPreviewRequestBuilder); + + // Now depending on the new mode we either want to restart the AF routine (if setting to auto) + // or we want to trigger a one-time focus and then set AF to idle (locked mode). switch (newMode) { case auto: + Log.i(TAG, "Triggering AF start with mode " + currentFocusMode); + // Reset state of autofocus so it goes back to passive scanning. + mPreviewRequestBuilder.set( + CaptureRequest.CONTROL_AF_TRIGGER, CaptureRequest.CONTROL_AF_TRIGGER_CANCEL); + + // Refresh preview session using repeating request as it will be in CONTROL_AF_MODE_CONTINUOUS_PICTURE refreshPreviewCaptureSession( null, (code, message) -> result.error("setFocusMode", message, null)); break; + case locked: - startAutoFocus(); + // AF mode will be in Auto so we just want to perform one AF routine + mPreviewRequestBuilder.set( + CaptureRequest.CONTROL_AF_TRIGGER, CaptureRequest.CONTROL_AF_TRIGGER_START); + + // Refresh the AF once. When the AF start is completed triggering then we will set it to idle mode. + // If we don't wait for the callback like this, then setting it to idle just resets the focus to infinity + // on some devices like Sony XZ. + try { + captureSession.capture(mPreviewRequestBuilder.build(), new CameraCaptureSession.CaptureCallback() { + @Override + public void onCaptureCompleted(@NonNull CameraCaptureSession session, + @NonNull CaptureRequest request, + @NonNull TotalCaptureResult _result) { + Log.i(TAG, "Success after triggering AF start for locked focus"); + + mPreviewRequestBuilder.set( + CaptureRequest.CONTROL_AF_TRIGGER, CaptureRequest.CONTROL_AF_TRIGGER_IDLE); + refreshPreviewCaptureSession( + null, (code, message) -> result.error("setFocusMode", message, null)); + } + }, + mBackgroundHandler); + } catch (CameraAccessException e) { + result.error("setFocusMode", e.getMessage(), null); + } break; } + result.success(null); } + public void setFocusPoint(@NonNull final Result result, Double x, Double y) throws CameraAccessException { // Check if focus point functionality is available. @@ -1304,40 +1361,48 @@ private void updateFpsRange() { mPreviewRequestBuilder.set(CaptureRequest.CONTROL_AE_TARGET_FPS_RANGE, fpsRange); } - private void updateFocus(CaptureRequest.Builder requestBuilder) { - if (useAutoFocus) { - int[] modes = cameraCharacteristics.get(CameraCharacteristics.CONTROL_AF_AVAILABLE_MODES); - // Auto focus is not supported - if (modes == null - || modes.length == 0 - || (modes.length == 1 && modes[0] == CameraCharacteristics.CONTROL_AF_MODE_OFF)) { - useAutoFocus = false; - mPreviewRequestBuilder.set( - CaptureRequest.CONTROL_AF_MODE, CaptureRequest.CONTROL_AF_MODE_OFF); - } else { - // Applying auto focus - switch (currentFocusMode) { - case locked: - requestBuilder.set( - CaptureRequest.CONTROL_AF_MODE, CaptureRequest.CONTROL_AF_MODE_AUTO); - break; - case auto: - mPreviewRequestBuilder.set( - CaptureRequest.CONTROL_AF_MODE, - recordingVideo - ? CaptureRequest.CONTROL_AF_MODE_CONTINUOUS_VIDEO - : CaptureRequest.CONTROL_AF_MODE_CONTINUOUS_PICTURE); - default: - break; - } - MeteringRectangle afRect = cameraRegions.getAFMeteringRectangle(); - mPreviewRequestBuilder.set( - CaptureRequest.CONTROL_AF_REGIONS, - afRect == null ? null : new MeteringRectangle[]{afRect}); - } + /** + * Sync the focus mode setting to the provided capture request builder. + * + * @param requestBuilder + */ + private void updateFocusMode(CaptureRequest.Builder requestBuilder) { + Log.i(TAG, "updateFocusMode"); + if (!mAutoFocusSupported) { + useAutoFocus = false; + requestBuilder.set( + CaptureRequest.CONTROL_AF_MODE, CaptureRequest.CONTROL_AF_MODE_OFF); } else { - mPreviewRequestBuilder.set(CaptureRequest.CONTROL_AF_MODE, CaptureRequest.CONTROL_AF_MODE_OFF); + switch (currentFocusMode) { + case locked: + useAutoFocus = false; + requestBuilder.set( + CaptureRequest.CONTROL_AF_MODE, CaptureRequest.CONTROL_AF_MODE_AUTO); + break; + + case auto: + useAutoFocus = true; + requestBuilder.set( + CaptureRequest.CONTROL_AF_MODE, + recordingVideo + ? CaptureRequest.CONTROL_AF_MODE_CONTINUOUS_VIDEO + : CaptureRequest.CONTROL_AF_MODE_CONTINUOUS_PICTURE); + default: + break; + } } + + // Some devices use an extremely high noise reduction setting by default (pixel 4 selfie mode), which + // causes the preview/capture to look blurry and out of focus. To fix this we set NR to off. + // TODO: we should add a noise reduction setting in dart/ios in the future. + requestBuilder.set(CaptureRequest.NOISE_REDUCTION_MODE, CaptureRequest.NOISE_REDUCTION_MODE_OFF); + + + // Update metering + MeteringRectangle afRect = cameraRegions.getAFMeteringRectangle(); + requestBuilder.set( + CaptureRequest.CONTROL_AF_REGIONS, + afRect == null ? null : new MeteringRectangle[]{afRect}); } public void startPreview() throws CameraAccessException { diff --git a/packages/camera/camera/example/lib/main.dart b/packages/camera/camera/example/lib/main.dart index 6244aa5a8e37..6c15b7686006 100644 --- a/packages/camera/camera/example/lib/main.dart +++ b/packages/camera/camera/example/lib/main.dart @@ -565,7 +565,7 @@ class _CameraExampleHomeState extends State } controller = CameraController( cameraDescription, - ResolutionPreset.medium, + ResolutionPreset.max, enableAudio: enableAudio, imageFormatGroup: ImageFormatGroup.jpeg, ); From 2f19f1fe46469adb0d999b26b88466bbd5e7e7cb Mon Sep 17 00:00:00 2001 From: Andrew Coutts Date: Mon, 1 Mar 2021 11:48:21 -0500 Subject: [PATCH 007/114] Photo capture works in focus auto/locked state now --- .../io/flutter/plugins/camera/Camera.java | 61 +++++++++---------- 1 file changed, 29 insertions(+), 32 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 15a04d446250..cbef9a3028c2 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 @@ -195,7 +195,7 @@ private void process(CaptureResult result) { Integer afState = result.get(CaptureResult.CONTROL_AF_STATE); if (cameraState != CameraState.STATE_PREVIEW) { - Log.i(TAG, "mCaptureCallback | state: " + cameraState + " | afState: " + afState + " | aeState: " + aeState); + Log.i(TAG, "mCaptureCallback | state: " + cameraState + " | afState: " + afState + " | aeState: " + aeState); } switch (cameraState) { @@ -599,17 +599,10 @@ private void refreshPreviewCaptureSession( } try { - captureSession.setRepeatingRequest(mPreviewRequestBuilder.build(), - new CameraCaptureSession.CaptureCallback() { - @Override - public void onCaptureCompleted(@NonNull CameraCaptureSession session, - @NonNull CaptureRequest request, - @NonNull TotalCaptureResult result) { - if (onSuccessCallback != null) { - onSuccessCallback.run(); - } - } - }, mBackgroundHandler); + captureSession.setRepeatingRequest( + mPreviewRequestBuilder.build(), + mCaptureCallback, + mBackgroundHandler); } catch (CameraAccessException | IllegalStateException | IllegalArgumentException e) { @@ -655,24 +648,29 @@ public void takePicture(@NonNull final Result result) { private void runPrecaptureSequence() { Log.i(TAG, "runPrecaptureSequence"); try { + // First set precapture state to idle or else it can hang in STATE_WAITING_PRECAPTURE + mPreviewRequestBuilder.set(CaptureRequest.CONTROL_AE_PRECAPTURE_TRIGGER, + CaptureRequest.CONTROL_AE_PRECAPTURE_TRIGGER_IDLE); + captureSession.capture(mPreviewRequestBuilder.build(), mCaptureCallback, + mBackgroundHandler); -// // First set precapture state to idle or else it can hang in STATE_WAITING_PRECAPTURE -// mPreviewRequestBuilder.set(CaptureRequest.CONTROL_AE_PRECAPTURE_TRIGGER, -// CaptureRequest.CONTROL_AE_PRECAPTURE_TRIGGER_IDLE); -// mPreviewSession.capture(mPreviewRequestBuilder.build(), mCaptureCallback, -// mBackgroundHandler); -// refreshPreviewCaptureSession( -// null, (code, message) -> pictureCaptureRequest.error(code, message, null)); + // Repeating request to refresh preview session + refreshPreviewCaptureSession( + null, + (code, message) -> pictureCaptureRequest.error("cameraAccess", message, null)); // Start precapture now cameraState = CameraState.STATE_WAITING_PRECAPTURE_START; mPreviewRequestBuilder.set(CaptureRequest.CONTROL_AE_PRECAPTURE_TRIGGER, CaptureRequest.CONTROL_AE_PRECAPTURE_TRIGGER_START); + + // Trigger one capture to start AE sequence captureSession.capture(mPreviewRequestBuilder.build(), mCaptureCallback, mBackgroundHandler); + } catch (CameraAccessException e) { - pictureCaptureRequest.error("cameraAccess", e.getMessage(), null); + e.printStackTrace(); } } @@ -869,22 +867,24 @@ private void runPictureAutoFocus() { assert (pictureCaptureRequest != null); cameraState = CameraState.STATE_WAITING_FOCUS; - lockAutoFocus(null); + lockAutoFocus(); } /** * Start the autofocus routine. */ - private void lockAutoFocus(Runnable callback) { + private void lockAutoFocus() { Log.i(TAG, "lockAutoFocus"); mPreviewRequestBuilder.set( CaptureRequest.CONTROL_AF_TRIGGER, CaptureRequest.CONTROL_AF_TRIGGER_START); - try { - captureSession.capture(mPreviewRequestBuilder.build(), mCaptureCallback, - mBackgroundHandler); - } catch (CameraAccessException e) { - } + refreshPreviewCaptureSession( + null, (code, message) -> pictureCaptureRequest.error(code, message, null)); +// try { +// captureSession.capture(mPreviewRequestBuilder.build(), mCaptureCallback, +// mBackgroundHandler); +// } catch (CameraAccessException e) { +// } } /** @@ -1127,10 +1127,6 @@ public void setExposurePoint(@NonNull final Result result, Double x, Double y) */ public void setFocusMode(@NonNull final Result result, FocusMode newMode) throws CameraAccessException { - if (currentFocusMode == newMode) { - return; - } - Log.i(TAG, "setFocusMode: " + newMode); // Set new focus mode @@ -1367,7 +1363,8 @@ private void updateFpsRange() { * @param requestBuilder */ private void updateFocusMode(CaptureRequest.Builder requestBuilder) { - Log.i(TAG, "updateFocusMode"); + Log.i(TAG, "updateFocusMode currentFocusMode: " + currentFocusMode); + if (!mAutoFocusSupported) { useAutoFocus = false; requestBuilder.set( From b9f1799dc84e3321a615203a7553ca5fb6b2e970 Mon Sep 17 00:00:00 2001 From: Andrew Coutts Date: Mon, 1 Mar 2021 13:08:58 -0500 Subject: [PATCH 008/114] Setting exposure mode works now --- .../java/io/flutter/plugins/camera/Camera.java | 17 ++++++++++------- .../plugins/camera/PictureCaptureRequest.java | 2 ++ 2 files changed, 12 insertions(+), 7 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 cbef9a3028c2..7a31b90ebfe9 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 @@ -168,7 +168,7 @@ public void onImageAvailable(ImageReader reader) { * Exposure mode setting of the current camera. Initialize to auto * because all cameras support autoexposure by default. */ - private ExposureMode exposureMode; + private ExposureMode currentExposureMode; /** * Focus mode setting of the current camera. Initialize to locked because * we don't know if the current camera supports autofocus yet. @@ -290,7 +290,7 @@ public Camera( this.cameraManager = (CameraManager) activity.getSystemService(Context.CAMERA_SERVICE); this.applicationContext = activity.getApplicationContext(); this.currentFlashMode = FlashMode.off; - this.exposureMode = ExposureMode.auto; + this.currentExposureMode = ExposureMode.auto; this.currentFocusMode = FocusMode.auto; this.exposureOffset = 0; @@ -434,7 +434,7 @@ public void onOpened(@NonNull CameraDevice device) { dartMessenger.sendCameraInitializedEvent( previewSize.getWidth(), previewSize.getHeight(), - exposureMode, + currentExposureMode, currentFocusMode, isExposurePointSupported(), isFocusPointSupported()); @@ -778,11 +778,11 @@ void updateExposureMode(CaptureRequest.Builder requestBuilder) { // Applying auto exposure MeteringRectangle aeRect = cameraRegions.getAEMeteringRectangle(); - mPreviewRequestBuilder.set( + requestBuilder.set( CaptureRequest.CONTROL_AE_REGIONS, aeRect == null ? null : new MeteringRectangle[]{cameraRegions.getAEMeteringRectangle()}); - switch (exposureMode) { + switch (currentExposureMode) { case locked: requestBuilder.set(CaptureRequest.CONTROL_AE_LOCK, true); break; @@ -1089,11 +1089,14 @@ public void setFlashMode(@NonNull final Result result, FlashMode newMode) { */ public void setExposureMode(@NonNull final Result result, ExposureMode newMode) throws CameraAccessException { - exposureMode = newMode; + currentExposureMode = newMode; + updateExposureMode(mPreviewRequestBuilder); refreshPreviewCaptureSession( - () -> result.success(null), + null, (code, message) -> result.error("setExposureModeFailed", "Could not set exposure mode.", null)); + + result.success(null); } public void setExposurePoint(@NonNull final Result result, Double x, Double y) diff --git a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/PictureCaptureRequest.java b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/PictureCaptureRequest.java index d6f3ee40b546..2debeda8be57 100644 --- a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/PictureCaptureRequest.java +++ b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/PictureCaptureRequest.java @@ -56,6 +56,7 @@ class PictureCaptureRequest { * The state of this picture capture request. */ private PictureCaptureRequestState state = PictureCaptureRequestState.STATE_IDLE; + private final Runnable timeoutCallback = () -> error("captureTimeout", "Picture capture request timed out", state.toString()); @@ -128,6 +129,7 @@ public void error( * @return true if the timeout is reached; otherwise false is returned. */ public boolean hitPreCaptureTimeout() { + Log.i("Camera", "hitPreCaptureTimeout | Time elapsed: " + (SystemClock.elapsedRealtime() - preCaptureStartTime)); return (SystemClock.elapsedRealtime() - preCaptureStartTime) > PRECAPTURE_TIMEOUT_MS; } From c0bcbb5e87fbc0dcdf056868128659655ca59bf7 Mon Sep 17 00:00:00 2001 From: Andrew Coutts Date: Mon, 1 Mar 2021 13:23:10 -0500 Subject: [PATCH 009/114] Cleanup --- .../java/io/flutter/plugins/camera/Camera.java | 17 ++++------------- .../plugins/camera/PictureCaptureRequest.java | 6 ------ 2 files changed, 4 insertions(+), 19 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 7a31b90ebfe9..997c85ed832f 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 @@ -206,8 +206,7 @@ private void process(CaptureResult result) { case STATE_WAITING_FOCUS: { if (afState == null) { - cameraState = CameraState.STATE_CAPTURING; - takePictureAfterPrecapture(); + return; } else if ( afState == CaptureRequest.CONTROL_AF_STATE_PASSIVE_SCAN || afState == CaptureRequest.CONTROL_AF_STATE_FOCUSED_LOCKED || @@ -216,7 +215,6 @@ private void process(CaptureResult result) { if (aeState == null || aeState == CaptureRequest.CONTROL_AE_STATE_CONVERGED) { - cameraState = CameraState.STATE_CAPTURING; takePictureAfterPrecapture(); } else { runPrecaptureSequence(); @@ -240,7 +238,6 @@ private void process(CaptureResult result) { case STATE_WAITING_PRECAPTURE_DONE: { // CONTROL_AE_STATE can be null on some devices if (aeState == null || aeState != CaptureResult.CONTROL_AE_STATE_PRECAPTURE) { - cameraState = CameraState.STATE_CAPTURING; takePictureAfterPrecapture(); } else { if (pictureCaptureRequest.hitPreCaptureTimeout()) { @@ -680,6 +677,9 @@ private void runPrecaptureSequence() { */ private void takePictureAfterPrecapture() { Log.i(TAG, "captureStillPicture"); + + cameraState = CameraState.STATE_CAPTURING; + try { if (null == cameraDevice) { return; @@ -730,12 +730,8 @@ public void onCaptureCompleted(@NonNull CameraCaptureSession session, } }; - Log.i(TAG, "stopRepeating"); captureSession.stopRepeating(); - - Log.i(TAG, "abortCaptures"); captureSession.abortCaptures(); - Log.i(TAG, "sending capture request"); captureSession.capture(stillBuilder.build(), CaptureCallback, mBackgroundHandler); // captureSession.setRepeatingRequest(mPreviewRequestBuilder.build(), null, mBackgroundHandler); @@ -880,11 +876,6 @@ private void lockAutoFocus() { refreshPreviewCaptureSession( null, (code, message) -> pictureCaptureRequest.error(code, message, null)); -// try { -// captureSession.capture(mPreviewRequestBuilder.build(), mCaptureCallback, -// mBackgroundHandler); -// } catch (CameraAccessException e) { -// } } /** diff --git a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/PictureCaptureRequest.java b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/PictureCaptureRequest.java index 2debeda8be57..ca6194e6ee65 100644 --- a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/PictureCaptureRequest.java +++ b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/PictureCaptureRequest.java @@ -68,7 +68,6 @@ class PictureCaptureRequest { * @param mFile */ public PictureCaptureRequest(MethodChannel.Result result, File mFile) { - Log.i("Camera", "PictureCaptureRequest constructor"); this.result = result; this.timeoutHandler = new TimeoutHandler(); this.mFile = mFile; @@ -155,14 +154,9 @@ private void onStateChange(PictureCaptureRequestState oldState) { break; case STATE_FINISHED: - timeoutHandler.clearTimeout(timeoutCallback); - break; - case STATE_ERROR: timeoutHandler.clearTimeout(timeoutCallback); - setState(PictureCaptureRequestState.STATE_FINISHED); break; - } } From 2c5f616bb5d656bcb3f029973df28b8c6a1d8da2 Mon Sep 17 00:00:00 2001 From: Andrew Coutts Date: Mon, 1 Mar 2021 13:42:00 -0500 Subject: [PATCH 010/114] Flash working now --- .../io/flutter/plugins/camera/Camera.java | 76 ++++--------------- 1 file changed, 16 insertions(+), 60 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 997c85ed832f..bee5a31caca3 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 @@ -195,7 +195,7 @@ private void process(CaptureResult result) { Integer afState = result.get(CaptureResult.CONTROL_AF_STATE); if (cameraState != CameraState.STATE_PREVIEW) { - Log.i(TAG, "mCaptureCallback | state: " + cameraState + " | afState: " + afState + " | aeState: " + aeState); + Log.i(TAG, "mCaptureCallback | state: " + cameraState + " | afState: " + afState + " | aeState: " + aeState); } switch (cameraState) { @@ -208,7 +208,7 @@ private void process(CaptureResult result) { if (afState == null) { return; } else if ( - afState == CaptureRequest.CONTROL_AF_STATE_PASSIVE_SCAN || +// afState == CaptureRequest.CONTROL_AF_STATE_PASSIVE_SCAN || afState == CaptureRequest.CONTROL_AF_STATE_FOCUSED_LOCKED || afState == CaptureRequest.CONTROL_AF_STATE_NOT_FOCUSED_LOCKED) { // CONTROL_AE_STATE can be null on some devices @@ -601,6 +601,10 @@ private void refreshPreviewCaptureSession( mCaptureCallback, mBackgroundHandler); + if (onSuccessCallback != null) { + onSuccessCallback.run(); + } + } catch (CameraAccessException | IllegalStateException | IllegalArgumentException e) { onErrorCallback.onError("cameraAccess", e.getMessage()); @@ -867,7 +871,7 @@ private void runPictureAutoFocus() { } /** - * Start the autofocus routine. + * Start the autofocus routine on the current capture request. */ private void lockAutoFocus() { Log.i(TAG, "lockAutoFocus"); @@ -1010,65 +1014,14 @@ public void resumeVideoRecording(@NonNull final Result result) { * @throws CameraAccessException */ public void setFlashMode(@NonNull final Result result, FlashMode newMode) { - if (currentFlashMode == newMode) { - return; - } - // Save the new flash mode setting final FlashMode oldFlashMode = currentFlashMode; currentFlashMode = newMode; updateFlash(mPreviewRequestBuilder); refreshPreviewCaptureSession( - () -> result.success(null), + () -> result.success(null), (code, message) -> result.error("setFlashModeFailed", "Could not set flash mode.", null)); - -// // If switching directly from torch to auto or on, make sure we turn off the torch. -// if (oldFlashMode == FlashMode.torch && newMode != FlashMode.torch && newMode != FlashMode.off) { -// // TODO: why cant we just call refresh preview here? -// captureSession.setRepeatingRequest( -// mPreviewRequestBuilder.build(), -// new CaptureCallback() { -// private boolean isFinished = false; -// -// @Override -// public void onCaptureCompleted( -// @NonNull CameraCaptureSession session, -// @NonNull CaptureRequest request, -// @NonNull TotalCaptureResult captureResult) { -// if (isFinished) { -// return; -// } -// -// updateFlash(mPreviewRequestBuilder); -// refreshPreviewCaptureSession( -// () -> { -// result.success(null); -// isFinished = true; -// }, -// (code, message) -> -// result.error("setFlashModeFailed", "Could not set flash mode.", null)); -// } -// -// @Override -// public void onCaptureFailed( -// @NonNull CameraCaptureSession session, -// @NonNull CaptureRequest request, -// @NonNull CaptureFailure failure) { -// if (isFinished) { -// return; -// } -// -// result.error("setFlashModeFailed", "Could not set flash mode.", null); -// isFinished = true; -// } -// }, -// null); -// } else { -// refreshPreviewCaptureSession( -// () -> result.success(null), -// (code, message) -> result.error("setFlashModeFailed", "Could not set flash mode.", null)); -// } } /** @@ -1140,7 +1093,7 @@ public void setFocusMode(@NonNull final Result result, FocusMode newMode) // Refresh preview session using repeating request as it will be in CONTROL_AF_MODE_CONTINUOUS_PICTURE refreshPreviewCaptureSession( - null, (code, message) -> result.error("setFocusMode", message, null)); + () -> result.success(null), (code, message) -> result.error("setFocusMode", message, null)); break; case locked: @@ -1166,13 +1119,15 @@ public void onCaptureCompleted(@NonNull CameraCaptureSession session, } }, mBackgroundHandler); + + result.success(null); } catch (CameraAccessException e) { result.error("setFocusMode", e.getMessage(), null); } break; } - result.success(null); + } @@ -1294,9 +1249,10 @@ public void setExposureOffset(@NonNull final Result result, double offset) exposureOffset = (int) (offset / stepSize); // Apply it updateExposureMode(mPreviewRequestBuilder); - // TODO: refresh preview session? - this.captureSession.setRepeatingRequest(mPreviewRequestBuilder.build(), null, mBackgroundHandler); - result.success(offset); + + // Refresh capture session + refreshPreviewCaptureSession(() -> result.success(null), + (code, message) -> result.error("setExposureModeFailed", "Could not set flash mode.", null)); } public float getMaxZoomLevel() { From 047d44aacb869fc735020d2da163d09421249fd6 Mon Sep 17 00:00:00 2001 From: Andrew Coutts Date: Mon, 1 Mar 2021 13:56:58 -0500 Subject: [PATCH 011/114] Stop background thread when camera is closed --- .../android/src/main/java/io/flutter/plugins/camera/Camera.java | 1 + 1 file changed, 1 insertion(+) 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 bee5a31caca3..a198429c0299 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 @@ -1432,6 +1432,7 @@ private void closeCaptureSession() { public void close() { Log.i(TAG, "close"); + stopBackgroundThread();; closeCaptureSession(); if (cameraDevice != null) { From c591424889c94ed05dea662b0adbd066d38ba9e4 Mon Sep 17 00:00:00 2001 From: Andrew Coutts Date: Mon, 1 Mar 2021 14:21:23 -0500 Subject: [PATCH 012/114] Dont cleanup background thread until cameras topped --- .../io/flutter/plugins/camera/Camera.java | 25 ++++++++----------- 1 file changed, 11 insertions(+), 14 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 a198429c0299..e82eaaf8810a 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 @@ -32,6 +32,7 @@ import android.os.Build.VERSION_CODES; import android.os.Handler; import android.os.HandlerThread; +import android.os.Looper; import android.util.Log; import android.util.Range; import android.util.Rational; @@ -760,10 +761,13 @@ private void startBackgroundThread() { * TODO: call when activity paused */ private void stopBackgroundThread() { - mBackgroundThread.quitSafely(); try { - mBackgroundThread.join(); - mBackgroundThread = null; + if (mBackgroundThread != null) { + mBackgroundThread.quitSafely(); + mBackgroundThread.join(); + mBackgroundThread = null; + } + mBackgroundHandler = null; } catch (InterruptedException e) { pictureCaptureRequest.error("cameraAccess", e.getMessage(), null); @@ -1405,21 +1409,14 @@ private void setImageStreamImageAvailableListener(final EventChannel.EventSink i imageBuffer.put("format", img.getFormat()); imageBuffer.put("planes", planes); - imageStreamSink.success(imageBuffer); + final Handler handler = new Handler(Looper.getMainLooper()); + handler.post(() -> imageStreamSink.success(imageBuffer)); img.close(); }, mBackgroundHandler ); } - public void stopImageStream() throws CameraAccessException { - if (imageStreamReader != null) { - imageStreamReader.setOnImageAvailableListener(null, mBackgroundHandler); - } - startPreview(); - } - - private void closeCaptureSession() { if (captureSession != null) { Log.i(TAG, "closeCaptureSession"); @@ -1431,8 +1428,6 @@ private void closeCaptureSession() { public void close() { Log.i(TAG, "close"); - - stopBackgroundThread();; closeCaptureSession(); if (cameraDevice != null) { @@ -1452,6 +1447,8 @@ public void close() { mediaRecorder.release(); mediaRecorder = null; } + + stopBackgroundThread(); } public void dispose() { From 31f7b1898c4434dc1d0b435c94802f24818c9020 Mon Sep 17 00:00:00 2001 From: Andrew Coutts Date: Mon, 1 Mar 2021 15:09:52 -0500 Subject: [PATCH 013/114] Make sure to set focus mode idle --- .../java/io/flutter/plugins/camera/Camera.java | 16 ++++++++-------- .../plugins/camera/PictureCaptureRequest.java | 11 +++++++++-- 2 files changed, 17 insertions(+), 10 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 e82eaaf8810a..9a348b6f2053 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 @@ -209,7 +209,7 @@ private void process(CaptureResult result) { if (afState == null) { return; } else if ( -// afState == CaptureRequest.CONTROL_AF_STATE_PASSIVE_SCAN || + afState == CaptureRequest.CONTROL_AF_STATE_PASSIVE_SCAN || afState == CaptureRequest.CONTROL_AF_STATE_FOCUSED_LOCKED || afState == CaptureRequest.CONTROL_AF_STATE_NOT_FOCUSED_LOCKED) { // CONTROL_AE_STATE can be null on some devices @@ -242,6 +242,7 @@ private void process(CaptureResult result) { takePictureAfterPrecapture(); } else { if (pictureCaptureRequest.hitPreCaptureTimeout()) { + Log.i(TAG, "===> Hit precapture timeout"); unlockAutoFocus(); } } @@ -627,7 +628,7 @@ public void takePicture(@NonNull final Result result) { final File file = File.createTempFile("CAP", ".jpg", outputDir); // Start a new capture - pictureCaptureRequest = new PictureCaptureRequest(result, file); + pictureCaptureRequest = new PictureCaptureRequest(result, file, dartMessenger); } catch (IOException | SecurityException e) { pictureCaptureRequest.error("cannotCreateFile", e.getMessage(), null); return; @@ -904,14 +905,13 @@ private void unlockAutoFocus() { // Set AF state to idle again mPreviewRequestBuilder.set( - CaptureRequest.CONTROL_AF_TRIGGER, CameraMetadata.CONTROL_AF_TRIGGER_CANCEL); - - updateFocusMode(mPreviewRequestBuilder); - updateFlash(mPreviewRequestBuilder); - updateExposureMode(mPreviewRequestBuilder); + CaptureRequest.CONTROL_AF_TRIGGER, CameraMetadata.CONTROL_AF_TRIGGER_IDLE); captureSession.capture(mPreviewRequestBuilder.build(), null, mBackgroundHandler); - } catch (CameraAccessException ignored) { + } catch (CameraAccessException e) { + Log.i(TAG, "Error unlocking focus: " + e.getMessage()); + dartMessenger.sendCameraErrorEvent(e.getMessage()); + return; } refreshPreviewCaptureSession( diff --git a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/PictureCaptureRequest.java b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/PictureCaptureRequest.java index ca6194e6ee65..74ac848030ab 100644 --- a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/PictureCaptureRequest.java +++ b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/PictureCaptureRequest.java @@ -52,6 +52,11 @@ class PictureCaptureRequest { */ private long preCaptureStartTime; + /** + * To send errors back to dart + */ + private final DartMessenger dartMessenger; + /** * The state of this picture capture request. */ @@ -67,10 +72,11 @@ class PictureCaptureRequest { * @param result * @param mFile */ - public PictureCaptureRequest(MethodChannel.Result result, File mFile) { + public PictureCaptureRequest(MethodChannel.Result result, File mFile, DartMessenger dartMessenger) { this.result = result; this.timeoutHandler = new TimeoutHandler(); this.mFile = mFile; + this.dartMessenger = dartMessenger; } /** @@ -92,7 +98,8 @@ public void setState(PictureCaptureRequestState newState) { // Once a request is finished, that's it for its lifecycle. if (state == PictureCaptureRequestState.STATE_FINISHED) { - throw new IllegalStateException("Request has already been finished"); + dartMessenger.sendCameraErrorEvent("Request has already been finished"); + return; } final PictureCaptureRequestState oldState = state; From 1cb86e857bea542b5e38f9cc6b44cf500c72b624 Mon Sep 17 00:00:00 2001 From: Andrew Coutts Date: Mon, 1 Mar 2021 16:38:08 -0500 Subject: [PATCH 014/114] Add nv21 image format for android --- .../io/flutter/plugins/camera/Camera.java | 5 ++-- packages/camera/camera/pubspec.yaml | 4 ++- .../lib/src/types/image_format_group.dart | 25 +++++++++++++++++++ 3 files changed, 31 insertions(+), 3 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 9a348b6f2053..8752abbcef45 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 @@ -91,8 +91,9 @@ public class Camera { // Current supported outputs static { supportedImageFormats = new HashMap<>(); - supportedImageFormats.put("yuv420", 35); - supportedImageFormats.put("jpeg", 256); + supportedImageFormats.put("yuv420", ImageFormat.YUV_420_888); + supportedImageFormats.put("jpeg", ImageFormat.JPEG); + supportedImageFormats.put("nv21", ImageFormat.NV21); } private final SurfaceTextureEntry flutterTexture; diff --git a/packages/camera/camera/pubspec.yaml b/packages/camera/camera/pubspec.yaml index 2d620505def2..5988d22def57 100644 --- a/packages/camera/camera/pubspec.yaml +++ b/packages/camera/camera/pubspec.yaml @@ -9,7 +9,9 @@ dependencies: flutter: sdk: flutter - camera_platform_interface: ^2.0.0-nullsafety + # camera_platform_interface: ^2.0.0-nullsafety + camera_platform_interface: + path: ../camera_platform_interface pedantic: ^1.10.0 quiver: ^3.0.0-nullsafety.3 diff --git a/packages/camera/camera_platform_interface/lib/src/types/image_format_group.dart b/packages/camera/camera_platform_interface/lib/src/types/image_format_group.dart index 3d2c0180fe65..b07f7af79e82 100644 --- a/packages/camera/camera_platform_interface/lib/src/types/image_format_group.dart +++ b/packages/camera/camera_platform_interface/lib/src/types/image_format_group.dart @@ -14,8 +14,33 @@ enum ImageFormatGroup { /// /// On iOS, this is `kCVPixelFormatType_420YpCbCr8BiPlanarVideoRange`. See /// https://developer.apple.com/documentation/corevideo/1563591-pixel_format_identifiers/kcvpixelformattype_420ypcbcr8biplanarvideorange?language=objc + /// + /// Note: if you are using YUV420 with Firebase ML Vision you need to concatenate the planes + /// using the following function: + /// ```dart + /// Uint8List concatenatePlanes(List planes) { + /// var totalBytes = 0; + /// for (var i = 0; i < planes.length; ++i) { + /// totalBytes += planes[i].bytes.length; + /// } + /// + /// final bytes = Uint8List(totalBytes); + /// + /// var byteOffset = 0; + /// for (var i = 0; i < planes.length; ++i) { + /// final length = planes[i].bytes.length; + /// bytes.setRange(byteOffset, byteOffset += length, planes[i].bytes); + /// } + /// return bytes; + /// } + /// ``` yuv420, + /// NV21 is only valid in Android and should be used when feeding images to Firebase ML Vision. If you + /// don't use this format then you need to concatenate the planes from YUV420 which is costly both in + /// memory and CPU. It's most efficient to just feed it NV21. + nv21, + /// 32-bit BGRA. /// /// On iOS, this is `kCVPixelFormatType_32BGRA`. See From a36f1f0875e033cd36c660028ce9afe544a54bb7 Mon Sep 17 00:00:00 2001 From: Andrew Coutts Date: Mon, 1 Mar 2021 17:40:45 -0500 Subject: [PATCH 015/114] Unit tests passing and capture timeout works again --- .../io/flutter/plugins/camera/Camera.java | 12 +- .../plugins/camera/PictureCaptureRequest.java | 55 +++- .../camera/PictureCaptureRequestState.java | 15 + .../camera/PictureCaptureRequestTest.java | 269 +++++++++--------- 4 files changed, 197 insertions(+), 154 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 8752abbcef45..a4d0e1b3e76a 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 @@ -232,7 +232,7 @@ private void process(CaptureResult result) { aeState == CaptureResult.CONTROL_AE_STATE_PRECAPTURE || aeState == CaptureResult.CONTROL_AE_STATE_FLASH_REQUIRED) { cameraState = CameraState.STATE_WAITING_PRECAPTURE_DONE; - pictureCaptureRequest.setPreCaptureStartTime(); + pictureCaptureRequest.setState(PictureCaptureRequestState.STATE_WAITING_PRECAPTURE_DONE); } break; } @@ -684,8 +684,9 @@ private void runPrecaptureSequence() { */ private void takePictureAfterPrecapture() { Log.i(TAG, "captureStillPicture"); - cameraState = CameraState.STATE_CAPTURING; + pictureCaptureRequest.setState(PictureCaptureRequestState.STATE_CAPTURING); + try { if (null == cameraDevice) { @@ -741,8 +742,6 @@ public void onCaptureCompleted(@NonNull CameraCaptureSession session, captureSession.abortCaptures(); Log.i(TAG, "sending capture request"); captureSession.capture(stillBuilder.build(), CaptureCallback, mBackgroundHandler); -// captureSession.setRepeatingRequest(mPreviewRequestBuilder.build(), null, mBackgroundHandler); -// pictureCaptureRequest.finish("a"); } catch (CameraAccessException e) { pictureCaptureRequest.error("cameraAccess", e.getMessage(), null); } @@ -873,6 +872,7 @@ private void runPictureAutoFocus() { assert (pictureCaptureRequest != null); cameraState = CameraState.STATE_WAITING_FOCUS; + pictureCaptureRequest.setState(PictureCaptureRequestState.STATE_WAITING_FOCUS); lockAutoFocus(); } @@ -881,6 +881,8 @@ private void runPictureAutoFocus() { */ private void lockAutoFocus() { Log.i(TAG, "lockAutoFocus"); + pictureCaptureRequest.setState(PictureCaptureRequestState.STATE_WAITING_PRECAPTURE_START); + mPreviewRequestBuilder.set( CaptureRequest.CONTROL_AF_TRIGGER, CaptureRequest.CONTROL_AF_TRIGGER_START); @@ -1025,7 +1027,7 @@ public void setFlashMode(@NonNull final Result result, FlashMode newMode) { updateFlash(mPreviewRequestBuilder); refreshPreviewCaptureSession( - () -> result.success(null), + () -> result.success(null), (code, message) -> result.error("setFlashModeFailed", "Could not set flash mode.", null)); } diff --git a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/PictureCaptureRequest.java b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/PictureCaptureRequest.java index 74ac848030ab..5788f8e97aa2 100644 --- a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/PictureCaptureRequest.java +++ b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/PictureCaptureRequest.java @@ -45,25 +45,25 @@ class PictureCaptureRequest { * Timeout handler. */ private final TimeoutHandler timeoutHandler; - + /** + * To send errors back to dart + */ + private final DartMessenger dartMessenger; /** * The time that the most recent capture started at. Used to check if * the current capture request has timed out. */ private long preCaptureStartTime; - - /** - * To send errors back to dart - */ - private final DartMessenger dartMessenger; - /** * The state of this picture capture request. */ private PictureCaptureRequestState state = PictureCaptureRequestState.STATE_IDLE; private final Runnable timeoutCallback = - () -> error("captureTimeout", "Picture capture request timed out", state.toString()); + () -> { + error("captureTimeout", "Picture capture request timed out", state.toString()); + setState(PictureCaptureRequestState.STATE_ERROR); + }; /** @@ -79,6 +79,16 @@ public PictureCaptureRequest(MethodChannel.Result result, File mFile, DartMessen this.dartMessenger = dartMessenger; } + /** + * Constructor for unit tests where we can mock the timeout handler + */ + public PictureCaptureRequest(MethodChannel.Result result, File mFile, DartMessenger dartMessenger, TimeoutHandler timeoutHandler) { + this.result = result; + this.timeoutHandler = timeoutHandler; + this.mFile = mFile; + this.dartMessenger = dartMessenger; + } + /** * Return the current state of this picture capture request. * @@ -94,9 +104,9 @@ public PictureCaptureRequestState getState() { * @param newState */ public void setState(PictureCaptureRequestState newState) { - Log.i("Camera", "PictureCaptureRequest setState: " + newState); + Log.i("Camera", "====> PictureCaptureRequest setState: " + newState); - // Once a request is finished, that's it for its lifecycle. + // Once a request is finished, that's it for its lifecycle. if (state == PictureCaptureRequestState.STATE_FINISHED) { dartMessenger.sendCameraErrorEvent("Request has already been finished"); return; @@ -108,7 +118,8 @@ public void setState(PictureCaptureRequestState newState) { } public boolean isFinished() { - return state == PictureCaptureRequestState.STATE_FINISHED; + return state == PictureCaptureRequestState.STATE_FINISHED || + state == PictureCaptureRequestState.STATE_ERROR; } /** @@ -117,6 +128,11 @@ public boolean isFinished() { * @param absolutePath */ public void finish(String absolutePath) { + if (state == PictureCaptureRequestState.STATE_ERROR) { + return; + } + + if (isFinished()) throw new IllegalStateException("Request has already been finished"); setState(PictureCaptureRequestState.STATE_FINISHED); Log.i("Camera", "PictureCaptureRequest finish"); result.success(absolutePath); @@ -125,6 +141,11 @@ public void finish(String absolutePath) { public void error( String errorCode, @Nullable String errorMessage, @Nullable Object errorDetails) { + if (state == PictureCaptureRequestState.STATE_ERROR) { + return; + } + + if (isFinished()) throw new IllegalStateException("Request has already been finished"); setState(PictureCaptureRequestState.STATE_ERROR); result.error(errorCode, errorMessage, errorDetails); } @@ -151,15 +172,18 @@ public void setPreCaptureStartTime() { */ private void onStateChange(PictureCaptureRequestState oldState) { switch (state) { - case STATE_IDLE: - // Nothing to do in idle state. + case STATE_CAPTURING: + case STATE_WAITING_FOCUS: + case STATE_WAITING_PRECAPTURE_START: + timeoutHandler.resetTimeout(timeoutCallback); break; - case STATE_CAPTURING: - // Started an image capture. + case STATE_WAITING_PRECAPTURE_DONE: + setPreCaptureStartTime(); timeoutHandler.resetTimeout(timeoutCallback); break; + case STATE_IDLE: case STATE_FINISHED: case STATE_ERROR: timeoutHandler.clearTimeout(timeoutCallback); @@ -180,6 +204,7 @@ static class TimeoutHandler { } public void resetTimeout(Runnable runnable) { + Log.i("Camear", "PictureCaptureRequest | resetting timeout"); clearTimeout(runnable); handler.postDelayed(runnable, REQUEST_TIMEOUT); } diff --git a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/PictureCaptureRequestState.java b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/PictureCaptureRequestState.java index fb8c2c7503bd..c506e2489e1c 100644 --- a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/PictureCaptureRequestState.java +++ b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/PictureCaptureRequestState.java @@ -16,6 +16,21 @@ public enum PictureCaptureRequestState { */ STATE_IDLE, + /** + * Starting and waiting for autofocus to complete. + */ + STATE_WAITING_FOCUS, + + /** + * Start performing autoexposure. + */ + STATE_WAITING_PRECAPTURE_START, + + /** + * waiting for autoexposure to complete. + */ + STATE_WAITING_PRECAPTURE_DONE, + /** * Picture is being captured. */ diff --git a/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/PictureCaptureRequestTest.java b/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/PictureCaptureRequestTest.java index 3252b3e111c4..ef81028e2bc6 100644 --- a/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/PictureCaptureRequestTest.java +++ b/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/PictureCaptureRequestTest.java @@ -4,6 +4,10 @@ package io.flutter.plugins.camera; +import org.junit.Test; + +import io.flutter.plugin.common.MethodChannel; + import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertTrue; @@ -13,140 +17,137 @@ import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; -import io.flutter.plugin.common.MethodChannel; -import org.junit.Test; - public class PictureCaptureRequestTest { - @Test - public void state_is_idle_by_default() { - PictureCaptureRequest req = new PictureCaptureRequest(null); - assertEquals("Default state is idle", req.getState(), PictureCaptureRequest.State.idle); - } - - @Test - public void setState_sets_state() { - PictureCaptureRequest req = new PictureCaptureRequest(null); - req.setState(PictureCaptureRequest.State.focusing); - assertEquals("State is focusing", req.getState(), PictureCaptureRequest.State.focusing); - req.setState(PictureCaptureRequest.State.preCapture); - assertEquals("State is preCapture", req.getState(), PictureCaptureRequest.State.preCapture); - req.setState(PictureCaptureRequest.State.waitingPreCaptureReady); - assertEquals( - "State is waitingPreCaptureReady", - req.getState(), - PictureCaptureRequest.State.waitingPreCaptureReady); - req.setState(PictureCaptureRequest.State.capturing); - assertEquals( - "State is awaitingPreCapture", req.getState(), PictureCaptureRequest.State.capturing); - } - - @Test - public void setState_resets_timeout() { - PictureCaptureRequest.TimeoutHandler mockTimeoutHandler = - mock(PictureCaptureRequest.TimeoutHandler.class); - PictureCaptureRequest req = new PictureCaptureRequest(null, mockTimeoutHandler); - req.setState(PictureCaptureRequest.State.focusing); - req.setState(PictureCaptureRequest.State.preCapture); - req.setState(PictureCaptureRequest.State.waitingPreCaptureReady); - req.setState(PictureCaptureRequest.State.capturing); - verify(mockTimeoutHandler, times(4)).resetTimeout(any()); - verify(mockTimeoutHandler, never()).clearTimeout(any()); - } - - @Test - public void setState_clears_timeout() { - PictureCaptureRequest.TimeoutHandler mockTimeoutHandler = - mock(PictureCaptureRequest.TimeoutHandler.class); - PictureCaptureRequest req = new PictureCaptureRequest(null, mockTimeoutHandler); - req.setState(PictureCaptureRequest.State.idle); - req.setState(PictureCaptureRequest.State.finished); - req = new PictureCaptureRequest(null, mockTimeoutHandler); - req.setState(PictureCaptureRequest.State.error); - verify(mockTimeoutHandler, never()).resetTimeout(any()); - verify(mockTimeoutHandler, times(3)).clearTimeout(any()); - } - - @Test - public void finish_sets_result_and_state() { - // Setup - MethodChannel.Result mockResult = mock(MethodChannel.Result.class); - PictureCaptureRequest req = new PictureCaptureRequest(mockResult); - // Act - req.finish("/test/path"); - // Test - verify(mockResult).success("/test/path"); - assertEquals("State is finished", req.getState(), PictureCaptureRequest.State.finished); - } - - @Test - public void finish_clears_timeout() { - PictureCaptureRequest.TimeoutHandler mockTimeoutHandler = - mock(PictureCaptureRequest.TimeoutHandler.class); - MethodChannel.Result mockResult = mock(MethodChannel.Result.class); - PictureCaptureRequest req = new PictureCaptureRequest(mockResult, mockTimeoutHandler); - req.finish("/test/path"); - verify(mockTimeoutHandler, never()).resetTimeout(any()); - verify(mockTimeoutHandler).clearTimeout(any()); - } - - @Test - public void isFinished_is_true_When_state_is_finished_or_error() { - // Setup - PictureCaptureRequest req = new PictureCaptureRequest(null); - // Test false states - req.setState(PictureCaptureRequest.State.idle); - assertFalse(req.isFinished()); - req.setState(PictureCaptureRequest.State.preCapture); - assertFalse(req.isFinished()); - req.setState(PictureCaptureRequest.State.capturing); - assertFalse(req.isFinished()); - // Test true states - req.setState(PictureCaptureRequest.State.finished); - assertTrue(req.isFinished()); - req = new PictureCaptureRequest(null); // Refresh - req.setState(PictureCaptureRequest.State.error); - assertTrue(req.isFinished()); - } - - @Test(expected = IllegalStateException.class) - public void finish_throws_When_already_finished() { - // Setup - PictureCaptureRequest req = new PictureCaptureRequest(null); - req.setState(PictureCaptureRequest.State.finished); - // Act - req.finish("/test/path"); - } - - @Test - public void error_sets_result_and_state() { - // Setup - MethodChannel.Result mockResult = mock(MethodChannel.Result.class); - PictureCaptureRequest req = new PictureCaptureRequest(mockResult); - // Act - req.error("ERROR_CODE", "Error Message", null); - // Test - verify(mockResult).error("ERROR_CODE", "Error Message", null); - assertEquals("State is error", req.getState(), PictureCaptureRequest.State.error); - } - - @Test - public void error_clears_timeout() { - PictureCaptureRequest.TimeoutHandler mockTimeoutHandler = - mock(PictureCaptureRequest.TimeoutHandler.class); - MethodChannel.Result mockResult = mock(MethodChannel.Result.class); - PictureCaptureRequest req = new PictureCaptureRequest(mockResult, mockTimeoutHandler); - req.error("ERROR_CODE", "Error Message", null); - verify(mockTimeoutHandler, never()).resetTimeout(any()); - verify(mockTimeoutHandler).clearTimeout(any()); - } - - @Test(expected = IllegalStateException.class) - public void error_throws_When_already_finished() { - // Setup - PictureCaptureRequest req = new PictureCaptureRequest(null); - req.setState(PictureCaptureRequest.State.finished); - // Act - req.error(null, null, null); - } + @Test + public void state_is_idle_by_default() { + PictureCaptureRequest req = new PictureCaptureRequest(null, null, null); + assertEquals("Default state is idle", req.getState(), PictureCaptureRequestState.STATE_IDLE); + } + + @Test + public void setState_sets_state() { + PictureCaptureRequest req = new PictureCaptureRequest(null, null, null); + req.setState(PictureCaptureRequestState.STATE_WAITING_FOCUS); + assertEquals("State is focusing", req.getState(), PictureCaptureRequestState.STATE_WAITING_FOCUS); + req.setState(PictureCaptureRequestState.STATE_WAITING_PRECAPTURE_START); + assertEquals("State is preCapture", req.getState(), PictureCaptureRequestState.STATE_WAITING_PRECAPTURE_START); + req.setState(PictureCaptureRequestState.STATE_WAITING_PRECAPTURE_DONE); + assertEquals( + "State is waitingPreCaptureReady", + req.getState(), + PictureCaptureRequestState.STATE_WAITING_PRECAPTURE_DONE); + req.setState(PictureCaptureRequestState.STATE_CAPTURING); + assertEquals( + "State is awaitingPreCapture", req.getState(), PictureCaptureRequestState.STATE_CAPTURING); + } + + @Test + public void setState_resets_timeout() { + PictureCaptureRequest.TimeoutHandler mockTimeoutHandler = + mock(PictureCaptureRequest.TimeoutHandler.class); + PictureCaptureRequest req = new PictureCaptureRequest(null, null, null, mockTimeoutHandler); + req.setState(PictureCaptureRequestState.STATE_WAITING_FOCUS); + req.setState(PictureCaptureRequestState.STATE_WAITING_PRECAPTURE_START); + req.setState(PictureCaptureRequestState.STATE_WAITING_PRECAPTURE_DONE); + req.setState(PictureCaptureRequestState.STATE_CAPTURING); + verify(mockTimeoutHandler, times(4)).resetTimeout(any()); + verify(mockTimeoutHandler, never()).clearTimeout(any()); + } + + @Test + public void setState_clears_timeout() { + PictureCaptureRequest.TimeoutHandler mockTimeoutHandler = + mock(PictureCaptureRequest.TimeoutHandler.class); + PictureCaptureRequest req = new PictureCaptureRequest(null, null, null, mockTimeoutHandler); + req.setState(PictureCaptureRequestState.STATE_IDLE); + req.setState(PictureCaptureRequestState.STATE_FINISHED); + req = new PictureCaptureRequest(null, null, null, mockTimeoutHandler); + req.setState(PictureCaptureRequestState.STATE_ERROR); + verify(mockTimeoutHandler, never()).resetTimeout(any()); + verify(mockTimeoutHandler, times(3)).clearTimeout(any()); + } + + @Test + public void finish_sets_result_and_state() { + // Setup + MethodChannel.Result mockResult = mock(MethodChannel.Result.class); + PictureCaptureRequest req = new PictureCaptureRequest(mockResult, null, null); + // Act + req.finish("/test/path"); + // Test + verify(mockResult).success("/test/path"); + assertEquals("State is finished", req.getState(), PictureCaptureRequestState.STATE_FINISHED); + } + + @Test + public void finish_clears_timeout() { + PictureCaptureRequest.TimeoutHandler mockTimeoutHandler = + mock(PictureCaptureRequest.TimeoutHandler.class); + MethodChannel.Result mockResult = mock(MethodChannel.Result.class); + PictureCaptureRequest req = new PictureCaptureRequest(mockResult, null, null, mockTimeoutHandler); + req.finish("/test/path"); + verify(mockTimeoutHandler, never()).resetTimeout(any()); + verify(mockTimeoutHandler).clearTimeout(any()); + } + + @Test + public void isFinished_is_true_When_state_is_finished_or_error() { + // Setup + PictureCaptureRequest req = new PictureCaptureRequest(null, null, null); + // Test false states + req.setState(PictureCaptureRequestState.STATE_IDLE); + assertFalse(req.isFinished()); + req.setState(PictureCaptureRequestState.STATE_WAITING_PRECAPTURE_START); + assertFalse(req.isFinished()); + req.setState(PictureCaptureRequestState.STATE_WAITING_PRECAPTURE_DONE); + assertFalse(req.isFinished()); + // Test true states + req.setState(PictureCaptureRequestState.STATE_FINISHED); + assertTrue(req.isFinished()); + req = new PictureCaptureRequest(null, null, null); // Refresh + req.setState(PictureCaptureRequestState.STATE_ERROR); + assertTrue(req.isFinished()); + } + + @Test(expected = IllegalStateException.class) + public void finish_throws_When_already_finished() { + // Setup + PictureCaptureRequest req = new PictureCaptureRequest(null, null, null); + req.setState(PictureCaptureRequestState.STATE_FINISHED); + // Act + req.finish("/test/path"); + } + + @Test + public void error_sets_result_and_state() { + // Setup + MethodChannel.Result mockResult = mock(MethodChannel.Result.class); + PictureCaptureRequest req = new PictureCaptureRequest(mockResult, null, null); + // Act + req.error("ERROR_CODE", "Error Message", null); + // Test + verify(mockResult).error("ERROR_CODE", "Error Message", null); + assertEquals("State is error", req.getState(), PictureCaptureRequestState.STATE_ERROR); + } + + @Test + public void error_clears_timeout() { + PictureCaptureRequest.TimeoutHandler mockTimeoutHandler = + mock(PictureCaptureRequest.TimeoutHandler.class); + MethodChannel.Result mockResult = mock(MethodChannel.Result.class); + PictureCaptureRequest req = new PictureCaptureRequest(mockResult, null, null, mockTimeoutHandler); + req.error("ERROR_CODE", "Error Message", null); + verify(mockTimeoutHandler, never()).resetTimeout(any()); + verify(mockTimeoutHandler).clearTimeout(any()); + } + + @Test(expected = IllegalStateException.class) + public void error_throws_When_already_finished() { + // Setup + PictureCaptureRequest req = new PictureCaptureRequest(null, null, null); + req.setState(PictureCaptureRequestState.STATE_FINISHED); + // Act + req.error(null, null, null); + } } From aada63c36c333ceb93a3dbbb86d1d757df954b8a Mon Sep 17 00:00:00 2001 From: Andrew Coutts Date: Mon, 1 Mar 2021 17:48:46 -0500 Subject: [PATCH 016/114] Remove comments and clean things up --- packages/camera/camera/CHANGELOG.md | 6 ++ .../io/flutter/plugins/camera/Camera.java | 82 +++++++++---------- .../plugins/camera/PictureCaptureRequest.java | 8 +- packages/camera/camera/pubspec.yaml | 7 +- .../camera_platform_interface/CHANGELOG.md | 3 + .../camera_platform_interface/pubspec.yaml | 2 +- 6 files changed, 57 insertions(+), 51 deletions(-) diff --git a/packages/camera/camera/CHANGELOG.md b/packages/camera/camera/CHANGELOG.md index 079aa1685bd5..e5c9d55118ef 100644 --- a/packages/camera/camera/CHANGELOG.md +++ b/packages/camera/camera/CHANGELOG.md @@ -1,3 +1,9 @@ +## 0.8.1 +* Complete rewrite of Android plugin to fix all capture, focus, flash, and exposure issues. +* Fixed crash when opening front-facing cameras on some legacy android devices like Sony XZ. +* Android Flash mode works with full precapture sequence. +* Added support for NV21 image stream format on Android. + ## 0.8.0-nullsafety.3 * Updates the example code listed in the [README.md](README.md), so it runs without errors when you simply copy/ paste it into a Flutter App. 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 a4d0e1b3e76a..6b400a17e392 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 @@ -136,7 +136,7 @@ public class Camera { @Override public void onImageAvailable(ImageReader reader) { - Log.i(TAG, "onImageAvailable"); + // Log.i(TAG, "onImageAvailable"); mBackgroundHandler.post(new ImageSaver(reader.acquireNextImage(), pictureCaptureRequest.mFile, pictureCaptureRequest)); cameraState = CameraState.STATE_PREVIEW; } @@ -197,7 +197,7 @@ private void process(CaptureResult result) { Integer afState = result.get(CaptureResult.CONTROL_AF_STATE); if (cameraState != CameraState.STATE_PREVIEW) { - Log.i(TAG, "mCaptureCallback | state: " + cameraState + " | afState: " + afState + " | aeState: " + aeState); + // Log.i(TAG, "mCaptureCallback | state: " + cameraState + " | afState: " + afState + " | aeState: " + aeState); } switch (cameraState) { @@ -243,7 +243,7 @@ private void process(CaptureResult result) { takePictureAfterPrecapture(); } else { if (pictureCaptureRequest.hitPreCaptureTimeout()) { - Log.i(TAG, "===> Hit precapture timeout"); + // Log.i(TAG, "===> Hit precapture timeout"); unlockAutoFocus(); } } @@ -277,7 +277,7 @@ public Camera( final String resolutionPreset, final boolean enableAudio) throws CameraAccessException { - Log.i(TAG, "Camear constructor"); + // Log.i(TAG, "Camear constructor"); if (activity == null) { throw new IllegalStateException("No activity available!"); @@ -331,9 +331,9 @@ public Camera( */ private void checkAutoFocusSupported() { int[] modes = cameraCharacteristics.get(CameraCharacteristics.CONTROL_AF_AVAILABLE_MODES); - Log.i(TAG, "checkAutoFocusSupported | modes:"); + // Log.i(TAG, "checkAutoFocusSupported | modes:"); for (int mode : modes) { - Log.i(TAG, "checkAutoFocusSupported | ==> " + mode); + // Log.i(TAG, "checkAutoFocusSupported | ==> " + mode); } // Check if fixed focal length lens. If LENS_INFO_MINIMUM_FOCUS_DISTANCE=0, then this is fixed. @@ -341,12 +341,12 @@ private void checkAutoFocusSupported() { float minFocus = cameraCharacteristics.get(CameraCharacteristics.LENS_INFO_MINIMUM_FOCUS_DISTANCE); float maxFocus = cameraCharacteristics.get(CameraCharacteristics.LENS_INFO_HYPERFOCAL_DISTANCE); final boolean isFixedLength = minFocus == 0; - Log.i(TAG, "checkAutoFocusSupported | minFocus " + minFocus + " | maxFocus: " + maxFocus); + // Log.i(TAG, "checkAutoFocusSupported | minFocus " + minFocus + " | maxFocus: " + maxFocus); mAutoFocusSupported = !(modes == null || modes.length == 0 || (modes.length == 1 && modes[0] == CameraCharacteristics.CONTROL_AF_MODE_OFF)); - Log.i(TAG, "checkAutoFocusSupported: " + mAutoFocusSupported); + // Log.i(TAG, "checkAutoFocusSupported: " + mAutoFocusSupported); } /** @@ -363,7 +363,7 @@ private void checkFlashSupported() { * @param cameraCharacteristics */ private void getAvailableFpsRange(CameraCharacteristics cameraCharacteristics) { - Log.i(TAG, "getAvailableFpsRange"); + // Log.i(TAG, "getAvailableFpsRange"); try { Range[] ranges = @@ -371,7 +371,7 @@ private void getAvailableFpsRange(CameraCharacteristics cameraCharacteristics) { if (ranges != null) { for (Range range : ranges) { int upper = range.getUpper(); - Log.i("Camera", "[FPS Range Available] is:" + range); + // Log.i("Camera", "[FPS Range Available] is:" + range); if (upper >= 10) { if (fpsRange == null || upper > fpsRange.getUpper()) { fpsRange = range; @@ -382,11 +382,11 @@ private void getAvailableFpsRange(CameraCharacteristics cameraCharacteristics) { } catch (Exception e) { pictureCaptureRequest.error("cameraAccess", e.getMessage(), null); } - Log.i("Camera", "[FPS Range] is:" + fpsRange); + // Log.i("Camera", "[FPS Range] is:" + fpsRange); } private void prepareMediaRecorder(String outputFilePath) throws IOException { - Log.i(TAG, "prepareMediaRecorder"); + // Log.i(TAG, "prepareMediaRecorder"); if (mediaRecorder != null) { mediaRecorder.release(); @@ -425,7 +425,7 @@ public void open(String imageFormatGroup) throws CameraAccessException { new CameraDevice.StateCallback() { @Override public void onOpened(@NonNull CameraDevice device) { - Log.i(TAG, "open | onOpened"); + // Log.i(TAG, "open | onOpened"); cameraDevice = device; @@ -446,7 +446,7 @@ public void onOpened(@NonNull CameraDevice device) { @Override public void onClosed(@NonNull CameraDevice camera) { - Log.i(TAG, "open | onClosed"); + // Log.i(TAG, "open | onClosed"); dartMessenger.sendCameraClosingEvent(); super.onClosed(camera); @@ -454,7 +454,7 @@ public void onClosed(@NonNull CameraDevice camera) { @Override public void onDisconnected(@NonNull CameraDevice cameraDevice) { - Log.i(TAG, "open | onDisconnected"); + // Log.i(TAG, "open | onDisconnected"); close(); dartMessenger.sendCameraErrorEvent("The camera was disconnected."); @@ -462,7 +462,7 @@ public void onDisconnected(@NonNull CameraDevice cameraDevice) { @Override public void onError(@NonNull CameraDevice cameraDevice, int errorCode) { - Log.i(TAG, "open | onError"); + // Log.i(TAG, "open | onError"); close(); String errorDescription; @@ -500,7 +500,7 @@ private void createCaptureSession(int templateType, Surface... surfaces) private void createCaptureSession( int templateType, Runnable onSuccessCallback, Surface... surfaces) throws CameraAccessException { - Log.i(TAG, "createCaptureSession"); + // Log.i(TAG, "createCaptureSession"); // Close any existing capture session. closeCaptureSession(); @@ -592,9 +592,9 @@ private void createCaptureSession( // Send a repeating request to refresh our capture session. private void refreshPreviewCaptureSession( @Nullable Runnable onSuccessCallback, @NonNull ErrorCallback onErrorCallback) { - Log.i(TAG, "refreshPreviewCaptureSession"); + // Log.i(TAG, "refreshPreviewCaptureSession"); if (captureSession == null) { - Log.i(TAG, "[refreshPreviewCaptureSession] mPreviewSession null, returning"); + // Log.i(TAG, "[refreshPreviewCaptureSession] mPreviewSession null, returning"); return; } @@ -615,7 +615,7 @@ private void refreshPreviewCaptureSession( } public void takePicture(@NonNull final Result result) { - Log.i(TAG, "takePicture | useAutoFocus: " + useAutoFocus); + // Log.i(TAG, "takePicture | useAutoFocus: " + useAutoFocus); // Only take one 1 picture at a time. if (pictureCaptureRequest != null && !pictureCaptureRequest.isFinished()) { @@ -650,7 +650,7 @@ public void takePicture(@NonNull final Result result) { * we get a response in {@link #mCaptureCallback} from lockFocus(). */ private void runPrecaptureSequence() { - Log.i(TAG, "runPrecaptureSequence"); + // Log.i(TAG, "runPrecaptureSequence"); try { // First set precapture state to idle or else it can hang in STATE_WAITING_PRECAPTURE mPreviewRequestBuilder.set(CaptureRequest.CONTROL_AE_PRECAPTURE_TRIGGER, @@ -683,7 +683,7 @@ private void runPrecaptureSequence() { * {@link #mCaptureCallback} from both lockFocus(). */ private void takePictureAfterPrecapture() { - Log.i(TAG, "captureStillPicture"); + // Log.i(TAG, "captureStillPicture"); cameraState = CameraState.STATE_CAPTURING; pictureCaptureRequest.setState(PictureCaptureRequestState.STATE_CAPTURING); @@ -719,28 +719,28 @@ public void onCaptureStarted(@NonNull CameraCaptureSession session, @NonNull CaptureRequest request, long timestamp, long frameNumber) { - Log.i(TAG, "onCaptureStarted"); + // Log.i(TAG, "onCaptureStarted"); } @Override public void onCaptureProgressed(@NonNull CameraCaptureSession session, @NonNull CaptureRequest request, @NonNull CaptureResult partialResult) { - Log.i(TAG, "onCaptureProgressed"); + // Log.i(TAG, "onCaptureProgressed"); } @Override public void onCaptureCompleted(@NonNull CameraCaptureSession session, @NonNull CaptureRequest request, @NonNull TotalCaptureResult result) { - Log.i(TAG, "onCaptureCompleted"); + // Log.i(TAG, "onCaptureCompleted"); unlockAutoFocus(); } }; captureSession.stopRepeating(); captureSession.abortCaptures(); - Log.i(TAG, "sending capture request"); + // Log.i(TAG, "sending capture request"); captureSession.capture(stillBuilder.build(), CaptureCallback, mBackgroundHandler); } catch (CameraAccessException e) { pictureCaptureRequest.error("cameraAccess", e.getMessage(), null); @@ -779,7 +779,7 @@ private void stopBackgroundThread() { * Sync the requestBuilder exposure mode setting ot the current exposure mode setting of the camera. */ void updateExposureMode(CaptureRequest.Builder requestBuilder) { - Log.i(TAG, "updateExposureMode"); + // Log.i(TAG, "updateExposureMode"); // Applying auto exposure MeteringRectangle aeRect = cameraRegions.getAEMeteringRectangle(); @@ -805,7 +805,7 @@ void updateExposureMode(CaptureRequest.Builder requestBuilder) { * Sync the requestBuilder flash setting to the current flash mode setting of the camera. */ void updateFlash(CaptureRequest.Builder requestBuilder) { - Log.i(TAG, "updateFlash"); + // Log.i(TAG, "updateFlash"); if (!mFlashSupported) { return; @@ -868,7 +868,7 @@ private int getOrientation(int rotation) { * Start capturing a picture, doing autofocus first. */ private void runPictureAutoFocus() { - Log.i(TAG, "runPictureAutoFocus"); + // Log.i(TAG, "runPictureAutoFocus"); assert (pictureCaptureRequest != null); cameraState = CameraState.STATE_WAITING_FOCUS; @@ -880,7 +880,7 @@ private void runPictureAutoFocus() { * Start the autofocus routine on the current capture request. */ private void lockAutoFocus() { - Log.i(TAG, "lockAutoFocus"); + // Log.i(TAG, "lockAutoFocus"); pictureCaptureRequest.setState(PictureCaptureRequestState.STATE_WAITING_PRECAPTURE_START); mPreviewRequestBuilder.set( @@ -894,7 +894,7 @@ private void lockAutoFocus() { * Cancel and reset auto focus state and refresh the preview session. */ private void unlockAutoFocus() { - Log.i(TAG, "unlockAutoFocus"); + // Log.i(TAG, "unlockAutoFocus"); try { // Cancel existing AF state mPreviewRequestBuilder.set( @@ -912,7 +912,7 @@ private void unlockAutoFocus() { captureSession.capture(mPreviewRequestBuilder.build(), null, mBackgroundHandler); } catch (CameraAccessException e) { - Log.i(TAG, "Error unlocking focus: " + e.getMessage()); + // Log.i(TAG, "Error unlocking focus: " + e.getMessage()); dartMessenger.sendCameraErrorEvent(e.getMessage()); return; } @@ -1081,7 +1081,7 @@ public void setExposurePoint(@NonNull final Result result, Double x, Double y) */ public void setFocusMode(@NonNull final Result result, FocusMode newMode) throws CameraAccessException { - Log.i(TAG, "setFocusMode: " + newMode); + // Log.i(TAG, "setFocusMode: " + newMode); // Set new focus mode currentFocusMode = newMode; @@ -1093,7 +1093,7 @@ public void setFocusMode(@NonNull final Result result, FocusMode newMode) // or we want to trigger a one-time focus and then set AF to idle (locked mode). switch (newMode) { case auto: - Log.i(TAG, "Triggering AF start with mode " + currentFocusMode); + // Log.i(TAG, "Triggering AF start with mode " + currentFocusMode); // Reset state of autofocus so it goes back to passive scanning. mPreviewRequestBuilder.set( CaptureRequest.CONTROL_AF_TRIGGER, CaptureRequest.CONTROL_AF_TRIGGER_CANCEL); @@ -1117,7 +1117,7 @@ public void setFocusMode(@NonNull final Result result, FocusMode newMode) public void onCaptureCompleted(@NonNull CameraCaptureSession session, @NonNull CaptureRequest request, @NonNull TotalCaptureResult _result) { - Log.i(TAG, "Success after triggering AF start for locked focus"); + // Log.i(TAG, "Success after triggering AF start for locked focus"); mPreviewRequestBuilder.set( CaptureRequest.CONTROL_AF_TRIGGER, CaptureRequest.CONTROL_AF_TRIGGER_IDLE); @@ -1320,7 +1320,7 @@ private void updateFpsRange() { * @param requestBuilder */ private void updateFocusMode(CaptureRequest.Builder requestBuilder) { - Log.i(TAG, "updateFocusMode currentFocusMode: " + currentFocusMode); + // Log.i(TAG, "updateFocusMode currentFocusMode: " + currentFocusMode); if (!mAutoFocusSupported) { useAutoFocus = false; @@ -1361,7 +1361,7 @@ private void updateFocusMode(CaptureRequest.Builder requestBuilder) { public void startPreview() throws CameraAccessException { if (pictureImageReader == null || pictureImageReader.getSurface() == null) return; - Log.i(TAG, "startPreview"); + // Log.i(TAG, "startPreview"); createCaptureSession(CameraDevice.TEMPLATE_PREVIEW, pictureImageReader.getSurface()); } @@ -1369,7 +1369,7 @@ public void startPreview() throws CameraAccessException { public void startPreviewWithImageStream(EventChannel imageStreamChannel) throws CameraAccessException { createCaptureSession(CameraDevice.TEMPLATE_RECORD, imageStreamReader.getSurface()); - Log.i(TAG, "startPreviewWithImageStream"); + // Log.i(TAG, "startPreviewWithImageStream"); imageStreamChannel.setStreamHandler( new EventChannel.StreamHandler() { @@ -1422,7 +1422,7 @@ private void setImageStreamImageAvailableListener(final EventChannel.EventSink i private void closeCaptureSession() { if (captureSession != null) { - Log.i(TAG, "closeCaptureSession"); + // Log.i(TAG, "closeCaptureSession"); captureSession.close(); captureSession = null; @@ -1430,7 +1430,7 @@ private void closeCaptureSession() { } public void close() { - Log.i(TAG, "close"); + // Log.i(TAG, "close"); closeCaptureSession(); if (cameraDevice != null) { @@ -1455,7 +1455,7 @@ public void close() { } public void dispose() { - Log.i(TAG, "dispose"); + // Log.i(TAG, "dispose"); close(); flutterTexture.release(); diff --git a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/PictureCaptureRequest.java b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/PictureCaptureRequest.java index 5788f8e97aa2..5e5c75b05d25 100644 --- a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/PictureCaptureRequest.java +++ b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/PictureCaptureRequest.java @@ -104,7 +104,7 @@ public PictureCaptureRequestState getState() { * @param newState */ public void setState(PictureCaptureRequestState newState) { - Log.i("Camera", "====> PictureCaptureRequest setState: " + newState); + // Log.i("Camera", "====> PictureCaptureRequest setState: " + newState); // Once a request is finished, that's it for its lifecycle. if (state == PictureCaptureRequestState.STATE_FINISHED) { @@ -134,7 +134,7 @@ public void finish(String absolutePath) { if (isFinished()) throw new IllegalStateException("Request has already been finished"); setState(PictureCaptureRequestState.STATE_FINISHED); - Log.i("Camera", "PictureCaptureRequest finish"); + // Log.i("Camera", "PictureCaptureRequest finish"); result.success(absolutePath); } @@ -156,7 +156,7 @@ public void error( * @return true if the timeout is reached; otherwise false is returned. */ public boolean hitPreCaptureTimeout() { - Log.i("Camera", "hitPreCaptureTimeout | Time elapsed: " + (SystemClock.elapsedRealtime() - preCaptureStartTime)); + // Log.i("Camera", "hitPreCaptureTimeout | Time elapsed: " + (SystemClock.elapsedRealtime() - preCaptureStartTime)); return (SystemClock.elapsedRealtime() - preCaptureStartTime) > PRECAPTURE_TIMEOUT_MS; } @@ -204,7 +204,7 @@ static class TimeoutHandler { } public void resetTimeout(Runnable runnable) { - Log.i("Camear", "PictureCaptureRequest | resetting timeout"); + // Log.i("Camear", "PictureCaptureRequest | resetting timeout"); clearTimeout(runnable); handler.postDelayed(runnable, REQUEST_TIMEOUT); } diff --git a/packages/camera/camera/pubspec.yaml b/packages/camera/camera/pubspec.yaml index 5988d22def57..6cad2e470e3c 100644 --- a/packages/camera/camera/pubspec.yaml +++ b/packages/camera/camera/pubspec.yaml @@ -2,17 +2,14 @@ name: camera description: A Flutter plugin for getting information about and controlling the camera on Android and iOS. Supports previewing the camera feed, capturing images, capturing video, and streaming image buffers to dart. -version: 0.8.0-nullsafety.3 +version: 0.8.1 homepage: https://github.com/flutter/plugins/tree/master/packages/camera/camera dependencies: flutter: sdk: flutter - # camera_platform_interface: ^2.0.0-nullsafety - camera_platform_interface: - path: ../camera_platform_interface - + camera_platform_interface: ^2.0.1 pedantic: ^1.10.0 quiver: ^3.0.0-nullsafety.3 diff --git a/packages/camera/camera_platform_interface/CHANGELOG.md b/packages/camera/camera_platform_interface/CHANGELOG.md index f7f78197d204..09536e48d1c7 100644 --- a/packages/camera/camera_platform_interface/CHANGELOG.md +++ b/packages/camera/camera_platform_interface/CHANGELOG.md @@ -1,3 +1,6 @@ +## 2.0.1 +- Added support for NV21 image stream format in Android. + ## 2.0.0 - Stable null safety release. diff --git a/packages/camera/camera_platform_interface/pubspec.yaml b/packages/camera/camera_platform_interface/pubspec.yaml index 10897073dc5c..b3caeabd7951 100644 --- a/packages/camera/camera_platform_interface/pubspec.yaml +++ b/packages/camera/camera_platform_interface/pubspec.yaml @@ -3,7 +3,7 @@ description: A common platform interface for the camera plugin. homepage: https://github.com/flutter/plugins/tree/master/packages/camera/camera_platform_interface # NOTE: We strongly prefer non-breaking changes, even at the expense of a # less-clean API. See https://flutter.dev/go/platform-interface-breaking-changes -version: 2.0.0 +version: 2.0.1 dependencies: flutter: From ebaba7aa05951ffea9128b165a90b35f4aacfc9b Mon Sep 17 00:00:00 2001 From: Andrew Coutts Date: Mon, 1 Mar 2021 17:49:47 -0500 Subject: [PATCH 017/114] Cleanup --- .../android/src/main/java/io/flutter/plugins/camera/Camera.java | 2 -- .../java/io/flutter/plugins/camera/PictureCaptureRequest.java | 1 - 2 files changed, 3 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 6b400a17e392..37d7659ed985 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 @@ -133,14 +133,12 @@ public class Camera { */ private final ImageReader.OnImageAvailableListener mOnImageAvailableListener = new ImageReader.OnImageAvailableListener() { - @Override public void onImageAvailable(ImageReader reader) { // Log.i(TAG, "onImageAvailable"); mBackgroundHandler.post(new ImageSaver(reader.acquireNextImage(), pictureCaptureRequest.mFile, pictureCaptureRequest)); cameraState = CameraState.STATE_PREVIEW; } - }; /** diff --git a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/PictureCaptureRequest.java b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/PictureCaptureRequest.java index 5e5c75b05d25..fd139cf16f43 100644 --- a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/PictureCaptureRequest.java +++ b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/PictureCaptureRequest.java @@ -12,7 +12,6 @@ import java.io.File; -import io.flutter.Log; import io.flutter.plugin.common.MethodChannel; /** From 6e7a3567b9d2ce0d911265e57df5ada1588831fa Mon Sep 17 00:00:00 2001 From: Andrew Coutts Date: Mon, 1 Mar 2021 18:14:51 -0500 Subject: [PATCH 018/114] Cleanup unused --- .../flutter/plugins/camera/AspectRatio.java | 175 ------------------ .../plugins/camera/CameraConstants.java | 26 --- 2 files changed, 201 deletions(-) delete mode 100644 packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/AspectRatio.java delete mode 100644 packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/CameraConstants.java diff --git a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/AspectRatio.java b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/AspectRatio.java deleted file mode 100644 index 455dd7ceaf62..000000000000 --- a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/AspectRatio.java +++ /dev/null @@ -1,175 +0,0 @@ -package io.flutter.plugins.camera; - -import android.os.Parcel; -import android.os.Parcelable; -import android.util.Size; - -import androidx.annotation.NonNull; -import androidx.collection.SparseArrayCompat; - -/** - * Immutable class for describing proportional relationship between width and height. - */ -public class AspectRatio implements Comparable, Parcelable { - - private final static SparseArrayCompat> sCache - = new SparseArrayCompat<>(16); - - private final int mX; - private final int mY; - - /** - * Returns an instance of {@link AspectRatio} specified by {@code x} and {@code y} values. - * The values {@code x} and {@code} will be reduced by their greatest common divider. - * - * @param x The width - * @param y The height - * @return An instance of {@link AspectRatio} - */ - public static AspectRatio of(int x, int y) { - int gcd = gcd(x, y); - x /= gcd; - y /= gcd; - SparseArrayCompat arrayX = sCache.get(x); - if (arrayX == null) { - AspectRatio ratio = new AspectRatio(x, y); - arrayX = new SparseArrayCompat<>(); - arrayX.put(y, ratio); - sCache.put(x, arrayX); - return ratio; - } else { - AspectRatio ratio = arrayX.get(y); - if (ratio == null) { - ratio = new AspectRatio(x, y); - arrayX.put(y, ratio); - } - return ratio; - } - } - - /** - * Parse an {@link AspectRatio} from a {@link String} formatted like "4:3". - * - * @param s The string representation of the aspect ratio - * @return The aspect ratio - * @throws IllegalArgumentException when the format is incorrect. - */ - public static AspectRatio parse(String s) { - int position = s.indexOf(':'); - if (position == -1) { - throw new IllegalArgumentException("Malformed aspect ratio: " + s); - } - try { - int x = Integer.parseInt(s.substring(0, position)); - int y = Integer.parseInt(s.substring(position + 1)); - return AspectRatio.of(x, y); - } catch (NumberFormatException e) { - throw new IllegalArgumentException("Malformed aspect ratio: " + s, e); - } - } - - private AspectRatio(int x, int y) { - mX = x; - mY = y; - } - - public int getX() { - return mX; - } - - public int getY() { - return mY; - } - - public boolean matches(Size size) { - int gcd = gcd(size.getWidth(), size.getHeight()); - int x = size.getWidth() / gcd; - int y = size.getHeight() / gcd; - return mX == x && mY == y; - } - - @Override - public boolean equals(Object o) { - if (o == null) { - return false; - } - if (this == o) { - return true; - } - if (o instanceof AspectRatio) { - AspectRatio ratio = (AspectRatio) o; - return mX == ratio.mX && mY == ratio.mY; - } - return false; - } - - @Override - public String toString() { - return mX + ":" + mY; - } - - public float toFloat() { - return (float) mX / mY; - } - - @Override - public int hashCode() { - // assuming most sizes are <2^16, doing a rotate will give us perfect hashing - return mY ^ ((mX << (Integer.SIZE / 2)) | (mX >>> (Integer.SIZE / 2))); - } - - @Override - public int compareTo(@NonNull AspectRatio another) { - if (equals(another)) { - return 0; - } else if (toFloat() - another.toFloat() > 0) { - return 1; - } - return -1; - } - - /** - * @return The inverse of this {@link AspectRatio}. - */ - public AspectRatio inverse() { - //noinspection SuspiciousNameCombination - return AspectRatio.of(mY, mX); - } - - private static int gcd(int a, int b) { - while (b != 0) { - int c = b; - b = a % b; - a = c; - } - return a; - } - - @Override - public int describeContents() { - return 0; - } - - @Override - public void writeToParcel(Parcel dest, int flags) { - dest.writeInt(mX); - dest.writeInt(mY); - } - - public static final Creator CREATOR - = new Creator() { - - @Override - public AspectRatio createFromParcel(Parcel source) { - int x = source.readInt(); - int y = source.readInt(); - return AspectRatio.of(x, y); - } - - @Override - public AspectRatio[] newArray(int size) { - return new AspectRatio[size]; - } - }; - -} diff --git a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/CameraConstants.java b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/CameraConstants.java deleted file mode 100644 index ad71203da028..000000000000 --- a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/CameraConstants.java +++ /dev/null @@ -1,26 +0,0 @@ -package io.flutter.plugins.camera; - -public class CameraConstants { - - public static final AspectRatio DEFAULT_ASPECT_RATIO = AspectRatio.of(16, 9); - - public static final long AUTO_FOCUS_TIMEOUT_MS = 800; //800ms timeout, Under normal circumstances need to a few hundred milliseconds - - public static final long OPEN_CAMERA_TIMEOUT_MS = 2500; //2.5s - - public static final int FOCUS_HOLD_MILLIS = 3000; - - public static final float METERING_REGION_FRACTION = 0.1225f; - - public static final int ZOOM_REGION_DEFAULT = 1; - - public static final int FLASH_OFF = 0; - public static final int FLASH_ON = 1; - public static final int FLASH_TORCH = 2; - public static final int FLASH_AUTO = 3; - public static final int FLASH_RED_EYE = 4; - - public static final int FACING_BACK = 0; - public static final int FACING_FRONT = 1; - -} From d87da417e0ef8bb49af9d09878f47d8d9ad42fcd Mon Sep 17 00:00:00 2001 From: Andrew Coutts Date: Mon, 1 Mar 2021 18:21:59 -0500 Subject: [PATCH 019/114] Bump version of platform interface --- packages/camera/camera/pubspec.yaml | 2 +- packages/camera/camera_platform_interface/CHANGELOG.md | 2 +- packages/camera/camera_platform_interface/pubspec.yaml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/camera/camera/pubspec.yaml b/packages/camera/camera/pubspec.yaml index 6cad2e470e3c..f08cd9388abf 100644 --- a/packages/camera/camera/pubspec.yaml +++ b/packages/camera/camera/pubspec.yaml @@ -9,7 +9,7 @@ dependencies: flutter: sdk: flutter - camera_platform_interface: ^2.0.1 + camera_platform_interface: ^2.0.2 pedantic: ^1.10.0 quiver: ^3.0.0-nullsafety.3 diff --git a/packages/camera/camera_platform_interface/CHANGELOG.md b/packages/camera/camera_platform_interface/CHANGELOG.md index 09536e48d1c7..7646a0037f22 100644 --- a/packages/camera/camera_platform_interface/CHANGELOG.md +++ b/packages/camera/camera_platform_interface/CHANGELOG.md @@ -1,4 +1,4 @@ -## 2.0.1 +## 2.0.2 - Added support for NV21 image stream format in Android. ## 2.0.0 diff --git a/packages/camera/camera_platform_interface/pubspec.yaml b/packages/camera/camera_platform_interface/pubspec.yaml index b3caeabd7951..894a2719c01d 100644 --- a/packages/camera/camera_platform_interface/pubspec.yaml +++ b/packages/camera/camera_platform_interface/pubspec.yaml @@ -3,7 +3,7 @@ description: A common platform interface for the camera plugin. homepage: https://github.com/flutter/plugins/tree/master/packages/camera/camera_platform_interface # NOTE: We strongly prefer non-breaking changes, even at the expense of a # less-clean API. See https://flutter.dev/go/platform-interface-breaking-changes -version: 2.0.1 +version: 2.0.2 dependencies: flutter: From 9302540eba1edc5ce32bf75433309f1b6e14dc2c Mon Sep 17 00:00:00 2001 From: Andrew Coutts Date: Tue, 2 Mar 2021 02:26:14 -0500 Subject: [PATCH 020/114] Clean up imports --- .../io/flutter/plugins/camera/CameraPermissions.java | 1 + .../java/io/flutter/plugins/camera/CameraPlugin.java | 2 ++ .../java/io/flutter/plugins/camera/CameraUtils.java | 6 ++++-- .../java/io/flutter/plugins/camera/CameraZoom.java | 1 + .../java/io/flutter/plugins/camera/DartMessenger.java | 7 +++++-- .../plugins/camera/DeviceOrientationManager.java | 1 + .../java/io/flutter/plugins/camera/ImageSaver.java | 1 - .../flutter/plugins/camera/MethodCallHandlerImpl.java | 7 +++++-- .../plugins/camera/media/MediaRecorderBuilder.java | 2 ++ .../io/flutter/plugins/camera/types/FlashMode.java | 10 ---------- 10 files changed, 21 insertions(+), 17 deletions(-) diff --git a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/CameraPermissions.java b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/CameraPermissions.java index 3529e69a2b0b..b9ca5da3a1a7 100644 --- a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/CameraPermissions.java +++ b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/CameraPermissions.java @@ -8,6 +8,7 @@ import android.Manifest.permission; import android.app.Activity; import android.content.pm.PackageManager; + import androidx.annotation.VisibleForTesting; import androidx.core.app.ActivityCompat; import androidx.core.content.ContextCompat; diff --git a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/CameraPlugin.java b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/CameraPlugin.java index 93183bb7c0a7..85af292a211b 100644 --- a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/CameraPlugin.java +++ b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/CameraPlugin.java @@ -6,8 +6,10 @@ import android.app.Activity; import android.os.Build; + import androidx.annotation.NonNull; import androidx.annotation.Nullable; + import io.flutter.embedding.engine.plugins.FlutterPlugin; import io.flutter.embedding.engine.plugins.activity.ActivityAware; import io.flutter.embedding.engine.plugins.activity.ActivityPluginBinding; diff --git a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/CameraUtils.java b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/CameraUtils.java index 03993a3b51f9..2cd3a8be261a 100644 --- a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/CameraUtils.java +++ b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/CameraUtils.java @@ -14,8 +14,7 @@ import android.hardware.camera2.params.StreamConfigurationMap; import android.media.CamcorderProfile; import android.util.Size; -import io.flutter.embedding.engine.systemchannels.PlatformChannel; -import io.flutter.plugins.camera.types.ResolutionPreset; + import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; @@ -24,6 +23,9 @@ import java.util.List; import java.util.Map; +import io.flutter.embedding.engine.systemchannels.PlatformChannel; +import io.flutter.plugins.camera.types.ResolutionPreset; + /** Provides various utilities for camera. */ public final class CameraUtils { diff --git a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/CameraZoom.java b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/CameraZoom.java index 5eed9f4734b7..36aee3b94777 100644 --- a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/CameraZoom.java +++ b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/CameraZoom.java @@ -5,6 +5,7 @@ package io.flutter.plugins.camera; import android.graphics.Rect; + import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.core.math.MathUtils; diff --git a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/DartMessenger.java b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/DartMessenger.java index 3892452892d9..0068421015dc 100644 --- a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/DartMessenger.java +++ b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/DartMessenger.java @@ -7,14 +7,17 @@ import android.os.Handler; import android.os.Looper; import android.text.TextUtils; + import androidx.annotation.Nullable; + +import java.util.HashMap; +import java.util.Map; + import io.flutter.embedding.engine.systemchannels.PlatformChannel; import io.flutter.plugin.common.BinaryMessenger; import io.flutter.plugin.common.MethodChannel; import io.flutter.plugins.camera.types.ExposureMode; import io.flutter.plugins.camera.types.FocusMode; -import java.util.HashMap; -import java.util.Map; class DartMessenger { @Nullable private MethodChannel cameraChannel; diff --git a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/DeviceOrientationManager.java b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/DeviceOrientationManager.java index b2a504b629d6..46f31cdab2c5 100644 --- a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/DeviceOrientationManager.java +++ b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/DeviceOrientationManager.java @@ -16,6 +16,7 @@ import android.view.OrientationEventListener; import android.view.Surface; import android.view.WindowManager; + import io.flutter.embedding.engine.systemchannels.PlatformChannel; class DeviceOrientationManager { diff --git a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/ImageSaver.java b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/ImageSaver.java index 954818606db9..589cfd1f553a 100644 --- a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/ImageSaver.java +++ b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/ImageSaver.java @@ -1,7 +1,6 @@ package io.flutter.plugins.camera; import android.media.Image; -import android.media.ImageReader; import android.os.Handler; import android.os.Looper; diff --git a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/MethodCallHandlerImpl.java b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/MethodCallHandlerImpl.java index aa7483f55679..19307a2f6a78 100644 --- a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/MethodCallHandlerImpl.java +++ b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/MethodCallHandlerImpl.java @@ -6,8 +6,13 @@ import android.app.Activity; import android.hardware.camera2.CameraAccessException; + import androidx.annotation.NonNull; import androidx.annotation.Nullable; + +import java.util.HashMap; +import java.util.Map; + import io.flutter.embedding.engine.systemchannels.PlatformChannel; import io.flutter.plugin.common.BinaryMessenger; import io.flutter.plugin.common.EventChannel; @@ -19,8 +24,6 @@ import io.flutter.plugins.camera.types.FlashMode; import io.flutter.plugins.camera.types.FocusMode; import io.flutter.view.TextureRegistry; -import java.util.HashMap; -import java.util.Map; final class MethodCallHandlerImpl implements MethodChannel.MethodCallHandler { private final Activity activity; diff --git a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/media/MediaRecorderBuilder.java b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/media/MediaRecorderBuilder.java index 4c3fb3add230..5e5d0c44c115 100644 --- a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/media/MediaRecorderBuilder.java +++ b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/media/MediaRecorderBuilder.java @@ -6,7 +6,9 @@ import android.media.CamcorderProfile; import android.media.MediaRecorder; + import androidx.annotation.NonNull; + import java.io.IOException; public class MediaRecorderBuilder { diff --git a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/types/FlashMode.java b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/types/FlashMode.java index 69db79859b80..b8ff2fac6652 100644 --- a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/types/FlashMode.java +++ b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/types/FlashMode.java @@ -4,16 +4,6 @@ package io.flutter.plugins.camera.types; -import android.hardware.camera2.CameraCaptureSession; -import android.hardware.camera2.CameraCharacteristics; -import android.hardware.camera2.CaptureFailure; -import android.hardware.camera2.CaptureRequest; -import android.hardware.camera2.TotalCaptureResult; - -import androidx.annotation.NonNull; - -import io.flutter.plugin.common.MethodChannel; - // Mirrors flash_mode.dart public enum FlashMode { off("off"), From 67af0655f1e3888eca9b96bdaf4926d2aa948992 Mon Sep 17 00:00:00 2001 From: Andrew Coutts Date: Tue, 2 Mar 2021 02:36:47 -0500 Subject: [PATCH 021/114] Handle LENS_INFO_MINIMUM_FOCUS_DISTANCE being null --- .../io/flutter/plugins/camera/Camera.java | 22 +++++++++++++------ 1 file changed, 15 insertions(+), 7 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 37d7659ed985..d7b28ae38265 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 @@ -325,7 +325,8 @@ public Camera( } /** - * Check if the auto focus is supported. + * Check if the auto focus is supported by the current camera. We look at the available AF modes + * and the available lens focusing distance to determine if its' a fixed length lens or not as well. */ private void checkAutoFocusSupported() { int[] modes = cameraCharacteristics.get(CameraCharacteristics.CONTROL_AF_AVAILABLE_MODES); @@ -336,15 +337,22 @@ private void checkAutoFocusSupported() { // Check if fixed focal length lens. If LENS_INFO_MINIMUM_FOCUS_DISTANCE=0, then this is fixed. // Can be null on some devices. - float minFocus = cameraCharacteristics.get(CameraCharacteristics.LENS_INFO_MINIMUM_FOCUS_DISTANCE); - float maxFocus = cameraCharacteristics.get(CameraCharacteristics.LENS_INFO_HYPERFOCAL_DISTANCE); - final boolean isFixedLength = minFocus == 0; + final Float minFocus = cameraCharacteristics.get(CameraCharacteristics.LENS_INFO_MINIMUM_FOCUS_DISTANCE); + // final Float maxFocus = cameraCharacteristics.get(CameraCharacteristics.LENS_INFO_HYPERFOCAL_DISTANCE); + + // Value can be null on some devices: + // https://developer.android.com/reference/android/hardware/camera2/CameraCharacteristics#LENS_INFO_MINIMUM_FOCUS_DISTANCE + boolean isFixedLength; + if (minFocus == null) { + isFixedLength = true; + } else { + isFixedLength = minFocus == 0; + } // Log.i(TAG, "checkAutoFocusSupported | minFocus " + minFocus + " | maxFocus: " + maxFocus); - - mAutoFocusSupported = !(modes == null || modes.length == 0 || + mAutoFocusSupported = !isFixedLength && !(modes == null || modes.length == 0 || (modes.length == 1 && modes[0] == CameraCharacteristics.CONTROL_AF_MODE_OFF)); - // Log.i(TAG, "checkAutoFocusSupported: " + mAutoFocusSupported); + Log.i(TAG, "checkAutoFocusSupported: " + mAutoFocusSupported); } /** From 6b110e151a9e0c142d54d74b32e48e4b21734c5e Mon Sep 17 00:00:00 2001 From: Andrew Coutts Date: Tue, 2 Mar 2021 03:06:29 -0500 Subject: [PATCH 022/114] Cleanup --- .../io/flutter/plugins/camera/Camera.java | 2530 ++++++++--------- .../plugins/camera/CameraPermissions.java | 1 - .../flutter/plugins/camera/CameraPlugin.java | 2 - .../flutter/plugins/camera/CameraState.java | 37 +- .../flutter/plugins/camera/CameraUtils.java | 6 +- .../io/flutter/plugins/camera/CameraZoom.java | 1 - .../flutter/plugins/camera/DartMessenger.java | 7 +- .../camera/DeviceOrientationManager.java | 1 - .../io/flutter/plugins/camera/ImageSaver.java | 109 +- .../plugins/camera/MethodCallHandlerImpl.java | 7 +- .../plugins/camera/PictureCaptureRequest.java | 353 ++- .../camera/PictureCaptureRequestState.java | 54 +- .../camera/media/MediaRecorderBuilder.java | 2 - .../plugins/camera/types/FlashMode.java | 5 +- .../camera/PictureCaptureRequestTest.java | 275 +- 15 files changed, 1645 insertions(+), 1745 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 d7b28ae38265..74b365e3f381 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 @@ -4,6 +4,8 @@ package io.flutter.plugins.camera; +import static io.flutter.plugins.camera.CameraUtils.computeBestPreviewSize; + import android.annotation.SuppressLint; import android.annotation.TargetApi; import android.app.Activity; @@ -39,10 +41,17 @@ import android.util.Size; import android.util.SparseIntArray; import android.view.Surface; - import androidx.annotation.NonNull; import androidx.annotation.Nullable; - +import io.flutter.embedding.engine.systemchannels.PlatformChannel; +import io.flutter.plugin.common.EventChannel; +import io.flutter.plugin.common.MethodChannel.Result; +import io.flutter.plugins.camera.media.MediaRecorderBuilder; +import io.flutter.plugins.camera.types.ExposureMode; +import io.flutter.plugins.camera.types.FlashMode; +import io.flutter.plugins.camera.types.FocusMode; +import io.flutter.plugins.camera.types.ResolutionPreset; +import io.flutter.view.TextureRegistry.SurfaceTextureEntry; import java.io.File; import java.io.IOException; import java.nio.ByteBuffer; @@ -54,1417 +63,1372 @@ import java.util.Map; import java.util.concurrent.Executors; -import io.flutter.embedding.engine.systemchannels.PlatformChannel; -import io.flutter.plugin.common.EventChannel; -import io.flutter.plugin.common.MethodChannel.Result; -import io.flutter.plugins.camera.media.MediaRecorderBuilder; -import io.flutter.plugins.camera.types.ExposureMode; -import io.flutter.plugins.camera.types.FlashMode; -import io.flutter.plugins.camera.types.FocusMode; -import io.flutter.plugins.camera.types.ResolutionPreset; -import io.flutter.view.TextureRegistry.SurfaceTextureEntry; - -import static io.flutter.plugins.camera.CameraUtils.computeBestPreviewSize; - @FunctionalInterface interface ErrorCallback { - void onError(String errorCode, String errorMessage); + void onError(String errorCode, String errorMessage); } public class Camera { - private static final String TAG = "Camera"; - - - /** - * Conversion from screen rotation to JPEG orientation. - */ - private static final SparseIntArray ORIENTATIONS = new SparseIntArray(); - private static final HashMap supportedImageFormats; - - static { - ORIENTATIONS.append(Surface.ROTATION_0, 90); - ORIENTATIONS.append(Surface.ROTATION_90, 0); - ORIENTATIONS.append(Surface.ROTATION_180, 270); - ORIENTATIONS.append(Surface.ROTATION_270, 180); - } - - // Current supported outputs - static { - supportedImageFormats = new HashMap<>(); - supportedImageFormats.put("yuv420", ImageFormat.YUV_420_888); - supportedImageFormats.put("jpeg", ImageFormat.JPEG); - supportedImageFormats.put("nv21", ImageFormat.NV21); - } - - private final SurfaceTextureEntry flutterTexture; - private final CameraManager cameraManager; - private final DeviceOrientationManager deviceOrientationListener; - private final boolean isFrontFacing; - private final int sensorOrientation; - private final String cameraName; - private final Size captureSize; - private final Size previewSize; - private final boolean enableAudio; - private final Context applicationContext; - private final CamcorderProfile recordingProfile; - private final DartMessenger dartMessenger; - private final CameraZoom cameraZoom; - private final CameraCharacteristics cameraCharacteristics; - private final Activity activity; - /** - * This manages the state of the camera and the current capture request. - */ - PictureCaptureRequest pictureCaptureRequest; - /** - * Whether the current camera device supports auto focus or not. - */ - private boolean mAutoFocusSupported = true; - /** - * The state of the camera. By default we are in the preview state. - */ - private CameraState cameraState = CameraState.STATE_PREVIEW; - /** - * A {@link Handler} for running tasks in the background. - */ - private Handler mBackgroundHandler; - /** - * This a callback object for the {@link ImageReader}. "onImageAvailable" will be called when a - * still image is ready to be saved. - */ - private final ImageReader.OnImageAvailableListener mOnImageAvailableListener - = new ImageReader.OnImageAvailableListener() { + private static final String TAG = "Camera"; + + /** Conversion from screen rotation to JPEG orientation. */ + private static final SparseIntArray ORIENTATIONS = new SparseIntArray(); + + private static final HashMap supportedImageFormats; + + static { + ORIENTATIONS.append(Surface.ROTATION_0, 90); + ORIENTATIONS.append(Surface.ROTATION_90, 0); + ORIENTATIONS.append(Surface.ROTATION_180, 270); + ORIENTATIONS.append(Surface.ROTATION_270, 180); + } + + // Current supported outputs + static { + supportedImageFormats = new HashMap<>(); + supportedImageFormats.put("yuv420", ImageFormat.YUV_420_888); + supportedImageFormats.put("jpeg", ImageFormat.JPEG); + supportedImageFormats.put("nv21", ImageFormat.NV21); + } + + private final SurfaceTextureEntry flutterTexture; + private final CameraManager cameraManager; + private final DeviceOrientationManager deviceOrientationListener; + private final boolean isFrontFacing; + private final int sensorOrientation; + private final String cameraName; + private final Size captureSize; + private final Size previewSize; + private final boolean enableAudio; + private final Context applicationContext; + private final CamcorderProfile recordingProfile; + private final DartMessenger dartMessenger; + private final CameraZoom cameraZoom; + private final CameraCharacteristics cameraCharacteristics; + private final Activity activity; + /** This manages the state of the camera and the current capture request. */ + PictureCaptureRequest pictureCaptureRequest; + /** Whether the current camera device supports auto focus or not. */ + private boolean mAutoFocusSupported = true; + /** The state of the camera. By default we are in the preview state. */ + private CameraState cameraState = CameraState.STATE_PREVIEW; + /** A {@link Handler} for running tasks in the background. */ + private Handler mBackgroundHandler; + /** + * This a callback object for the {@link ImageReader}. "onImageAvailable" will be called when a + * still image is ready to be saved. + */ + private final ImageReader.OnImageAvailableListener mOnImageAvailableListener = + new ImageReader.OnImageAvailableListener() { @Override public void onImageAvailable(ImageReader reader) { - // Log.i(TAG, "onImageAvailable"); - mBackgroundHandler.post(new ImageSaver(reader.acquireNextImage(), pictureCaptureRequest.mFile, pictureCaptureRequest)); - cameraState = CameraState.STATE_PREVIEW; + // Log.i(TAG, "onImageAvailable"); + mBackgroundHandler.post( + new ImageSaver( + reader.acquireLatestImage(), pictureCaptureRequest.mFile, pictureCaptureRequest)); + cameraState = CameraState.STATE_PREVIEW; } - }; - - /** - * An additional thread for running tasks that shouldn't block the UI. - */ - private HandlerThread mBackgroundThread; - private CameraDevice cameraDevice; - private CameraCaptureSession captureSession; - private ImageReader pictureImageReader; - private ImageReader imageStreamReader; - /** - * {@link CaptureRequest.Builder} for the camera preview - */ - private CaptureRequest.Builder mPreviewRequestBuilder; - /** - * {@link CaptureRequest} generated by {@link #mPreviewRequestBuilder} - */ - private MediaRecorder mediaRecorder; - private boolean recordingVideo; - private File videoRecordingFile; - /** - * Flash mode setting of the current camera. Initialize to off because - * we don't know if the current camera supports flash yet. - */ - private FlashMode currentFlashMode; - /** - * Exposure mode setting of the current camera. Initialize to auto - * because all cameras support autoexposure by default. - */ - private ExposureMode currentExposureMode; - /** - * Focus mode setting of the current camera. Initialize to locked because - * we don't know if the current camera supports autofocus yet. - */ - private FocusMode currentFocusMode; - /** - * Whether or not to use autofocus. - */ - private boolean useAutoFocus = false; - /** - * Whether the current camera device supports Flash or not. - */ - private boolean mFlashSupported = false; - private CameraRegions cameraRegions; - private int exposureOffset; - /** - * A {@link CameraCaptureSession.CaptureCallback} that handles events related to JPEG capture. - */ - private final CameraCaptureSession.CaptureCallback mCaptureCallback - = new CameraCaptureSession.CaptureCallback() { + }; + + /** An additional thread for running tasks that shouldn't block the UI. */ + private HandlerThread mBackgroundThread; + + private CameraDevice cameraDevice; + private CameraCaptureSession captureSession; + private ImageReader pictureImageReader; + private ImageReader imageStreamReader; + /** {@link CaptureRequest.Builder} for the camera preview */ + private CaptureRequest.Builder mPreviewRequestBuilder; + /** {@link CaptureRequest} generated by {@link #mPreviewRequestBuilder} */ + private MediaRecorder mediaRecorder; + + private boolean recordingVideo; + private File videoRecordingFile; + /** + * Flash mode setting of the current camera. Initialize to off because we don't know if the + * current camera supports flash yet. + */ + private FlashMode currentFlashMode; + /** + * Exposure mode setting of the current camera. Initialize to auto because all cameras support + * autoexposure by default. + */ + private ExposureMode currentExposureMode; + /** + * Focus mode setting of the current camera. Initialize to locked because we don't know if the + * current camera supports autofocus yet. + */ + private FocusMode currentFocusMode; + /** Whether or not to use autofocus. */ + private boolean useAutoFocus = false; + /** Whether the current camera device supports Flash or not. */ + private boolean mFlashSupported = false; + + private CameraRegions cameraRegions; + private int exposureOffset; + /** A {@link CameraCaptureSession.CaptureCallback} that handles events related to JPEG capture. */ + private final CameraCaptureSession.CaptureCallback mCaptureCallback = + new CameraCaptureSession.CaptureCallback() { private void process(CaptureResult result) { - Integer aeState = result.get(CaptureResult.CONTROL_AE_STATE); - Integer afState = result.get(CaptureResult.CONTROL_AF_STATE); - - if (cameraState != CameraState.STATE_PREVIEW) { - // Log.i(TAG, "mCaptureCallback | state: " + cameraState + " | afState: " + afState + " | aeState: " + aeState); - } + Integer aeState = result.get(CaptureResult.CONTROL_AE_STATE); + Integer afState = result.get(CaptureResult.CONTROL_AF_STATE); - switch (cameraState) { - case STATE_PREVIEW: { - // We have nothing to do when the camera preview is working normally. - break; - } + if (cameraState != CameraState.STATE_PREVIEW) { + // Log.i(TAG, "mCaptureCallback | state: " + cameraState + " | afState: " + afState + " | aeState: " + aeState); + } - case STATE_WAITING_FOCUS: { - if (afState == null) { - return; - } else if ( - afState == CaptureRequest.CONTROL_AF_STATE_PASSIVE_SCAN || - afState == CaptureRequest.CONTROL_AF_STATE_FOCUSED_LOCKED || - afState == CaptureRequest.CONTROL_AF_STATE_NOT_FOCUSED_LOCKED) { - // CONTROL_AE_STATE can be null on some devices - - if (aeState == null || - aeState == CaptureRequest.CONTROL_AE_STATE_CONVERGED) { - takePictureAfterPrecapture(); - } else { - runPrecaptureSequence(); - } - } - break; + switch (cameraState) { + case STATE_PREVIEW: + { + // We have nothing to do when the camera preview is working normally. + break; + } + + case STATE_WAITING_FOCUS: + { + if (afState == null) { + return; + } else if (afState == CaptureRequest.CONTROL_AF_STATE_PASSIVE_SCAN + || afState == CaptureRequest.CONTROL_AF_STATE_FOCUSED_LOCKED + || afState == CaptureRequest.CONTROL_AF_STATE_NOT_FOCUSED_LOCKED) { + // CONTROL_AE_STATE can be null on some devices + + if (aeState == null || aeState == CaptureRequest.CONTROL_AE_STATE_CONVERGED) { + takePictureAfterPrecapture(); + } else { + runPrecaptureSequence(); + } } - - case STATE_WAITING_PRECAPTURE_START: { - // CONTROL_AE_STATE can be null on some devices - if (aeState == null || - aeState == CaptureResult.CONTROL_AE_STATE_CONVERGED || - aeState == CaptureResult.CONTROL_AE_STATE_PRECAPTURE || - aeState == CaptureResult.CONTROL_AE_STATE_FLASH_REQUIRED) { - cameraState = CameraState.STATE_WAITING_PRECAPTURE_DONE; - pictureCaptureRequest.setState(PictureCaptureRequestState.STATE_WAITING_PRECAPTURE_DONE); - } - break; + break; + } + + case STATE_WAITING_PRECAPTURE_START: + { + // CONTROL_AE_STATE can be null on some devices + if (aeState == null + || aeState == CaptureResult.CONTROL_AE_STATE_CONVERGED + || aeState == CaptureResult.CONTROL_AE_STATE_PRECAPTURE + || aeState == CaptureResult.CONTROL_AE_STATE_FLASH_REQUIRED) { + cameraState = CameraState.STATE_WAITING_PRECAPTURE_DONE; + pictureCaptureRequest.setState( + PictureCaptureRequestState.STATE_WAITING_PRECAPTURE_DONE); } - - case STATE_WAITING_PRECAPTURE_DONE: { - // CONTROL_AE_STATE can be null on some devices - if (aeState == null || aeState != CaptureResult.CONTROL_AE_STATE_PRECAPTURE) { - takePictureAfterPrecapture(); - } else { - if (pictureCaptureRequest.hitPreCaptureTimeout()) { - // Log.i(TAG, "===> Hit precapture timeout"); - unlockAutoFocus(); - } - } - break; + break; + } + + case STATE_WAITING_PRECAPTURE_DONE: + { + // CONTROL_AE_STATE can be null on some devices + if (aeState == null || aeState != CaptureResult.CONTROL_AE_STATE_PRECAPTURE) { + takePictureAfterPrecapture(); + } else { + if (pictureCaptureRequest.hitPreCaptureTimeout()) { + // Log.i(TAG, "===> Hit precapture timeout"); + unlockAutoFocus(); + } } - } + break; + } + } } @Override - public void onCaptureProgressed(@NonNull CameraCaptureSession session, - @NonNull CaptureRequest request, - @NonNull CaptureResult partialResult) { - process(partialResult); + public void onCaptureProgressed( + @NonNull CameraCaptureSession session, + @NonNull CaptureRequest request, + @NonNull CaptureResult partialResult) { + process(partialResult); } @Override - public void onCaptureCompleted(@NonNull CameraCaptureSession session, - @NonNull CaptureRequest request, - @NonNull TotalCaptureResult result) { - process(result); - } - }; - private Range fpsRange; - private PlatformChannel.DeviceOrientation lockedCaptureOrientation; - - public Camera( - final Activity activity, - final SurfaceTextureEntry flutterTexture, - final DartMessenger dartMessenger, - final String cameraName, - final String resolutionPreset, - final boolean enableAudio) - throws CameraAccessException { - // Log.i(TAG, "Camear constructor"); - - if (activity == null) { - throw new IllegalStateException("No activity available!"); + public void onCaptureCompleted( + @NonNull CameraCaptureSession session, + @NonNull CaptureRequest request, + @NonNull TotalCaptureResult result) { + process(result); } - this.activity = activity; - this.cameraName = cameraName; - this.enableAudio = enableAudio; - this.flutterTexture = flutterTexture; - this.dartMessenger = dartMessenger; - this.cameraManager = (CameraManager) activity.getSystemService(Context.CAMERA_SERVICE); - this.applicationContext = activity.getApplicationContext(); - this.currentFlashMode = FlashMode.off; - this.currentExposureMode = ExposureMode.auto; - this.currentFocusMode = FocusMode.auto; - this.exposureOffset = 0; - - // Get camera characteristics and check for supported features - cameraCharacteristics = cameraManager.getCameraCharacteristics(cameraName); - getAvailableFpsRange(cameraCharacteristics); - checkAutoFocusSupported(); - checkFlashSupported(); - - // Setup orientation - sensorOrientation = cameraCharacteristics.get(CameraCharacteristics.SENSOR_ORIENTATION); - isFrontFacing = - cameraCharacteristics.get(CameraCharacteristics.LENS_FACING) - == CameraMetadata.LENS_FACING_FRONT; - deviceOrientationListener = - new DeviceOrientationManager(activity, dartMessenger, isFrontFacing, sensorOrientation); - deviceOrientationListener.start(); - - // Resolution configuration - ResolutionPreset preset = ResolutionPreset.valueOf(resolutionPreset); - recordingProfile = - CameraUtils.getBestAvailableCamcorderProfileForResolutionPreset(cameraName, preset); - captureSize = new Size(recordingProfile.videoFrameWidth, recordingProfile.videoFrameHeight); - previewSize = computeBestPreviewSize(cameraName, preset); - - // Zoom setup - cameraZoom = - new CameraZoom( - cameraCharacteristics.get(CameraCharacteristics.SENSOR_INFO_ACTIVE_ARRAY_SIZE), - cameraCharacteristics.get(CameraCharacteristics.SCALER_AVAILABLE_MAX_DIGITAL_ZOOM)); - - // Start background thread. - startBackgroundThread(); + }; + + private Range fpsRange; + private PlatformChannel.DeviceOrientation lockedCaptureOrientation; + + public Camera( + final Activity activity, + final SurfaceTextureEntry flutterTexture, + final DartMessenger dartMessenger, + final String cameraName, + final String resolutionPreset, + final boolean enableAudio) + throws CameraAccessException { + // Log.i(TAG, "Camear constructor"); + + if (activity == null) { + throw new IllegalStateException("No activity available!"); } - - /** - * Check if the auto focus is supported by the current camera. We look at the available AF modes - * and the available lens focusing distance to determine if its' a fixed length lens or not as well. - */ - private void checkAutoFocusSupported() { - int[] modes = cameraCharacteristics.get(CameraCharacteristics.CONTROL_AF_AVAILABLE_MODES); - // Log.i(TAG, "checkAutoFocusSupported | modes:"); - for (int mode : modes) { - // Log.i(TAG, "checkAutoFocusSupported | ==> " + mode); - } - - // Check if fixed focal length lens. If LENS_INFO_MINIMUM_FOCUS_DISTANCE=0, then this is fixed. - // Can be null on some devices. - final Float minFocus = cameraCharacteristics.get(CameraCharacteristics.LENS_INFO_MINIMUM_FOCUS_DISTANCE); - // final Float maxFocus = cameraCharacteristics.get(CameraCharacteristics.LENS_INFO_HYPERFOCAL_DISTANCE); - - // Value can be null on some devices: - // https://developer.android.com/reference/android/hardware/camera2/CameraCharacteristics#LENS_INFO_MINIMUM_FOCUS_DISTANCE - boolean isFixedLength; - if (minFocus == null) { - isFixedLength = true; - } else { - isFixedLength = minFocus == 0; - } - // Log.i(TAG, "checkAutoFocusSupported | minFocus " + minFocus + " | maxFocus: " + maxFocus); - - mAutoFocusSupported = !isFixedLength && !(modes == null || modes.length == 0 || - (modes.length == 1 && modes[0] == CameraCharacteristics.CONTROL_AF_MODE_OFF)); - Log.i(TAG, "checkAutoFocusSupported: " + mAutoFocusSupported); + this.activity = activity; + this.cameraName = cameraName; + this.enableAudio = enableAudio; + this.flutterTexture = flutterTexture; + this.dartMessenger = dartMessenger; + this.cameraManager = (CameraManager) activity.getSystemService(Context.CAMERA_SERVICE); + this.applicationContext = activity.getApplicationContext(); + this.currentFlashMode = FlashMode.off; + this.currentExposureMode = ExposureMode.auto; + this.currentFocusMode = FocusMode.auto; + this.exposureOffset = 0; + + // Get camera characteristics and check for supported features + cameraCharacteristics = cameraManager.getCameraCharacteristics(cameraName); + getAvailableFpsRange(cameraCharacteristics); + checkAutoFocusSupported(); + checkFlashSupported(); + + // Setup orientation + sensorOrientation = cameraCharacteristics.get(CameraCharacteristics.SENSOR_ORIENTATION); + isFrontFacing = + cameraCharacteristics.get(CameraCharacteristics.LENS_FACING) + == CameraMetadata.LENS_FACING_FRONT; + deviceOrientationListener = + new DeviceOrientationManager(activity, dartMessenger, isFrontFacing, sensorOrientation); + deviceOrientationListener.start(); + + // Resolution configuration + ResolutionPreset preset = ResolutionPreset.valueOf(resolutionPreset); + recordingProfile = + CameraUtils.getBestAvailableCamcorderProfileForResolutionPreset(cameraName, preset); + captureSize = new Size(recordingProfile.videoFrameWidth, recordingProfile.videoFrameHeight); + Log.i(TAG, "captureSize: " + captureSize); + + previewSize = computeBestPreviewSize(cameraName, preset); + + // Zoom setup + cameraZoom = + new CameraZoom( + cameraCharacteristics.get(CameraCharacteristics.SENSOR_INFO_ACTIVE_ARRAY_SIZE), + cameraCharacteristics.get(CameraCharacteristics.SCALER_AVAILABLE_MAX_DIGITAL_ZOOM)); + + // Start background thread. + startBackgroundThread(); + } + + /** + * Check if the auto focus is supported by the current camera. We look at the available AF modes + * and the available lens focusing distance to determine if its' a fixed length lens or not as + * well. + */ + private void checkAutoFocusSupported() { + int[] modes = cameraCharacteristics.get(CameraCharacteristics.CONTROL_AF_AVAILABLE_MODES); + // Log.i(TAG, "checkAutoFocusSupported | modes:"); + for (int mode : modes) { + // Log.i(TAG, "checkAutoFocusSupported | ==> " + mode); } - /** - * Check if the flash is supported. - */ - private void checkFlashSupported() { - Boolean available = cameraCharacteristics.get(CameraCharacteristics.FLASH_INFO_AVAILABLE); - mFlashSupported = available == null ? false : available; + // Check if fixed focal length lens. If LENS_INFO_MINIMUM_FOCUS_DISTANCE=0, then this is fixed. + // Can be null on some devices. + final Float minFocus = + cameraCharacteristics.get(CameraCharacteristics.LENS_INFO_MINIMUM_FOCUS_DISTANCE); + // final Float maxFocus = cameraCharacteristics.get(CameraCharacteristics.LENS_INFO_HYPERFOCAL_DISTANCE); + + // Value can be null on some devices: + // https://developer.android.com/reference/android/hardware/camera2/CameraCharacteristics#LENS_INFO_MINIMUM_FOCUS_DISTANCE + boolean isFixedLength; + if (minFocus == null) { + isFixedLength = true; + } else { + isFixedLength = minFocus == 0; } - - /** - * Load available FPS range for the current camera and update the available fps range with it. - * - * @param cameraCharacteristics - */ - private void getAvailableFpsRange(CameraCharacteristics cameraCharacteristics) { - // Log.i(TAG, "getAvailableFpsRange"); - - try { - Range[] ranges = - cameraCharacteristics.get(CameraCharacteristics.CONTROL_AE_AVAILABLE_TARGET_FPS_RANGES); - if (ranges != null) { - for (Range range : ranges) { - int upper = range.getUpper(); - // Log.i("Camera", "[FPS Range Available] is:" + range); - if (upper >= 10) { - if (fpsRange == null || upper > fpsRange.getUpper()) { - fpsRange = range; - } - } - } + // Log.i(TAG, "checkAutoFocusSupported | minFocus " + minFocus + " | maxFocus: " + maxFocus); + + mAutoFocusSupported = + !isFixedLength + && !(modes == null + || modes.length == 0 + || (modes.length == 1 && modes[0] == CameraCharacteristics.CONTROL_AF_MODE_OFF)); + Log.i(TAG, "checkAutoFocusSupported: " + mAutoFocusSupported); + } + + /** Check if the flash is supported. */ + private void checkFlashSupported() { + Boolean available = cameraCharacteristics.get(CameraCharacteristics.FLASH_INFO_AVAILABLE); + mFlashSupported = available == null ? false : available; + } + + /** + * Load available FPS range for the current camera and update the available fps range with it. + * + * @param cameraCharacteristics + */ + private void getAvailableFpsRange(CameraCharacteristics cameraCharacteristics) { + // Log.i(TAG, "getAvailableFpsRange"); + + try { + Range[] ranges = + cameraCharacteristics.get(CameraCharacteristics.CONTROL_AE_AVAILABLE_TARGET_FPS_RANGES); + if (ranges != null) { + for (Range range : ranges) { + int upper = range.getUpper(); + // Log.i("Camera", "[FPS Range Available] is:" + range); + if (upper >= 10) { + if (fpsRange == null || upper > fpsRange.getUpper()) { + fpsRange = range; } - } catch (Exception e) { - pictureCaptureRequest.error("cameraAccess", e.getMessage(), null); + } } - // Log.i("Camera", "[FPS Range] is:" + fpsRange); + } + } catch (Exception e) { + pictureCaptureRequest.error("cameraAccess", e.getMessage(), null); } + // Log.i("Camera", "[FPS Range] is:" + fpsRange); + } - private void prepareMediaRecorder(String outputFilePath) throws IOException { - // Log.i(TAG, "prepareMediaRecorder"); + private void prepareMediaRecorder(String outputFilePath) throws IOException { + // Log.i(TAG, "prepareMediaRecorder"); - if (mediaRecorder != null) { - mediaRecorder.release(); - } - - mediaRecorder = - new MediaRecorderBuilder(recordingProfile, outputFilePath) - .setEnableAudio(enableAudio) - .setMediaOrientation( - lockedCaptureOrientation == null - ? deviceOrientationListener.getMediaOrientation() - : deviceOrientationListener.getMediaOrientation(lockedCaptureOrientation)) - .build(); + if (mediaRecorder != null) { + mediaRecorder.release(); } - @SuppressLint("MissingPermission") - public void open(String imageFormatGroup) throws CameraAccessException { - pictureImageReader = - ImageReader.newInstance( - captureSize.getWidth(), - captureSize.getHeight(), - ImageFormat.JPEG, 2); - - Integer imageFormat = supportedImageFormats.get(imageFormatGroup); - if (imageFormat == null) { - Log.w(TAG, "The selected imageFormatGroup is not supported by Android. Defaulting to yuv420"); - imageFormat = ImageFormat.YUV_420_888; - } - - // Used to steam image byte data to dart side. - imageStreamReader = - ImageReader.newInstance(previewSize.getWidth(), previewSize.getHeight(), imageFormat, 2); - - cameraManager.openCamera( - cameraName, - new CameraDevice.StateCallback() { - @Override - public void onOpened(@NonNull CameraDevice device) { - // Log.i(TAG, "open | onOpened"); - - - cameraDevice = device; - try { - startPreview(); - dartMessenger.sendCameraInitializedEvent( - previewSize.getWidth(), - previewSize.getHeight(), - currentExposureMode, - currentFocusMode, - isExposurePointSupported(), - isFocusPointSupported()); - } catch (CameraAccessException e) { - dartMessenger.sendCameraErrorEvent(e.getMessage()); - close(); - } - } - - @Override - public void onClosed(@NonNull CameraDevice camera) { - // Log.i(TAG, "open | onClosed"); - - dartMessenger.sendCameraClosingEvent(); - super.onClosed(camera); - } - - @Override - public void onDisconnected(@NonNull CameraDevice cameraDevice) { - // Log.i(TAG, "open | onDisconnected"); - - close(); - dartMessenger.sendCameraErrorEvent("The camera was disconnected."); - } - - @Override - public void onError(@NonNull CameraDevice cameraDevice, int errorCode) { - // Log.i(TAG, "open | onError"); - - close(); - String errorDescription; - switch (errorCode) { - case ERROR_CAMERA_IN_USE: - errorDescription = "The camera device is in use already."; - break; - case ERROR_MAX_CAMERAS_IN_USE: - errorDescription = "Max cameras in use"; - break; - case ERROR_CAMERA_DISABLED: - errorDescription = "The camera device could not be opened due to a device policy."; - break; - case ERROR_CAMERA_DEVICE: - errorDescription = "The camera device has encountered a fatal error"; - break; - case ERROR_CAMERA_SERVICE: - errorDescription = "The camera service has encountered a fatal error."; - break; - default: - errorDescription = "Unknown camera error"; - } - dartMessenger.sendCameraErrorEvent(errorDescription); - } - }, - mBackgroundHandler - ); + mediaRecorder = + new MediaRecorderBuilder(recordingProfile, outputFilePath) + .setEnableAudio(enableAudio) + .setMediaOrientation( + lockedCaptureOrientation == null + ? deviceOrientationListener.getMediaOrientation() + : deviceOrientationListener.getMediaOrientation(lockedCaptureOrientation)) + .build(); + } + + @SuppressLint("MissingPermission") + public void open(String imageFormatGroup) throws CameraAccessException { + pictureImageReader = + ImageReader.newInstance( + captureSize.getWidth(), captureSize.getHeight(), ImageFormat.JPEG, 2); + + Integer imageFormat = supportedImageFormats.get(imageFormatGroup); + if (imageFormat == null) { + Log.w(TAG, "The selected imageFormatGroup is not supported by Android. Defaulting to yuv420"); + imageFormat = ImageFormat.YUV_420_888; } - private void createCaptureSession(int templateType, Surface... surfaces) - throws CameraAccessException { - createCaptureSession(templateType, null, surfaces); - } - - private void createCaptureSession( - int templateType, Runnable onSuccessCallback, Surface... surfaces) - throws CameraAccessException { - // Log.i(TAG, "createCaptureSession"); - - // Close any existing capture session. - closeCaptureSession(); + // Used to steam image byte data to dart side. + imageStreamReader = + ImageReader.newInstance(previewSize.getWidth(), previewSize.getHeight(), imageFormat, 2); - // Create a new capture builder. - mPreviewRequestBuilder = cameraDevice.createCaptureRequest(templateType); + cameraManager.openCamera( + cameraName, + new CameraDevice.StateCallback() { + @Override + public void onOpened(@NonNull CameraDevice device) { + // Log.i(TAG, "open | onOpened"); - // Build Flutter surface to render to - SurfaceTexture surfaceTexture = flutterTexture.surfaceTexture(); - surfaceTexture.setDefaultBufferSize(previewSize.getWidth(), previewSize.getHeight()); - Surface flutterSurface = new Surface(surfaceTexture); - mPreviewRequestBuilder.addTarget(flutterSurface); - - List remainingSurfaces = Arrays.asList(surfaces); - if (templateType != CameraDevice.TEMPLATE_PREVIEW) { - // If it is not preview mode, add all surfaces as targets. - for (Surface surface : remainingSurfaces) { - mPreviewRequestBuilder.addTarget(surface); + cameraDevice = device; + try { + startPreview(); + dartMessenger.sendCameraInitializedEvent( + previewSize.getWidth(), + previewSize.getHeight(), + currentExposureMode, + currentFocusMode, + isExposurePointSupported(), + isFocusPointSupported()); + } catch (CameraAccessException e) { + dartMessenger.sendCameraErrorEvent(e.getMessage()); + close(); } - } + } - cameraRegions = new CameraRegions(getRegionBoundaries()); - - // Prepare the callback - CameraCaptureSession.StateCallback callback = - new CameraCaptureSession.StateCallback() { - @Override - public void onConfigured(@NonNull CameraCaptureSession session) { - // Camera was already closed. - if (cameraDevice == null) { - dartMessenger.sendCameraErrorEvent("The camera was closed during configuration."); - return; - } - captureSession = session; - - updateFpsRange(); - updateFocusMode(mPreviewRequestBuilder); - updateFlash(mPreviewRequestBuilder); - updateExposureMode(mPreviewRequestBuilder); - - refreshPreviewCaptureSession( - onSuccessCallback, (code, message) -> dartMessenger.sendCameraErrorEvent(message)); - } - - @Override - public void onConfigureFailed(@NonNull CameraCaptureSession cameraCaptureSession) { - dartMessenger.sendCameraErrorEvent("Failed to configure camera session."); - } - }; - - // Start the session - if (VERSION.SDK_INT >= VERSION_CODES.P) { - // Collect all surfaces we want to render to. - List configs = new ArrayList<>(); - configs.add(new OutputConfiguration(flutterSurface)); - for (Surface surface : remainingSurfaces) { - configs.add(new OutputConfiguration(surface)); - } - createCaptureSessionWithSessionConfig(configs, callback); - } else { - // Collect all surfaces we want to render to. - List surfaceList = new ArrayList<>(); - surfaceList.add(flutterSurface); - surfaceList.addAll(remainingSurfaces); - createCaptureSession(surfaceList, callback); - } - } + @Override + public void onClosed(@NonNull CameraDevice camera) { + // Log.i(TAG, "open | onClosed"); - @TargetApi(VERSION_CODES.P) - private void createCaptureSessionWithSessionConfig( - List outputConfigs, CameraCaptureSession.StateCallback callback) - throws CameraAccessException { - cameraDevice.createCaptureSession( - new SessionConfiguration( - SessionConfiguration.SESSION_REGULAR, - outputConfigs, - Executors.newSingleThreadExecutor(), - callback)); - } + dartMessenger.sendCameraClosingEvent(); + super.onClosed(camera); + } - @TargetApi(VERSION_CODES.LOLLIPOP) - @SuppressWarnings("deprecation") - private void createCaptureSession( - List surfaces, CameraCaptureSession.StateCallback callback) - throws CameraAccessException { - cameraDevice.createCaptureSession(surfaces, callback, mBackgroundHandler); - } + @Override + public void onDisconnected(@NonNull CameraDevice cameraDevice) { + // Log.i(TAG, "open | onDisconnected"); - // Send a repeating request to refresh our capture session. - private void refreshPreviewCaptureSession( - @Nullable Runnable onSuccessCallback, @NonNull ErrorCallback onErrorCallback) { - // Log.i(TAG, "refreshPreviewCaptureSession"); - if (captureSession == null) { - // Log.i(TAG, "[refreshPreviewCaptureSession] mPreviewSession null, returning"); - return; - } + close(); + dartMessenger.sendCameraErrorEvent("The camera was disconnected."); + } - try { - captureSession.setRepeatingRequest( - mPreviewRequestBuilder.build(), - mCaptureCallback, - mBackgroundHandler); + @Override + public void onError(@NonNull CameraDevice cameraDevice, int errorCode) { + // Log.i(TAG, "open | onError"); - if (onSuccessCallback != null) { - onSuccessCallback.run(); + close(); + String errorDescription; + switch (errorCode) { + case ERROR_CAMERA_IN_USE: + errorDescription = "The camera device is in use already."; + break; + case ERROR_MAX_CAMERAS_IN_USE: + errorDescription = "Max cameras in use"; + break; + case ERROR_CAMERA_DISABLED: + errorDescription = "The camera device could not be opened due to a device policy."; + break; + case ERROR_CAMERA_DEVICE: + errorDescription = "The camera device has encountered a fatal error"; + break; + case ERROR_CAMERA_SERVICE: + errorDescription = "The camera service has encountered a fatal error."; + break; + default: + errorDescription = "Unknown camera error"; } - - - } catch (CameraAccessException | IllegalStateException | IllegalArgumentException e) { - onErrorCallback.onError("cameraAccess", e.getMessage()); - } + dartMessenger.sendCameraErrorEvent(errorDescription); + } + }, + mBackgroundHandler); + } + + private void createCaptureSession(int templateType, Surface... surfaces) + throws CameraAccessException { + createCaptureSession(templateType, null, surfaces); + } + + private void createCaptureSession( + int templateType, Runnable onSuccessCallback, Surface... surfaces) + throws CameraAccessException { + // Log.i(TAG, "createCaptureSession"); + + // Close any existing capture session. + closeCaptureSession(); + + // Create a new capture builder. + mPreviewRequestBuilder = cameraDevice.createCaptureRequest(templateType); + + // Build Flutter surface to render to + SurfaceTexture surfaceTexture = flutterTexture.surfaceTexture(); + surfaceTexture.setDefaultBufferSize(previewSize.getWidth(), previewSize.getHeight()); + Surface flutterSurface = new Surface(surfaceTexture); + mPreviewRequestBuilder.addTarget(flutterSurface); + + List remainingSurfaces = Arrays.asList(surfaces); + if (templateType != CameraDevice.TEMPLATE_PREVIEW) { + // If it is not preview mode, add all surfaces as targets. + for (Surface surface : remainingSurfaces) { + mPreviewRequestBuilder.addTarget(surface); + } } - public void takePicture(@NonNull final Result result) { - // Log.i(TAG, "takePicture | useAutoFocus: " + useAutoFocus); - - // Only take one 1 picture at a time. - if (pictureCaptureRequest != null && !pictureCaptureRequest.isFinished()) { - result.error("captureAlreadyActive", "Picture is currently already being captured", null); - return; - } - - // Create temporary file - final File outputDir = applicationContext.getCacheDir(); - try { - final File file = File.createTempFile("CAP", ".jpg", outputDir); - - // Start a new capture - pictureCaptureRequest = new PictureCaptureRequest(result, file, dartMessenger); - } catch (IOException | SecurityException e) { - pictureCaptureRequest.error("cannotCreateFile", e.getMessage(), null); - return; - } - - // Listen for picture being taken - pictureImageReader.setOnImageAvailableListener(mOnImageAvailableListener, mBackgroundHandler); - - if (useAutoFocus) { - runPictureAutoFocus(); - } else { - runPrecaptureSequence(); - } - } + cameraRegions = new CameraRegions(getRegionBoundaries()); + + // Prepare the callback + CameraCaptureSession.StateCallback callback = + new CameraCaptureSession.StateCallback() { + @Override + public void onConfigured(@NonNull CameraCaptureSession session) { + // Camera was already closed. + if (cameraDevice == null) { + dartMessenger.sendCameraErrorEvent("The camera was closed during configuration."); + return; + } + captureSession = session; - /** - * Run the precapture sequence for capturing a still image. This method should be called when - * we get a response in {@link #mCaptureCallback} from lockFocus(). - */ - private void runPrecaptureSequence() { - // Log.i(TAG, "runPrecaptureSequence"); - try { - // First set precapture state to idle or else it can hang in STATE_WAITING_PRECAPTURE - mPreviewRequestBuilder.set(CaptureRequest.CONTROL_AE_PRECAPTURE_TRIGGER, - CaptureRequest.CONTROL_AE_PRECAPTURE_TRIGGER_IDLE); - captureSession.capture(mPreviewRequestBuilder.build(), mCaptureCallback, - mBackgroundHandler); + updateFpsRange(); + updateFocusMode(mPreviewRequestBuilder); + updateFlash(mPreviewRequestBuilder); + updateExposureMode(mPreviewRequestBuilder); - // Repeating request to refresh preview session refreshPreviewCaptureSession( - null, - (code, message) -> pictureCaptureRequest.error("cameraAccess", message, null)); - - // Start precapture now - cameraState = CameraState.STATE_WAITING_PRECAPTURE_START; - - mPreviewRequestBuilder.set(CaptureRequest.CONTROL_AE_PRECAPTURE_TRIGGER, - CaptureRequest.CONTROL_AE_PRECAPTURE_TRIGGER_START); - - // Trigger one capture to start AE sequence - captureSession.capture(mPreviewRequestBuilder.build(), mCaptureCallback, - mBackgroundHandler); - - } catch (CameraAccessException e) { - e.printStackTrace(); - } + onSuccessCallback, (code, message) -> dartMessenger.sendCameraErrorEvent(message)); + } + + @Override + public void onConfigureFailed(@NonNull CameraCaptureSession cameraCaptureSession) { + dartMessenger.sendCameraErrorEvent("Failed to configure camera session."); + } + }; + + // Start the session + if (VERSION.SDK_INT >= VERSION_CODES.P) { + // Collect all surfaces we want to render to. + List configs = new ArrayList<>(); + configs.add(new OutputConfiguration(flutterSurface)); + for (Surface surface : remainingSurfaces) { + configs.add(new OutputConfiguration(surface)); + } + createCaptureSessionWithSessionConfig(configs, callback); + } else { + // Collect all surfaces we want to render to. + List surfaceList = new ArrayList<>(); + surfaceList.add(flutterSurface); + surfaceList.addAll(remainingSurfaces); + createCaptureSession(surfaceList, callback); + } + } + + @TargetApi(VERSION_CODES.P) + private void createCaptureSessionWithSessionConfig( + List outputConfigs, CameraCaptureSession.StateCallback callback) + throws CameraAccessException { + cameraDevice.createCaptureSession( + new SessionConfiguration( + SessionConfiguration.SESSION_REGULAR, + outputConfigs, + Executors.newSingleThreadExecutor(), + callback)); + } + + @TargetApi(VERSION_CODES.LOLLIPOP) + @SuppressWarnings("deprecation") + private void createCaptureSession( + List surfaces, CameraCaptureSession.StateCallback callback) + throws CameraAccessException { + cameraDevice.createCaptureSession(surfaces, callback, mBackgroundHandler); + } + + // Send a repeating request to refresh our capture session. + private void refreshPreviewCaptureSession( + @Nullable Runnable onSuccessCallback, @NonNull ErrorCallback onErrorCallback) { + // Log.i(TAG, "refreshPreviewCaptureSession"); + if (captureSession == null) { + // Log.i(TAG, "[refreshPreviewCaptureSession] mPreviewSession null, returning"); + return; } - /** - * Capture a still picture. This method should be called when we get a response in - * {@link #mCaptureCallback} from both lockFocus(). - */ - private void takePictureAfterPrecapture() { - // Log.i(TAG, "captureStillPicture"); - cameraState = CameraState.STATE_CAPTURING; - pictureCaptureRequest.setState(PictureCaptureRequestState.STATE_CAPTURING); - - - try { - if (null == cameraDevice) { - return; - } - // This is the CaptureRequest.Builder that we use to take a picture. - final CaptureRequest.Builder stillBuilder = - cameraDevice.createCaptureRequest(CameraDevice.TEMPLATE_STILL_CAPTURE); - stillBuilder.addTarget(pictureImageReader.getSurface()); + try { + captureSession.setRepeatingRequest( + mPreviewRequestBuilder.build(), mCaptureCallback, mBackgroundHandler); - // Zoom - stillBuilder.set( - CaptureRequest.SCALER_CROP_REGION, - mPreviewRequestBuilder.get(CaptureRequest.SCALER_CROP_REGION)); + if (onSuccessCallback != null) { + onSuccessCallback.run(); + } - // Set focus / flash from preview mode - updateFlash(stillBuilder); - updateFocusMode(stillBuilder); - updateExposureMode(stillBuilder); + } catch (CameraAccessException | IllegalStateException | IllegalArgumentException e) { + onErrorCallback.onError("cameraAccess", e.getMessage()); + } + } - // Orientation - int rotation = activity.getWindowManager().getDefaultDisplay().getRotation(); - stillBuilder.set(CaptureRequest.JPEG_ORIENTATION, getOrientation(rotation)); + public void takePicture(@NonNull final Result result) { + // Log.i(TAG, "takePicture | useAutoFocus: " + useAutoFocus); - CameraCaptureSession.CaptureCallback CaptureCallback - = new CameraCaptureSession.CaptureCallback() { + // Only take one 1 picture at a time. + if (pictureCaptureRequest != null && !pictureCaptureRequest.isFinished()) { + result.error("captureAlreadyActive", "Picture is currently already being captured", null); + return; + } - @Override - public void onCaptureStarted(@NonNull CameraCaptureSession session, - @NonNull CaptureRequest request, - long timestamp, - long frameNumber) { - // Log.i(TAG, "onCaptureStarted"); - } + // Create temporary file + final File outputDir = applicationContext.getCacheDir(); + try { + final File file = File.createTempFile("CAP", ".jpg", outputDir); - @Override - public void onCaptureProgressed(@NonNull CameraCaptureSession session, - @NonNull CaptureRequest request, - @NonNull CaptureResult partialResult) { - // Log.i(TAG, "onCaptureProgressed"); - } + // Start a new capture + pictureCaptureRequest = new PictureCaptureRequest(result, file, dartMessenger); + } catch (IOException | SecurityException e) { + pictureCaptureRequest.error("cannotCreateFile", e.getMessage(), null); + return; + } - @Override - public void onCaptureCompleted(@NonNull CameraCaptureSession session, - @NonNull CaptureRequest request, - @NonNull TotalCaptureResult result) { - // Log.i(TAG, "onCaptureCompleted"); - unlockAutoFocus(); - } - }; + // Listen for picture being taken + pictureImageReader.setOnImageAvailableListener(mOnImageAvailableListener, mBackgroundHandler); - captureSession.stopRepeating(); - captureSession.abortCaptures(); - // Log.i(TAG, "sending capture request"); - captureSession.capture(stillBuilder.build(), CaptureCallback, mBackgroundHandler); - } catch (CameraAccessException e) { - pictureCaptureRequest.error("cameraAccess", e.getMessage(), null); - } + if (useAutoFocus) { + runPictureAutoFocus(); + } else { + runPrecaptureSequence(); } - - /** - * Starts a background thread and its {@link Handler}. - * TODO: call when activity resumed - */ - private void startBackgroundThread() { - mBackgroundThread = new HandlerThread("CameraBackground"); - mBackgroundThread.start(); - mBackgroundHandler = new Handler(mBackgroundThread.getLooper()); + } + + /** + * Run the precapture sequence for capturing a still image. This method should be called when we + * get a response in {@link #mCaptureCallback} from lockFocus(). + */ + private void runPrecaptureSequence() { + // Log.i(TAG, "runPrecaptureSequence"); + try { + // First set precapture state to idle or else it can hang in STATE_WAITING_PRECAPTURE + mPreviewRequestBuilder.set( + CaptureRequest.CONTROL_AE_PRECAPTURE_TRIGGER, + CaptureRequest.CONTROL_AE_PRECAPTURE_TRIGGER_IDLE); + captureSession.capture(mPreviewRequestBuilder.build(), mCaptureCallback, mBackgroundHandler); + + // Repeating request to refresh preview session + refreshPreviewCaptureSession( + null, (code, message) -> pictureCaptureRequest.error("cameraAccess", message, null)); + + // Start precapture now + cameraState = CameraState.STATE_WAITING_PRECAPTURE_START; + + mPreviewRequestBuilder.set( + CaptureRequest.CONTROL_AE_PRECAPTURE_TRIGGER, + CaptureRequest.CONTROL_AE_PRECAPTURE_TRIGGER_START); + + // Trigger one capture to start AE sequence + captureSession.capture(mPreviewRequestBuilder.build(), mCaptureCallback, mBackgroundHandler); + + } catch (CameraAccessException e) { + e.printStackTrace(); } + } + + /** + * Capture a still picture. This method should be called when we get a response in {@link + * #mCaptureCallback} from both lockFocus(). + */ + private void takePictureAfterPrecapture() { + // Log.i(TAG, "captureStillPicture"); + cameraState = CameraState.STATE_CAPTURING; + pictureCaptureRequest.setState(PictureCaptureRequestState.STATE_CAPTURING); + + try { + if (null == cameraDevice) { + return; + } + // This is the CaptureRequest.Builder that we use to take a picture. + final CaptureRequest.Builder stillBuilder = + cameraDevice.createCaptureRequest(CameraDevice.TEMPLATE_STILL_CAPTURE); + stillBuilder.addTarget(pictureImageReader.getSurface()); + + // Zoom + stillBuilder.set( + CaptureRequest.SCALER_CROP_REGION, + mPreviewRequestBuilder.get(CaptureRequest.SCALER_CROP_REGION)); + + // Set focus / flash from preview mode + updateFlash(stillBuilder); + updateFocusMode(stillBuilder); + updateExposureMode(stillBuilder); + + // Orientation + int rotation = activity.getWindowManager().getDefaultDisplay().getRotation(); + stillBuilder.set(CaptureRequest.JPEG_ORIENTATION, getOrientation(rotation)); + + CameraCaptureSession.CaptureCallback captureCallback = + new CameraCaptureSession.CaptureCallback() { + + @Override + public void onCaptureStarted( + @NonNull CameraCaptureSession session, + @NonNull CaptureRequest request, + long timestamp, + long frameNumber) { + // Log.i(TAG, "onCaptureStarted"); + } - /** - * Stops the background thread and its {@link Handler}. - * TODO: call when activity paused - */ - private void stopBackgroundThread() { - try { - if (mBackgroundThread != null) { - mBackgroundThread.quitSafely(); - mBackgroundThread.join(); - mBackgroundThread = null; + @Override + public void onCaptureProgressed( + @NonNull CameraCaptureSession session, + @NonNull CaptureRequest request, + @NonNull CaptureResult partialResult) { + // Log.i(TAG, "onCaptureProgressed"); } - mBackgroundHandler = null; - } catch (InterruptedException e) { - pictureCaptureRequest.error("cameraAccess", e.getMessage(), null); - } + @Override + public void onCaptureCompleted( + @NonNull CameraCaptureSession session, + @NonNull CaptureRequest request, + @NonNull TotalCaptureResult result) { + // Log.i(TAG, "onCaptureCompleted"); + unlockAutoFocus(); + } + }; + + captureSession.stopRepeating(); + captureSession.abortCaptures(); + // Log.i(TAG, "sending capture request"); + captureSession.capture(stillBuilder.build(), captureCallback, mBackgroundHandler); + } catch (CameraAccessException e) { + pictureCaptureRequest.error("cameraAccess", e.getMessage(), null); } - - /** - * Sync the requestBuilder exposure mode setting ot the current exposure mode setting of the camera. - */ - void updateExposureMode(CaptureRequest.Builder requestBuilder) { - // Log.i(TAG, "updateExposureMode"); - - // Applying auto exposure - MeteringRectangle aeRect = cameraRegions.getAEMeteringRectangle(); - requestBuilder.set( - CaptureRequest.CONTROL_AE_REGIONS, - aeRect == null ? null : new MeteringRectangle[]{cameraRegions.getAEMeteringRectangle()}); - - switch (currentExposureMode) { - case locked: - requestBuilder.set(CaptureRequest.CONTROL_AE_LOCK, true); - break; - case auto: - default: - requestBuilder.set(CaptureRequest.CONTROL_AE_LOCK, false); - break; - } - - // TODO: move this to its own setting (exposure offset) - requestBuilder.set(CaptureRequest.CONTROL_AE_EXPOSURE_COMPENSATION, exposureOffset); + } + + /** Starts a background thread and its {@link Handler}. TODO: call when activity resumed */ + private void startBackgroundThread() { + mBackgroundThread = new HandlerThread("CameraBackground"); + mBackgroundThread.start(); + mBackgroundHandler = new Handler(mBackgroundThread.getLooper()); + } + + /** Stops the background thread and its {@link Handler}. TODO: call when activity paused */ + private void stopBackgroundThread() { + try { + if (mBackgroundThread != null) { + mBackgroundThread.quitSafely(); + mBackgroundThread.join(); + mBackgroundThread = null; + } + + mBackgroundHandler = null; + } catch (InterruptedException e) { + pictureCaptureRequest.error("cameraAccess", e.getMessage(), null); + } + } + + /** + * Sync the requestBuilder exposure mode setting ot the current exposure mode setting of the + * camera. + */ + void updateExposureMode(CaptureRequest.Builder requestBuilder) { + // Log.i(TAG, "updateExposureMode"); + + // Applying auto exposure + MeteringRectangle aeRect = cameraRegions.getAEMeteringRectangle(); + requestBuilder.set( + CaptureRequest.CONTROL_AE_REGIONS, + aeRect == null ? null : new MeteringRectangle[] {cameraRegions.getAEMeteringRectangle()}); + + switch (currentExposureMode) { + case locked: + requestBuilder.set(CaptureRequest.CONTROL_AE_LOCK, true); + break; + case auto: + default: + requestBuilder.set(CaptureRequest.CONTROL_AE_LOCK, false); + break; } - /** - * Sync the requestBuilder flash setting to the current flash mode setting of the camera. - */ - void updateFlash(CaptureRequest.Builder requestBuilder) { - // Log.i(TAG, "updateFlash"); + // TODO: move this to its own setting (exposure offset) + requestBuilder.set(CaptureRequest.CONTROL_AE_EXPOSURE_COMPENSATION, exposureOffset); + } - if (!mFlashSupported) { - return; - } + /** Sync the requestBuilder flash setting to the current flash mode setting of the camera. */ + void updateFlash(CaptureRequest.Builder requestBuilder) { + // Log.i(TAG, "updateFlash"); - switch (currentFlashMode) { - case off: - requestBuilder.set(CaptureRequest.CONTROL_AE_MODE, - CaptureRequest.CONTROL_AE_MODE_ON); - requestBuilder.set(CaptureRequest.FLASH_MODE, - CaptureRequest.FLASH_MODE_OFF); - break; + if (!mFlashSupported) { + return; + } - case always: - requestBuilder.set(CaptureRequest.CONTROL_AE_MODE, - CaptureRequest.CONTROL_AE_MODE_ON_ALWAYS_FLASH); - requestBuilder.set(CaptureRequest.FLASH_MODE, - CaptureRequest.FLASH_MODE_OFF); - break; + switch (currentFlashMode) { + case off: + requestBuilder.set(CaptureRequest.CONTROL_AE_MODE, CaptureRequest.CONTROL_AE_MODE_ON); + requestBuilder.set(CaptureRequest.FLASH_MODE, CaptureRequest.FLASH_MODE_OFF); + break; - case torch: - requestBuilder.set(CaptureRequest.CONTROL_AE_MODE, - CaptureRequest.CONTROL_AE_MODE_ON); - requestBuilder.set(CaptureRequest.FLASH_MODE, - CaptureRequest.FLASH_MODE_TORCH); - break; + case always: + requestBuilder.set( + CaptureRequest.CONTROL_AE_MODE, CaptureRequest.CONTROL_AE_MODE_ON_ALWAYS_FLASH); + requestBuilder.set(CaptureRequest.FLASH_MODE, CaptureRequest.FLASH_MODE_OFF); + break; - case auto: - requestBuilder.set(CaptureRequest.CONTROL_AE_MODE, - CaptureRequest.CONTROL_AE_MODE_ON_AUTO_FLASH); - requestBuilder.set(CaptureRequest.FLASH_MODE, - CaptureRequest.FLASH_MODE_OFF); - break; + case torch: + requestBuilder.set(CaptureRequest.CONTROL_AE_MODE, CaptureRequest.CONTROL_AE_MODE_ON); + requestBuilder.set(CaptureRequest.FLASH_MODE, CaptureRequest.FLASH_MODE_TORCH); + break; - // TODO: to be implemented someday. Need to add it to dart/iOS as another flash mode setting. -// case autoRedEye: -// requestBuilder.set(CaptureRequest.CONTROL_AE_MODE, -// CaptureRequest.CONTROL_AE_MODE_ON_AUTO_FLASH_REDEYE); -// requestBuilder.set(CaptureRequest.FLASH_MODE, -// CaptureRequest.FLASH_MODE_OFF); -// break; - } + case auto: + requestBuilder.set( + CaptureRequest.CONTROL_AE_MODE, CaptureRequest.CONTROL_AE_MODE_ON_AUTO_FLASH); + requestBuilder.set(CaptureRequest.FLASH_MODE, CaptureRequest.FLASH_MODE_OFF); + break; + + // TODO: to be implemented someday. Need to add it to dart/iOS as another flash mode setting. + // case autoRedEye: + // requestBuilder.set(CaptureRequest.CONTROL_AE_MODE, + // CaptureRequest.CONTROL_AE_MODE_ON_AUTO_FLASH_REDEYE); + // requestBuilder.set(CaptureRequest.FLASH_MODE, + // CaptureRequest.FLASH_MODE_OFF); + // break; } - - /** - * Retrieves the JPEG orientation from the specified screen rotation. - * - * @param rotation The screen rotation. - * @return The JPEG orientation (one of 0, 90, 270, and 360) - */ - private int getOrientation(int rotation) { - // Sensor orientation is 90 for most devices, or 270 for some devices (eg. Nexus 5X) - // We have to take that into account and rotate JPEG properly. - // For devices with orientation of 90, we simply return our mapping from ORIENTATIONS. - // For devices with orientation of 270, we need to rotate the JPEG 180 degrees. - return (ORIENTATIONS.get(rotation) + sensorOrientation + 270) % 360; + } + + /** + * Retrieves the JPEG orientation from the specified screen rotation. + * + * @param rotation The screen rotation. + * @return The JPEG orientation (one of 0, 90, 270, and 360) + */ + private int getOrientation(int rotation) { + // Sensor orientation is 90 for most devices, or 270 for some devices (eg. Nexus 5X) + // We have to take that into account and rotate JPEG properly. + // For devices with orientation of 90, we simply return our mapping from ORIENTATIONS. + // For devices with orientation of 270, we need to rotate the JPEG 180 degrees. + return (ORIENTATIONS.get(rotation) + sensorOrientation + 270) % 360; + } + + /** Start capturing a picture, doing autofocus first. */ + private void runPictureAutoFocus() { + // Log.i(TAG, "runPictureAutoFocus"); + assert (pictureCaptureRequest != null); + + cameraState = CameraState.STATE_WAITING_FOCUS; + pictureCaptureRequest.setState(PictureCaptureRequestState.STATE_WAITING_FOCUS); + lockAutoFocus(); + } + + /** Start the autofocus routine on the current capture request. */ + private void lockAutoFocus() { + // Log.i(TAG, "lockAutoFocus"); + pictureCaptureRequest.setState(PictureCaptureRequestState.STATE_WAITING_PRECAPTURE_START); + + mPreviewRequestBuilder.set( + CaptureRequest.CONTROL_AF_TRIGGER, CaptureRequest.CONTROL_AF_TRIGGER_START); + + refreshPreviewCaptureSession( + null, (code, message) -> pictureCaptureRequest.error(code, message, null)); + } + + /** Cancel and reset auto focus state and refresh the preview session. */ + private void unlockAutoFocus() { + // Log.i(TAG, "unlockAutoFocus"); + try { + // Cancel existing AF state + mPreviewRequestBuilder.set( + CaptureRequest.CONTROL_AF_TRIGGER, CameraMetadata.CONTROL_AF_TRIGGER_CANCEL); + captureSession.capture(mPreviewRequestBuilder.build(), null, mBackgroundHandler); + + // Set AE state to idle again + mPreviewRequestBuilder.set( + CaptureRequest.CONTROL_AE_PRECAPTURE_TRIGGER, + CaptureRequest.CONTROL_AE_PRECAPTURE_TRIGGER_IDLE); + + // Set AF state to idle again + mPreviewRequestBuilder.set( + CaptureRequest.CONTROL_AF_TRIGGER, CameraMetadata.CONTROL_AF_TRIGGER_IDLE); + + captureSession.capture(mPreviewRequestBuilder.build(), null, mBackgroundHandler); + } catch (CameraAccessException e) { + // Log.i(TAG, "Error unlocking focus: " + e.getMessage()); + dartMessenger.sendCameraErrorEvent(e.getMessage()); + return; } - /** - * Start capturing a picture, doing autofocus first. - */ - private void runPictureAutoFocus() { - // Log.i(TAG, "runPictureAutoFocus"); - assert (pictureCaptureRequest != null); - - cameraState = CameraState.STATE_WAITING_FOCUS; - pictureCaptureRequest.setState(PictureCaptureRequestState.STATE_WAITING_FOCUS); - lockAutoFocus(); + refreshPreviewCaptureSession( + null, + (errorCode, errorMessage) -> pictureCaptureRequest.error(errorCode, errorMessage, null)); + } + + public void startVideoRecording(Result result) { + final File outputDir = applicationContext.getCacheDir(); + try { + videoRecordingFile = File.createTempFile("REC", ".mp4", outputDir); + } catch (IOException | SecurityException e) { + result.error("cannotCreateFile", e.getMessage(), null); + return; } - /** - * Start the autofocus routine on the current capture request. - */ - private void lockAutoFocus() { - // Log.i(TAG, "lockAutoFocus"); - pictureCaptureRequest.setState(PictureCaptureRequestState.STATE_WAITING_PRECAPTURE_START); - - mPreviewRequestBuilder.set( - CaptureRequest.CONTROL_AF_TRIGGER, CaptureRequest.CONTROL_AF_TRIGGER_START); - - refreshPreviewCaptureSession( - null, (code, message) -> pictureCaptureRequest.error(code, message, null)); + try { + prepareMediaRecorder(videoRecordingFile.getAbsolutePath()); + recordingVideo = true; + createCaptureSession( + CameraDevice.TEMPLATE_RECORD, () -> mediaRecorder.start(), mediaRecorder.getSurface()); + result.success(null); + } catch (CameraAccessException | IOException e) { + recordingVideo = false; + videoRecordingFile = null; + result.error("videoRecordingFailed", e.getMessage(), null); } + } - /** - * Cancel and reset auto focus state and refresh the preview session. - */ - private void unlockAutoFocus() { - // Log.i(TAG, "unlockAutoFocus"); - try { - // Cancel existing AF state - mPreviewRequestBuilder.set( - CaptureRequest.CONTROL_AF_TRIGGER, CameraMetadata.CONTROL_AF_TRIGGER_CANCEL); - captureSession.capture(mPreviewRequestBuilder.build(), null, - mBackgroundHandler); - - // Set AE state to idle again - mPreviewRequestBuilder.set(CaptureRequest.CONTROL_AE_PRECAPTURE_TRIGGER, - CaptureRequest.CONTROL_AE_PRECAPTURE_TRIGGER_IDLE); - - // Set AF state to idle again - mPreviewRequestBuilder.set( - CaptureRequest.CONTROL_AF_TRIGGER, CameraMetadata.CONTROL_AF_TRIGGER_IDLE); - - captureSession.capture(mPreviewRequestBuilder.build(), null, mBackgroundHandler); - } catch (CameraAccessException e) { - // Log.i(TAG, "Error unlocking focus: " + e.getMessage()); - dartMessenger.sendCameraErrorEvent(e.getMessage()); - return; - } - - refreshPreviewCaptureSession( - null, - (errorCode, errorMessage) -> pictureCaptureRequest.error(errorCode, errorMessage, null)); + public void stopVideoRecording(@NonNull final Result result) { + if (!recordingVideo) { + result.success(null); + return; } - public void startVideoRecording(Result result) { - final File outputDir = applicationContext.getCacheDir(); - try { - videoRecordingFile = File.createTempFile("REC", ".mp4", outputDir); - } catch (IOException | SecurityException e) { - result.error("cannotCreateFile", e.getMessage(), null); - return; - } - - try { - prepareMediaRecorder(videoRecordingFile.getAbsolutePath()); - recordingVideo = true; - createCaptureSession( - CameraDevice.TEMPLATE_RECORD, () -> mediaRecorder.start(), mediaRecorder.getSurface()); - result.success(null); - } catch (CameraAccessException | IOException e) { - recordingVideo = false; - videoRecordingFile = null; - result.error("videoRecordingFailed", e.getMessage(), null); - } + try { + recordingVideo = false; + + try { + captureSession.abortCaptures(); + mediaRecorder.stop(); + } catch (CameraAccessException | IllegalStateException e) { + // Ignore exceptions and try to continue (changes are camera session already aborted capture) + } + + mediaRecorder.reset(); + startPreview(); + result.success(videoRecordingFile.getAbsolutePath()); + videoRecordingFile = null; + } catch (CameraAccessException | IllegalStateException e) { + result.error("videoRecordingFailed", e.getMessage(), null); } + } - public void stopVideoRecording(@NonNull final Result result) { - if (!recordingVideo) { - result.success(null); - return; - } - - try { - recordingVideo = false; - - try { - captureSession.abortCaptures(); - mediaRecorder.stop(); - } catch (CameraAccessException | IllegalStateException e) { - // Ignore exceptions and try to continue (changes are camera session already aborted capture) - } - - mediaRecorder.reset(); - startPreview(); - result.success(videoRecordingFile.getAbsolutePath()); - videoRecordingFile = null; - } catch (CameraAccessException | IllegalStateException e) { - result.error("videoRecordingFailed", e.getMessage(), null); - } + public void pauseVideoRecording(@NonNull final Result result) { + if (!recordingVideo) { + result.success(null); + return; } - public void pauseVideoRecording(@NonNull final Result result) { - if (!recordingVideo) { - result.success(null); - return; - } - - try { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { - mediaRecorder.pause(); - } else { - result.error("videoRecordingFailed", "pauseVideoRecording requires Android API +24.", null); - return; - } - } catch (IllegalStateException e) { - result.error("videoRecordingFailed", e.getMessage(), null); - return; - } - - result.success(null); + try { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { + mediaRecorder.pause(); + } else { + result.error("videoRecordingFailed", "pauseVideoRecording requires Android API +24.", null); + return; + } + } catch (IllegalStateException e) { + result.error("videoRecordingFailed", e.getMessage(), null); + return; } - public void resumeVideoRecording(@NonNull final Result result) { - if (!recordingVideo) { - result.success(null); - return; - } - - try { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { - mediaRecorder.resume(); - } else { - result.error( - "videoRecordingFailed", "resumeVideoRecording requires Android API +24.", null); - return; - } - } catch (IllegalStateException e) { - result.error("videoRecordingFailed", e.getMessage(), null); - return; - } + result.success(null); + } - result.success(null); + public void resumeVideoRecording(@NonNull final Result result) { + if (!recordingVideo) { + result.success(null); + return; } - /** - * Dart handler when it's time to set a new flash mode. This will try to set a new - * flash mode to the current camera. - * - * @param result - * @param newMode - * @throws CameraAccessException - */ - public void setFlashMode(@NonNull final Result result, FlashMode newMode) { - // Save the new flash mode setting - final FlashMode oldFlashMode = currentFlashMode; - currentFlashMode = newMode; - updateFlash(mPreviewRequestBuilder); - - refreshPreviewCaptureSession( - () -> result.success(null), - (code, message) -> result.error("setFlashModeFailed", "Could not set flash mode.", null)); + try { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { + mediaRecorder.resume(); + } else { + result.error( + "videoRecordingFailed", "resumeVideoRecording requires Android API +24.", null); + return; + } + } catch (IllegalStateException e) { + result.error("videoRecordingFailed", e.getMessage(), null); + return; } - /** - * Dart handler for setting new exposure mode setting. - * - * @param result - * @param newMode - * @throws CameraAccessException - */ - public void setExposureMode(@NonNull final Result result, ExposureMode newMode) - throws CameraAccessException { - currentExposureMode = newMode; - updateExposureMode(mPreviewRequestBuilder); - - refreshPreviewCaptureSession( - null, - (code, message) -> result.error("setExposureModeFailed", "Could not set exposure mode.", null)); - - result.success(null); + result.success(null); + } + + /** + * Dart handler when it's time to set a new flash mode. This will try to set a new flash mode to + * the current camera. + * + * @param result + * @param newMode + * @throws CameraAccessException + */ + public void setFlashMode(@NonNull final Result result, FlashMode newMode) { + // Save the new flash mode setting + final FlashMode oldFlashMode = currentFlashMode; + currentFlashMode = newMode; + updateFlash(mPreviewRequestBuilder); + + refreshPreviewCaptureSession( + () -> result.success(null), + (code, message) -> result.error("setFlashModeFailed", "Could not set flash mode.", null)); + } + + /** + * Dart handler for setting new exposure mode setting. + * + * @param result + * @param newMode + * @throws CameraAccessException + */ + public void setExposureMode(@NonNull final Result result, ExposureMode newMode) + throws CameraAccessException { + currentExposureMode = newMode; + updateExposureMode(mPreviewRequestBuilder); + + refreshPreviewCaptureSession( + null, + (code, message) -> + result.error("setExposureModeFailed", "Could not set exposure mode.", null)); + + result.success(null); + } + + public void setExposurePoint(@NonNull final Result result, Double x, Double y) + throws CameraAccessException { + // Check if exposure point functionality is available. + if (!isExposurePointSupported()) { + result.error( + "setExposurePointFailed", "Device does not have exposure point capabilities", null); + return; + } + // Check if the current region boundaries are known + if (cameraRegions.getMaxBoundaries() == null) { + result.error("setExposurePointFailed", "Could not determine max region boundaries", null); + return; } + // Set the metering rectangle + if (x == null || y == null) cameraRegions.resetAutoExposureMeteringRectangle(); + else cameraRegions.setAutoExposureMeteringRectangleFromPoint(y, 1 - x); + // Apply it + updateExposureMode(mPreviewRequestBuilder); + refreshPreviewCaptureSession( + () -> result.success(null), (code, message) -> result.error("CameraAccess", message, null)); + } + + /** + * Set new focus mode from dart. + * + * @param result + * @param newMode + * @throws CameraAccessException + */ + public void setFocusMode(@NonNull final Result result, FocusMode newMode) + throws CameraAccessException { + // Log.i(TAG, "setFocusMode: " + newMode); + + // Set new focus mode + currentFocusMode = newMode; + + // Sync new focus mode to the current capture request builder + updateFocusMode(mPreviewRequestBuilder); + + // Now depending on the new mode we either want to restart the AF routine (if setting to auto) + // or we want to trigger a one-time focus and then set AF to idle (locked mode). + switch (newMode) { + case auto: + // Log.i(TAG, "Triggering AF start with mode " + currentFocusMode); + // Reset state of autofocus so it goes back to passive scanning. + mPreviewRequestBuilder.set( + CaptureRequest.CONTROL_AF_TRIGGER, CaptureRequest.CONTROL_AF_TRIGGER_CANCEL); - public void setExposurePoint(@NonNull final Result result, Double x, Double y) - throws CameraAccessException { - // Check if exposure point functionality is available. - if (!isExposurePointSupported()) { - result.error( - "setExposurePointFailed", "Device does not have exposure point capabilities", null); - return; - } - // Check if the current region boundaries are known - if (cameraRegions.getMaxBoundaries() == null) { - result.error("setExposurePointFailed", "Could not determine max region boundaries", null); - return; - } - // Set the metering rectangle - if (x == null || y == null) cameraRegions.resetAutoExposureMeteringRectangle(); - else cameraRegions.setAutoExposureMeteringRectangleFromPoint(y, 1 - x); - // Apply it - updateExposureMode(mPreviewRequestBuilder); + // Refresh preview session using repeating request as it will be in CONTROL_AF_MODE_CONTINUOUS_PICTURE refreshPreviewCaptureSession( - () -> result.success(null), (code, message) -> result.error("CameraAccess", message, null)); - } + () -> result.success(null), + (code, message) -> result.error("setFocusMode", message, null)); + break; - /** - * Set new focus mode from dart. - * - * @param result - * @param newMode - * @throws CameraAccessException - */ - public void setFocusMode(@NonNull final Result result, FocusMode newMode) - throws CameraAccessException { - // Log.i(TAG, "setFocusMode: " + newMode); - - // Set new focus mode - currentFocusMode = newMode; - - // Sync new focus mode to the current capture request builder - updateFocusMode(mPreviewRequestBuilder); - - // Now depending on the new mode we either want to restart the AF routine (if setting to auto) - // or we want to trigger a one-time focus and then set AF to idle (locked mode). - switch (newMode) { - case auto: - // Log.i(TAG, "Triggering AF start with mode " + currentFocusMode); - // Reset state of autofocus so it goes back to passive scanning. - mPreviewRequestBuilder.set( - CaptureRequest.CONTROL_AF_TRIGGER, CaptureRequest.CONTROL_AF_TRIGGER_CANCEL); - - // Refresh preview session using repeating request as it will be in CONTROL_AF_MODE_CONTINUOUS_PICTURE - refreshPreviewCaptureSession( - () -> result.success(null), (code, message) -> result.error("setFocusMode", message, null)); - break; + case locked: + // AF mode will be in Auto so we just want to perform one AF routine + mPreviewRequestBuilder.set( + CaptureRequest.CONTROL_AF_TRIGGER, CaptureRequest.CONTROL_AF_TRIGGER_START); - case locked: - // AF mode will be in Auto so we just want to perform one AF routine - mPreviewRequestBuilder.set( - CaptureRequest.CONTROL_AF_TRIGGER, CaptureRequest.CONTROL_AF_TRIGGER_START); - - // Refresh the AF once. When the AF start is completed triggering then we will set it to idle mode. - // If we don't wait for the callback like this, then setting it to idle just resets the focus to infinity - // on some devices like Sony XZ. - try { - captureSession.capture(mPreviewRequestBuilder.build(), new CameraCaptureSession.CaptureCallback() { - @Override - public void onCaptureCompleted(@NonNull CameraCaptureSession session, - @NonNull CaptureRequest request, - @NonNull TotalCaptureResult _result) { - // Log.i(TAG, "Success after triggering AF start for locked focus"); - - mPreviewRequestBuilder.set( - CaptureRequest.CONTROL_AF_TRIGGER, CaptureRequest.CONTROL_AF_TRIGGER_IDLE); - refreshPreviewCaptureSession( - null, (code, message) -> result.error("setFocusMode", message, null)); - } - }, - mBackgroundHandler); - - result.success(null); - } catch (CameraAccessException e) { - result.error("setFocusMode", e.getMessage(), null); + // Refresh the AF once. When the AF start is completed triggering then we will set it to idle mode. + // If we don't wait for the callback like this, then setting it to idle just resets the focus to infinity + // on some devices like Sony XZ. + try { + captureSession.capture( + mPreviewRequestBuilder.build(), + new CameraCaptureSession.CaptureCallback() { + @Override + public void onCaptureCompleted( + @NonNull CameraCaptureSession session, + @NonNull CaptureRequest request, + @NonNull TotalCaptureResult _result) { + // Log.i(TAG, "Success after triggering AF start for locked focus"); + + mPreviewRequestBuilder.set( + CaptureRequest.CONTROL_AF_TRIGGER, CaptureRequest.CONTROL_AF_TRIGGER_IDLE); + refreshPreviewCaptureSession( + null, (code, message) -> result.error("setFocusMode", message, null)); } - break; - } - - - } + }, + mBackgroundHandler); - - public void setFocusPoint(@NonNull final Result result, Double x, Double y) - throws CameraAccessException { - // Check if focus point functionality is available. - if (!isFocusPointSupported()) { - result.error("setFocusPointFailed", "Device does not have focus point capabilities", null); - return; - } - - // Check if the current region boundaries are known - if (cameraRegions.getMaxBoundaries() == null) { - result.error("setFocusPointFailed", "Could not determine max region boundaries", null); - return; - } - - // Set the metering rectangle - if (x == null || y == null) { - cameraRegions.resetAutoFocusMeteringRectangle(); - } else { - cameraRegions.setAutoFocusMeteringRectangleFromPoint(y, 1 - x); + result.success(null); + } catch (CameraAccessException e) { + result.error("setFocusMode", e.getMessage(), null); } - - // Apply the new metering rectangle - setFocusMode(result, currentFocusMode); + break; } - - @TargetApi(VERSION_CODES.P) - private boolean supportsDistortionCorrection() throws CameraAccessException { - int[] availableDistortionCorrectionModes = - cameraManager - .getCameraCharacteristics(cameraDevice.getId()) - .get(CameraCharacteristics.DISTORTION_CORRECTION_AVAILABLE_MODES); - if (availableDistortionCorrectionModes == null) - availableDistortionCorrectionModes = new int[0]; - long nonOffModesSupported = - Arrays.stream(availableDistortionCorrectionModes) - .filter((value) -> value != CaptureRequest.DISTORTION_CORRECTION_MODE_OFF) - .count(); - return nonOffModesSupported > 0; + } + + public void setFocusPoint(@NonNull final Result result, Double x, Double y) + throws CameraAccessException { + // Check if focus point functionality is available. + if (!isFocusPointSupported()) { + result.error("setFocusPointFailed", "Device does not have focus point capabilities", null); + return; } - private Size getRegionBoundaries() throws CameraAccessException { - // No distortion correction support - if (android.os.Build.VERSION.SDK_INT < VERSION_CODES.P || !supportsDistortionCorrection()) { - return cameraManager - .getCameraCharacteristics(cameraDevice.getId()) - .get(CameraCharacteristics.SENSOR_INFO_PIXEL_ARRAY_SIZE); - } - // Get the current distortion correction mode - Integer distortionCorrectionMode = - mPreviewRequestBuilder.get(CaptureRequest.DISTORTION_CORRECTION_MODE); - // Return the correct boundaries depending on the mode - android.graphics.Rect rect; - if (distortionCorrectionMode == null - || distortionCorrectionMode == CaptureRequest.DISTORTION_CORRECTION_MODE_OFF) { - rect = - cameraManager - .getCameraCharacteristics(cameraDevice.getId()) - .get(CameraCharacteristics.SENSOR_INFO_PRE_CORRECTION_ACTIVE_ARRAY_SIZE); - } else { - rect = - cameraManager - .getCameraCharacteristics(cameraDevice.getId()) - .get(CameraCharacteristics.SENSOR_INFO_ACTIVE_ARRAY_SIZE); - } - return rect == null ? null : new Size(rect.width(), rect.height()); + // Check if the current region boundaries are known + if (cameraRegions.getMaxBoundaries() == null) { + result.error("setFocusPointFailed", "Could not determine max region boundaries", null); + return; } - private boolean isExposurePointSupported() throws CameraAccessException { - Integer supportedRegions = - cameraManager - .getCameraCharacteristics(cameraDevice.getId()) - .get(CameraCharacteristics.CONTROL_MAX_REGIONS_AE); - return supportedRegions != null && supportedRegions > 0; + // Set the metering rectangle + if (x == null || y == null) { + cameraRegions.resetAutoFocusMeteringRectangle(); + } else { + cameraRegions.setAutoFocusMeteringRectangleFromPoint(y, 1 - x); } - private boolean isFocusPointSupported() throws CameraAccessException { - Integer supportedRegions = - cameraManager - .getCameraCharacteristics(cameraDevice.getId()) - .get(CameraCharacteristics.CONTROL_MAX_REGIONS_AF); - return supportedRegions != null && supportedRegions > 0; + // Apply the new metering rectangle + setFocusMode(result, currentFocusMode); + } + + @TargetApi(VERSION_CODES.P) + private boolean supportsDistortionCorrection() throws CameraAccessException { + int[] availableDistortionCorrectionModes = + cameraManager + .getCameraCharacteristics(cameraDevice.getId()) + .get(CameraCharacteristics.DISTORTION_CORRECTION_AVAILABLE_MODES); + if (availableDistortionCorrectionModes == null) availableDistortionCorrectionModes = new int[0]; + long nonOffModesSupported = + Arrays.stream(availableDistortionCorrectionModes) + .filter((value) -> value != CaptureRequest.DISTORTION_CORRECTION_MODE_OFF) + .count(); + return nonOffModesSupported > 0; + } + + private Size getRegionBoundaries() throws CameraAccessException { + // No distortion correction support + if (android.os.Build.VERSION.SDK_INT < VERSION_CODES.P || !supportsDistortionCorrection()) { + return cameraManager + .getCameraCharacteristics(cameraDevice.getId()) + .get(CameraCharacteristics.SENSOR_INFO_PIXEL_ARRAY_SIZE); } - - public double getMinExposureOffset() throws CameraAccessException { - Range range = - cameraManager - .getCameraCharacteristics(cameraDevice.getId()) - .get(CameraCharacteristics.CONTROL_AE_COMPENSATION_RANGE); - double minStepped = range == null ? 0 : range.getLower(); - double stepSize = getExposureOffsetStepSize(); - return minStepped * stepSize; + // Get the current distortion correction mode + Integer distortionCorrectionMode = + mPreviewRequestBuilder.get(CaptureRequest.DISTORTION_CORRECTION_MODE); + // Return the correct boundaries depending on the mode + android.graphics.Rect rect; + if (distortionCorrectionMode == null + || distortionCorrectionMode == CaptureRequest.DISTORTION_CORRECTION_MODE_OFF) { + rect = + cameraManager + .getCameraCharacteristics(cameraDevice.getId()) + .get(CameraCharacteristics.SENSOR_INFO_PRE_CORRECTION_ACTIVE_ARRAY_SIZE); + } else { + rect = + cameraManager + .getCameraCharacteristics(cameraDevice.getId()) + .get(CameraCharacteristics.SENSOR_INFO_ACTIVE_ARRAY_SIZE); } - - public double getMaxExposureOffset() throws CameraAccessException { - Range range = - cameraManager - .getCameraCharacteristics(cameraDevice.getId()) - .get(CameraCharacteristics.CONTROL_AE_COMPENSATION_RANGE); - double maxStepped = range == null ? 0 : range.getUpper(); - double stepSize = getExposureOffsetStepSize(); - return maxStepped * stepSize; + return rect == null ? null : new Size(rect.width(), rect.height()); + } + + private boolean isExposurePointSupported() throws CameraAccessException { + Integer supportedRegions = + cameraManager + .getCameraCharacteristics(cameraDevice.getId()) + .get(CameraCharacteristics.CONTROL_MAX_REGIONS_AE); + return supportedRegions != null && supportedRegions > 0; + } + + private boolean isFocusPointSupported() throws CameraAccessException { + Integer supportedRegions = + cameraManager + .getCameraCharacteristics(cameraDevice.getId()) + .get(CameraCharacteristics.CONTROL_MAX_REGIONS_AF); + return supportedRegions != null && supportedRegions > 0; + } + + public double getMinExposureOffset() throws CameraAccessException { + Range range = + cameraManager + .getCameraCharacteristics(cameraDevice.getId()) + .get(CameraCharacteristics.CONTROL_AE_COMPENSATION_RANGE); + double minStepped = range == null ? 0 : range.getLower(); + double stepSize = getExposureOffsetStepSize(); + return minStepped * stepSize; + } + + public double getMaxExposureOffset() throws CameraAccessException { + Range range = + cameraManager + .getCameraCharacteristics(cameraDevice.getId()) + .get(CameraCharacteristics.CONTROL_AE_COMPENSATION_RANGE); + double maxStepped = range == null ? 0 : range.getUpper(); + double stepSize = getExposureOffsetStepSize(); + return maxStepped * stepSize; + } + + public double getExposureOffsetStepSize() throws CameraAccessException { + Rational stepSize = + cameraManager + .getCameraCharacteristics(cameraDevice.getId()) + .get(CameraCharacteristics.CONTROL_AE_COMPENSATION_STEP); + return stepSize == null ? 0.0 : stepSize.doubleValue(); + } + + public void setExposureOffset(@NonNull final Result result, double offset) + throws CameraAccessException { + // Set the exposure offset + double stepSize = getExposureOffsetStepSize(); + exposureOffset = (int) (offset / stepSize); + // Apply it + updateExposureMode(mPreviewRequestBuilder); + + // Refresh capture session + refreshPreviewCaptureSession( + () -> result.success(null), + (code, message) -> + result.error("setExposureModeFailed", "Could not set flash mode.", null)); + } + + public float getMaxZoomLevel() { + return cameraZoom.maxZoom; + } + + public float getMinZoomLevel() { + return CameraZoom.DEFAULT_ZOOM_FACTOR; + } + + public void setZoomLevel(@NonNull final Result result, float zoom) throws CameraAccessException { + float maxZoom = cameraZoom.maxZoom; + float minZoom = CameraZoom.DEFAULT_ZOOM_FACTOR; + + if (zoom > maxZoom || zoom < minZoom) { + String errorMessage = + String.format( + Locale.ENGLISH, + "Zoom level out of bounds (zoom level should be between %f and %f).", + minZoom, + maxZoom); + result.error("ZOOM_ERROR", errorMessage, null); + return; } - public double getExposureOffsetStepSize() throws CameraAccessException { - Rational stepSize = - cameraManager - .getCameraCharacteristics(cameraDevice.getId()) - .get(CameraCharacteristics.CONTROL_AE_COMPENSATION_STEP); - return stepSize == null ? 0.0 : stepSize.doubleValue(); + //Zoom area is calculated relative to sensor area (activeRect) + if (mPreviewRequestBuilder != null) { + final Rect computedZoom = cameraZoom.computeZoom(zoom); + mPreviewRequestBuilder.set(CaptureRequest.SCALER_CROP_REGION, computedZoom); + captureSession.setRepeatingRequest(mPreviewRequestBuilder.build(), null, mBackgroundHandler); } - public void setExposureOffset(@NonNull final Result result, double offset) - throws CameraAccessException { - // Set the exposure offset - double stepSize = getExposureOffsetStepSize(); - exposureOffset = (int) (offset / stepSize); - // Apply it - updateExposureMode(mPreviewRequestBuilder); - - // Refresh capture session - refreshPreviewCaptureSession(() -> result.success(null), - (code, message) -> result.error("setExposureModeFailed", "Could not set flash mode.", null)); - } + result.success(null); + } - public float getMaxZoomLevel() { - return cameraZoom.maxZoom; - } + public void lockCaptureOrientation(PlatformChannel.DeviceOrientation orientation) { + this.lockedCaptureOrientation = orientation; + } - public float getMinZoomLevel() { - return CameraZoom.DEFAULT_ZOOM_FACTOR; - } + public void unlockCaptureOrientation() { + this.lockedCaptureOrientation = null; + } - public void setZoomLevel(@NonNull final Result result, float zoom) throws CameraAccessException { - float maxZoom = cameraZoom.maxZoom; - float minZoom = CameraZoom.DEFAULT_ZOOM_FACTOR; - - if (zoom > maxZoom || zoom < minZoom) { - String errorMessage = - String.format( - Locale.ENGLISH, - "Zoom level out of bounds (zoom level should be between %f and %f).", - minZoom, - maxZoom); - result.error("ZOOM_ERROR", errorMessage, null); - return; - } - - //Zoom area is calculated relative to sensor area (activeRect) - if (mPreviewRequestBuilder != null) { - final Rect computedZoom = cameraZoom.computeZoom(zoom); - mPreviewRequestBuilder.set(CaptureRequest.SCALER_CROP_REGION, computedZoom); - captureSession.setRepeatingRequest(mPreviewRequestBuilder.build(), null, mBackgroundHandler); - } - - result.success(null); + /** Set current fps range setting to the current preview request builder */ + private void updateFpsRange() { + if (fpsRange == null) { + return; } - public void lockCaptureOrientation(PlatformChannel.DeviceOrientation orientation) { - this.lockedCaptureOrientation = orientation; + mPreviewRequestBuilder.set(CaptureRequest.CONTROL_AE_TARGET_FPS_RANGE, fpsRange); + } + + /** + * Sync the focus mode setting to the provided capture request builder. + * + * @param requestBuilder + */ + private void updateFocusMode(CaptureRequest.Builder requestBuilder) { + // Log.i(TAG, "updateFocusMode currentFocusMode: " + currentFocusMode); + + if (!mAutoFocusSupported) { + useAutoFocus = false; + requestBuilder.set(CaptureRequest.CONTROL_AF_MODE, CaptureRequest.CONTROL_AF_MODE_OFF); + } else { + switch (currentFocusMode) { + case locked: + useAutoFocus = false; + requestBuilder.set(CaptureRequest.CONTROL_AF_MODE, CaptureRequest.CONTROL_AF_MODE_AUTO); + break; + + case auto: + useAutoFocus = true; + requestBuilder.set( + CaptureRequest.CONTROL_AF_MODE, + recordingVideo + ? CaptureRequest.CONTROL_AF_MODE_CONTINUOUS_VIDEO + : CaptureRequest.CONTROL_AF_MODE_CONTINUOUS_PICTURE); + default: + break; + } } - public void unlockCaptureOrientation() { - this.lockedCaptureOrientation = null; + // Some devices use an extremely high noise reduction setting by default (pixel 4 selfie mode), which + // causes the preview/capture to look blurry and out of focus. To fix this we set NR to off. + // TODO: we should add a noise reduction setting in dart/ios in the future. + requestBuilder.set( + CaptureRequest.NOISE_REDUCTION_MODE, CaptureRequest.NOISE_REDUCTION_MODE_OFF); + + // Update metering + MeteringRectangle afRect = cameraRegions.getAFMeteringRectangle(); + requestBuilder.set( + CaptureRequest.CONTROL_AF_REGIONS, + afRect == null ? null : new MeteringRectangle[] {afRect}); + } + + public void startPreview() throws CameraAccessException { + if (pictureImageReader == null || pictureImageReader.getSurface() == null) return; + // Log.i(TAG, "startPreview"); + + createCaptureSession(CameraDevice.TEMPLATE_PREVIEW, pictureImageReader.getSurface()); + } + + public void startPreviewWithImageStream(EventChannel imageStreamChannel) + throws CameraAccessException { + createCaptureSession(CameraDevice.TEMPLATE_RECORD, imageStreamReader.getSurface()); + // Log.i(TAG, "startPreviewWithImageStream"); + + imageStreamChannel.setStreamHandler( + new EventChannel.StreamHandler() { + @Override + public void onListen(Object o, EventChannel.EventSink imageStreamSink) { + setImageStreamImageAvailableListener(imageStreamSink); + } + + @Override + public void onCancel(Object o) { + imageStreamReader.setOnImageAvailableListener(null, mBackgroundHandler); + } + }); + } + + private void setImageStreamImageAvailableListener(final EventChannel.EventSink imageStreamSink) { + imageStreamReader.setOnImageAvailableListener( + reader -> { + Image img = reader.acquireLatestImage(); + if (img == null) return; + + List> planes = new ArrayList<>(); + for (Image.Plane plane : img.getPlanes()) { + ByteBuffer buffer = plane.getBuffer(); + + byte[] bytes = new byte[buffer.remaining()]; + buffer.get(bytes, 0, bytes.length); + + Map planeBuffer = new HashMap<>(); + planeBuffer.put("bytesPerRow", plane.getRowStride()); + planeBuffer.put("bytesPerPixel", plane.getPixelStride()); + planeBuffer.put("bytes", bytes); + + planes.add(planeBuffer); + } + + Map imageBuffer = new HashMap<>(); + imageBuffer.put("width", img.getWidth()); + imageBuffer.put("height", img.getHeight()); + imageBuffer.put("format", img.getFormat()); + imageBuffer.put("planes", planes); + + final Handler handler = new Handler(Looper.getMainLooper()); + handler.post(() -> imageStreamSink.success(imageBuffer)); + img.close(); + }, + mBackgroundHandler); + } + + private void closeCaptureSession() { + if (captureSession != null) { + // Log.i(TAG, "closeCaptureSession"); + + captureSession.close(); + captureSession = null; } + } - /** - * Set current fps range setting to the current preview request builder - */ - private void updateFpsRange() { - if (fpsRange == null) { - return; - } + public void close() { + // Log.i(TAG, "close"); + closeCaptureSession(); - mPreviewRequestBuilder.set(CaptureRequest.CONTROL_AE_TARGET_FPS_RANGE, fpsRange); + if (cameraDevice != null) { + cameraDevice.close(); + cameraDevice = null; } - - /** - * Sync the focus mode setting to the provided capture request builder. - * - * @param requestBuilder - */ - private void updateFocusMode(CaptureRequest.Builder requestBuilder) { - // Log.i(TAG, "updateFocusMode currentFocusMode: " + currentFocusMode); - - if (!mAutoFocusSupported) { - useAutoFocus = false; - requestBuilder.set( - CaptureRequest.CONTROL_AF_MODE, CaptureRequest.CONTROL_AF_MODE_OFF); - } else { - switch (currentFocusMode) { - case locked: - useAutoFocus = false; - requestBuilder.set( - CaptureRequest.CONTROL_AF_MODE, CaptureRequest.CONTROL_AF_MODE_AUTO); - break; - - case auto: - useAutoFocus = true; - requestBuilder.set( - CaptureRequest.CONTROL_AF_MODE, - recordingVideo - ? CaptureRequest.CONTROL_AF_MODE_CONTINUOUS_VIDEO - : CaptureRequest.CONTROL_AF_MODE_CONTINUOUS_PICTURE); - default: - break; - } - } - - // Some devices use an extremely high noise reduction setting by default (pixel 4 selfie mode), which - // causes the preview/capture to look blurry and out of focus. To fix this we set NR to off. - // TODO: we should add a noise reduction setting in dart/ios in the future. - requestBuilder.set(CaptureRequest.NOISE_REDUCTION_MODE, CaptureRequest.NOISE_REDUCTION_MODE_OFF); - - - // Update metering - MeteringRectangle afRect = cameraRegions.getAFMeteringRectangle(); - requestBuilder.set( - CaptureRequest.CONTROL_AF_REGIONS, - afRect == null ? null : new MeteringRectangle[]{afRect}); + if (pictureImageReader != null) { + pictureImageReader.close(); + pictureImageReader = null; } - - public void startPreview() throws CameraAccessException { - if (pictureImageReader == null || pictureImageReader.getSurface() == null) return; - // Log.i(TAG, "startPreview"); - - createCaptureSession(CameraDevice.TEMPLATE_PREVIEW, pictureImageReader.getSurface()); + if (imageStreamReader != null) { + imageStreamReader.close(); + imageStreamReader = null; } - - public void startPreviewWithImageStream(EventChannel imageStreamChannel) - throws CameraAccessException { - createCaptureSession(CameraDevice.TEMPLATE_RECORD, imageStreamReader.getSurface()); - // Log.i(TAG, "startPreviewWithImageStream"); - - imageStreamChannel.setStreamHandler( - new EventChannel.StreamHandler() { - @Override - public void onListen(Object o, EventChannel.EventSink imageStreamSink) { - setImageStreamImageAvailableListener(imageStreamSink); - } - - @Override - public void onCancel(Object o) { - imageStreamReader.setOnImageAvailableListener(null, mBackgroundHandler); - } - }); + if (mediaRecorder != null) { + mediaRecorder.reset(); + mediaRecorder.release(); + mediaRecorder = null; } - private void setImageStreamImageAvailableListener(final EventChannel.EventSink imageStreamSink) { - imageStreamReader.setOnImageAvailableListener( - reader -> { - Image img = reader.acquireLatestImage(); - if (img == null) return; - - List> planes = new ArrayList<>(); - for (Image.Plane plane : img.getPlanes()) { - ByteBuffer buffer = plane.getBuffer(); - - byte[] bytes = new byte[buffer.remaining()]; - buffer.get(bytes, 0, bytes.length); - - Map planeBuffer = new HashMap<>(); - planeBuffer.put("bytesPerRow", plane.getRowStride()); - planeBuffer.put("bytesPerPixel", plane.getPixelStride()); - planeBuffer.put("bytes", bytes); - - planes.add(planeBuffer); - } - - Map imageBuffer = new HashMap<>(); - imageBuffer.put("width", img.getWidth()); - imageBuffer.put("height", img.getHeight()); - imageBuffer.put("format", img.getFormat()); - imageBuffer.put("planes", planes); - - final Handler handler = new Handler(Looper.getMainLooper()); - handler.post(() -> imageStreamSink.success(imageBuffer)); - img.close(); - }, - mBackgroundHandler - ); - } - - private void closeCaptureSession() { - if (captureSession != null) { - // Log.i(TAG, "closeCaptureSession"); - - captureSession.close(); - captureSession = null; - } - } - - public void close() { - // Log.i(TAG, "close"); - closeCaptureSession(); - - if (cameraDevice != null) { - cameraDevice.close(); - cameraDevice = null; - } - if (pictureImageReader != null) { - pictureImageReader.close(); - pictureImageReader = null; - } - if (imageStreamReader != null) { - imageStreamReader.close(); - imageStreamReader = null; - } - if (mediaRecorder != null) { - mediaRecorder.reset(); - mediaRecorder.release(); - mediaRecorder = null; - } + stopBackgroundThread(); + } - stopBackgroundThread(); - } - - public void dispose() { - // Log.i(TAG, "dispose"); + public void dispose() { + // Log.i(TAG, "dispose"); - close(); - flutterTexture.release(); - deviceOrientationListener.stop(); - } + close(); + flutterTexture.release(); + deviceOrientationListener.stop(); + } } diff --git a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/CameraPermissions.java b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/CameraPermissions.java index b9ca5da3a1a7..3529e69a2b0b 100644 --- a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/CameraPermissions.java +++ b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/CameraPermissions.java @@ -8,7 +8,6 @@ import android.Manifest.permission; import android.app.Activity; import android.content.pm.PackageManager; - import androidx.annotation.VisibleForTesting; import androidx.core.app.ActivityCompat; import androidx.core.content.ContextCompat; diff --git a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/CameraPlugin.java b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/CameraPlugin.java index 85af292a211b..93183bb7c0a7 100644 --- a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/CameraPlugin.java +++ b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/CameraPlugin.java @@ -6,10 +6,8 @@ import android.app.Activity; import android.os.Build; - import androidx.annotation.NonNull; import androidx.annotation.Nullable; - import io.flutter.embedding.engine.plugins.FlutterPlugin; import io.flutter.embedding.engine.plugins.activity.ActivityAware; import io.flutter.embedding.engine.plugins.activity.ActivityPluginBinding; diff --git a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/CameraState.java b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/CameraState.java index 0bb9c0c2cee0..99dd3f0c6825 100644 --- a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/CameraState.java +++ b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/CameraState.java @@ -1,34 +1,23 @@ package io.flutter.plugins.camera; - /** - * These are the states that the camera can be in. The camera can only take one photo at a time - * so this state describes the state of the camera itself. The camera works like a pipeline where - * we feed it requests through. It can only process one tasks at a time. + * These are the states that the camera can be in. The camera can only take one photo at a time so + * this state describes the state of the camera itself. The camera works like a pipeline where we + * feed it requests through. It can only process one tasks at a time. */ public enum CameraState { - /** - * Idle, showing preview and not capturing anything. - */ - STATE_PREVIEW, + /** Idle, showing preview and not capturing anything. */ + STATE_PREVIEW, - /** - * Starting and waiting for autofocus to complete. - */ - STATE_WAITING_FOCUS, + /** Starting and waiting for autofocus to complete. */ + STATE_WAITING_FOCUS, - /** - * Start performing autoexposure. - */ - STATE_WAITING_PRECAPTURE_START, + /** Start performing autoexposure. */ + STATE_WAITING_PRECAPTURE_START, - /** - * waiting for autoexposure to complete. - */ - STATE_WAITING_PRECAPTURE_DONE, + /** waiting for autoexposure to complete. */ + STATE_WAITING_PRECAPTURE_DONE, - /** - * Capturing an image. - */ - STATE_CAPTURING, + /** Capturing an image. */ + STATE_CAPTURING, } diff --git a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/CameraUtils.java b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/CameraUtils.java index 2cd3a8be261a..03993a3b51f9 100644 --- a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/CameraUtils.java +++ b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/CameraUtils.java @@ -14,7 +14,8 @@ import android.hardware.camera2.params.StreamConfigurationMap; import android.media.CamcorderProfile; import android.util.Size; - +import io.flutter.embedding.engine.systemchannels.PlatformChannel; +import io.flutter.plugins.camera.types.ResolutionPreset; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; @@ -23,9 +24,6 @@ import java.util.List; import java.util.Map; -import io.flutter.embedding.engine.systemchannels.PlatformChannel; -import io.flutter.plugins.camera.types.ResolutionPreset; - /** Provides various utilities for camera. */ public final class CameraUtils { diff --git a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/CameraZoom.java b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/CameraZoom.java index 36aee3b94777..5eed9f4734b7 100644 --- a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/CameraZoom.java +++ b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/CameraZoom.java @@ -5,7 +5,6 @@ package io.flutter.plugins.camera; import android.graphics.Rect; - import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.core.math.MathUtils; diff --git a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/DartMessenger.java b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/DartMessenger.java index 0068421015dc..3892452892d9 100644 --- a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/DartMessenger.java +++ b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/DartMessenger.java @@ -7,17 +7,14 @@ import android.os.Handler; import android.os.Looper; import android.text.TextUtils; - import androidx.annotation.Nullable; - -import java.util.HashMap; -import java.util.Map; - import io.flutter.embedding.engine.systemchannels.PlatformChannel; import io.flutter.plugin.common.BinaryMessenger; import io.flutter.plugin.common.MethodChannel; import io.flutter.plugins.camera.types.ExposureMode; import io.flutter.plugins.camera.types.FocusMode; +import java.util.HashMap; +import java.util.Map; class DartMessenger { @Nullable private MethodChannel cameraChannel; diff --git a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/DeviceOrientationManager.java b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/DeviceOrientationManager.java index 46f31cdab2c5..b2a504b629d6 100644 --- a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/DeviceOrientationManager.java +++ b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/DeviceOrientationManager.java @@ -16,7 +16,6 @@ import android.view.OrientationEventListener; import android.view.Surface; import android.view.WindowManager; - import io.flutter.embedding.engine.systemchannels.PlatformChannel; class DeviceOrientationManager { diff --git a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/ImageSaver.java b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/ImageSaver.java index 589cfd1f553a..e3b93c15fcd0 100644 --- a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/ImageSaver.java +++ b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/ImageSaver.java @@ -3,82 +3,69 @@ import android.media.Image; import android.os.Handler; import android.os.Looper; - +import android.os.SystemClock; +import android.util.Log; import java.io.File; import java.io.FileOutputStream; import java.io.IOException; import java.nio.ByteBuffer; -/** - * Saves a JPEG {@link Image} into the specified {@link File}. - */ +/** Saves a JPEG {@link Image} into the specified {@link File}. */ public class ImageSaver implements Runnable { - /** - * The JPEG image - */ - private final Image mImage; - /** - * The file we save the image into. - */ - private final File mFile; + /** The JPEG image */ + private final Image mImage; - /** - * For running background tasks - */ - private Handler mBackgroundHandler; + /** The file we save the image into. */ + private final File mFile; + /** Used to finish the picture capture request */ + private final PictureCaptureRequest mPictureCaptureRequest; - /** - * - * Used to finish the picture capture request - */ - private PictureCaptureRequest mPictureCaptureRequest; + ImageSaver(Image image, File file, PictureCaptureRequest pictureCaptureRequest) { + mImage = image; + mFile = file; + mPictureCaptureRequest = pictureCaptureRequest; + } - ImageSaver(Image image, File file, PictureCaptureRequest pictureCaptureRequest) { - mImage = image; - mFile = file; - mPictureCaptureRequest = pictureCaptureRequest; - mBackgroundHandler = mBackgroundHandler; - } + @Override + public void run() { + // We need to call the method channel stuff on main thread + final Handler handler = new Handler(Looper.getMainLooper()); - @Override - public void run() { - final Handler handler = new Handler(Looper.getMainLooper()); - ByteBuffer buffer = mImage.getPlanes()[0].getBuffer(); - byte[] bytes = new byte[buffer.remaining()]; - buffer.get(bytes); - FileOutputStream output = null; - try { - output = new FileOutputStream(mFile); - output.write(bytes); + ByteBuffer buffer = mImage.getPlanes()[0].getBuffer(); + byte[] bytes = new byte[buffer.remaining()]; + buffer.get(bytes); + FileOutputStream output = null; + try { + output = new FileOutputStream(mFile); + output.write(bytes); - handler.post(new Runnable() { - @Override - public void run() { - mPictureCaptureRequest.finish(mFile.getAbsolutePath()); - } - }); + handler.post( + new Runnable() { + @Override + public void run() { + mPictureCaptureRequest.finish(mFile.getAbsolutePath()); + } + }); + } catch (IOException e) { + mPictureCaptureRequest.error("IOError", "Failed saving image", null); + } finally { + mImage.close(); + if (null != output) { + try { + output.close(); } catch (IOException e) { - mPictureCaptureRequest.error("IOError", "Failed saving image", null); - } finally { - mImage.close(); - if (null != output) { - try { - output.close(); - } catch (IOException e) { - handler.post(new Runnable() { - @Override - public void run() { - mPictureCaptureRequest.error("cameraAccess", e.getMessage(), null); - } - }); - - + handler.post( + new Runnable() { + @Override + public void run() { + mPictureCaptureRequest.error("cameraAccess", e.getMessage(), null); } - } + }); } + } } - -} \ No newline at end of file + } +} diff --git a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/MethodCallHandlerImpl.java b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/MethodCallHandlerImpl.java index 19307a2f6a78..aa7483f55679 100644 --- a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/MethodCallHandlerImpl.java +++ b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/MethodCallHandlerImpl.java @@ -6,13 +6,8 @@ import android.app.Activity; import android.hardware.camera2.CameraAccessException; - import androidx.annotation.NonNull; import androidx.annotation.Nullable; - -import java.util.HashMap; -import java.util.Map; - import io.flutter.embedding.engine.systemchannels.PlatformChannel; import io.flutter.plugin.common.BinaryMessenger; import io.flutter.plugin.common.EventChannel; @@ -24,6 +19,8 @@ import io.flutter.plugins.camera.types.FlashMode; import io.flutter.plugins.camera.types.FocusMode; import io.flutter.view.TextureRegistry; +import java.util.HashMap; +import java.util.Map; final class MethodCallHandlerImpl implements MethodChannel.MethodCallHandler { private final Activity activity; diff --git a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/PictureCaptureRequest.java b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/PictureCaptureRequest.java index fd139cf16f43..d5d49cec93eb 100644 --- a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/PictureCaptureRequest.java +++ b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/PictureCaptureRequest.java @@ -7,209 +7,192 @@ import android.os.Handler; import android.os.Looper; import android.os.SystemClock; - import androidx.annotation.Nullable; - -import java.io.File; - import io.flutter.plugin.common.MethodChannel; +import java.io.File; /** - * This is where we store the state of the camera. This conveniently - * allows us to handle capture results and send results back to flutter - * so we can handle errors. - *

- * It also handles a capture timeout so if a capture doesn't happen within - * 5 seconds it will return an error to dart. + * This is where we store the state of the camera. This conveniently allows us to handle capture + * results and send results back to flutter so we can handle errors. + * + *

It also handles a capture timeout so if a capture doesn't happen within 5 seconds it will + * return an error to dart. */ class PictureCaptureRequest { - /** - * Timeout for the pre-capture sequence. - */ - private static final long PRECAPTURE_TIMEOUT_MS = 1000; - - /** - * This is the output file for the curent capture. The file is created - * in Camera and passed here as reference to it. - */ - final File mFile; - - /** - * Dart method chanel result. - */ - private final MethodChannel.Result result; - - /** - * Timeout handler. - */ - private final TimeoutHandler timeoutHandler; - /** - * To send errors back to dart - */ - private final DartMessenger dartMessenger; - /** - * The time that the most recent capture started at. Used to check if - * the current capture request has timed out. - */ - private long preCaptureStartTime; - /** - * The state of this picture capture request. - */ - private PictureCaptureRequestState state = PictureCaptureRequestState.STATE_IDLE; - - private final Runnable timeoutCallback = - () -> { - error("captureTimeout", "Picture capture request timed out", state.toString()); - setState(PictureCaptureRequestState.STATE_ERROR); - }; - - - /** - * Constructor to create a picture capture request. - * - * @param result - * @param mFile - */ - public PictureCaptureRequest(MethodChannel.Result result, File mFile, DartMessenger dartMessenger) { - this.result = result; - this.timeoutHandler = new TimeoutHandler(); - this.mFile = mFile; - this.dartMessenger = dartMessenger; - } - - /** - * Constructor for unit tests where we can mock the timeout handler - */ - public PictureCaptureRequest(MethodChannel.Result result, File mFile, DartMessenger dartMessenger, TimeoutHandler timeoutHandler) { - this.result = result; - this.timeoutHandler = timeoutHandler; - this.mFile = mFile; - this.dartMessenger = dartMessenger; - } - - /** - * Return the current state of this picture capture request. - * - * @return - */ - public PictureCaptureRequestState getState() { - return state; - } - - /** - * Set the picture capture request to a new state. - * - * @param newState - */ - public void setState(PictureCaptureRequestState newState) { - // Log.i("Camera", "====> PictureCaptureRequest setState: " + newState); - - // Once a request is finished, that's it for its lifecycle. - if (state == PictureCaptureRequestState.STATE_FINISHED) { - dartMessenger.sendCameraErrorEvent("Request has already been finished"); - return; - } - - final PictureCaptureRequestState oldState = state; - state = newState; - onStateChange(oldState); - } - - public boolean isFinished() { - return state == PictureCaptureRequestState.STATE_FINISHED || - state == PictureCaptureRequestState.STATE_ERROR; + /** Timeout for the pre-capture sequence. */ + private static final long PRECAPTURE_TIMEOUT_MS = 1000; + + /** + * This is the output file for the curent capture. The file is created in Camera and passed here + * as reference to it. + */ + final File mFile; + + /** Dart method chanel result. */ + private final MethodChannel.Result result; + + /** Timeout handler. */ + private final TimeoutHandler timeoutHandler; + /** To send errors back to dart */ + private final DartMessenger dartMessenger; + /** + * The time that the most recent capture started at. Used to check if the current capture request + * has timed out. + */ + private long preCaptureStartTime; + /** The state of this picture capture request. */ + private PictureCaptureRequestState state = PictureCaptureRequestState.STATE_IDLE; + + private final Runnable timeoutCallback = + () -> { + error("captureTimeout", "Picture capture request timed out", state.toString()); + setState(PictureCaptureRequestState.STATE_ERROR); + }; + + /** + * Constructor to create a picture capture request. + * + * @param result + * @param mFile + */ + public PictureCaptureRequest( + MethodChannel.Result result, File mFile, DartMessenger dartMessenger) { + this.result = result; + this.timeoutHandler = new TimeoutHandler(); + this.mFile = mFile; + this.dartMessenger = dartMessenger; + } + + /** Constructor for unit tests where we can mock the timeout handler */ + public PictureCaptureRequest( + MethodChannel.Result result, + File mFile, + DartMessenger dartMessenger, + TimeoutHandler timeoutHandler) { + this.result = result; + this.timeoutHandler = timeoutHandler; + this.mFile = mFile; + this.dartMessenger = dartMessenger; + } + + /** + * Return the current state of this picture capture request. + * + * @return + */ + public PictureCaptureRequestState getState() { + return state; + } + + /** + * Set the picture capture request to a new state. + * + * @param newState + */ + public void setState(PictureCaptureRequestState newState) { + // Log.i("Camera", "====> PictureCaptureRequest setState: " + newState); + + // Once a request is finished, that's it for its lifecycle. + if (state == PictureCaptureRequestState.STATE_FINISHED) { + dartMessenger.sendCameraErrorEvent("Request has already been finished"); + return; } - /** - * Send the picture result back to Flutter. Returns the image path. - * - * @param absolutePath - */ - public void finish(String absolutePath) { - if (state == PictureCaptureRequestState.STATE_ERROR) { - return; - } - - if (isFinished()) throw new IllegalStateException("Request has already been finished"); - setState(PictureCaptureRequestState.STATE_FINISHED); - // Log.i("Camera", "PictureCaptureRequest finish"); - result.success(absolutePath); + final PictureCaptureRequestState oldState = state; + state = newState; + onStateChange(oldState); + } + + public boolean isFinished() { + return state == PictureCaptureRequestState.STATE_FINISHED + || state == PictureCaptureRequestState.STATE_ERROR; + } + + /** + * Send the picture result back to Flutter. Returns the image path. + * + * @param absolutePath + */ + public void finish(String absolutePath) { + if (state == PictureCaptureRequestState.STATE_ERROR) { + return; } - public void error( - String errorCode, @Nullable String errorMessage, - @Nullable Object errorDetails) { - if (state == PictureCaptureRequestState.STATE_ERROR) { - return; - } + if (isFinished()) throw new IllegalStateException("Request has already been finished"); + setState(PictureCaptureRequestState.STATE_FINISHED); + // Log.i("Camera", "PictureCaptureRequest finish"); + result.success(absolutePath); + } - if (isFinished()) throw new IllegalStateException("Request has already been finished"); - setState(PictureCaptureRequestState.STATE_ERROR); - result.error(errorCode, errorMessage, errorDetails); + public void error( + String errorCode, @Nullable String errorMessage, @Nullable Object errorDetails) { + if (state == PictureCaptureRequestState.STATE_ERROR) { + return; } - /** - * Check if the timeout for the pre-capture sequence has been reached. - * - * @return true if the timeout is reached; otherwise false is returned. - */ - public boolean hitPreCaptureTimeout() { - // Log.i("Camera", "hitPreCaptureTimeout | Time elapsed: " + (SystemClock.elapsedRealtime() - preCaptureStartTime)); - return (SystemClock.elapsedRealtime() - preCaptureStartTime) > PRECAPTURE_TIMEOUT_MS; + if (isFinished()) throw new IllegalStateException("Request has already been finished"); + setState(PictureCaptureRequestState.STATE_ERROR); + result.error(errorCode, errorMessage, errorDetails); + } + + /** + * Check if the timeout for the pre-capture sequence has been reached. + * + * @return true if the timeout is reached; otherwise false is returned. + */ + public boolean hitPreCaptureTimeout() { + // Log.i("Camera", "hitPreCaptureTimeout | Time elapsed: " + (SystemClock.elapsedRealtime() - preCaptureStartTime)); + return (SystemClock.elapsedRealtime() - preCaptureStartTime) > PRECAPTURE_TIMEOUT_MS; + } + + /** Sets the time the pre-capture sequence started. */ + public void setPreCaptureStartTime() { + preCaptureStartTime = SystemClock.elapsedRealtime(); + } + + /** Handle new state changes. */ + private void onStateChange(PictureCaptureRequestState oldState) { + switch (state) { + case STATE_CAPTURING: + case STATE_WAITING_FOCUS: + case STATE_WAITING_PRECAPTURE_START: + timeoutHandler.resetTimeout(timeoutCallback); + break; + + case STATE_WAITING_PRECAPTURE_DONE: + setPreCaptureStartTime(); + timeoutHandler.resetTimeout(timeoutCallback); + break; + + case STATE_IDLE: + case STATE_FINISHED: + case STATE_ERROR: + timeoutHandler.clearTimeout(timeoutCallback); + break; } - - /** - * Sets the time the pre-capture sequence started. - */ - public void setPreCaptureStartTime() { - preCaptureStartTime = SystemClock.elapsedRealtime(); + } + + /** + * This handles the timeout for capture requests so they return within a reasonable amount of + * time. + */ + static class TimeoutHandler { + private static final int REQUEST_TIMEOUT = 5000; + private final Handler handler; + + TimeoutHandler() { + this.handler = new Handler(Looper.getMainLooper()); } - /** - * Handle new state changes. - */ - private void onStateChange(PictureCaptureRequestState oldState) { - switch (state) { - case STATE_CAPTURING: - case STATE_WAITING_FOCUS: - case STATE_WAITING_PRECAPTURE_START: - timeoutHandler.resetTimeout(timeoutCallback); - break; - - case STATE_WAITING_PRECAPTURE_DONE: - setPreCaptureStartTime(); - timeoutHandler.resetTimeout(timeoutCallback); - break; - - case STATE_IDLE: - case STATE_FINISHED: - case STATE_ERROR: - timeoutHandler.clearTimeout(timeoutCallback); - break; - } + public void resetTimeout(Runnable runnable) { + // Log.i("Camear", "PictureCaptureRequest | resetting timeout"); + clearTimeout(runnable); + handler.postDelayed(runnable, REQUEST_TIMEOUT); } - /** - * This handles the timeout for capture requests so they return within a - * reasonable amount of time. - */ - static class TimeoutHandler { - private static final int REQUEST_TIMEOUT = 5000; - private final Handler handler; - - TimeoutHandler() { - this.handler = new Handler(Looper.getMainLooper()); - } - - public void resetTimeout(Runnable runnable) { - // Log.i("Camear", "PictureCaptureRequest | resetting timeout"); - clearTimeout(runnable); - handler.postDelayed(runnable, REQUEST_TIMEOUT); - } - - public void clearTimeout(Runnable runnable) { - handler.removeCallbacks(runnable); - } + public void clearTimeout(Runnable runnable) { + handler.removeCallbacks(runnable); } + } } diff --git a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/PictureCaptureRequestState.java b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/PictureCaptureRequestState.java index c506e2489e1c..979346c9bb09 100644 --- a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/PictureCaptureRequestState.java +++ b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/PictureCaptureRequestState.java @@ -1,48 +1,32 @@ package io.flutter.plugins.camera; /** - * This describes the state of the current picture capture request. - * This is different from the camera state because this simply says - * whether or not the current capture is finished and if there was - * an error. + * This describes the state of the current picture capture request. This is different from the + * camera state because this simply says whether or not the current capture is finished and if there + * was an error. * - * We have to separate this state because a picture capture request - * only exists in the context of a dart call where we have a result - * to return. + *

We have to separate this state because a picture capture request only exists in the context of + * a dart call where we have a result to return. */ public enum PictureCaptureRequestState { - /** - * Not doing anything yet. - */ - STATE_IDLE, + /** Not doing anything yet. */ + STATE_IDLE, - /** - * Starting and waiting for autofocus to complete. - */ - STATE_WAITING_FOCUS, + /** Starting and waiting for autofocus to complete. */ + STATE_WAITING_FOCUS, - /** - * Start performing autoexposure. - */ - STATE_WAITING_PRECAPTURE_START, + /** Start performing autoexposure. */ + STATE_WAITING_PRECAPTURE_START, - /** - * waiting for autoexposure to complete. - */ - STATE_WAITING_PRECAPTURE_DONE, + /** waiting for autoexposure to complete. */ + STATE_WAITING_PRECAPTURE_DONE, - /** - * Picture is being captured. - */ - STATE_CAPTURING, + /** Picture is being captured. */ + STATE_CAPTURING, - /** - * Picture capture is finished. - */ - STATE_FINISHED, + /** Picture capture is finished. */ + STATE_FINISHED, - /** - * An error occurred. - */ - STATE_ERROR, + /** An error occurred. */ + STATE_ERROR, } diff --git a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/media/MediaRecorderBuilder.java b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/media/MediaRecorderBuilder.java index 5e5d0c44c115..4c3fb3add230 100644 --- a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/media/MediaRecorderBuilder.java +++ b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/media/MediaRecorderBuilder.java @@ -6,9 +6,7 @@ import android.media.CamcorderProfile; import android.media.MediaRecorder; - import androidx.annotation.NonNull; - import java.io.IOException; public class MediaRecorderBuilder { diff --git a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/types/FlashMode.java b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/types/FlashMode.java index b8ff2fac6652..75437e80e1d4 100644 --- a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/types/FlashMode.java +++ b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/types/FlashMode.java @@ -4,6 +4,9 @@ package io.flutter.plugins.camera.types; + + + // Mirrors flash_mode.dart public enum FlashMode { off("off"), @@ -28,4 +31,4 @@ public static FlashMode getValueForString(String modeStr) { public String toString() { return strValue; } -} \ No newline at end of file +} diff --git a/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/PictureCaptureRequestTest.java b/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/PictureCaptureRequestTest.java index ef81028e2bc6..99d905180c96 100644 --- a/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/PictureCaptureRequestTest.java +++ b/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/PictureCaptureRequestTest.java @@ -4,10 +4,6 @@ package io.flutter.plugins.camera; -import org.junit.Test; - -import io.flutter.plugin.common.MethodChannel; - import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertTrue; @@ -17,137 +13,146 @@ import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; -public class PictureCaptureRequestTest { - - @Test - public void state_is_idle_by_default() { - PictureCaptureRequest req = new PictureCaptureRequest(null, null, null); - assertEquals("Default state is idle", req.getState(), PictureCaptureRequestState.STATE_IDLE); - } - - @Test - public void setState_sets_state() { - PictureCaptureRequest req = new PictureCaptureRequest(null, null, null); - req.setState(PictureCaptureRequestState.STATE_WAITING_FOCUS); - assertEquals("State is focusing", req.getState(), PictureCaptureRequestState.STATE_WAITING_FOCUS); - req.setState(PictureCaptureRequestState.STATE_WAITING_PRECAPTURE_START); - assertEquals("State is preCapture", req.getState(), PictureCaptureRequestState.STATE_WAITING_PRECAPTURE_START); - req.setState(PictureCaptureRequestState.STATE_WAITING_PRECAPTURE_DONE); - assertEquals( - "State is waitingPreCaptureReady", - req.getState(), - PictureCaptureRequestState.STATE_WAITING_PRECAPTURE_DONE); - req.setState(PictureCaptureRequestState.STATE_CAPTURING); - assertEquals( - "State is awaitingPreCapture", req.getState(), PictureCaptureRequestState.STATE_CAPTURING); - } - - @Test - public void setState_resets_timeout() { - PictureCaptureRequest.TimeoutHandler mockTimeoutHandler = - mock(PictureCaptureRequest.TimeoutHandler.class); - PictureCaptureRequest req = new PictureCaptureRequest(null, null, null, mockTimeoutHandler); - req.setState(PictureCaptureRequestState.STATE_WAITING_FOCUS); - req.setState(PictureCaptureRequestState.STATE_WAITING_PRECAPTURE_START); - req.setState(PictureCaptureRequestState.STATE_WAITING_PRECAPTURE_DONE); - req.setState(PictureCaptureRequestState.STATE_CAPTURING); - verify(mockTimeoutHandler, times(4)).resetTimeout(any()); - verify(mockTimeoutHandler, never()).clearTimeout(any()); - } - - @Test - public void setState_clears_timeout() { - PictureCaptureRequest.TimeoutHandler mockTimeoutHandler = - mock(PictureCaptureRequest.TimeoutHandler.class); - PictureCaptureRequest req = new PictureCaptureRequest(null, null, null, mockTimeoutHandler); - req.setState(PictureCaptureRequestState.STATE_IDLE); - req.setState(PictureCaptureRequestState.STATE_FINISHED); - req = new PictureCaptureRequest(null, null, null, mockTimeoutHandler); - req.setState(PictureCaptureRequestState.STATE_ERROR); - verify(mockTimeoutHandler, never()).resetTimeout(any()); - verify(mockTimeoutHandler, times(3)).clearTimeout(any()); - } - - @Test - public void finish_sets_result_and_state() { - // Setup - MethodChannel.Result mockResult = mock(MethodChannel.Result.class); - PictureCaptureRequest req = new PictureCaptureRequest(mockResult, null, null); - // Act - req.finish("/test/path"); - // Test - verify(mockResult).success("/test/path"); - assertEquals("State is finished", req.getState(), PictureCaptureRequestState.STATE_FINISHED); - } - - @Test - public void finish_clears_timeout() { - PictureCaptureRequest.TimeoutHandler mockTimeoutHandler = - mock(PictureCaptureRequest.TimeoutHandler.class); - MethodChannel.Result mockResult = mock(MethodChannel.Result.class); - PictureCaptureRequest req = new PictureCaptureRequest(mockResult, null, null, mockTimeoutHandler); - req.finish("/test/path"); - verify(mockTimeoutHandler, never()).resetTimeout(any()); - verify(mockTimeoutHandler).clearTimeout(any()); - } - - @Test - public void isFinished_is_true_When_state_is_finished_or_error() { - // Setup - PictureCaptureRequest req = new PictureCaptureRequest(null, null, null); - // Test false states - req.setState(PictureCaptureRequestState.STATE_IDLE); - assertFalse(req.isFinished()); - req.setState(PictureCaptureRequestState.STATE_WAITING_PRECAPTURE_START); - assertFalse(req.isFinished()); - req.setState(PictureCaptureRequestState.STATE_WAITING_PRECAPTURE_DONE); - assertFalse(req.isFinished()); - // Test true states - req.setState(PictureCaptureRequestState.STATE_FINISHED); - assertTrue(req.isFinished()); - req = new PictureCaptureRequest(null, null, null); // Refresh - req.setState(PictureCaptureRequestState.STATE_ERROR); - assertTrue(req.isFinished()); - } - - @Test(expected = IllegalStateException.class) - public void finish_throws_When_already_finished() { - // Setup - PictureCaptureRequest req = new PictureCaptureRequest(null, null, null); - req.setState(PictureCaptureRequestState.STATE_FINISHED); - // Act - req.finish("/test/path"); - } - - @Test - public void error_sets_result_and_state() { - // Setup - MethodChannel.Result mockResult = mock(MethodChannel.Result.class); - PictureCaptureRequest req = new PictureCaptureRequest(mockResult, null, null); - // Act - req.error("ERROR_CODE", "Error Message", null); - // Test - verify(mockResult).error("ERROR_CODE", "Error Message", null); - assertEquals("State is error", req.getState(), PictureCaptureRequestState.STATE_ERROR); - } +import io.flutter.plugin.common.MethodChannel; +import org.junit.Test; - @Test - public void error_clears_timeout() { - PictureCaptureRequest.TimeoutHandler mockTimeoutHandler = - mock(PictureCaptureRequest.TimeoutHandler.class); - MethodChannel.Result mockResult = mock(MethodChannel.Result.class); - PictureCaptureRequest req = new PictureCaptureRequest(mockResult, null, null, mockTimeoutHandler); - req.error("ERROR_CODE", "Error Message", null); - verify(mockTimeoutHandler, never()).resetTimeout(any()); - verify(mockTimeoutHandler).clearTimeout(any()); - } +public class PictureCaptureRequestTest { - @Test(expected = IllegalStateException.class) - public void error_throws_When_already_finished() { - // Setup - PictureCaptureRequest req = new PictureCaptureRequest(null, null, null); - req.setState(PictureCaptureRequestState.STATE_FINISHED); - // Act - req.error(null, null, null); - } + @Test + public void state_is_idle_by_default() { + PictureCaptureRequest req = new PictureCaptureRequest(null, null, null); + assertEquals("Default state is idle", req.getState(), PictureCaptureRequestState.STATE_IDLE); + } + + @Test + public void setState_sets_state() { + PictureCaptureRequest req = new PictureCaptureRequest(null, null, null); + req.setState(PictureCaptureRequestState.STATE_WAITING_FOCUS); + assertEquals( + "State is focusing", req.getState(), PictureCaptureRequestState.STATE_WAITING_FOCUS); + req.setState(PictureCaptureRequestState.STATE_WAITING_PRECAPTURE_START); + assertEquals( + "State is preCapture", + req.getState(), + PictureCaptureRequestState.STATE_WAITING_PRECAPTURE_START); + req.setState(PictureCaptureRequestState.STATE_WAITING_PRECAPTURE_DONE); + assertEquals( + "State is waitingPreCaptureReady", + req.getState(), + PictureCaptureRequestState.STATE_WAITING_PRECAPTURE_DONE); + req.setState(PictureCaptureRequestState.STATE_CAPTURING); + assertEquals( + "State is awaitingPreCapture", req.getState(), PictureCaptureRequestState.STATE_CAPTURING); + } + + @Test + public void setState_resets_timeout() { + PictureCaptureRequest.TimeoutHandler mockTimeoutHandler = + mock(PictureCaptureRequest.TimeoutHandler.class); + PictureCaptureRequest req = new PictureCaptureRequest(null, null, null, mockTimeoutHandler); + req.setState(PictureCaptureRequestState.STATE_WAITING_FOCUS); + req.setState(PictureCaptureRequestState.STATE_WAITING_PRECAPTURE_START); + req.setState(PictureCaptureRequestState.STATE_WAITING_PRECAPTURE_DONE); + req.setState(PictureCaptureRequestState.STATE_CAPTURING); + verify(mockTimeoutHandler, times(4)).resetTimeout(any()); + verify(mockTimeoutHandler, never()).clearTimeout(any()); + } + + @Test + public void setState_clears_timeout() { + PictureCaptureRequest.TimeoutHandler mockTimeoutHandler = + mock(PictureCaptureRequest.TimeoutHandler.class); + PictureCaptureRequest req = new PictureCaptureRequest(null, null, null, mockTimeoutHandler); + req.setState(PictureCaptureRequestState.STATE_IDLE); + req.setState(PictureCaptureRequestState.STATE_FINISHED); + req = new PictureCaptureRequest(null, null, null, mockTimeoutHandler); + req.setState(PictureCaptureRequestState.STATE_ERROR); + verify(mockTimeoutHandler, never()).resetTimeout(any()); + verify(mockTimeoutHandler, times(3)).clearTimeout(any()); + } + + @Test + public void finish_sets_result_and_state() { + // Setup + MethodChannel.Result mockResult = mock(MethodChannel.Result.class); + PictureCaptureRequest req = new PictureCaptureRequest(mockResult, null, null); + // Act + req.finish("/test/path"); + // Test + verify(mockResult).success("/test/path"); + assertEquals("State is finished", req.getState(), PictureCaptureRequestState.STATE_FINISHED); + } + + @Test + public void finish_clears_timeout() { + PictureCaptureRequest.TimeoutHandler mockTimeoutHandler = + mock(PictureCaptureRequest.TimeoutHandler.class); + MethodChannel.Result mockResult = mock(MethodChannel.Result.class); + PictureCaptureRequest req = + new PictureCaptureRequest(mockResult, null, null, mockTimeoutHandler); + req.finish("/test/path"); + verify(mockTimeoutHandler, never()).resetTimeout(any()); + verify(mockTimeoutHandler).clearTimeout(any()); + } + + @Test + public void isFinished_is_true_When_state_is_finished_or_error() { + // Setup + PictureCaptureRequest req = new PictureCaptureRequest(null, null, null); + // Test false states + req.setState(PictureCaptureRequestState.STATE_IDLE); + assertFalse(req.isFinished()); + req.setState(PictureCaptureRequestState.STATE_WAITING_PRECAPTURE_START); + assertFalse(req.isFinished()); + req.setState(PictureCaptureRequestState.STATE_WAITING_PRECAPTURE_DONE); + assertFalse(req.isFinished()); + // Test true states + req.setState(PictureCaptureRequestState.STATE_FINISHED); + assertTrue(req.isFinished()); + req = new PictureCaptureRequest(null, null, null); // Refresh + req.setState(PictureCaptureRequestState.STATE_ERROR); + assertTrue(req.isFinished()); + } + + @Test(expected = IllegalStateException.class) + public void finish_throws_When_already_finished() { + // Setup + PictureCaptureRequest req = new PictureCaptureRequest(null, null, null); + req.setState(PictureCaptureRequestState.STATE_FINISHED); + // Act + req.finish("/test/path"); + } + + @Test + public void error_sets_result_and_state() { + // Setup + MethodChannel.Result mockResult = mock(MethodChannel.Result.class); + PictureCaptureRequest req = new PictureCaptureRequest(mockResult, null, null); + // Act + req.error("ERROR_CODE", "Error Message", null); + // Test + verify(mockResult).error("ERROR_CODE", "Error Message", null); + assertEquals("State is error", req.getState(), PictureCaptureRequestState.STATE_ERROR); + } + + @Test + public void error_clears_timeout() { + PictureCaptureRequest.TimeoutHandler mockTimeoutHandler = + mock(PictureCaptureRequest.TimeoutHandler.class); + MethodChannel.Result mockResult = mock(MethodChannel.Result.class); + PictureCaptureRequest req = + new PictureCaptureRequest(mockResult, null, null, mockTimeoutHandler); + req.error("ERROR_CODE", "Error Message", null); + verify(mockTimeoutHandler, never()).resetTimeout(any()); + verify(mockTimeoutHandler).clearTimeout(any()); + } + + @Test(expected = IllegalStateException.class) + public void error_throws_When_already_finished() { + // Setup + PictureCaptureRequest req = new PictureCaptureRequest(null, null, null); + req.setState(PictureCaptureRequestState.STATE_FINISHED); + // Act + req.error(null, null, null); + } } From 7da87ea973652760f85316076ab947d4ca809c82 Mon Sep 17 00:00:00 2001 From: Andrew Coutts Date: Tue, 2 Mar 2021 03:11:49 -0500 Subject: [PATCH 023/114] Formatting --- .../src/main/java/io/flutter/plugins/camera/ImageSaver.java | 2 -- .../main/java/io/flutter/plugins/camera/types/FlashMode.java | 3 --- 2 files changed, 5 deletions(-) diff --git a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/ImageSaver.java b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/ImageSaver.java index e3b93c15fcd0..b467185c9096 100644 --- a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/ImageSaver.java +++ b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/ImageSaver.java @@ -3,8 +3,6 @@ import android.media.Image; import android.os.Handler; import android.os.Looper; -import android.os.SystemClock; -import android.util.Log; import java.io.File; import java.io.FileOutputStream; import java.io.IOException; diff --git a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/types/FlashMode.java b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/types/FlashMode.java index 75437e80e1d4..c4f0998c418a 100644 --- a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/types/FlashMode.java +++ b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/types/FlashMode.java @@ -4,9 +4,6 @@ package io.flutter.plugins.camera.types; - - - // Mirrors flash_mode.dart public enum FlashMode { off("off"), From d26b9f2027fd9068f54d07119bc5852ba4f55e13 Mon Sep 17 00:00:00 2001 From: Andrew Coutts Date: Tue, 2 Mar 2021 03:34:32 -0500 Subject: [PATCH 024/114] Remove log --- .../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 74b365e3f381..c32e18d631e0 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 @@ -298,7 +298,7 @@ public Camera( recordingProfile = CameraUtils.getBestAvailableCamcorderProfileForResolutionPreset(cameraName, preset); captureSize = new Size(recordingProfile.videoFrameWidth, recordingProfile.videoFrameHeight); - Log.i(TAG, "captureSize: " + captureSize); + // Log.i(TAG, "captureSize: " + captureSize); previewSize = computeBestPreviewSize(cameraName, preset); From c88bc92ee202a40f9f81e01493ae4e6f9cb5960d Mon Sep 17 00:00:00 2001 From: Andrew Coutts Date: Tue, 2 Mar 2021 04:30:44 -0500 Subject: [PATCH 025/114] Reduce overhead for capture requests and streaming by only requesting an pictureImageReader for 1 image at a time to reduce memory usage --- .../java/io/flutter/plugins/camera/Camera.java | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 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 c32e18d631e0..a3b42cbfd013 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 @@ -123,9 +123,11 @@ public class Camera { @Override public void onImageAvailable(ImageReader reader) { // Log.i(TAG, "onImageAvailable"); + + // Use acquireNextImage since our image reader is only for 1 image. mBackgroundHandler.post( new ImageSaver( - reader.acquireLatestImage(), pictureCaptureRequest.mFile, pictureCaptureRequest)); + reader.acquireNextImage(), pictureCaptureRequest.mFile, pictureCaptureRequest)); cameraState = CameraState.STATE_PREVIEW; } }; @@ -345,7 +347,7 @@ private void checkAutoFocusSupported() { && !(modes == null || modes.length == 0 || (modes.length == 1 && modes[0] == CameraCharacteristics.CONTROL_AF_MODE_OFF)); - Log.i(TAG, "checkAutoFocusSupported: " + mAutoFocusSupported); + // Log.i(TAG, "checkAutoFocusSupported: " + mAutoFocusSupported); } /** Check if the flash is supported. */ @@ -401,20 +403,21 @@ private void prepareMediaRecorder(String outputFilePath) throws IOException { @SuppressLint("MissingPermission") public void open(String imageFormatGroup) throws CameraAccessException { + // We always capture using JPEG format. pictureImageReader = ImageReader.newInstance( - captureSize.getWidth(), captureSize.getHeight(), ImageFormat.JPEG, 2); + captureSize.getWidth(), captureSize.getHeight(), ImageFormat.JPEG, 1); + // For image streaming, we use the provided image format or fall back to YUV420. Integer imageFormat = supportedImageFormats.get(imageFormatGroup); if (imageFormat == null) { Log.w(TAG, "The selected imageFormatGroup is not supported by Android. Defaulting to yuv420"); imageFormat = ImageFormat.YUV_420_888; } - - // Used to steam image byte data to dart side. imageStreamReader = - ImageReader.newInstance(previewSize.getWidth(), previewSize.getHeight(), imageFormat, 2); + ImageReader.newInstance(previewSize.getWidth(), previewSize.getHeight(), imageFormat, 1); + // Open the camera now cameraManager.openCamera( cameraName, new CameraDevice.StateCallback() { @@ -1359,7 +1362,8 @@ public void onCancel(Object o) { private void setImageStreamImageAvailableListener(final EventChannel.EventSink imageStreamSink) { imageStreamReader.setOnImageAvailableListener( reader -> { - Image img = reader.acquireLatestImage(); + // Use acquireNextImage since our image reader is only for 1 image. + Image img = reader.acquireNextImage(); if (img == null) return; List> planes = new ArrayList<>(); From 8d13496ef844815e581cb85ef383d3097d90a7f8 Mon Sep 17 00:00:00 2001 From: Andrew Coutts Date: Tue, 2 Mar 2021 04:43:10 -0500 Subject: [PATCH 026/114] Cleanup --- .../android/src/main/java/io/flutter/plugins/camera/Camera.java | 1 - .../src/main/java/io/flutter/plugins/camera/CameraPlugin.java | 2 +- 2 files changed, 1 insertion(+), 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 a3b42cbfd013..4b53991b35c8 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 @@ -997,7 +997,6 @@ public void resumeVideoRecording(@NonNull final Result result) { */ public void setFlashMode(@NonNull final Result result, FlashMode newMode) { // Save the new flash mode setting - final FlashMode oldFlashMode = currentFlashMode; currentFlashMode = newMode; updateFlash(mPreviewRequestBuilder); diff --git a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/CameraPlugin.java b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/CameraPlugin.java index 93183bb7c0a7..036fcf1d4f30 100644 --- a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/CameraPlugin.java +++ b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/CameraPlugin.java @@ -26,7 +26,7 @@ */ public final class CameraPlugin implements FlutterPlugin, ActivityAware { - private static final String TAG = "CameraPlugin"; + // private static final String TAG = "CameraPlugin"; private @Nullable FlutterPluginBinding flutterPluginBinding; private @Nullable MethodCallHandlerImpl methodCallHandler; From a964f4ccfec008d421cda9b45f968a08eb138847 Mon Sep 17 00:00:00 2001 From: Andrew Coutts Date: Tue, 2 Mar 2021 07:26:58 -0500 Subject: [PATCH 027/114] Remove nv21 --- .../io/flutter/plugins/camera/Camera.java | 1 - packages/camera/camera/pubspec.yaml | 2 +- .../lib/src/types/image_format_group.dart | 25 ------------------- 3 files changed, 1 insertion(+), 27 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 4b53991b35c8..4912ff64d169 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 @@ -88,7 +88,6 @@ public class Camera { supportedImageFormats = new HashMap<>(); supportedImageFormats.put("yuv420", ImageFormat.YUV_420_888); supportedImageFormats.put("jpeg", ImageFormat.JPEG); - supportedImageFormats.put("nv21", ImageFormat.NV21); } private final SurfaceTextureEntry flutterTexture; diff --git a/packages/camera/camera/pubspec.yaml b/packages/camera/camera/pubspec.yaml index f08cd9388abf..a0f57f797474 100644 --- a/packages/camera/camera/pubspec.yaml +++ b/packages/camera/camera/pubspec.yaml @@ -9,7 +9,7 @@ dependencies: flutter: sdk: flutter - camera_platform_interface: ^2.0.2 + camera_platform_interface: ^2.0.0 pedantic: ^1.10.0 quiver: ^3.0.0-nullsafety.3 diff --git a/packages/camera/camera_platform_interface/lib/src/types/image_format_group.dart b/packages/camera/camera_platform_interface/lib/src/types/image_format_group.dart index b07f7af79e82..3d2c0180fe65 100644 --- a/packages/camera/camera_platform_interface/lib/src/types/image_format_group.dart +++ b/packages/camera/camera_platform_interface/lib/src/types/image_format_group.dart @@ -14,33 +14,8 @@ enum ImageFormatGroup { /// /// On iOS, this is `kCVPixelFormatType_420YpCbCr8BiPlanarVideoRange`. See /// https://developer.apple.com/documentation/corevideo/1563591-pixel_format_identifiers/kcvpixelformattype_420ypcbcr8biplanarvideorange?language=objc - /// - /// Note: if you are using YUV420 with Firebase ML Vision you need to concatenate the planes - /// using the following function: - /// ```dart - /// Uint8List concatenatePlanes(List planes) { - /// var totalBytes = 0; - /// for (var i = 0; i < planes.length; ++i) { - /// totalBytes += planes[i].bytes.length; - /// } - /// - /// final bytes = Uint8List(totalBytes); - /// - /// var byteOffset = 0; - /// for (var i = 0; i < planes.length; ++i) { - /// final length = planes[i].bytes.length; - /// bytes.setRange(byteOffset, byteOffset += length, planes[i].bytes); - /// } - /// return bytes; - /// } - /// ``` yuv420, - /// NV21 is only valid in Android and should be used when feeding images to Firebase ML Vision. If you - /// don't use this format then you need to concatenate the planes from YUV420 which is costly both in - /// memory and CPU. It's most efficient to just feed it NV21. - nv21, - /// 32-bit BGRA. /// /// On iOS, this is `kCVPixelFormatType_32BGRA`. See From 354991fbccb3ee0788ceac76d2faf8c8704b0f6b Mon Sep 17 00:00:00 2001 From: Andrew Coutts Date: Tue, 2 Mar 2021 12:37:02 -0500 Subject: [PATCH 028/114] Added a (not working yet) unit test trying to mock camera characteristics --- .../io/flutter/plugins/camera/Camera.java | 13 +++-- .../io/flutter/plugins/camera/CameraTest.java | 51 +++++++++++++++++++ 2 files changed, 61 insertions(+), 3 deletions(-) create mode 100644 packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/CameraTest.java 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 4912ff64d169..aa3806b4ef89 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 @@ -282,7 +282,7 @@ public Camera( // Get camera characteristics and check for supported features cameraCharacteristics = cameraManager.getCameraCharacteristics(cameraName); getAvailableFpsRange(cameraCharacteristics); - checkAutoFocusSupported(); + mAutoFocusSupported = checkAutoFocusSupported(cameraCharacteristics); checkFlashSupported(); // Setup orientation @@ -313,12 +313,19 @@ public Camera( startBackgroundThread(); } + /** + * Get the current camera state (use for testing). + */ + public CameraState getState() { + return this.cameraState; + } + /** * Check if the auto focus is supported by the current camera. We look at the available AF modes * and the available lens focusing distance to determine if its' a fixed length lens or not as * well. */ - private void checkAutoFocusSupported() { + public static boolean checkAutoFocusSupported(CameraCharacteristics cameraCharacteristics) { int[] modes = cameraCharacteristics.get(CameraCharacteristics.CONTROL_AF_AVAILABLE_MODES); // Log.i(TAG, "checkAutoFocusSupported | modes:"); for (int mode : modes) { @@ -341,7 +348,7 @@ private void checkAutoFocusSupported() { } // Log.i(TAG, "checkAutoFocusSupported | minFocus " + minFocus + " | maxFocus: " + maxFocus); - mAutoFocusSupported = + return !isFixedLength && !(modes == null || modes.length == 0 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 new file mode 100644 index 000000000000..73e1a6620c62 --- /dev/null +++ b/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/CameraTest.java @@ -0,0 +1,51 @@ +package io.flutter.plugins.camera; + +import android.app.Activity; +import android.content.Context; +import android.hardware.camera2.CameraAccessException; +import android.hardware.camera2.CameraCharacteristics; +import android.hardware.camera2.CameraManager; +import android.util.Range; + +import org.junit.Test; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.isNotNull; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import io.flutter.view.TextureRegistry; + +public class CameraTest { + @Test + public void should_create_camera_plugin() throws CameraAccessException { + final Activity mockActivity = mock(Activity.class); + final TextureRegistry.SurfaceTextureEntry flutterTextureMock = mock(TextureRegistry.SurfaceTextureEntry.class); + final DartMessenger dartMessengerMock = mock(DartMessenger.class); + final String cameraName = "camera1"; + final String resolutionPreset = "high"; + final boolean enableAudio = false; + final CameraCharacteristics mockCameraCharacteristics = mock(CameraCharacteristics.class); + + // Mocks + final CameraManager mockCameraManager = mock(CameraManager.class); + when(mockActivity.getSystemService(Context.CAMERA_SERVICE)).thenReturn(mockCameraManager); + when(mockCameraManager.getCameraCharacteristics(cameraName)).thenReturn(mockCameraCharacteristics); + when(mockCameraCharacteristics.get(CameraCharacteristics.CONTROL_AF_AVAILABLE_MODES)).thenReturn(new int[]{0, 1, 2}); + when(mockCameraCharacteristics.get(CameraCharacteristics.CONTROL_AE_AVAILABLE_TARGET_FPS_RANGES)).thenReturn(null); + + final Camera camera = new Camera(mockActivity, + flutterTextureMock, + dartMessengerMock, + cameraName, + resolutionPreset, + enableAudio); + + assertEquals("should create a camera", camera, isNotNull()); + assertEquals("should be in preview state from the start", camera.getState(), CameraState.STATE_PREVIEW); + } +} From 9a08980da787997f40a70f5fc0b76cbfa52be1fa Mon Sep 17 00:00:00 2001 From: Andrew Coutts Date: Tue, 2 Mar 2021 14:36:54 -0500 Subject: [PATCH 029/114] Update changelog to remove NV21 references. --- packages/camera/camera/CHANGELOG.md | 1 - packages/camera/camera_platform_interface/CHANGELOG.md | 4 ---- 2 files changed, 5 deletions(-) diff --git a/packages/camera/camera/CHANGELOG.md b/packages/camera/camera/CHANGELOG.md index e5c9d55118ef..42d3a2c5f9f6 100644 --- a/packages/camera/camera/CHANGELOG.md +++ b/packages/camera/camera/CHANGELOG.md @@ -2,7 +2,6 @@ * Complete rewrite of Android plugin to fix all capture, focus, flash, and exposure issues. * Fixed crash when opening front-facing cameras on some legacy android devices like Sony XZ. * Android Flash mode works with full precapture sequence. -* Added support for NV21 image stream format on Android. ## 0.8.0-nullsafety.3 diff --git a/packages/camera/camera_platform_interface/CHANGELOG.md b/packages/camera/camera_platform_interface/CHANGELOG.md index 6cb312e7ec74..73f708bbc2e1 100644 --- a/packages/camera/camera_platform_interface/CHANGELOG.md +++ b/packages/camera/camera_platform_interface/CHANGELOG.md @@ -1,7 +1,3 @@ -## 2.0.2 - -- Added support for NV21 image stream format in Android. - ## 2.0.1 - Update platform_plugin_interface version requirement. From 52bce5cae84e9ed1658e9ca4924113419ac24a20 Mon Sep 17 00:00:00 2001 From: Andrew Coutts Date: Sun, 28 Feb 2021 12:30:30 -0500 Subject: [PATCH 030/114] More refactoring --- .../flutter/plugins/camera/AspectRatio.java | 175 +++ .../io/flutter/plugins/camera/Camera.java | 1233 ++++++++++------- .../plugins/camera/CameraConstants.java | 26 + .../plugins/camera/CaptureSessionState.java | 11 + .../io/flutter/plugins/camera/ImageSaver.java | 78 ++ .../plugins/camera/PictureCaptureRequest.java | 36 +- .../camera/example/android/gradle.properties | 17 +- 7 files changed, 1022 insertions(+), 554 deletions(-) create mode 100644 packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/AspectRatio.java create mode 100644 packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/CameraConstants.java create mode 100644 packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/CaptureSessionState.java create mode 100644 packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/ImageSaver.java diff --git a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/AspectRatio.java b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/AspectRatio.java new file mode 100644 index 000000000000..455dd7ceaf62 --- /dev/null +++ b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/AspectRatio.java @@ -0,0 +1,175 @@ +package io.flutter.plugins.camera; + +import android.os.Parcel; +import android.os.Parcelable; +import android.util.Size; + +import androidx.annotation.NonNull; +import androidx.collection.SparseArrayCompat; + +/** + * Immutable class for describing proportional relationship between width and height. + */ +public class AspectRatio implements Comparable, Parcelable { + + private final static SparseArrayCompat> sCache + = new SparseArrayCompat<>(16); + + private final int mX; + private final int mY; + + /** + * Returns an instance of {@link AspectRatio} specified by {@code x} and {@code y} values. + * The values {@code x} and {@code} will be reduced by their greatest common divider. + * + * @param x The width + * @param y The height + * @return An instance of {@link AspectRatio} + */ + public static AspectRatio of(int x, int y) { + int gcd = gcd(x, y); + x /= gcd; + y /= gcd; + SparseArrayCompat arrayX = sCache.get(x); + if (arrayX == null) { + AspectRatio ratio = new AspectRatio(x, y); + arrayX = new SparseArrayCompat<>(); + arrayX.put(y, ratio); + sCache.put(x, arrayX); + return ratio; + } else { + AspectRatio ratio = arrayX.get(y); + if (ratio == null) { + ratio = new AspectRatio(x, y); + arrayX.put(y, ratio); + } + return ratio; + } + } + + /** + * Parse an {@link AspectRatio} from a {@link String} formatted like "4:3". + * + * @param s The string representation of the aspect ratio + * @return The aspect ratio + * @throws IllegalArgumentException when the format is incorrect. + */ + public static AspectRatio parse(String s) { + int position = s.indexOf(':'); + if (position == -1) { + throw new IllegalArgumentException("Malformed aspect ratio: " + s); + } + try { + int x = Integer.parseInt(s.substring(0, position)); + int y = Integer.parseInt(s.substring(position + 1)); + return AspectRatio.of(x, y); + } catch (NumberFormatException e) { + throw new IllegalArgumentException("Malformed aspect ratio: " + s, e); + } + } + + private AspectRatio(int x, int y) { + mX = x; + mY = y; + } + + public int getX() { + return mX; + } + + public int getY() { + return mY; + } + + public boolean matches(Size size) { + int gcd = gcd(size.getWidth(), size.getHeight()); + int x = size.getWidth() / gcd; + int y = size.getHeight() / gcd; + return mX == x && mY == y; + } + + @Override + public boolean equals(Object o) { + if (o == null) { + return false; + } + if (this == o) { + return true; + } + if (o instanceof AspectRatio) { + AspectRatio ratio = (AspectRatio) o; + return mX == ratio.mX && mY == ratio.mY; + } + return false; + } + + @Override + public String toString() { + return mX + ":" + mY; + } + + public float toFloat() { + return (float) mX / mY; + } + + @Override + public int hashCode() { + // assuming most sizes are <2^16, doing a rotate will give us perfect hashing + return mY ^ ((mX << (Integer.SIZE / 2)) | (mX >>> (Integer.SIZE / 2))); + } + + @Override + public int compareTo(@NonNull AspectRatio another) { + if (equals(another)) { + return 0; + } else if (toFloat() - another.toFloat() > 0) { + return 1; + } + return -1; + } + + /** + * @return The inverse of this {@link AspectRatio}. + */ + public AspectRatio inverse() { + //noinspection SuspiciousNameCombination + return AspectRatio.of(mY, mX); + } + + private static int gcd(int a, int b) { + while (b != 0) { + int c = b; + b = a % b; + a = c; + } + return a; + } + + @Override + public int describeContents() { + return 0; + } + + @Override + public void writeToParcel(Parcel dest, int flags) { + dest.writeInt(mX); + dest.writeInt(mY); + } + + public static final Creator CREATOR + = new Creator() { + + @Override + public AspectRatio createFromParcel(Parcel source) { + int x = source.readInt(); + int y = source.readInt(); + return AspectRatio.of(x, y); + } + + @Override + public AspectRatio[] newArray(int size) { + return new AspectRatio[size]; + } + }; + +} 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 5169a3babb74..ea879f4e2bf1 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 @@ -4,8 +4,6 @@ package io.flutter.plugins.camera; -import static io.flutter.plugins.camera.CameraUtils.computeBestPreviewSize; - import android.annotation.SuppressLint; import android.annotation.TargetApi; import android.app.Activity; @@ -35,27 +33,19 @@ import android.os.Build.VERSION; import android.os.Build.VERSION_CODES; import android.os.Handler; -import android.os.Looper; +import android.os.HandlerThread; import android.os.SystemClock; import android.util.Log; import android.util.Range; import android.util.Rational; import android.util.Size; +import android.util.SparseIntArray; import android.view.Surface; + import androidx.annotation.NonNull; import androidx.annotation.Nullable; -import io.flutter.embedding.engine.systemchannels.PlatformChannel; -import io.flutter.plugin.common.EventChannel; -import io.flutter.plugin.common.MethodChannel.Result; -import io.flutter.plugins.camera.PictureCaptureRequest.State; -import io.flutter.plugins.camera.media.MediaRecorderBuilder; -import io.flutter.plugins.camera.types.ExposureMode; -import io.flutter.plugins.camera.types.FlashMode; -import io.flutter.plugins.camera.types.FocusMode; -import io.flutter.plugins.camera.types.ResolutionPreset; -import io.flutter.view.TextureRegistry.SurfaceTextureEntry; + import java.io.File; -import java.io.FileOutputStream; import java.io.IOException; import java.nio.ByteBuffer; import java.util.ArrayList; @@ -66,6 +56,18 @@ import java.util.Map; import java.util.concurrent.Executors; +import io.flutter.embedding.engine.systemchannels.PlatformChannel; +import io.flutter.plugin.common.EventChannel; +import io.flutter.plugin.common.MethodChannel.Result; +import io.flutter.plugins.camera.media.MediaRecorderBuilder; +import io.flutter.plugins.camera.types.ExposureMode; +import io.flutter.plugins.camera.types.FlashMode; +import io.flutter.plugins.camera.types.FocusMode; +import io.flutter.plugins.camera.types.ResolutionPreset; +import io.flutter.view.TextureRegistry.SurfaceTextureEntry; + +import static io.flutter.plugins.camera.CameraUtils.computeBestPreviewSize; + @FunctionalInterface interface ErrorCallback { void onError(String errorCode, String errorMessage); @@ -74,8 +76,29 @@ interface ErrorCallback { public class Camera { private static final String TAG = "Camera"; - /** Timeout for the pre-capture sequence. */ + /** + * Timeout for the pre-capture sequence. + */ private static final long PRECAPTURE_TIMEOUT_MS = 1000; + /** + * Conversion from screen rotation to JPEG orientation. + */ + private static final SparseIntArray ORIENTATIONS = new SparseIntArray(); + private static final HashMap supportedImageFormats; + + static { + ORIENTATIONS.append(Surface.ROTATION_0, 90); + ORIENTATIONS.append(Surface.ROTATION_90, 0); + ORIENTATIONS.append(Surface.ROTATION_180, 270); + ORIENTATIONS.append(Surface.ROTATION_270, 180); + } + + // Current supported outputs + static { + supportedImageFormats = new HashMap<>(); + supportedImageFormats.put("yuv420", 35); + supportedImageFormats.put("jpeg", 256); + } private final SurfaceTextureEntry flutterTexture; private final CameraManager cameraManager; @@ -91,19 +114,78 @@ public class Camera { private final DartMessenger dartMessenger; private final CameraZoom cameraZoom; private final CameraCharacteristics cameraCharacteristics; + private final Activity activity; + /** + * Whether the current camera device supports Flash or not. + */ + private final boolean mFlashSupported; + /** + * A {@link Handler} for running tasks in the background. + */ + private Handler mBackgroundHandler; + /** + * An additional thread for running tasks that shouldn't block the UI. + */ + private HandlerThread mBackgroundThread; private CameraDevice cameraDevice; - private CameraCaptureSession cameraCaptureSession; + private CameraCaptureSession mPreviewSession; private ImageReader pictureImageReader; private ImageReader imageStreamReader; - private CaptureRequest.Builder captureRequestBuilder; + + /** + * {@link CaptureRequest.Builder} for the camera preview + */ + private CaptureRequest.Builder mPreviewRequestBuilder; + + /** + * {@link CaptureRequest} generated by {@link #mPreviewRequestBuilder} + */ + private CaptureRequest mPreviewRequest; + + /** + * The current camera auto focus mode + */ + private boolean mAutoFocus = true; + + /** + * Whether the current camera device supports auto focus or not. + */ + private boolean mAutoFocusSupported = true; + private MediaRecorder mediaRecorder; private boolean recordingVideo; private File videoRecordingFile; - private FlashMode flashMode; + + /** + * Flash mode setting of the current camera + */ + private FlashMode currentFlashMode; + + /** + * Exposure mode setting of the current camera. + */ private ExposureMode exposureMode; - private FocusMode focusMode; + + /** + * Focus mode setting of the current camera. + */ + private FocusMode currentFocusMode; private PictureCaptureRequest pictureCaptureRequest; + /** + * This a callback object for the {@link ImageReader}. "onImageAvailable" will be called when a + * still image is ready to be saved. + */ + private final ImageReader.OnImageAvailableListener mOnImageAvailableListener + = new ImageReader.OnImageAvailableListener() { + + @Override + public void onImageAvailable(ImageReader reader) { + Log.i(TAG, "onImageAvailable"); + mBackgroundHandler.post(new ImageSaver(reader.acquireNextImage(), pictureCaptureRequest.mFile, pictureCaptureRequest)); + } + + }; private CameraRegions cameraRegions; private int exposureOffset; private boolean useAutoFocus = true; @@ -111,61 +193,64 @@ public class Camera { private PlatformChannel.DeviceOrientation lockedCaptureOrientation; private long preCaptureStartTime; - private static final HashMap supportedImageFormats; - // Current supported outputs - static { - supportedImageFormats = new HashMap<>(); - supportedImageFormats.put("yuv420", 35); - supportedImageFormats.put("jpeg", 256); - } public Camera( - final Activity activity, - final SurfaceTextureEntry flutterTexture, - final DartMessenger dartMessenger, - final String cameraName, - final String resolutionPreset, - final boolean enableAudio) - throws CameraAccessException { + final Activity activity, + final SurfaceTextureEntry flutterTexture, + final DartMessenger dartMessenger, + final String cameraName, + final String resolutionPreset, + final boolean enableAudio) + throws CameraAccessException { if (activity == null) { throw new IllegalStateException("No activity available!"); } + this.activity = activity; this.cameraName = cameraName; this.enableAudio = enableAudio; this.flutterTexture = flutterTexture; this.dartMessenger = dartMessenger; this.cameraManager = (CameraManager) activity.getSystemService(Context.CAMERA_SERVICE); this.applicationContext = activity.getApplicationContext(); - this.flashMode = FlashMode.auto; + this.currentFlashMode = FlashMode.off; this.exposureMode = ExposureMode.auto; - this.focusMode = FocusMode.auto; + this.currentFocusMode = FocusMode.auto; this.exposureOffset = 0; cameraCharacteristics = cameraManager.getCameraCharacteristics(cameraName); - initFps(cameraCharacteristics); + getAvailableFpsRange(cameraCharacteristics); sensorOrientation = cameraCharacteristics.get(CameraCharacteristics.SENSOR_ORIENTATION); isFrontFacing = - cameraCharacteristics.get(CameraCharacteristics.LENS_FACING) - == CameraMetadata.LENS_FACING_FRONT; + cameraCharacteristics.get(CameraCharacteristics.LENS_FACING) + == CameraMetadata.LENS_FACING_FRONT; ResolutionPreset preset = ResolutionPreset.valueOf(resolutionPreset); recordingProfile = - CameraUtils.getBestAvailableCamcorderProfileForResolutionPreset(cameraName, preset); + CameraUtils.getBestAvailableCamcorderProfileForResolutionPreset(cameraName, preset); captureSize = new Size(recordingProfile.videoFrameWidth, recordingProfile.videoFrameHeight); previewSize = computeBestPreviewSize(cameraName, preset); cameraZoom = - new CameraZoom( - cameraCharacteristics.get(CameraCharacteristics.SENSOR_INFO_ACTIVE_ARRAY_SIZE), - cameraCharacteristics.get(CameraCharacteristics.SCALER_AVAILABLE_MAX_DIGITAL_ZOOM)); + new CameraZoom( + cameraCharacteristics.get(CameraCharacteristics.SENSOR_INFO_ACTIVE_ARRAY_SIZE), + cameraCharacteristics.get(CameraCharacteristics.SCALER_AVAILABLE_MAX_DIGITAL_ZOOM)); deviceOrientationListener = - new DeviceOrientationManager(activity, dartMessenger, isFrontFacing, sensorOrientation); + new DeviceOrientationManager(activity, dartMessenger, isFrontFacing, sensorOrientation); deviceOrientationListener.start(); + + // Check if the flash is supported. + Boolean flashAvailable = cameraCharacteristics.get(CameraCharacteristics.FLASH_INFO_AVAILABLE); + mFlashSupported = flashAvailable == null ? false : flashAvailable; + startBackgroundThread(); } - private void initFps(CameraCharacteristics cameraCharacteristics) { + /** + * Load available FPS range for the current camera and update the available fps range with it. + * @param cameraCharacteristics + */ + private void getAvailableFpsRange(CameraCharacteristics cameraCharacteristics) { try { Range[] ranges = - cameraCharacteristics.get(CameraCharacteristics.CONTROL_AE_AVAILABLE_TARGET_FPS_RANGES); + cameraCharacteristics.get(CameraCharacteristics.CONTROL_AE_AVAILABLE_TARGET_FPS_RANGES); if (ranges != null) { for (Range range : ranges) { int upper = range.getUpper(); @@ -178,7 +263,7 @@ private void initFps(CameraCharacteristics cameraCharacteristics) { } } } catch (Exception e) { - e.printStackTrace(); + pictureCaptureRequest.error("cameraAccess", e.getMessage(), null); } Log.i("Camera", "[FPS Range] is:" + fpsRange); } @@ -189,20 +274,20 @@ private void prepareMediaRecorder(String outputFilePath) throws IOException { } mediaRecorder = - new MediaRecorderBuilder(recordingProfile, outputFilePath) - .setEnableAudio(enableAudio) - .setMediaOrientation( - lockedCaptureOrientation == null - ? deviceOrientationListener.getMediaOrientation() - : deviceOrientationListener.getMediaOrientation(lockedCaptureOrientation)) - .build(); + new MediaRecorderBuilder(recordingProfile, outputFilePath) + .setEnableAudio(enableAudio) + .setMediaOrientation( + lockedCaptureOrientation == null + ? deviceOrientationListener.getMediaOrientation() + : deviceOrientationListener.getMediaOrientation(lockedCaptureOrientation)) + .build(); } @SuppressLint("MissingPermission") public void open(String imageFormatGroup) throws CameraAccessException { pictureImageReader = - ImageReader.newInstance( - captureSize.getWidth(), captureSize.getHeight(), ImageFormat.JPEG, 2); + ImageReader.newInstance( + captureSize.getWidth(), captureSize.getHeight(), ImageFormat.JPEG, 2); Integer imageFormat = supportedImageFormats.get(imageFormatGroup); if (imageFormat == null) { @@ -212,95 +297,95 @@ public void open(String imageFormatGroup) throws CameraAccessException { // Used to steam image byte data to dart side. imageStreamReader = - ImageReader.newInstance(previewSize.getWidth(), previewSize.getHeight(), imageFormat, 2); + ImageReader.newInstance(previewSize.getWidth(), previewSize.getHeight(), imageFormat, 2); cameraManager.openCamera( - cameraName, - new CameraDevice.StateCallback() { - @Override - public void onOpened(@NonNull CameraDevice device) { - cameraDevice = device; - try { - startPreview(); - dartMessenger.sendCameraInitializedEvent( - previewSize.getWidth(), - previewSize.getHeight(), - exposureMode, - focusMode, - isExposurePointSupported(), - isFocusPointSupported()); - } catch (CameraAccessException e) { - dartMessenger.sendCameraErrorEvent(e.getMessage()); - close(); - } - } + cameraName, + new CameraDevice.StateCallback() { + @Override + public void onOpened(@NonNull CameraDevice device) { + cameraDevice = device; + try { + startPreview(); + dartMessenger.sendCameraInitializedEvent( + previewSize.getWidth(), + previewSize.getHeight(), + exposureMode, + currentFocusMode, + isExposurePointSupported(), + isFocusPointSupported()); + } catch (CameraAccessException e) { + dartMessenger.sendCameraErrorEvent(e.getMessage()); + close(); + } + } - @Override - public void onClosed(@NonNull CameraDevice camera) { - dartMessenger.sendCameraClosingEvent(); - super.onClosed(camera); - } + @Override + public void onClosed(@NonNull CameraDevice camera) { + dartMessenger.sendCameraClosingEvent(); + super.onClosed(camera); + } - @Override - public void onDisconnected(@NonNull CameraDevice cameraDevice) { - close(); - dartMessenger.sendCameraErrorEvent("The camera was disconnected."); - } + @Override + public void onDisconnected(@NonNull CameraDevice cameraDevice) { + close(); + dartMessenger.sendCameraErrorEvent("The camera was disconnected."); + } - @Override - public void onError(@NonNull CameraDevice cameraDevice, int errorCode) { - close(); - String errorDescription; - switch (errorCode) { - case ERROR_CAMERA_IN_USE: - errorDescription = "The camera device is in use already."; - break; - case ERROR_MAX_CAMERAS_IN_USE: - errorDescription = "Max cameras in use"; - break; - case ERROR_CAMERA_DISABLED: - errorDescription = "The camera device could not be opened due to a device policy."; - break; - case ERROR_CAMERA_DEVICE: - errorDescription = "The camera device has encountered a fatal error"; - break; - case ERROR_CAMERA_SERVICE: - errorDescription = "The camera service has encountered a fatal error."; - break; - default: - errorDescription = "Unknown camera error"; - } - dartMessenger.sendCameraErrorEvent(errorDescription); - } - }, - null); + @Override + public void onError(@NonNull CameraDevice cameraDevice, int errorCode) { + close(); + String errorDescription; + switch (errorCode) { + case ERROR_CAMERA_IN_USE: + errorDescription = "The camera device is in use already."; + break; + case ERROR_MAX_CAMERAS_IN_USE: + errorDescription = "Max cameras in use"; + break; + case ERROR_CAMERA_DISABLED: + errorDescription = "The camera device could not be opened due to a device policy."; + break; + case ERROR_CAMERA_DEVICE: + errorDescription = "The camera device has encountered a fatal error"; + break; + case ERROR_CAMERA_SERVICE: + errorDescription = "The camera service has encountered a fatal error."; + break; + default: + errorDescription = "Unknown camera error"; + } + dartMessenger.sendCameraErrorEvent(errorDescription); + } + }, + null); } private void createCaptureSession(int templateType, Surface... surfaces) - throws CameraAccessException { + throws CameraAccessException { createCaptureSession(templateType, null, surfaces); } private void createCaptureSession( - int templateType, Runnable onSuccessCallback, Surface... surfaces) - throws CameraAccessException { + int templateType, Runnable onSuccessCallback, Surface... surfaces) + throws CameraAccessException { // Close any existing capture session. closeCaptureSession(); // Create a new capture builder. - captureRequestBuilder = cameraDevice.createCaptureRequest(templateType); + mPreviewRequestBuilder = cameraDevice.createCaptureRequest(templateType); // Build Flutter surface to render to SurfaceTexture surfaceTexture = flutterTexture.surfaceTexture(); surfaceTexture.setDefaultBufferSize(previewSize.getWidth(), previewSize.getHeight()); Surface flutterSurface = new Surface(surfaceTexture); - captureRequestBuilder.addTarget(flutterSurface); + mPreviewRequestBuilder.addTarget(flutterSurface); List remainingSurfaces = Arrays.asList(surfaces); if (templateType != CameraDevice.TEMPLATE_PREVIEW) { // If it is not preview mode, add all surfaces as targets. for (Surface surface : remainingSurfaces) { - captureRequestBuilder.addTarget(surface); + mPreviewRequestBuilder.addTarget(surface); } } @@ -308,29 +393,30 @@ private void createCaptureSession( // Prepare the callback CameraCaptureSession.StateCallback callback = - new CameraCaptureSession.StateCallback() { - @Override - public void onConfigured(@NonNull CameraCaptureSession session) { - if (cameraDevice == null) { - dartMessenger.sendCameraErrorEvent("The camera was closed during configuration."); - return; - } - cameraCaptureSession = session; + new CameraCaptureSession.StateCallback() { + @Override + public void onConfigured(@NonNull CameraCaptureSession session) { + // Camera was already closed. + if (cameraDevice == null) { + dartMessenger.sendCameraErrorEvent("The camera was closed during configuration."); + return; + } + mPreviewSession = session; - updateFpsRange(); - updateFocus(focusMode); - updateFlash(flashMode); - updateExposure(exposureMode); + updateFpsRange(); + updateFocus(mPreviewRequestBuilder); + updateFlash(mPreviewRequestBuilder); + updateExposure(exposureMode); - refreshPreviewCaptureSession( - onSuccessCallback, (code, message) -> dartMessenger.sendCameraErrorEvent(message)); - } + refreshPreviewCaptureSession( + onSuccessCallback, (code, message) -> dartMessenger.sendCameraErrorEvent(message)); + } - @Override - public void onConfigureFailed(@NonNull CameraCaptureSession cameraCaptureSession) { - dartMessenger.sendCameraErrorEvent("Failed to configure camera session."); - } - }; + @Override + public void onConfigureFailed(@NonNull CameraCaptureSession cameraCaptureSession) { + dartMessenger.sendCameraErrorEvent("Failed to configure camera session."); + } + }; // Start the session if (VERSION.SDK_INT >= VERSION_CODES.P) { @@ -352,35 +438,36 @@ public void onConfigureFailed(@NonNull CameraCaptureSession cameraCaptureSession @TargetApi(VERSION_CODES.P) private void createCaptureSessionWithSessionConfig( - List outputConfigs, CameraCaptureSession.StateCallback callback) - throws CameraAccessException { + List outputConfigs, CameraCaptureSession.StateCallback callback) + throws CameraAccessException { cameraDevice.createCaptureSession( - new SessionConfiguration( - SessionConfiguration.SESSION_REGULAR, - outputConfigs, - Executors.newSingleThreadExecutor(), - callback)); + new SessionConfiguration( + SessionConfiguration.SESSION_REGULAR, + outputConfigs, + Executors.newSingleThreadExecutor(), + callback)); } @TargetApi(VERSION_CODES.LOLLIPOP) @SuppressWarnings("deprecation") private void createCaptureSession( - List surfaces, CameraCaptureSession.StateCallback callback) - throws CameraAccessException { + List surfaces, CameraCaptureSession.StateCallback callback) + throws CameraAccessException { cameraDevice.createCaptureSession(surfaces, callback, null); } private void refreshPreviewCaptureSession( - @Nullable Runnable onSuccessCallback, @NonNull ErrorCallback onErrorCallback) { - if (cameraCaptureSession == null) { + @Nullable Runnable onSuccessCallback, @NonNull ErrorCallback onErrorCallback) { + Log.i(TAG, "refreshPreviewCaptureSession"); + if (mPreviewSession == null) { + Log.i(TAG, "[refreshPreviewCaptureSession] mPreviewSession null, returning"); return; } try { - cameraCaptureSession.setRepeatingRequest( - captureRequestBuilder.build(), - pictureCaptureCallback, - new Handler(Looper.getMainLooper())); + mPreviewRequest = mPreviewRequestBuilder.build(); + mPreviewSession.setRepeatingRequest(mPreviewRequest, + mCaptureCallback, mBackgroundHandler); if (onSuccessCallback != null) { onSuccessCallback.run(); @@ -390,233 +477,330 @@ private void refreshPreviewCaptureSession( } } - private void writeToFile(ByteBuffer buffer, File file) throws IOException { - try (FileOutputStream outputStream = new FileOutputStream(file)) { - while (0 < buffer.remaining()) { - outputStream.getChannel().write(buffer); - } - } - } - public void takePicture(@NonNull final Result result) { + Log.i(TAG, "takePicture | useAutoFocus: " + useAutoFocus); + // Only take 1 picture at a time if (pictureCaptureRequest != null && !pictureCaptureRequest.isFinished()) { result.error("captureAlreadyActive", "Picture is currently already being captured", null); return; } // Store the result - this.pictureCaptureRequest = new PictureCaptureRequest(result); + pictureCaptureRequest = new PictureCaptureRequest(result); // Create temporary file final File outputDir = applicationContext.getCacheDir(); - final File file; try { - file = File.createTempFile("CAP", ".jpg", outputDir); + pictureCaptureRequest.mFile = File.createTempFile("CAP", ".jpg", outputDir); } catch (IOException | SecurityException e) { pictureCaptureRequest.error("cannotCreateFile", e.getMessage(), null); return; } // Listen for picture being taken - pictureImageReader.setOnImageAvailableListener( - reader -> { - try (Image image = reader.acquireLatestImage()) { - ByteBuffer buffer = image.getPlanes()[0].getBuffer(); - writeToFile(buffer, file); - pictureCaptureRequest.finish(file.getAbsolutePath()); - } catch (IOException e) { - pictureCaptureRequest.error("IOError", "Failed saving image", null); - } - }, - null); + pictureImageReader.setOnImageAvailableListener(mOnImageAvailableListener, mBackgroundHandler); if (useAutoFocus) { runPictureAutoFocus(); } else { - runPicturePreCapture(); + runPrecaptureSequence(); } } - private final CameraCaptureSession.CaptureCallback pictureCaptureCallback = - new CameraCaptureSession.CaptureCallback() { - @Override - public void onCaptureCompleted( - @NonNull CameraCaptureSession session, - @NonNull CaptureRequest request, - @NonNull TotalCaptureResult result) { - processCapture(result); - } + /** + * Run the precapture sequence for capturing a still image. This method should be called when + * we get a response in {@link #mCaptureCallback} from lockFocus(). + */ + private void runPrecaptureSequence() { + Log.i(TAG, "runPrecaptureSequence"); + try { + // This is how to tell the camera to trigger. + mPreviewRequestBuilder.set(CaptureRequest.CONTROL_AE_PRECAPTURE_TRIGGER, + CaptureRequest.CONTROL_AE_PRECAPTURE_TRIGGER_START); + // Tell #mCaptureCallback to wait for the precapture sequence to be set. + pictureCaptureRequest.setState(CaptureSessionState.STATE_WAITING_PRECAPTURE); + mPreviewSession.capture(mPreviewRequestBuilder.build(), mCaptureCallback, + mBackgroundHandler); + } catch (CameraAccessException e) { + pictureCaptureRequest.error("cameraAccess", e.getMessage(), null); + } + } - @Override - public void onCaptureProgressed( - @NonNull CameraCaptureSession session, - @NonNull CaptureRequest request, - @NonNull CaptureResult partialResult) { - processCapture(partialResult); - } + /** + * Capture a still picture. This method should be called when we get a response in + * {@link #mCaptureCallback} from both lockFocus(). + */ + private void captureStillPicture() { + Log.i(TAG, "captureStillPicture"); + try { + if (null == cameraDevice) { + return; + } + // This is the CaptureRequest.Builder that we use to take a picture. + final CaptureRequest.Builder captureBuilder = + cameraDevice.createCaptureRequest(CameraDevice.TEMPLATE_STILL_CAPTURE); + captureBuilder.addTarget(pictureImageReader.getSurface()); - @Override - public void onCaptureFailed( - @NonNull CameraCaptureSession session, - @NonNull CaptureRequest request, - @NonNull CaptureFailure failure) { - if (pictureCaptureRequest == null || pictureCaptureRequest.isFinished()) { - return; - } - String reason; - boolean fatalFailure = false; - switch (failure.getReason()) { - case CaptureFailure.REASON_ERROR: - reason = "An error happened in the framework"; - break; - case CaptureFailure.REASON_FLUSHED: - reason = "The capture has failed due to an abortCaptures() call"; - fatalFailure = true; - break; - default: - reason = "Unknown reason"; - } - Log.w("Camera", "pictureCaptureCallback.onCaptureFailed(): " + reason); - if (fatalFailure) pictureCaptureRequest.error("captureFailure", reason, null); - } + // Zoom + captureBuilder.set( + CaptureRequest.SCALER_CROP_REGION, + mPreviewRequestBuilder.get(CaptureRequest.SCALER_CROP_REGION)); - private void processCapture(CaptureResult result) { - if (pictureCaptureRequest == null) { - return; - } + // Set focus / flash from preview mode + updateFlash(captureBuilder); + updateFocus(captureBuilder); - Integer aeState = result.get(CaptureResult.CONTROL_AE_STATE); - Integer afState = result.get(CaptureResult.CONTROL_AF_STATE); - switch (pictureCaptureRequest.getState()) { - case focusing: - if (afState == null) { - return; - } else if (afState == CaptureResult.CONTROL_AF_STATE_FOCUSED_LOCKED - || afState == CaptureResult.CONTROL_AF_STATE_NOT_FOCUSED_LOCKED) { - // Some devices might return null here, in which case we will also continue. - if (aeState == null || aeState == CaptureResult.CONTROL_AE_STATE_CONVERGED) { - runPictureCapture(); - } else { - runPicturePreCapture(); - } - } - break; - case preCapture: - // Some devices might return null here, in which case we will also continue. - if (aeState == null - || aeState == CaptureRequest.CONTROL_AE_STATE_PRECAPTURE - || aeState == CaptureRequest.CONTROL_AE_STATE_FLASH_REQUIRED - || aeState == CaptureRequest.CONTROL_AE_STATE_CONVERGED) { - pictureCaptureRequest.setState(State.waitingPreCaptureReady); - setPreCaptureStartTime(); - } - break; - case waitingPreCaptureReady: - if (aeState == null || aeState != CaptureRequest.CONTROL_AE_STATE_PRECAPTURE) { - runPictureCapture(); - } else { - if (hitPreCaptureTimeout()) { - unlockAutoFocus(); - } - } - } + // Orientation + int rotation = activity.getWindowManager().getDefaultDisplay().getRotation(); + captureBuilder.set(CaptureRequest.JPEG_ORIENTATION, getOrientation(rotation)); + + CameraCaptureSession.CaptureCallback CaptureCallback + = new CameraCaptureSession.CaptureCallback() { + + @Override + public void onCaptureCompleted(@NonNull CameraCaptureSession session, + @NonNull CaptureRequest request, + @NonNull TotalCaptureResult result) { + unlockAutoFocus(); } }; - private void runPictureAutoFocus() { - assert (pictureCaptureRequest != null); + mPreviewSession.stopRepeating(); + mPreviewSession.abortCaptures(); + mPreviewSession.capture(captureBuilder.build(), CaptureCallback, null); + } catch (CameraAccessException e) { + pictureCaptureRequest.error("cameraAccess", e.getMessage(), null); + } + } - pictureCaptureRequest.setState(PictureCaptureRequest.State.focusing); - lockAutoFocus(pictureCaptureCallback); + /** + * Starts a background thread and its {@link Handler}. + * TODO: call when activity resumed + */ + private void startBackgroundThread() { + mBackgroundThread = new HandlerThread("CameraBackground"); + mBackgroundThread.start(); + mBackgroundHandler = new Handler(mBackgroundThread.getLooper()); } - private void runPicturePreCapture() { - assert (pictureCaptureRequest != null); - pictureCaptureRequest.setState(PictureCaptureRequest.State.preCapture); + /** + * Stops the background thread and its {@link Handler}. + * TODO: call when activity paused + */ + private void stopBackgroundThread() { + mBackgroundThread.quitSafely(); + try { + mBackgroundThread.join(); + mBackgroundThread = null; + mBackgroundHandler = null; + } catch (InterruptedException e) { + pictureCaptureRequest.error("cameraAccess", e.getMessage(), null); + } + } - captureRequestBuilder.set( - CaptureRequest.CONTROL_AE_PRECAPTURE_TRIGGER, - CaptureRequest.CONTROL_AE_PRECAPTURE_TRIGGER_START); - refreshPreviewCaptureSession( - () -> - captureRequestBuilder.set( - CaptureRequest.CONTROL_AE_PRECAPTURE_TRIGGER, - CaptureRequest.CONTROL_AE_PRECAPTURE_TRIGGER_IDLE), - (code, message) -> pictureCaptureRequest.error(code, message, null)); + + /** + * Sync the requestBuilder flash setting to the current flash mode setting of the camera. + */ + void updateFlash(CaptureRequest.Builder requestBuilder) { + if (!mFlashSupported) { + return; + } + + switch (currentFlashMode) { + case off: + requestBuilder.set(CaptureRequest.CONTROL_AE_MODE, + CaptureRequest.CONTROL_AE_MODE_ON); + requestBuilder.set(CaptureRequest.FLASH_MODE, + CaptureRequest.FLASH_MODE_OFF); + break; + + case always: + requestBuilder.set(CaptureRequest.CONTROL_AE_MODE, + CaptureRequest.CONTROL_AE_MODE_ON_ALWAYS_FLASH); + requestBuilder.set(CaptureRequest.FLASH_MODE, + CaptureRequest.FLASH_MODE_OFF); + break; + + case torch: + requestBuilder.set(CaptureRequest.CONTROL_AE_MODE, + CaptureRequest.CONTROL_AE_MODE_ON); + requestBuilder.set(CaptureRequest.FLASH_MODE, + CaptureRequest.FLASH_MODE_TORCH); + break; + + case auto: + requestBuilder.set(CaptureRequest.CONTROL_AE_MODE, + CaptureRequest.CONTROL_AE_MODE_ON_AUTO_FLASH); + requestBuilder.set(CaptureRequest.FLASH_MODE, + CaptureRequest.FLASH_MODE_OFF); + break; + + // TODO: to be implemented someday. Need to add it to dart and iOS. +// case autoRedEye: +// requestBuilder.set(CaptureRequest.CONTROL_AE_MODE, +// CaptureRequest.CONTROL_AE_MODE_ON_AUTO_FLASH_REDEYE); +// requestBuilder.set(CaptureRequest.FLASH_MODE, +// CaptureRequest.FLASH_MODE_OFF); +// break; + } } - private void runPictureCapture() { - assert (pictureCaptureRequest != null); - pictureCaptureRequest.setState(PictureCaptureRequest.State.capturing); - try { - final CaptureRequest.Builder captureBuilder = - cameraDevice.createCaptureRequest(CameraDevice.TEMPLATE_STILL_CAPTURE); - captureBuilder.addTarget(pictureImageReader.getSurface()); - captureBuilder.set( - CaptureRequest.SCALER_CROP_REGION, - captureRequestBuilder.get(CaptureRequest.SCALER_CROP_REGION)); - captureBuilder.set( - CaptureRequest.JPEG_ORIENTATION, - lockedCaptureOrientation == null - ? deviceOrientationListener.getMediaOrientation() - : deviceOrientationListener.getMediaOrientation(lockedCaptureOrientation)); - - switch (flashMode) { - case off: - captureBuilder.set(CaptureRequest.CONTROL_AE_MODE, CaptureRequest.CONTROL_AE_MODE_ON); - captureBuilder.set(CaptureRequest.FLASH_MODE, CaptureRequest.FLASH_MODE_OFF); + /** + * Retrieves the JPEG orientation from the specified screen rotation. + * + * @param rotation The screen rotation. + * @return The JPEG orientation (one of 0, 90, 270, and 360) + */ + private int getOrientation(int rotation) { + // Sensor orientation is 90 for most devices, or 270 for some devices (eg. Nexus 5X) + // We have to take that into account and rotate JPEG properly. + // For devices with orientation of 90, we simply return our mapping from ORIENTATIONS. + // For devices with orientation of 270, we need to rotate the JPEG 180 degrees. + return (ORIENTATIONS.get(rotation) + sensorOrientation + 270) % 360; + } + + /** + * A {@link CameraCaptureSession.CaptureCallback} that handles events related to JPEG capture. + */ + private final CameraCaptureSession.CaptureCallback mCaptureCallback + = new CameraCaptureSession.CaptureCallback() { + + private void process(CaptureResult result) { + if (pictureCaptureRequest == null) { + return; + } + + Integer aeState = result.get(CaptureResult.CONTROL_AE_STATE); + Integer afState = result.get(CaptureResult.CONTROL_AF_STATE); + + if (pictureCaptureRequest.getState() != CaptureSessionState.IDLE) { + Log.i(TAG, "mCaptureCallback | state: " + pictureCaptureRequest.getState() + " | afState: " + afState + " | aeState: " + aeState); + } + + switch (pictureCaptureRequest.getState()) { + case IDLE: { + // We have nothing to do when the camera preview is working normally. break; - case auto: - captureBuilder.set( - CaptureRequest.CONTROL_AE_MODE, CaptureRequest.CONTROL_AE_MODE_ON_AUTO_FLASH); + } + case FOCUSING: { + if (afState == null) { + captureStillPicture(); + } else if (afState == CaptureResult.CONTROL_AF_STATE_FOCUSED_LOCKED || + afState == CaptureResult.CONTROL_AF_STATE_NOT_FOCUSED_LOCKED) { + // CONTROL_AE_STATE can be null on some devices + + if (aeState == null || + aeState == CaptureResult.CONTROL_AE_STATE_CONVERGED) { + pictureCaptureRequest.setState(CaptureSessionState.CAPTURING); + + captureStillPicture(); + } else { + runPrecaptureSequence(); + } + } break; - case always: - default: - captureBuilder.set( - CaptureRequest.CONTROL_AE_MODE, CaptureRequest.CONTROL_AE_MODE_ON_ALWAYS_FLASH); + } + case STATE_WAITING_PRECAPTURE: { + // CONTROL_AE_STATE can be null on some devices + if (aeState == null || + aeState == CaptureResult.CONTROL_AE_STATE_PRECAPTURE || + aeState == CaptureRequest.CONTROL_AE_STATE_FLASH_REQUIRED) { + pictureCaptureRequest.setState(CaptureSessionState.STATE_WAITING_NON_PRECAPTURE); + setPreCaptureStartTime(); + } break; - } - cameraCaptureSession.stopRepeating(); - cameraCaptureSession.capture( - captureBuilder.build(), - new CameraCaptureSession.CaptureCallback() { - @Override - public void onCaptureCompleted( - @NonNull CameraCaptureSession session, - @NonNull CaptureRequest request, - @NonNull TotalCaptureResult result) { + } + case STATE_WAITING_NON_PRECAPTURE: { + // CONTROL_AE_STATE can be null on some devices + if (aeState == null || aeState != CaptureResult.CONTROL_AE_STATE_PRECAPTURE) { + pictureCaptureRequest.setState(CaptureSessionState.CAPTURING); + captureStillPicture(); + } else { + if (hitPreCaptureTimeout()) { unlockAutoFocus(); } - }, - null); + } + break; + } + } + } + + @Override + public void onCaptureProgressed(@NonNull CameraCaptureSession session, + @NonNull CaptureRequest request, + @NonNull CaptureResult partialResult) { + process(partialResult); + } + + @Override + public void onCaptureCompleted(@NonNull CameraCaptureSession session, + @NonNull CaptureRequest request, + @NonNull TotalCaptureResult result) { + process(result); + } + }; + + /** + * Trigger auto focus to start and refresh preview capture session. + */ + private void startAutoFocus() { + Log.i(TAG, "startAutoFocus"); + mPreviewRequestBuilder.set( + CaptureRequest.CONTROL_AF_TRIGGER, CaptureRequest.CONTROL_AF_TRIGGER_START); + + try { + mPreviewSession.capture(mPreviewRequestBuilder.build(), mCaptureCallback, + mBackgroundHandler); } catch (CameraAccessException e) { - pictureCaptureRequest.error("cameraAccess", e.getMessage(), null); + pictureCaptureRequest.error("cannotStartAutofocus", e.getMessage(), null); } } - private void lockAutoFocus(CaptureCallback callback) { - captureRequestBuilder.set( - CaptureRequest.CONTROL_AF_TRIGGER, CaptureRequest.CONTROL_AF_TRIGGER_START); + /** + * Start capturing a picture, doing autofocus first. + */ + private void runPictureAutoFocus() { + Log.i(TAG, "runPictureAutoFocus"); + assert (pictureCaptureRequest != null); + + pictureCaptureRequest.setState(CaptureSessionState.FOCUSING); + lockAutoFocus(); + } + + /** + * Start the autofocus routine. + */ + private void lockAutoFocus() { + Log.i(TAG, "lockAutoFocus"); + mPreviewRequestBuilder.set( + CaptureRequest.CONTROL_AF_TRIGGER, CaptureRequest.CONTROL_AF_TRIGGER_START); refreshPreviewCaptureSession( - null, (code, message) -> pictureCaptureRequest.error(code, message, null)); + null, (code, message) -> pictureCaptureRequest.error(code, message, null)); } + /** + * Cancel and reset auto focus state and refresh the preview session. + */ private void unlockAutoFocus() { - captureRequestBuilder.set( - CaptureRequest.CONTROL_AF_TRIGGER, CameraMetadata.CONTROL_AF_TRIGGER_CANCEL); - updateFocus(focusMode); + Log.i(TAG, "unlockAutoFocus"); + + mPreviewRequestBuilder.set( + CaptureRequest.CONTROL_AF_TRIGGER, CameraMetadata.CONTROL_AF_TRIGGER_CANCEL); + + updateFocus(mPreviewRequestBuilder); + updateFlash(mPreviewRequestBuilder); + try { - cameraCaptureSession.capture(captureRequestBuilder.build(), null, null); + mPreviewSession.capture(mPreviewRequestBuilder.build(), null, null); } catch (CameraAccessException ignored) { } - captureRequestBuilder.set( - CaptureRequest.CONTROL_AF_TRIGGER, CaptureRequest.CONTROL_AF_TRIGGER_IDLE); refreshPreviewCaptureSession( - null, - (errorCode, errorMessage) -> pictureCaptureRequest.error(errorCode, errorMessage, null)); + null, + (errorCode, errorMessage) -> pictureCaptureRequest.error(errorCode, errorMessage, null)); } public void startVideoRecording(Result result) { @@ -632,7 +816,7 @@ public void startVideoRecording(Result result) { prepareMediaRecorder(videoRecordingFile.getAbsolutePath()); recordingVideo = true; createCaptureSession( - CameraDevice.TEMPLATE_RECORD, () -> mediaRecorder.start(), mediaRecorder.getSurface()); + CameraDevice.TEMPLATE_RECORD, () -> mediaRecorder.start(), mediaRecorder.getSurface()); result.success(null); } catch (CameraAccessException | IOException e) { recordingVideo = false; @@ -651,7 +835,7 @@ public void stopVideoRecording(@NonNull final Result result) { recordingVideo = false; try { - cameraCaptureSession.abortCaptures(); + mPreviewSession.abortCaptures(); mediaRecorder.stop(); } catch (CameraAccessException | IllegalStateException e) { // Ignore exceptions and try to continue (changes are camera session already aborted capture) @@ -698,7 +882,7 @@ public void resumeVideoRecording(@NonNull final Result result) { mediaRecorder.resume(); } else { result.error( - "videoRecordingFailed", "resumeVideoRecording requires Android API +24.", null); + "videoRecordingFailed", "resumeVideoRecording requires Android API +24.", null); return; } } catch (IllegalStateException e) { @@ -709,13 +893,20 @@ public void resumeVideoRecording(@NonNull final Result result) { result.success(null); } - public void setFlashMode(@NonNull final Result result, FlashMode mode) - throws CameraAccessException { - // Get the flash availability + /** + * Dart handler when it's time to set a new flash mode. This will try to set a new + * flash mode to the current camera. + * @param result + * @param newMode + * @throws CameraAccessException + */ + public void setFlashMode(@NonNull final Result result, FlashMode newMode) + throws CameraAccessException { + // Check if the current camera can modify the flash mode. Boolean flashAvailable = - cameraManager - .getCameraCharacteristics(cameraDevice.getId()) - .get(CameraCharacteristics.FLASH_INFO_AVAILABLE); + cameraManager + .getCameraCharacteristics(cameraDevice.getId()) + .get(CameraCharacteristics.FLASH_INFO_AVAILABLE); // Check if flash is available. if (flashAvailable == null || !flashAvailable) { @@ -723,70 +914,72 @@ public void setFlashMode(@NonNull final Result result, FlashMode mode) return; } - // If switching directly from torch to auto or on, make sure we turn off the torch. - if (flashMode == FlashMode.torch && mode != FlashMode.torch && mode != FlashMode.off) { - updateFlash(FlashMode.off); - - this.cameraCaptureSession.setRepeatingRequest( - captureRequestBuilder.build(), - new CaptureCallback() { - private boolean isFinished = false; - - @Override - public void onCaptureCompleted( - @NonNull CameraCaptureSession session, - @NonNull CaptureRequest request, - @NonNull TotalCaptureResult captureResult) { - if (isFinished) { - return; - } - - updateFlash(mode); - refreshPreviewCaptureSession( - () -> { - result.success(null); - isFinished = true; - }, - (code, message) -> - result.error("setFlashModeFailed", "Could not set flash mode.", null)); - } + // Save the new flash mode setting + final FlashMode oldFlashMode = currentFlashMode; + currentFlashMode = newMode; + updateFlash(mPreviewRequestBuilder); - @Override - public void onCaptureFailed( - @NonNull CameraCaptureSession session, - @NonNull CaptureRequest request, - @NonNull CaptureFailure failure) { - if (isFinished) { - return; - } + // If switching directly from torch to auto or on, make sure we turn off the torch. + if (oldFlashMode == FlashMode.torch && newMode != FlashMode.torch && newMode != FlashMode.off) { + // TODO: why cant we just call refresh preview here? + mPreviewSession.setRepeatingRequest( + mPreviewRequestBuilder.build(), + new CaptureCallback() { + private boolean isFinished = false; + + @Override + public void onCaptureCompleted( + @NonNull CameraCaptureSession session, + @NonNull CaptureRequest request, + @NonNull TotalCaptureResult captureResult) { + if (isFinished) { + return; + } + + updateFlash(mPreviewRequestBuilder); + refreshPreviewCaptureSession( + () -> { + result.success(null); + isFinished = true; + }, + (code, message) -> + result.error("setFlashModeFailed", "Could not set flash mode.", null)); + } - result.error("setFlashModeFailed", "Could not set flash mode.", null); - isFinished = true; - } - }, - null); + @Override + public void onCaptureFailed( + @NonNull CameraCaptureSession session, + @NonNull CaptureRequest request, + @NonNull CaptureFailure failure) { + if (isFinished) { + return; + } + + result.error("setFlashModeFailed", "Could not set flash mode.", null); + isFinished = true; + } + }, + null); } else { - updateFlash(mode); - refreshPreviewCaptureSession( - () -> result.success(null), - (code, message) -> result.error("setFlashModeFailed", "Could not set flash mode.", null)); + () -> result.success(null), + (code, message) -> result.error("setFlashModeFailed", "Could not set flash mode.", null)); } } public void setExposureMode(@NonNull final Result result, ExposureMode mode) - throws CameraAccessException { + throws CameraAccessException { updateExposure(mode); - cameraCaptureSession.setRepeatingRequest(captureRequestBuilder.build(), null, null); + mPreviewSession.setRepeatingRequest(mPreviewRequestBuilder.build(), null, null); result.success(null); } public void setExposurePoint(@NonNull final Result result, Double x, Double y) - throws CameraAccessException { + throws CameraAccessException { // Check if exposure point functionality is available. if (!isExposurePointSupported()) { result.error( - "setExposurePointFailed", "Device does not have exposure point capabilities", null); + "setExposurePointFailed", "Device does not have exposure point capabilities", null); return; } // Check if the current region boundaries are known @@ -800,38 +993,34 @@ public void setExposurePoint(@NonNull final Result result, Double x, Double y) // Apply it updateExposure(exposureMode); refreshPreviewCaptureSession( - () -> result.success(null), (code, message) -> result.error("CameraAccess", message, null)); + () -> result.success(null), (code, message) -> result.error("CameraAccess", message, null)); } - public void setFocusMode(@NonNull final Result result, FocusMode mode) - throws CameraAccessException { - this.focusMode = mode; - - updateFocus(mode); + /** + * Set new focus mode from dart. + * @param result + * @param newMode + * @throws CameraAccessException + */ + public void setFocusMode(@NonNull final Result result, FocusMode newMode) + throws CameraAccessException { + currentFocusMode = newMode; + updateFocus(mPreviewRequestBuilder); - switch (mode) { + switch (newMode) { case auto: refreshPreviewCaptureSession( - null, (code, message) -> result.error("setFocusMode", message, null)); + null, (code, message) -> result.error("setFocusMode", message, null)); break; case locked: - lockAutoFocus( - new CaptureCallback() { - @Override - public void onCaptureCompleted( - @NonNull CameraCaptureSession session, - @NonNull CaptureRequest request, - @NonNull TotalCaptureResult result) { - unlockAutoFocus(); - } - }); + startAutoFocus(); break; } result.success(null); } public void setFocusPoint(@NonNull final Result result, Double x, Double y) - throws CameraAccessException { + throws CameraAccessException { // Check if focus point functionality is available. if (!isFocusPointSupported()) { result.error("setFocusPointFailed", "Device does not have focus point capabilities", null); @@ -852,20 +1041,21 @@ public void setFocusPoint(@NonNull final Result result, Double x, Double y) } // Apply the new metering rectangle - setFocusMode(result, focusMode); + setFocusMode(result, currentFocusMode); } @TargetApi(VERSION_CODES.P) private boolean supportsDistortionCorrection() throws CameraAccessException { int[] availableDistortionCorrectionModes = - cameraManager - .getCameraCharacteristics(cameraDevice.getId()) - .get(CameraCharacteristics.DISTORTION_CORRECTION_AVAILABLE_MODES); - if (availableDistortionCorrectionModes == null) availableDistortionCorrectionModes = new int[0]; + cameraManager + .getCameraCharacteristics(cameraDevice.getId()) + .get(CameraCharacteristics.DISTORTION_CORRECTION_AVAILABLE_MODES); + if (availableDistortionCorrectionModes == null) + availableDistortionCorrectionModes = new int[0]; long nonOffModesSupported = - Arrays.stream(availableDistortionCorrectionModes) - .filter((value) -> value != CaptureRequest.DISTORTION_CORRECTION_MODE_OFF) - .count(); + Arrays.stream(availableDistortionCorrectionModes) + .filter((value) -> value != CaptureRequest.DISTORTION_CORRECTION_MODE_OFF) + .count(); return nonOffModesSupported > 0; } @@ -873,50 +1063,50 @@ private Size getRegionBoundaries() throws CameraAccessException { // No distortion correction support if (android.os.Build.VERSION.SDK_INT < VERSION_CODES.P || !supportsDistortionCorrection()) { return cameraManager - .getCameraCharacteristics(cameraDevice.getId()) - .get(CameraCharacteristics.SENSOR_INFO_PIXEL_ARRAY_SIZE); + .getCameraCharacteristics(cameraDevice.getId()) + .get(CameraCharacteristics.SENSOR_INFO_PIXEL_ARRAY_SIZE); } // Get the current distortion correction mode Integer distortionCorrectionMode = - captureRequestBuilder.get(CaptureRequest.DISTORTION_CORRECTION_MODE); + mPreviewRequestBuilder.get(CaptureRequest.DISTORTION_CORRECTION_MODE); // Return the correct boundaries depending on the mode android.graphics.Rect rect; if (distortionCorrectionMode == null - || distortionCorrectionMode == CaptureRequest.DISTORTION_CORRECTION_MODE_OFF) { + || distortionCorrectionMode == CaptureRequest.DISTORTION_CORRECTION_MODE_OFF) { rect = - cameraManager - .getCameraCharacteristics(cameraDevice.getId()) - .get(CameraCharacteristics.SENSOR_INFO_PRE_CORRECTION_ACTIVE_ARRAY_SIZE); + cameraManager + .getCameraCharacteristics(cameraDevice.getId()) + .get(CameraCharacteristics.SENSOR_INFO_PRE_CORRECTION_ACTIVE_ARRAY_SIZE); } else { rect = - cameraManager - .getCameraCharacteristics(cameraDevice.getId()) - .get(CameraCharacteristics.SENSOR_INFO_ACTIVE_ARRAY_SIZE); + cameraManager + .getCameraCharacteristics(cameraDevice.getId()) + .get(CameraCharacteristics.SENSOR_INFO_ACTIVE_ARRAY_SIZE); } return rect == null ? null : new Size(rect.width(), rect.height()); } private boolean isExposurePointSupported() throws CameraAccessException { Integer supportedRegions = - cameraManager - .getCameraCharacteristics(cameraDevice.getId()) - .get(CameraCharacteristics.CONTROL_MAX_REGIONS_AE); + cameraManager + .getCameraCharacteristics(cameraDevice.getId()) + .get(CameraCharacteristics.CONTROL_MAX_REGIONS_AE); return supportedRegions != null && supportedRegions > 0; } private boolean isFocusPointSupported() throws CameraAccessException { Integer supportedRegions = - cameraManager - .getCameraCharacteristics(cameraDevice.getId()) - .get(CameraCharacteristics.CONTROL_MAX_REGIONS_AF); + cameraManager + .getCameraCharacteristics(cameraDevice.getId()) + .get(CameraCharacteristics.CONTROL_MAX_REGIONS_AF); return supportedRegions != null && supportedRegions > 0; } public double getMinExposureOffset() throws CameraAccessException { Range range = - cameraManager - .getCameraCharacteristics(cameraDevice.getId()) - .get(CameraCharacteristics.CONTROL_AE_COMPENSATION_RANGE); + cameraManager + .getCameraCharacteristics(cameraDevice.getId()) + .get(CameraCharacteristics.CONTROL_AE_COMPENSATION_RANGE); double minStepped = range == null ? 0 : range.getLower(); double stepSize = getExposureOffsetStepSize(); return minStepped * stepSize; @@ -924,9 +1114,9 @@ public double getMinExposureOffset() throws CameraAccessException { public double getMaxExposureOffset() throws CameraAccessException { Range range = - cameraManager - .getCameraCharacteristics(cameraDevice.getId()) - .get(CameraCharacteristics.CONTROL_AE_COMPENSATION_RANGE); + cameraManager + .getCameraCharacteristics(cameraDevice.getId()) + .get(CameraCharacteristics.CONTROL_AE_COMPENSATION_RANGE); double maxStepped = range == null ? 0 : range.getUpper(); double stepSize = getExposureOffsetStepSize(); return maxStepped * stepSize; @@ -934,20 +1124,20 @@ public double getMaxExposureOffset() throws CameraAccessException { public double getExposureOffsetStepSize() throws CameraAccessException { Rational stepSize = - cameraManager - .getCameraCharacteristics(cameraDevice.getId()) - .get(CameraCharacteristics.CONTROL_AE_COMPENSATION_STEP); + cameraManager + .getCameraCharacteristics(cameraDevice.getId()) + .get(CameraCharacteristics.CONTROL_AE_COMPENSATION_STEP); return stepSize == null ? 0.0 : stepSize.doubleValue(); } public void setExposureOffset(@NonNull final Result result, double offset) - throws CameraAccessException { + throws CameraAccessException { // Set the exposure offset double stepSize = getExposureOffsetStepSize(); exposureOffset = (int) (offset / stepSize); // Apply it updateExposure(exposureMode); - this.cameraCaptureSession.setRepeatingRequest(captureRequestBuilder.build(), null, null); + this.mPreviewSession.setRepeatingRequest(mPreviewRequestBuilder.build(), null, null); result.success(offset); } @@ -965,20 +1155,20 @@ public void setZoomLevel(@NonNull final Result result, float zoom) throws Camera if (zoom > maxZoom || zoom < minZoom) { String errorMessage = - String.format( - Locale.ENGLISH, - "Zoom level out of bounds (zoom level should be between %f and %f).", - minZoom, - maxZoom); + String.format( + Locale.ENGLISH, + "Zoom level out of bounds (zoom level should be between %f and %f).", + minZoom, + maxZoom); result.error("ZOOM_ERROR", errorMessage, null); return; } //Zoom area is calculated relative to sensor area (activeRect) - if (captureRequestBuilder != null) { + if (mPreviewRequestBuilder != null) { final Rect computedZoom = cameraZoom.computeZoom(zoom); - captureRequestBuilder.set(CaptureRequest.SCALER_CROP_REGION, computedZoom); - cameraCaptureSession.setRepeatingRequest(captureRequestBuilder.build(), null, null); + mPreviewRequestBuilder.set(CaptureRequest.SCALER_CROP_REGION, computedZoom); + mPreviewSession.setRepeatingRequest(mPreviewRequestBuilder.build(), null, null); } result.success(null); @@ -992,47 +1182,50 @@ public void unlockCaptureOrientation() { this.lockedCaptureOrientation = null; } + /** + * Set current fps range setting to the current preview request builder + */ private void updateFpsRange() { if (fpsRange == null) { return; } - captureRequestBuilder.set(CaptureRequest.CONTROL_AE_TARGET_FPS_RANGE, fpsRange); + mPreviewRequestBuilder.set(CaptureRequest.CONTROL_AE_TARGET_FPS_RANGE, fpsRange); } - private void updateFocus(FocusMode mode) { + private void updateFocus(CaptureRequest.Builder requestBuilder) { if (useAutoFocus) { int[] modes = cameraCharacteristics.get(CameraCharacteristics.CONTROL_AF_AVAILABLE_MODES); // Auto focus is not supported if (modes == null - || modes.length == 0 - || (modes.length == 1 && modes[0] == CameraCharacteristics.CONTROL_AF_MODE_OFF)) { + || modes.length == 0 + || (modes.length == 1 && modes[0] == CameraCharacteristics.CONTROL_AF_MODE_OFF)) { useAutoFocus = false; - captureRequestBuilder.set( - CaptureRequest.CONTROL_AF_MODE, CaptureRequest.CONTROL_AF_MODE_OFF); + mPreviewRequestBuilder.set( + CaptureRequest.CONTROL_AF_MODE, CaptureRequest.CONTROL_AF_MODE_OFF); } else { // Applying auto focus - switch (mode) { + switch (currentFocusMode) { case locked: - captureRequestBuilder.set( - CaptureRequest.CONTROL_AF_MODE, CaptureRequest.CONTROL_AF_MODE_AUTO); + requestBuilder.set( + CaptureRequest.CONTROL_AF_MODE, CaptureRequest.CONTROL_AF_MODE_AUTO); break; case auto: - captureRequestBuilder.set( - CaptureRequest.CONTROL_AF_MODE, - recordingVideo - ? CaptureRequest.CONTROL_AF_MODE_CONTINUOUS_VIDEO - : CaptureRequest.CONTROL_AF_MODE_CONTINUOUS_PICTURE); + mPreviewRequestBuilder.set( + CaptureRequest.CONTROL_AF_MODE, + recordingVideo + ? CaptureRequest.CONTROL_AF_MODE_CONTINUOUS_VIDEO + : CaptureRequest.CONTROL_AF_MODE_CONTINUOUS_PICTURE); default: break; } MeteringRectangle afRect = cameraRegions.getAFMeteringRectangle(); - captureRequestBuilder.set( - CaptureRequest.CONTROL_AF_REGIONS, - afRect == null ? null : new MeteringRectangle[] {afRect}); + mPreviewRequestBuilder.set( + CaptureRequest.CONTROL_AF_REGIONS, + afRect == null ? null : new MeteringRectangle[]{afRect}); } } else { - captureRequestBuilder.set(CaptureRequest.CONTROL_AF_MODE, CaptureRequest.CONTROL_AF_MODE_OFF); + mPreviewRequestBuilder.set(CaptureRequest.CONTROL_AF_MODE, CaptureRequest.CONTROL_AF_MODE_OFF); } } @@ -1041,51 +1234,21 @@ private void updateExposure(ExposureMode mode) { // Applying auto exposure MeteringRectangle aeRect = cameraRegions.getAEMeteringRectangle(); - captureRequestBuilder.set( - CaptureRequest.CONTROL_AE_REGIONS, - aeRect == null ? null : new MeteringRectangle[] {cameraRegions.getAEMeteringRectangle()}); + mPreviewRequestBuilder.set( + CaptureRequest.CONTROL_AE_REGIONS, + aeRect == null ? null : new MeteringRectangle[]{cameraRegions.getAEMeteringRectangle()}); switch (mode) { case locked: - captureRequestBuilder.set(CaptureRequest.CONTROL_AE_LOCK, true); + mPreviewRequestBuilder.set(CaptureRequest.CONTROL_AE_LOCK, true); break; case auto: default: - captureRequestBuilder.set(CaptureRequest.CONTROL_AE_LOCK, false); + mPreviewRequestBuilder.set(CaptureRequest.CONTROL_AE_LOCK, false); break; } - captureRequestBuilder.set(CaptureRequest.CONTROL_AE_EXPOSURE_COMPENSATION, exposureOffset); - } - - private void updateFlash(FlashMode mode) { - // Get flash - flashMode = mode; - - // Applying flash modes - switch (flashMode) { - case off: - captureRequestBuilder.set( - CaptureRequest.CONTROL_AE_MODE, CaptureRequest.CONTROL_AE_MODE_ON); - captureRequestBuilder.set(CaptureRequest.FLASH_MODE, CaptureRequest.FLASH_MODE_OFF); - break; - case auto: - captureRequestBuilder.set( - CaptureRequest.CONTROL_AE_MODE, CaptureRequest.CONTROL_AE_MODE_ON_AUTO_FLASH); - captureRequestBuilder.set(CaptureRequest.FLASH_MODE, CaptureRequest.FLASH_MODE_OFF); - break; - case always: - captureRequestBuilder.set( - CaptureRequest.CONTROL_AE_MODE, CaptureRequest.CONTROL_AE_MODE_ON_ALWAYS_FLASH); - captureRequestBuilder.set(CaptureRequest.FLASH_MODE, CaptureRequest.FLASH_MODE_OFF); - break; - case torch: - default: - captureRequestBuilder.set( - CaptureRequest.CONTROL_AE_MODE, CaptureRequest.CONTROL_AE_MODE_ON); - captureRequestBuilder.set(CaptureRequest.FLASH_MODE, CaptureRequest.FLASH_MODE_TORCH); - break; - } + mPreviewRequestBuilder.set(CaptureRequest.CONTROL_AE_EXPOSURE_COMPENSATION, exposureOffset); } public void startPreview() throws CameraAccessException { @@ -1095,54 +1258,54 @@ public void startPreview() throws CameraAccessException { } public void startPreviewWithImageStream(EventChannel imageStreamChannel) - throws CameraAccessException { + throws CameraAccessException { createCaptureSession(CameraDevice.TEMPLATE_RECORD, imageStreamReader.getSurface()); imageStreamChannel.setStreamHandler( - new EventChannel.StreamHandler() { - @Override - public void onListen(Object o, EventChannel.EventSink imageStreamSink) { - setImageStreamImageAvailableListener(imageStreamSink); - } + new EventChannel.StreamHandler() { + @Override + public void onListen(Object o, EventChannel.EventSink imageStreamSink) { + setImageStreamImageAvailableListener(imageStreamSink); + } - @Override - public void onCancel(Object o) { - imageStreamReader.setOnImageAvailableListener(null, null); - } - }); + @Override + public void onCancel(Object o) { + imageStreamReader.setOnImageAvailableListener(null, null); + } + }); } private void setImageStreamImageAvailableListener(final EventChannel.EventSink imageStreamSink) { imageStreamReader.setOnImageAvailableListener( - reader -> { - Image img = reader.acquireLatestImage(); - if (img == null) return; + reader -> { + Image img = reader.acquireLatestImage(); + if (img == null) return; - List> planes = new ArrayList<>(); - for (Image.Plane plane : img.getPlanes()) { - ByteBuffer buffer = plane.getBuffer(); + List> planes = new ArrayList<>(); + for (Image.Plane plane : img.getPlanes()) { + ByteBuffer buffer = plane.getBuffer(); - byte[] bytes = new byte[buffer.remaining()]; - buffer.get(bytes, 0, bytes.length); + byte[] bytes = new byte[buffer.remaining()]; + buffer.get(bytes, 0, bytes.length); - Map planeBuffer = new HashMap<>(); - planeBuffer.put("bytesPerRow", plane.getRowStride()); - planeBuffer.put("bytesPerPixel", plane.getPixelStride()); - planeBuffer.put("bytes", bytes); + Map planeBuffer = new HashMap<>(); + planeBuffer.put("bytesPerRow", plane.getRowStride()); + planeBuffer.put("bytesPerPixel", plane.getPixelStride()); + planeBuffer.put("bytes", bytes); - planes.add(planeBuffer); - } + planes.add(planeBuffer); + } - Map imageBuffer = new HashMap<>(); - imageBuffer.put("width", img.getWidth()); - imageBuffer.put("height", img.getHeight()); - imageBuffer.put("format", img.getFormat()); - imageBuffer.put("planes", planes); + Map imageBuffer = new HashMap<>(); + imageBuffer.put("width", img.getWidth()); + imageBuffer.put("height", img.getHeight()); + imageBuffer.put("format", img.getFormat()); + imageBuffer.put("planes", planes); - imageStreamSink.success(imageBuffer); - img.close(); - }, - null); + imageStreamSink.success(imageBuffer); + img.close(); + }, + null); } public void stopImageStream() throws CameraAccessException { @@ -1152,7 +1315,9 @@ public void stopImageStream() throws CameraAccessException { startPreview(); } - /** Sets the time the pre-capture sequence started. */ + /** + * Sets the time the pre-capture sequence started. + */ private void setPreCaptureStartTime() { preCaptureStartTime = SystemClock.elapsedRealtime(); } @@ -1167,9 +1332,9 @@ private boolean hitPreCaptureTimeout() { } private void closeCaptureSession() { - if (cameraCaptureSession != null) { - cameraCaptureSession.close(); - cameraCaptureSession = null; + if (mPreviewSession != null) { + mPreviewSession.close(); + mPreviewSession = null; } } diff --git a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/CameraConstants.java b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/CameraConstants.java new file mode 100644 index 000000000000..ad71203da028 --- /dev/null +++ b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/CameraConstants.java @@ -0,0 +1,26 @@ +package io.flutter.plugins.camera; + +public class CameraConstants { + + public static final AspectRatio DEFAULT_ASPECT_RATIO = AspectRatio.of(16, 9); + + public static final long AUTO_FOCUS_TIMEOUT_MS = 800; //800ms timeout, Under normal circumstances need to a few hundred milliseconds + + public static final long OPEN_CAMERA_TIMEOUT_MS = 2500; //2.5s + + public static final int FOCUS_HOLD_MILLIS = 3000; + + public static final float METERING_REGION_FRACTION = 0.1225f; + + public static final int ZOOM_REGION_DEFAULT = 1; + + public static final int FLASH_OFF = 0; + public static final int FLASH_ON = 1; + public static final int FLASH_TORCH = 2; + public static final int FLASH_AUTO = 3; + public static final int FLASH_RED_EYE = 4; + + public static final int FACING_BACK = 0; + public static final int FACING_FRONT = 1; + +} diff --git a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/CaptureSessionState.java b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/CaptureSessionState.java new file mode 100644 index 000000000000..9853c706ca34 --- /dev/null +++ b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/CaptureSessionState.java @@ -0,0 +1,11 @@ +package io.flutter.plugins.camera; + +public enum CaptureSessionState { + IDLE, // Idle + FOCUSING, // Focusing + STATE_WAITING_PRECAPTURE, // Precapture + STATE_WAITING_NON_PRECAPTURE, // Waiting precapture ready + CAPTURING, // Capturing + FINISHED, // Finished + ERROR, // Error +} diff --git a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/ImageSaver.java b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/ImageSaver.java new file mode 100644 index 000000000000..12d2accce199 --- /dev/null +++ b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/ImageSaver.java @@ -0,0 +1,78 @@ +package io.flutter.plugins.camera; + +import android.media.Image; +import android.media.ImageReader; +import android.os.Handler; +import android.os.Looper; + +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.nio.ByteBuffer; + +/** + * Saves a JPEG {@link Image} into the specified {@link File}. + */ +public class ImageSaver implements Runnable { + + /** + * The JPEG image + */ + private final Image mImage; + /** + * The file we save the image into. + */ + private final File mFile; + + /** + * + * Used to finish the picture capture request + */ + private PictureCaptureRequest mPictureCaptureRequest; + + ImageSaver(Image image, File file, PictureCaptureRequest pictureCaptureRequest) { + mImage = image; + mFile = file; + mPictureCaptureRequest = pictureCaptureRequest; + } + + @Override + public void run() { + final Handler handler = new Handler(Looper.getMainLooper()); + ByteBuffer buffer = mImage.getPlanes()[0].getBuffer(); + byte[] bytes = new byte[buffer.remaining()]; + buffer.get(bytes); + FileOutputStream output = null; + try { + output = new FileOutputStream(mFile); + output.write(bytes); + + handler.post(new Runnable() { + @Override + public void run() { + mPictureCaptureRequest.finish(mFile.getAbsolutePath()); + } + }); + + } catch (IOException e) { + mPictureCaptureRequest.error("IOError", "Failed saving image", null); + } finally { + mImage.close(); + if (null != output) { + try { + output.close(); + } catch (IOException e) { + handler.post(new Runnable() { + @Override + public void run() { + mPictureCaptureRequest.error("cameraAccess", e.getMessage(), null); + } + }); + + + } + } + } + } + +} \ No newline at end of file diff --git a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/PictureCaptureRequest.java b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/PictureCaptureRequest.java index 396f782a2a08..99301f961a6b 100644 --- a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/PictureCaptureRequest.java +++ b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/PictureCaptureRequest.java @@ -6,21 +6,16 @@ import android.os.Handler; import android.os.Looper; + import androidx.annotation.Nullable; + +import java.io.File; + +import io.flutter.Log; import io.flutter.plugin.common.MethodChannel; class PictureCaptureRequest { - enum State { - idle, - focusing, - preCapture, - waitingPreCaptureReady, - capturing, - finished, - error, - } - private final Runnable timeoutCallback = new Runnable() { @Override @@ -31,7 +26,12 @@ public void run() { private final MethodChannel.Result result; private final TimeoutHandler timeoutHandler; - private State state; + private CaptureSessionState state = CaptureSessionState.IDLE; + + /** + * This is the output file for our picture. + */ + File mFile; public PictureCaptureRequest(MethodChannel.Result result) { this(result, new TimeoutHandler()); @@ -39,33 +39,33 @@ public PictureCaptureRequest(MethodChannel.Result result) { public PictureCaptureRequest(MethodChannel.Result result, TimeoutHandler timeoutHandler) { this.result = result; - this.state = State.idle; this.timeoutHandler = timeoutHandler; } - public void setState(State state) { + public void setState(CaptureSessionState state) { if (isFinished()) throw new IllegalStateException("Request has already been finished"); this.state = state; - if (state != State.idle && state != State.finished && state != State.error) { + if (state != CaptureSessionState.IDLE && state != CaptureSessionState.FINISHED && state != CaptureSessionState.ERROR) { this.timeoutHandler.resetTimeout(timeoutCallback); } else { this.timeoutHandler.clearTimeout(timeoutCallback); } } - public State getState() { + public CaptureSessionState getState() { return state; } public boolean isFinished() { - return state == State.finished || state == State.error; + return state == CaptureSessionState.FINISHED || state == CaptureSessionState.ERROR; } public void finish(String absolutePath) { if (isFinished()) throw new IllegalStateException("Request has already been finished"); + Log.i("Camera", "PictureCaptureRequest finish"); this.timeoutHandler.clearTimeout(timeoutCallback); result.success(absolutePath); - state = State.finished; + setState(CaptureSessionState.FINISHED); } public void error( @@ -73,7 +73,7 @@ public void error( if (isFinished()) throw new IllegalStateException("Request has already been finished"); this.timeoutHandler.clearTimeout(timeoutCallback); result.error(errorCode, errorMessage, errorDetails); - state = State.error; + setState(CaptureSessionState.ERROR); } static class TimeoutHandler { diff --git a/packages/camera/camera/example/android/gradle.properties b/packages/camera/camera/example/android/gradle.properties index a6738207fd15..7ab5b529f76c 100644 --- a/packages/camera/camera/example/android/gradle.properties +++ b/packages/camera/camera/example/android/gradle.properties @@ -1,4 +1,17 @@ -org.gradle.jvmargs=-Xmx1536M -android.useAndroidX=true +## For more details on how to configure your build environment visit +# http://www.gradle.org/docs/current/userguide/build_environment.html +# +# Specifies the JVM arguments used for the daemon process. +# The setting is particularly useful for tweaking memory settings. +# Default value: -Xmx1024m -XX:MaxPermSize=256m +# org.gradle.jvmargs=-Xmx2048m -XX:MaxPermSize=512m -XX:+HeapDumpOnOutOfMemoryError -Dfile.encoding=UTF-8 +# +# When configured, Gradle will run in incubating parallel mode. +# This option should only be used with decoupled projects. More details, visit +# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects +# org.gradle.parallel=true +#Sun Feb 28 11:56:11 EST 2021 android.enableJetifier=true android.enableR8=true +android.useAndroidX=true +org.gradle.jvmargs=-Xmx2048M -Dkotlin.daemon.jvm.options\="-Xmx2048M" From 2c70c07502f3b1f1b7767b20ccc5ae70946fd739 Mon Sep 17 00:00:00 2001 From: Andrew Coutts Date: Mon, 1 Mar 2021 05:09:47 -0500 Subject: [PATCH 031/114] Tweaks --- .../io/flutter/plugins/camera/Camera.java | 149 ++++++----- .../flutter/plugins/camera/CameraState.java | 34 +++ .../plugins/camera/CaptureSessionState.java | 11 - .../plugins/camera/PictureCaptureRequest.java | 231 ++++++++++++------ .../camera/PictureCaptureRequestState.java | 33 +++ .../plugins/camera/types/FlashMode.java | 12 +- 6 files changed, 331 insertions(+), 139 deletions(-) create mode 100644 packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/CameraState.java delete mode 100644 packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/CaptureSessionState.java create mode 100644 packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/PictureCaptureRequestState.java 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 ea879f4e2bf1..debd0d77fff8 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 @@ -47,6 +47,7 @@ import java.io.File; import java.io.IOException; +import java.lang.reflect.Field; import java.nio.ByteBuffer; import java.util.ArrayList; import java.util.Arrays; @@ -76,10 +77,7 @@ interface ErrorCallback { public class Camera { private static final String TAG = "Camera"; - /** - * Timeout for the pre-capture sequence. - */ - private static final long PRECAPTURE_TIMEOUT_MS = 1000; + /** * Conversion from screen rotation to JPEG orientation. */ @@ -116,18 +114,21 @@ public class Camera { private final CameraCharacteristics cameraCharacteristics; private final Activity activity; - /** - * Whether the current camera device supports Flash or not. - */ - private final boolean mFlashSupported; +/** + * The state of the camera. By default we are in the preview state. + */ + private CameraState cameraState = CameraState.STATE_PREVIEW; + /** * A {@link Handler} for running tasks in the background. */ private Handler mBackgroundHandler; + /** * An additional thread for running tasks that shouldn't block the UI. */ private HandlerThread mBackgroundThread; + private CameraDevice cameraDevice; private CameraCaptureSession mPreviewSession; private ImageReader pictureImageReader; @@ -143,35 +144,48 @@ public class Camera { */ private CaptureRequest mPreviewRequest; + private MediaRecorder mediaRecorder; + private boolean recordingVideo; + private File videoRecordingFile; + /** - * The current camera auto focus mode + * Flash mode setting of the current camera. Initialize to off because + * we don't know if the current camera supports flash yet. */ - private boolean mAutoFocus = true; + private FlashMode currentFlashMode = FlashMode.off; /** - * Whether the current camera device supports auto focus or not. + * Exposure mode setting of the current camera. Initialize to auto + * because all cameras support autoexposure by default. */ - private boolean mAutoFocusSupported = true; + private ExposureMode exposureMode = ExposureMode.auto; - private MediaRecorder mediaRecorder; - private boolean recordingVideo; - private File videoRecordingFile; + /** + * Focus mode setting of the current camera. Initialize to locked because + * we don't know if the current camera supports autofocus yet. + */ + private FocusMode currentFocusMode = FocusMode.locked; /** - * Flash mode setting of the current camera + * Whether the current camera device supports auto focus or not. */ - private FlashMode currentFlashMode; + private boolean mAutoFocusSupported = false; /** - * Exposure mode setting of the current camera. + * Whether or not to use autofocus. */ - private ExposureMode exposureMode; + private boolean useAutoFocus = false; /** - * Focus mode setting of the current camera. + * Whether the current camera device supports Flash or not. */ - private FocusMode currentFocusMode; - private PictureCaptureRequest pictureCaptureRequest; + private boolean mFlashSupported = false; + + /** + * This manages the state of the camera and the current capture request. + */ + PictureCaptureRequest pictureCaptureRequest; + /** * This a callback object for the {@link ImageReader}. "onImageAvailable" will be called when a * still image is ready to be saved. @@ -188,10 +202,10 @@ public void onImageAvailable(ImageReader reader) { }; private CameraRegions cameraRegions; private int exposureOffset; - private boolean useAutoFocus = true; + private Range fpsRange; private PlatformChannel.DeviceOrientation lockedCaptureOrientation; - private long preCaptureStartTime; + public Camera( @@ -202,6 +216,8 @@ public Camera( final String resolutionPreset, final boolean enableAudio) throws CameraAccessException { + Log.i(TAG, "Camear constructor"); + if (activity == null) { throw new IllegalStateException("No activity available!"); } @@ -248,6 +264,8 @@ public Camera( * @param cameraCharacteristics */ private void getAvailableFpsRange(CameraCharacteristics cameraCharacteristics) { + Log.i(TAG, "getAvailableFpsRange"); + try { Range[] ranges = cameraCharacteristics.get(CameraCharacteristics.CONTROL_AE_AVAILABLE_TARGET_FPS_RANGES); @@ -269,6 +287,8 @@ private void getAvailableFpsRange(CameraCharacteristics cameraCharacteristics) { } private void prepareMediaRecorder(String outputFilePath) throws IOException { + Log.i(TAG, "prepareMediaRecorder"); + if (mediaRecorder != null) { mediaRecorder.release(); } @@ -304,6 +324,9 @@ public void open(String imageFormatGroup) throws CameraAccessException { new CameraDevice.StateCallback() { @Override public void onOpened(@NonNull CameraDevice device) { + Log.i(TAG, "open | onOpened"); + + cameraDevice = device; try { startPreview(); @@ -322,18 +345,24 @@ public void onOpened(@NonNull CameraDevice device) { @Override public void onClosed(@NonNull CameraDevice camera) { + Log.i(TAG, "open | onClosed"); + dartMessenger.sendCameraClosingEvent(); super.onClosed(camera); } @Override public void onDisconnected(@NonNull CameraDevice cameraDevice) { + Log.i(TAG, "open | onDisconnected"); + close(); dartMessenger.sendCameraErrorEvent("The camera was disconnected."); } @Override public void onError(@NonNull CameraDevice cameraDevice, int errorCode) { + Log.i(TAG, "open | onError"); + close(); String errorDescription; switch (errorCode) { @@ -369,6 +398,8 @@ private void createCaptureSession(int templateType, Surface... surfaces) private void createCaptureSession( int templateType, Runnable onSuccessCallback, Surface... surfaces) throws CameraAccessException { + Log.i(TAG, "createCaptureSession"); + // Close any existing capture session. closeCaptureSession(); @@ -469,6 +500,9 @@ private void refreshPreviewCaptureSession( mPreviewSession.setRepeatingRequest(mPreviewRequest, mCaptureCallback, mBackgroundHandler); + // Put camera back to preview mode + cameraState = CameraState.STATE_PREVIEW; + if (onSuccessCallback != null) { onSuccessCallback.run(); } @@ -480,18 +514,19 @@ private void refreshPreviewCaptureSession( public void takePicture(@NonNull final Result result) { Log.i(TAG, "takePicture | useAutoFocus: " + useAutoFocus); - // Only take 1 picture at a time + // Only take one 1 picture at a time. if (pictureCaptureRequest != null && !pictureCaptureRequest.isFinished()) { result.error("captureAlreadyActive", "Picture is currently already being captured", null); return; } - // Store the result - pictureCaptureRequest = new PictureCaptureRequest(result); // Create temporary file final File outputDir = applicationContext.getCacheDir(); try { - pictureCaptureRequest.mFile = File.createTempFile("CAP", ".jpg", outputDir); + final File file = File.createTempFile("CAP", ".jpg", outputDir); + + // Start a new capture + pictureCaptureRequest = new PictureCaptureRequest(result, file); } catch (IOException | SecurityException e) { pictureCaptureRequest.error("cannotCreateFile", e.getMessage(), null); return; @@ -518,7 +553,7 @@ private void runPrecaptureSequence() { mPreviewRequestBuilder.set(CaptureRequest.CONTROL_AE_PRECAPTURE_TRIGGER, CaptureRequest.CONTROL_AE_PRECAPTURE_TRIGGER_START); // Tell #mCaptureCallback to wait for the precapture sequence to be set. - pictureCaptureRequest.setState(CaptureSessionState.STATE_WAITING_PRECAPTURE); + cameraState = CameraState.STATE_WAITING_PRECAPTURE; mPreviewSession.capture(mPreviewRequestBuilder.build(), mCaptureCallback, mBackgroundHandler); } catch (CameraAccessException e) { @@ -675,17 +710,19 @@ private void process(CaptureResult result) { Integer aeState = result.get(CaptureResult.CONTROL_AE_STATE); Integer afState = result.get(CaptureResult.CONTROL_AF_STATE); - if (pictureCaptureRequest.getState() != CaptureSessionState.IDLE) { - Log.i(TAG, "mCaptureCallback | state: " + pictureCaptureRequest.getState() + " | afState: " + afState + " | aeState: " + aeState); + if (cameraState != CameraState.STATE_PREVIEW) { + Log.i(TAG, "mCaptureCallback | state: " + cameraState + " | afState: " + afState + " | aeState: " + aeState); } - switch (pictureCaptureRequest.getState()) { - case IDLE: { + switch (cameraState) { + case STATE_PREVIEW: { // We have nothing to do when the camera preview is working normally. break; } - case FOCUSING: { + + case STATE_WAITING_FOCUS: { if (afState == null) { + cameraState = CameraState.STATE_CAPTURING; captureStillPicture(); } else if (afState == CaptureResult.CONTROL_AF_STATE_FOCUSED_LOCKED || afState == CaptureResult.CONTROL_AF_STATE_NOT_FOCUSED_LOCKED) { @@ -693,8 +730,7 @@ private void process(CaptureResult result) { if (aeState == null || aeState == CaptureResult.CONTROL_AE_STATE_CONVERGED) { - pictureCaptureRequest.setState(CaptureSessionState.CAPTURING); - + cameraState = CameraState.STATE_CAPTURING; captureStillPicture(); } else { runPrecaptureSequence(); @@ -702,23 +738,26 @@ private void process(CaptureResult result) { } break; } + case STATE_WAITING_PRECAPTURE: { // CONTROL_AE_STATE can be null on some devices if (aeState == null || + aeState == CaptureResult.CONTROL_AE_STATE_CONVERGED || aeState == CaptureResult.CONTROL_AE_STATE_PRECAPTURE || - aeState == CaptureRequest.CONTROL_AE_STATE_FLASH_REQUIRED) { - pictureCaptureRequest.setState(CaptureSessionState.STATE_WAITING_NON_PRECAPTURE); - setPreCaptureStartTime(); + aeState == CaptureResult.CONTROL_AE_STATE_FLASH_REQUIRED) { + cameraState = CameraState.STATE_WAITING_PRECAPTURE_READY; + pictureCaptureRequest.setPreCaptureStartTime(); } break; } - case STATE_WAITING_NON_PRECAPTURE: { + + case STATE_WAITING_PRECAPTURE_READY: { // CONTROL_AE_STATE can be null on some devices if (aeState == null || aeState != CaptureResult.CONTROL_AE_STATE_PRECAPTURE) { - pictureCaptureRequest.setState(CaptureSessionState.CAPTURING); + cameraState = CameraState.STATE_CAPTURING; captureStillPicture(); } else { - if (hitPreCaptureTimeout()) { + if (pictureCaptureRequest.hitPreCaptureTimeout()) { unlockAutoFocus(); } } @@ -765,7 +804,7 @@ private void runPictureAutoFocus() { Log.i(TAG, "runPictureAutoFocus"); assert (pictureCaptureRequest != null); - pictureCaptureRequest.setState(CaptureSessionState.FOCUSING); + cameraState = CameraState.STATE_WAITING_FOCUS; lockAutoFocus(); } @@ -1253,6 +1292,7 @@ private void updateExposure(ExposureMode mode) { public void startPreview() throws CameraAccessException { if (pictureImageReader == null || pictureImageReader.getSurface() == null) return; + Log.i(TAG, "startPreview"); createCaptureSession(CameraDevice.TEMPLATE_PREVIEW, pictureImageReader.getSurface()); } @@ -1260,6 +1300,7 @@ public void startPreview() throws CameraAccessException { public void startPreviewWithImageStream(EventChannel imageStreamChannel) throws CameraAccessException { createCaptureSession(CameraDevice.TEMPLATE_RECORD, imageStreamReader.getSurface()); + Log.i(TAG, "startPreviewWithImageStream"); imageStreamChannel.setStreamHandler( new EventChannel.StreamHandler() { @@ -1315,30 +1356,22 @@ public void stopImageStream() throws CameraAccessException { startPreview(); } - /** - * Sets the time the pre-capture sequence started. - */ - private void setPreCaptureStartTime() { - preCaptureStartTime = SystemClock.elapsedRealtime(); - } - /** - * Check if the timeout for the pre-capture sequence has been reached. - * - * @return true if the timeout is reached; otherwise false is returned. - */ - private boolean hitPreCaptureTimeout() { - return (SystemClock.elapsedRealtime() - preCaptureStartTime) > PRECAPTURE_TIMEOUT_MS; - } + + private void closeCaptureSession() { if (mPreviewSession != null) { + Log.i(TAG, "closeCaptureSession"); + mPreviewSession.close(); mPreviewSession = null; } } public void close() { + Log.i(TAG, "close"); + closeCaptureSession(); if (cameraDevice != null) { @@ -1361,6 +1394,8 @@ public void close() { } public void dispose() { + Log.i(TAG, "dispose"); + close(); flutterTexture.release(); deviceOrientationListener.stop(); diff --git a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/CameraState.java b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/CameraState.java new file mode 100644 index 000000000000..fca1e0099e40 --- /dev/null +++ b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/CameraState.java @@ -0,0 +1,34 @@ +package io.flutter.plugins.camera; + + +/** + * These are the states that the camera can be in. The camera can only take one photo at a time + * so this state describes the state of the camera itself. The camera works like a pipeline where + * we feed it requests through. It can only process one tasks at a time. + */ +public enum CameraState { + /** + * Idle, showing preview and not capturing anything. + */ + STATE_PREVIEW, + + /** + * Starting and waiting for autofocus to complete. + */ + STATE_WAITING_FOCUS, + + /** + * Start performing autoexposure. + */ + STATE_WAITING_PRECAPTURE, + + /** + * waiting for autoexposure to complete. + */ + STATE_WAITING_PRECAPTURE_READY, + + /** + * Capturing an image. + */ + STATE_CAPTURING, +} diff --git a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/CaptureSessionState.java b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/CaptureSessionState.java deleted file mode 100644 index 9853c706ca34..000000000000 --- a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/CaptureSessionState.java +++ /dev/null @@ -1,11 +0,0 @@ -package io.flutter.plugins.camera; - -public enum CaptureSessionState { - IDLE, // Idle - FOCUSING, // Focusing - STATE_WAITING_PRECAPTURE, // Precapture - STATE_WAITING_NON_PRECAPTURE, // Waiting precapture ready - CAPTURING, // Capturing - FINISHED, // Finished - ERROR, // Error -} diff --git a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/PictureCaptureRequest.java b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/PictureCaptureRequest.java index 99301f961a6b..de60c68d37eb 100644 --- a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/PictureCaptureRequest.java +++ b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/PictureCaptureRequest.java @@ -6,6 +6,7 @@ import android.os.Handler; import android.os.Looper; +import android.os.SystemClock; import androidx.annotation.Nullable; @@ -14,83 +15,173 @@ import io.flutter.Log; import io.flutter.plugin.common.MethodChannel; +/** + * This is where we store the state of the camera. This conveniently + * allows us to handle capture results and send results back to flutter + * so we can handle errors. + *

+ * It also handles a capture timeout so if a capture doesn't happen within + * 5 seconds it will return an error to dart. + */ class PictureCaptureRequest { - private final Runnable timeoutCallback = - new Runnable() { - @Override - public void run() { - error("captureTimeout", "Picture capture request timed out", state.toString()); + /** + * Timeout for the pre-capture sequence. + */ + private static final long PRECAPTURE_TIMEOUT_MS = 1000; + + /** + * This is the output file for the curent capture. The file is created + * in Camera and passed here as reference to it. + */ + final File mFile; + + /** + * Dart method chanel result. + */ + private final MethodChannel.Result result; + + /** + * Timeout handler. + */ + private final TimeoutHandler timeoutHandler; + + /** + * The time that the most recent capture started at. Used to check if + * the current capture request has timed out. + */ + private long preCaptureStartTime; + + /** + * The state of this picture capture request. + */ + private PictureCaptureRequestState state = PictureCaptureRequestState.STATE_IDLE; + private final Runnable timeoutCallback = + () -> error("captureTimeout", "Picture capture request timed out", state.toString()); + + + /** + * Constructor to create a picture capture request. + * + * @param result + * @param mFile + */ + public PictureCaptureRequest(MethodChannel.Result result, File mFile) { + this.result = result; + this.timeoutHandler = new TimeoutHandler(); + this.mFile = mFile; + } + + /** + * Return the current state of this picture capture request. + * + * @return + */ + public PictureCaptureRequestState getState() { + return state; + } + + /** + * Set the picture capture request to a new state. + * + * @param newState + */ + public void setState(PictureCaptureRequestState newState) { + Log.i("Camera", "PictureCaptureRequest setState: " + newState); + + // Once a request is finished, that's it for its lifecycle. + if (state == PictureCaptureRequestState.STATE_FINISHED) { + throw new IllegalStateException("Request has already been finished"); } - }; - - private final MethodChannel.Result result; - private final TimeoutHandler timeoutHandler; - private CaptureSessionState state = CaptureSessionState.IDLE; - - /** - * This is the output file for our picture. - */ - File mFile; - - public PictureCaptureRequest(MethodChannel.Result result) { - this(result, new TimeoutHandler()); - } - - public PictureCaptureRequest(MethodChannel.Result result, TimeoutHandler timeoutHandler) { - this.result = result; - this.timeoutHandler = timeoutHandler; - } - - public void setState(CaptureSessionState state) { - if (isFinished()) throw new IllegalStateException("Request has already been finished"); - this.state = state; - if (state != CaptureSessionState.IDLE && state != CaptureSessionState.FINISHED && state != CaptureSessionState.ERROR) { - this.timeoutHandler.resetTimeout(timeoutCallback); - } else { - this.timeoutHandler.clearTimeout(timeoutCallback); + + final PictureCaptureRequestState oldState = state; + state = newState; + onStateChange(oldState); + } + + public boolean isFinished() { + return state == PictureCaptureRequestState.STATE_FINISHED; + } + + /** + * Send the picture result back to Flutter. Returns the image path. + * + * @param absolutePath + */ + public void finish(String absolutePath) { + setState(PictureCaptureRequestState.STATE_FINISHED); + Log.i("Camera", "PictureCaptureRequest finish"); + result.success(absolutePath); + } + + public void error( + String errorCode, @Nullable String errorMessage, + @Nullable Object errorDetails) { + setState(PictureCaptureRequestState.STATE_ERROR); + result.error(errorCode, errorMessage, errorDetails); } - } - - public CaptureSessionState getState() { - return state; - } - - public boolean isFinished() { - return state == CaptureSessionState.FINISHED || state == CaptureSessionState.ERROR; - } - - public void finish(String absolutePath) { - if (isFinished()) throw new IllegalStateException("Request has already been finished"); - Log.i("Camera", "PictureCaptureRequest finish"); - this.timeoutHandler.clearTimeout(timeoutCallback); - result.success(absolutePath); - setState(CaptureSessionState.FINISHED); - } - - public void error( - String errorCode, @Nullable String errorMessage, @Nullable Object errorDetails) { - if (isFinished()) throw new IllegalStateException("Request has already been finished"); - this.timeoutHandler.clearTimeout(timeoutCallback); - result.error(errorCode, errorMessage, errorDetails); - setState(CaptureSessionState.ERROR); - } - - static class TimeoutHandler { - private static final int REQUEST_TIMEOUT = 5000; - private final Handler handler; - - TimeoutHandler() { - this.handler = new Handler(Looper.getMainLooper()); + + /** + * Check if the timeout for the pre-capture sequence has been reached. + * + * @return true if the timeout is reached; otherwise false is returned. + */ + public boolean hitPreCaptureTimeout() { + return (SystemClock.elapsedRealtime() - preCaptureStartTime) > PRECAPTURE_TIMEOUT_MS; + } + + /** + * Sets the time the pre-capture sequence started. + */ + public void setPreCaptureStartTime() { + preCaptureStartTime = SystemClock.elapsedRealtime(); } - public void resetTimeout(Runnable runnable) { - clearTimeout(runnable); - handler.postDelayed(runnable, REQUEST_TIMEOUT); + /** + * Handle new state changes. + */ + private void onStateChange(PictureCaptureRequestState oldState) { + switch (state) { + case STATE_IDLE: + // Nothing to do in idle state. + break; + + case STATE_CAPTURING: + // Started an image capture. + timeoutHandler.resetTimeout(timeoutCallback); + break; + + case STATE_FINISHED: + timeoutHandler.clearTimeout(timeoutCallback); + break; + + case STATE_ERROR: + timeoutHandler.clearTimeout(timeoutCallback); + setState(PictureCaptureRequestState.STATE_FINISHED); + break; + + } } - public void clearTimeout(Runnable runnable) { - handler.removeCallbacks(runnable); + /** + * This handles the timeout for capture requests so they return within a + * reasonable amount of time. + */ + static class TimeoutHandler { + private static final int REQUEST_TIMEOUT = 5000; + private final Handler handler; + + TimeoutHandler() { + this.handler = new Handler(Looper.getMainLooper()); + } + + public void resetTimeout(Runnable runnable) { + clearTimeout(runnable); + handler.postDelayed(runnable, REQUEST_TIMEOUT); + } + + public void clearTimeout(Runnable runnable) { + handler.removeCallbacks(runnable); + } } - } } diff --git a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/PictureCaptureRequestState.java b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/PictureCaptureRequestState.java new file mode 100644 index 000000000000..fb8c2c7503bd --- /dev/null +++ b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/PictureCaptureRequestState.java @@ -0,0 +1,33 @@ +package io.flutter.plugins.camera; + +/** + * This describes the state of the current picture capture request. + * This is different from the camera state because this simply says + * whether or not the current capture is finished and if there was + * an error. + * + * We have to separate this state because a picture capture request + * only exists in the context of a dart call where we have a result + * to return. + */ +public enum PictureCaptureRequestState { + /** + * Not doing anything yet. + */ + STATE_IDLE, + + /** + * Picture is being captured. + */ + STATE_CAPTURING, + + /** + * Picture capture is finished. + */ + STATE_FINISHED, + + /** + * An error occurred. + */ + STATE_ERROR, +} diff --git a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/types/FlashMode.java b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/types/FlashMode.java index c4f0998c418a..69db79859b80 100644 --- a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/types/FlashMode.java +++ b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/types/FlashMode.java @@ -4,6 +4,16 @@ package io.flutter.plugins.camera.types; +import android.hardware.camera2.CameraCaptureSession; +import android.hardware.camera2.CameraCharacteristics; +import android.hardware.camera2.CaptureFailure; +import android.hardware.camera2.CaptureRequest; +import android.hardware.camera2.TotalCaptureResult; + +import androidx.annotation.NonNull; + +import io.flutter.plugin.common.MethodChannel; + // Mirrors flash_mode.dart public enum FlashMode { off("off"), @@ -28,4 +38,4 @@ public static FlashMode getValueForString(String modeStr) { public String toString() { return strValue; } -} +} \ No newline at end of file From e0830514d04028dfbb90a06ba73241449659b48d Mon Sep 17 00:00:00 2001 From: Andrew Coutts Date: Mon, 1 Mar 2021 06:36:00 -0500 Subject: [PATCH 032/114] Can now capture repeated images with no AF but with AE working --- .../io/flutter/plugins/camera/Camera.java | 2417 +++++++++-------- .../flutter/plugins/camera/CameraState.java | 4 +- .../plugins/camera/PictureCaptureRequest.java | 1 + 3 files changed, 1225 insertions(+), 1197 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 debd0d77fff8..9058c1112235 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 @@ -34,7 +34,6 @@ import android.os.Build.VERSION_CODES; import android.os.Handler; import android.os.HandlerThread; -import android.os.SystemClock; import android.util.Log; import android.util.Range; import android.util.Rational; @@ -47,7 +46,6 @@ import java.io.File; import java.io.IOException; -import java.lang.reflect.Field; import java.nio.ByteBuffer; import java.util.ArrayList; import java.util.Arrays; @@ -71,1333 +69,1362 @@ @FunctionalInterface interface ErrorCallback { - void onError(String errorCode, String errorMessage); + void onError(String errorCode, String errorMessage); } public class Camera { - private static final String TAG = "Camera"; - - - /** - * Conversion from screen rotation to JPEG orientation. - */ - private static final SparseIntArray ORIENTATIONS = new SparseIntArray(); - private static final HashMap supportedImageFormats; - - static { - ORIENTATIONS.append(Surface.ROTATION_0, 90); - ORIENTATIONS.append(Surface.ROTATION_90, 0); - ORIENTATIONS.append(Surface.ROTATION_180, 270); - ORIENTATIONS.append(Surface.ROTATION_270, 180); - } - - // Current supported outputs - static { - supportedImageFormats = new HashMap<>(); - supportedImageFormats.put("yuv420", 35); - supportedImageFormats.put("jpeg", 256); - } - - private final SurfaceTextureEntry flutterTexture; - private final CameraManager cameraManager; - private final DeviceOrientationManager deviceOrientationListener; - private final boolean isFrontFacing; - private final int sensorOrientation; - private final String cameraName; - private final Size captureSize; - private final Size previewSize; - private final boolean enableAudio; - private final Context applicationContext; - private final CamcorderProfile recordingProfile; - private final DartMessenger dartMessenger; - private final CameraZoom cameraZoom; - private final CameraCharacteristics cameraCharacteristics; - private final Activity activity; - -/** - * The state of the camera. By default we are in the preview state. - */ - private CameraState cameraState = CameraState.STATE_PREVIEW; - - /** - * A {@link Handler} for running tasks in the background. - */ - private Handler mBackgroundHandler; - - /** - * An additional thread for running tasks that shouldn't block the UI. - */ - private HandlerThread mBackgroundThread; - - private CameraDevice cameraDevice; - private CameraCaptureSession mPreviewSession; - private ImageReader pictureImageReader; - private ImageReader imageStreamReader; - - /** - * {@link CaptureRequest.Builder} for the camera preview - */ - private CaptureRequest.Builder mPreviewRequestBuilder; - - /** - * {@link CaptureRequest} generated by {@link #mPreviewRequestBuilder} - */ - private CaptureRequest mPreviewRequest; - - private MediaRecorder mediaRecorder; - private boolean recordingVideo; - private File videoRecordingFile; - - /** - * Flash mode setting of the current camera. Initialize to off because - * we don't know if the current camera supports flash yet. - */ - private FlashMode currentFlashMode = FlashMode.off; - - /** - * Exposure mode setting of the current camera. Initialize to auto - * because all cameras support autoexposure by default. - */ - private ExposureMode exposureMode = ExposureMode.auto; - - /** - * Focus mode setting of the current camera. Initialize to locked because - * we don't know if the current camera supports autofocus yet. - */ - private FocusMode currentFocusMode = FocusMode.locked; - - /** - * Whether the current camera device supports auto focus or not. - */ - private boolean mAutoFocusSupported = false; - - /** - * Whether or not to use autofocus. - */ - private boolean useAutoFocus = false; - - /** - * Whether the current camera device supports Flash or not. - */ - private boolean mFlashSupported = false; - - /** - * This manages the state of the camera and the current capture request. - */ - PictureCaptureRequest pictureCaptureRequest; - - /** - * This a callback object for the {@link ImageReader}. "onImageAvailable" will be called when a - * still image is ready to be saved. - */ - private final ImageReader.OnImageAvailableListener mOnImageAvailableListener - = new ImageReader.OnImageAvailableListener() { - - @Override - public void onImageAvailable(ImageReader reader) { - Log.i(TAG, "onImageAvailable"); - mBackgroundHandler.post(new ImageSaver(reader.acquireNextImage(), pictureCaptureRequest.mFile, pictureCaptureRequest)); + private static final String TAG = "Camera"; + + + /** + * Conversion from screen rotation to JPEG orientation. + */ + private static final SparseIntArray ORIENTATIONS = new SparseIntArray(); + private static final HashMap supportedImageFormats; + + static { + ORIENTATIONS.append(Surface.ROTATION_0, 90); + ORIENTATIONS.append(Surface.ROTATION_90, 0); + ORIENTATIONS.append(Surface.ROTATION_180, 270); + ORIENTATIONS.append(Surface.ROTATION_270, 180); } - }; - private CameraRegions cameraRegions; - private int exposureOffset; + // Current supported outputs + static { + supportedImageFormats = new HashMap<>(); + supportedImageFormats.put("yuv420", 35); + supportedImageFormats.put("jpeg", 256); + } - private Range fpsRange; - private PlatformChannel.DeviceOrientation lockedCaptureOrientation; + private final SurfaceTextureEntry flutterTexture; + private final CameraManager cameraManager; + private final DeviceOrientationManager deviceOrientationListener; + private final boolean isFrontFacing; + private final int sensorOrientation; + private final String cameraName; + private final Size captureSize; + private final Size previewSize; + private final boolean enableAudio; + private final Context applicationContext; + private final CamcorderProfile recordingProfile; + private final DartMessenger dartMessenger; + private final CameraZoom cameraZoom; + private final CameraCharacteristics cameraCharacteristics; + private final Activity activity; + + /** + * The state of the camera. By default we are in the preview state. + */ + private CameraState cameraState = CameraState.STATE_PREVIEW; + + /** + * A {@link Handler} for running tasks in the background. + */ + private Handler mBackgroundHandler; + + /** + * An additional thread for running tasks that shouldn't block the UI. + */ + private HandlerThread mBackgroundThread; + + private CameraDevice cameraDevice; + private CameraCaptureSession mPreviewSession; + private ImageReader pictureImageReader; + private ImageReader imageStreamReader; + + /** + * {@link CaptureRequest.Builder} for the camera preview + */ + private CaptureRequest.Builder mPreviewRequestBuilder; + + /** + * {@link CaptureRequest} generated by {@link #mPreviewRequestBuilder} + */ + private CaptureRequest mPreviewRequest; + + private MediaRecorder mediaRecorder; + private boolean recordingVideo; + private File videoRecordingFile; + + /** + * Flash mode setting of the current camera. Initialize to off because + * we don't know if the current camera supports flash yet. + */ + private FlashMode currentFlashMode = FlashMode.off; + + /** + * Exposure mode setting of the current camera. Initialize to auto + * because all cameras support autoexposure by default. + */ + private ExposureMode exposureMode = ExposureMode.auto; + + /** + * Focus mode setting of the current camera. Initialize to locked because + * we don't know if the current camera supports autofocus yet. + */ + private FocusMode currentFocusMode = FocusMode.locked; + + /** + * Whether the current camera device supports auto focus or not. + */ + private boolean mAutoFocusSupported = false; + + /** + * Whether or not to use autofocus. + */ + private boolean useAutoFocus = false; + + /** + * Whether the current camera device supports Flash or not. + */ + private boolean mFlashSupported = false; + + /** + * This manages the state of the camera and the current capture request. + */ + PictureCaptureRequest pictureCaptureRequest; + + /** + * This a callback object for the {@link ImageReader}. "onImageAvailable" will be called when a + * still image is ready to be saved. + */ + private final ImageReader.OnImageAvailableListener mOnImageAvailableListener + = new ImageReader.OnImageAvailableListener() { + + @Override + public void onImageAvailable(ImageReader reader) { + Log.i(TAG, "onImageAvailable"); + mBackgroundHandler.post(new ImageSaver(reader.acquireNextImage(), pictureCaptureRequest.mFile, pictureCaptureRequest)); + cameraState = CameraState.STATE_PREVIEW; + } + }; + private CameraRegions cameraRegions; + private int exposureOffset; + private Range fpsRange; + private PlatformChannel.DeviceOrientation lockedCaptureOrientation; - public Camera( - final Activity activity, - final SurfaceTextureEntry flutterTexture, - final DartMessenger dartMessenger, - final String cameraName, - final String resolutionPreset, - final boolean enableAudio) - throws CameraAccessException { - Log.i(TAG, "Camear constructor"); - if (activity == null) { - throw new IllegalStateException("No activity available!"); + public Camera( + final Activity activity, + final SurfaceTextureEntry flutterTexture, + final DartMessenger dartMessenger, + final String cameraName, + final String resolutionPreset, + final boolean enableAudio) + throws CameraAccessException { + Log.i(TAG, "Camear constructor"); + + if (activity == null) { + throw new IllegalStateException("No activity available!"); + } + this.activity = activity; + this.cameraName = cameraName; + this.enableAudio = enableAudio; + this.flutterTexture = flutterTexture; + this.dartMessenger = dartMessenger; + this.cameraManager = (CameraManager) activity.getSystemService(Context.CAMERA_SERVICE); + this.applicationContext = activity.getApplicationContext(); + this.currentFlashMode = FlashMode.off; + this.exposureMode = ExposureMode.auto; + this.currentFocusMode = FocusMode.auto; + this.exposureOffset = 0; + + cameraCharacteristics = cameraManager.getCameraCharacteristics(cameraName); + getAvailableFpsRange(cameraCharacteristics); + sensorOrientation = cameraCharacteristics.get(CameraCharacteristics.SENSOR_ORIENTATION); + isFrontFacing = + cameraCharacteristics.get(CameraCharacteristics.LENS_FACING) + == CameraMetadata.LENS_FACING_FRONT; + ResolutionPreset preset = ResolutionPreset.valueOf(resolutionPreset); + recordingProfile = + CameraUtils.getBestAvailableCamcorderProfileForResolutionPreset(cameraName, preset); + captureSize = new Size(recordingProfile.videoFrameWidth, recordingProfile.videoFrameHeight); + previewSize = computeBestPreviewSize(cameraName, preset); + cameraZoom = + new CameraZoom( + cameraCharacteristics.get(CameraCharacteristics.SENSOR_INFO_ACTIVE_ARRAY_SIZE), + cameraCharacteristics.get(CameraCharacteristics.SCALER_AVAILABLE_MAX_DIGITAL_ZOOM)); + + deviceOrientationListener = + new DeviceOrientationManager(activity, dartMessenger, isFrontFacing, sensorOrientation); + deviceOrientationListener.start(); + + // Check if the flash is supported. + Boolean flashAvailable = cameraCharacteristics.get(CameraCharacteristics.FLASH_INFO_AVAILABLE); + mFlashSupported = flashAvailable == null ? false : flashAvailable; + startBackgroundThread(); } - this.activity = activity; - this.cameraName = cameraName; - this.enableAudio = enableAudio; - this.flutterTexture = flutterTexture; - this.dartMessenger = dartMessenger; - this.cameraManager = (CameraManager) activity.getSystemService(Context.CAMERA_SERVICE); - this.applicationContext = activity.getApplicationContext(); - this.currentFlashMode = FlashMode.off; - this.exposureMode = ExposureMode.auto; - this.currentFocusMode = FocusMode.auto; - this.exposureOffset = 0; - - cameraCharacteristics = cameraManager.getCameraCharacteristics(cameraName); - getAvailableFpsRange(cameraCharacteristics); - sensorOrientation = cameraCharacteristics.get(CameraCharacteristics.SENSOR_ORIENTATION); - isFrontFacing = - cameraCharacteristics.get(CameraCharacteristics.LENS_FACING) - == CameraMetadata.LENS_FACING_FRONT; - ResolutionPreset preset = ResolutionPreset.valueOf(resolutionPreset); - recordingProfile = - CameraUtils.getBestAvailableCamcorderProfileForResolutionPreset(cameraName, preset); - captureSize = new Size(recordingProfile.videoFrameWidth, recordingProfile.videoFrameHeight); - previewSize = computeBestPreviewSize(cameraName, preset); - cameraZoom = - new CameraZoom( - cameraCharacteristics.get(CameraCharacteristics.SENSOR_INFO_ACTIVE_ARRAY_SIZE), - cameraCharacteristics.get(CameraCharacteristics.SCALER_AVAILABLE_MAX_DIGITAL_ZOOM)); - - deviceOrientationListener = - new DeviceOrientationManager(activity, dartMessenger, isFrontFacing, sensorOrientation); - deviceOrientationListener.start(); - - // Check if the flash is supported. - Boolean flashAvailable = cameraCharacteristics.get(CameraCharacteristics.FLASH_INFO_AVAILABLE); - mFlashSupported = flashAvailable == null ? false : flashAvailable; - startBackgroundThread(); - } - - /** - * Load available FPS range for the current camera and update the available fps range with it. - * @param cameraCharacteristics - */ - private void getAvailableFpsRange(CameraCharacteristics cameraCharacteristics) { - Log.i(TAG, "getAvailableFpsRange"); - - try { - Range[] ranges = - cameraCharacteristics.get(CameraCharacteristics.CONTROL_AE_AVAILABLE_TARGET_FPS_RANGES); - if (ranges != null) { - for (Range range : ranges) { - int upper = range.getUpper(); - Log.i("Camera", "[FPS Range Available] is:" + range); - if (upper >= 10) { - if (fpsRange == null || upper > fpsRange.getUpper()) { - fpsRange = range; + + /** + * Load available FPS range for the current camera and update the available fps range with it. + * + * @param cameraCharacteristics + */ + private void getAvailableFpsRange(CameraCharacteristics cameraCharacteristics) { + Log.i(TAG, "getAvailableFpsRange"); + + try { + Range[] ranges = + cameraCharacteristics.get(CameraCharacteristics.CONTROL_AE_AVAILABLE_TARGET_FPS_RANGES); + if (ranges != null) { + for (Range range : ranges) { + int upper = range.getUpper(); + Log.i("Camera", "[FPS Range Available] is:" + range); + if (upper >= 10) { + if (fpsRange == null || upper > fpsRange.getUpper()) { + fpsRange = range; + } + } + } } - } + } catch (Exception e) { + pictureCaptureRequest.error("cameraAccess", e.getMessage(), null); } - } - } catch (Exception e) { - pictureCaptureRequest.error("cameraAccess", e.getMessage(), null); + Log.i("Camera", "[FPS Range] is:" + fpsRange); } - Log.i("Camera", "[FPS Range] is:" + fpsRange); - } - private void prepareMediaRecorder(String outputFilePath) throws IOException { - Log.i(TAG, "prepareMediaRecorder"); + private void prepareMediaRecorder(String outputFilePath) throws IOException { + Log.i(TAG, "prepareMediaRecorder"); - if (mediaRecorder != null) { - mediaRecorder.release(); - } + if (mediaRecorder != null) { + mediaRecorder.release(); + } - mediaRecorder = - new MediaRecorderBuilder(recordingProfile, outputFilePath) - .setEnableAudio(enableAudio) - .setMediaOrientation( - lockedCaptureOrientation == null - ? deviceOrientationListener.getMediaOrientation() - : deviceOrientationListener.getMediaOrientation(lockedCaptureOrientation)) - .build(); - } - - @SuppressLint("MissingPermission") - public void open(String imageFormatGroup) throws CameraAccessException { - pictureImageReader = - ImageReader.newInstance( - captureSize.getWidth(), captureSize.getHeight(), ImageFormat.JPEG, 2); - - Integer imageFormat = supportedImageFormats.get(imageFormatGroup); - if (imageFormat == null) { - Log.w(TAG, "The selected imageFormatGroup is not supported by Android. Defaulting to yuv420"); - imageFormat = ImageFormat.YUV_420_888; + mediaRecorder = + new MediaRecorderBuilder(recordingProfile, outputFilePath) + .setEnableAudio(enableAudio) + .setMediaOrientation( + lockedCaptureOrientation == null + ? deviceOrientationListener.getMediaOrientation() + : deviceOrientationListener.getMediaOrientation(lockedCaptureOrientation)) + .build(); } - // Used to steam image byte data to dart side. - imageStreamReader = - ImageReader.newInstance(previewSize.getWidth(), previewSize.getHeight(), imageFormat, 2); - - cameraManager.openCamera( - cameraName, - new CameraDevice.StateCallback() { - @Override - public void onOpened(@NonNull CameraDevice device) { - Log.i(TAG, "open | onOpened"); - - - cameraDevice = device; - try { - startPreview(); - dartMessenger.sendCameraInitializedEvent( - previewSize.getWidth(), - previewSize.getHeight(), - exposureMode, - currentFocusMode, - isExposurePointSupported(), - isFocusPointSupported()); - } catch (CameraAccessException e) { - dartMessenger.sendCameraErrorEvent(e.getMessage()); - close(); - } - } + @SuppressLint("MissingPermission") + public void open(String imageFormatGroup) throws CameraAccessException { + pictureImageReader = + ImageReader.newInstance( + captureSize.getWidth(), captureSize.getHeight(), ImageFormat.JPEG, 2); - @Override - public void onClosed(@NonNull CameraDevice camera) { - Log.i(TAG, "open | onClosed"); + Integer imageFormat = supportedImageFormats.get(imageFormatGroup); + if (imageFormat == null) { + Log.w(TAG, "The selected imageFormatGroup is not supported by Android. Defaulting to yuv420"); + imageFormat = ImageFormat.YUV_420_888; + } - dartMessenger.sendCameraClosingEvent(); - super.onClosed(camera); - } + // Used to steam image byte data to dart side. + imageStreamReader = + ImageReader.newInstance(previewSize.getWidth(), previewSize.getHeight(), imageFormat, 2); + + cameraManager.openCamera( + cameraName, + new CameraDevice.StateCallback() { + @Override + public void onOpened(@NonNull CameraDevice device) { + Log.i(TAG, "open | onOpened"); + + + cameraDevice = device; + try { + startPreview(); + dartMessenger.sendCameraInitializedEvent( + previewSize.getWidth(), + previewSize.getHeight(), + exposureMode, + currentFocusMode, + isExposurePointSupported(), + isFocusPointSupported()); + } catch (CameraAccessException e) { + dartMessenger.sendCameraErrorEvent(e.getMessage()); + close(); + } + } + + @Override + public void onClosed(@NonNull CameraDevice camera) { + Log.i(TAG, "open | onClosed"); + + dartMessenger.sendCameraClosingEvent(); + super.onClosed(camera); + } + + @Override + public void onDisconnected(@NonNull CameraDevice cameraDevice) { + Log.i(TAG, "open | onDisconnected"); + + close(); + dartMessenger.sendCameraErrorEvent("The camera was disconnected."); + } + + @Override + public void onError(@NonNull CameraDevice cameraDevice, int errorCode) { + Log.i(TAG, "open | onError"); + + close(); + String errorDescription; + switch (errorCode) { + case ERROR_CAMERA_IN_USE: + errorDescription = "The camera device is in use already."; + break; + case ERROR_MAX_CAMERAS_IN_USE: + errorDescription = "Max cameras in use"; + break; + case ERROR_CAMERA_DISABLED: + errorDescription = "The camera device could not be opened due to a device policy."; + break; + case ERROR_CAMERA_DEVICE: + errorDescription = "The camera device has encountered a fatal error"; + break; + case ERROR_CAMERA_SERVICE: + errorDescription = "The camera service has encountered a fatal error."; + break; + default: + errorDescription = "Unknown camera error"; + } + dartMessenger.sendCameraErrorEvent(errorDescription); + } + }, + null); + } - @Override - public void onDisconnected(@NonNull CameraDevice cameraDevice) { - Log.i(TAG, "open | onDisconnected"); + private void createCaptureSession(int templateType, Surface... surfaces) + throws CameraAccessException { + createCaptureSession(templateType, null, surfaces); + } - close(); - dartMessenger.sendCameraErrorEvent("The camera was disconnected."); - } + private void createCaptureSession( + int templateType, Runnable onSuccessCallback, Surface... surfaces) + throws CameraAccessException { + Log.i(TAG, "createCaptureSession"); - @Override - public void onError(@NonNull CameraDevice cameraDevice, int errorCode) { - Log.i(TAG, "open | onError"); + // Close any existing capture session. + closeCaptureSession(); - close(); - String errorDescription; - switch (errorCode) { - case ERROR_CAMERA_IN_USE: - errorDescription = "The camera device is in use already."; - break; - case ERROR_MAX_CAMERAS_IN_USE: - errorDescription = "Max cameras in use"; - break; - case ERROR_CAMERA_DISABLED: - errorDescription = "The camera device could not be opened due to a device policy."; - break; - case ERROR_CAMERA_DEVICE: - errorDescription = "The camera device has encountered a fatal error"; - break; - case ERROR_CAMERA_SERVICE: - errorDescription = "The camera service has encountered a fatal error."; - break; - default: - errorDescription = "Unknown camera error"; - } - dartMessenger.sendCameraErrorEvent(errorDescription); - } - }, - null); - } - - private void createCaptureSession(int templateType, Surface... surfaces) - throws CameraAccessException { - createCaptureSession(templateType, null, surfaces); - } - - private void createCaptureSession( - int templateType, Runnable onSuccessCallback, Surface... surfaces) - throws CameraAccessException { - Log.i(TAG, "createCaptureSession"); - - // Close any existing capture session. - closeCaptureSession(); - - // Create a new capture builder. - mPreviewRequestBuilder = cameraDevice.createCaptureRequest(templateType); - - // Build Flutter surface to render to - SurfaceTexture surfaceTexture = flutterTexture.surfaceTexture(); - surfaceTexture.setDefaultBufferSize(previewSize.getWidth(), previewSize.getHeight()); - Surface flutterSurface = new Surface(surfaceTexture); - mPreviewRequestBuilder.addTarget(flutterSurface); - - List remainingSurfaces = Arrays.asList(surfaces); - if (templateType != CameraDevice.TEMPLATE_PREVIEW) { - // If it is not preview mode, add all surfaces as targets. - for (Surface surface : remainingSurfaces) { - mPreviewRequestBuilder.addTarget(surface); - } - } + // Create a new capture builder. + mPreviewRequestBuilder = cameraDevice.createCaptureRequest(templateType); - cameraRegions = new CameraRegions(getRegionBoundaries()); - - // Prepare the callback - CameraCaptureSession.StateCallback callback = - new CameraCaptureSession.StateCallback() { - @Override - public void onConfigured(@NonNull CameraCaptureSession session) { - // Camera was already closed. - if (cameraDevice == null) { - dartMessenger.sendCameraErrorEvent("The camera was closed during configuration."); - return; - } - mPreviewSession = session; + // Build Flutter surface to render to + SurfaceTexture surfaceTexture = flutterTexture.surfaceTexture(); + surfaceTexture.setDefaultBufferSize(previewSize.getWidth(), previewSize.getHeight()); + Surface flutterSurface = new Surface(surfaceTexture); + mPreviewRequestBuilder.addTarget(flutterSurface); - updateFpsRange(); - updateFocus(mPreviewRequestBuilder); - updateFlash(mPreviewRequestBuilder); - updateExposure(exposureMode); - - refreshPreviewCaptureSession( - onSuccessCallback, (code, message) -> dartMessenger.sendCameraErrorEvent(message)); - } + List remainingSurfaces = Arrays.asList(surfaces); + if (templateType != CameraDevice.TEMPLATE_PREVIEW) { + // If it is not preview mode, add all surfaces as targets. + for (Surface surface : remainingSurfaces) { + mPreviewRequestBuilder.addTarget(surface); + } + } - @Override - public void onConfigureFailed(@NonNull CameraCaptureSession cameraCaptureSession) { - dartMessenger.sendCameraErrorEvent("Failed to configure camera session."); - } - }; + cameraRegions = new CameraRegions(getRegionBoundaries()); + + // Prepare the callback + CameraCaptureSession.StateCallback callback = + new CameraCaptureSession.StateCallback() { + @Override + public void onConfigured(@NonNull CameraCaptureSession session) { + // Camera was already closed. + if (cameraDevice == null) { + dartMessenger.sendCameraErrorEvent("The camera was closed during configuration."); + return; + } + mPreviewSession = session; + + updateFpsRange(); + updateFocus(mPreviewRequestBuilder); + updateFlash(mPreviewRequestBuilder); + updateExposureMode(mPreviewRequestBuilder); + + refreshPreviewCaptureSession( + onSuccessCallback, (code, message) -> dartMessenger.sendCameraErrorEvent(message)); + } + + @Override + public void onConfigureFailed(@NonNull CameraCaptureSession cameraCaptureSession) { + dartMessenger.sendCameraErrorEvent("Failed to configure camera session."); + } + }; + + // Start the session + if (VERSION.SDK_INT >= VERSION_CODES.P) { + // Collect all surfaces we want to render to. + List configs = new ArrayList<>(); + configs.add(new OutputConfiguration(flutterSurface)); + for (Surface surface : remainingSurfaces) { + configs.add(new OutputConfiguration(surface)); + } + createCaptureSessionWithSessionConfig(configs, callback); + } else { + // Collect all surfaces we want to render to. + List surfaceList = new ArrayList<>(); + surfaceList.add(flutterSurface); + surfaceList.addAll(remainingSurfaces); + createCaptureSession(surfaceList, callback); + } + } - // Start the session - if (VERSION.SDK_INT >= VERSION_CODES.P) { - // Collect all surfaces we want to render to. - List configs = new ArrayList<>(); - configs.add(new OutputConfiguration(flutterSurface)); - for (Surface surface : remainingSurfaces) { - configs.add(new OutputConfiguration(surface)); - } - createCaptureSessionWithSessionConfig(configs, callback); - } else { - // Collect all surfaces we want to render to. - List surfaceList = new ArrayList<>(); - surfaceList.add(flutterSurface); - surfaceList.addAll(remainingSurfaces); - createCaptureSession(surfaceList, callback); + @TargetApi(VERSION_CODES.P) + private void createCaptureSessionWithSessionConfig( + List outputConfigs, CameraCaptureSession.StateCallback callback) + throws CameraAccessException { + cameraDevice.createCaptureSession( + new SessionConfiguration( + SessionConfiguration.SESSION_REGULAR, + outputConfigs, + Executors.newSingleThreadExecutor(), + callback)); } - } - - @TargetApi(VERSION_CODES.P) - private void createCaptureSessionWithSessionConfig( - List outputConfigs, CameraCaptureSession.StateCallback callback) - throws CameraAccessException { - cameraDevice.createCaptureSession( - new SessionConfiguration( - SessionConfiguration.SESSION_REGULAR, - outputConfigs, - Executors.newSingleThreadExecutor(), - callback)); - } - - @TargetApi(VERSION_CODES.LOLLIPOP) - @SuppressWarnings("deprecation") - private void createCaptureSession( - List surfaces, CameraCaptureSession.StateCallback callback) - throws CameraAccessException { - cameraDevice.createCaptureSession(surfaces, callback, null); - } - - private void refreshPreviewCaptureSession( - @Nullable Runnable onSuccessCallback, @NonNull ErrorCallback onErrorCallback) { - Log.i(TAG, "refreshPreviewCaptureSession"); - if (mPreviewSession == null) { - Log.i(TAG, "[refreshPreviewCaptureSession] mPreviewSession null, returning"); - return; + + @TargetApi(VERSION_CODES.LOLLIPOP) + @SuppressWarnings("deprecation") + private void createCaptureSession( + List surfaces, CameraCaptureSession.StateCallback callback) + throws CameraAccessException { + cameraDevice.createCaptureSession(surfaces, callback, null); } - try { - mPreviewRequest = mPreviewRequestBuilder.build(); - mPreviewSession.setRepeatingRequest(mPreviewRequest, - mCaptureCallback, mBackgroundHandler); + private void refreshPreviewCaptureSession( + @Nullable Runnable onSuccessCallback, @NonNull ErrorCallback onErrorCallback) { + Log.i(TAG, "refreshPreviewCaptureSession"); + if (mPreviewSession == null) { + Log.i(TAG, "[refreshPreviewCaptureSession] mPreviewSession null, returning"); + return; + } - // Put camera back to preview mode - cameraState = CameraState.STATE_PREVIEW; + try { +// mPreviewRequestBuilder.set(CaptureRequest.CONTROL_AE_PRECAPTURE_TRIGGER, +// CaptureRequest.CONTROL_AE_PRECAPTURE_TRIGGER_START); + mPreviewSession.setRepeatingRequest(mPreviewRequestBuilder.build(), + mCaptureCallback, mBackgroundHandler); - if (onSuccessCallback != null) { - onSuccessCallback.run(); - } - } catch (CameraAccessException | IllegalStateException | IllegalArgumentException e) { - onErrorCallback.onError("cameraAccess", e.getMessage()); + if (onSuccessCallback != null) { + onSuccessCallback.run(); + } + } catch (CameraAccessException | IllegalStateException | IllegalArgumentException e) { + onErrorCallback.onError("cameraAccess", e.getMessage()); + } } - } - public void takePicture(@NonNull final Result result) { - Log.i(TAG, "takePicture | useAutoFocus: " + useAutoFocus); + public void takePicture(@NonNull final Result result) { + Log.i(TAG, "takePicture | useAutoFocus: " + useAutoFocus); - // Only take one 1 picture at a time. - if (pictureCaptureRequest != null && !pictureCaptureRequest.isFinished()) { - result.error("captureAlreadyActive", "Picture is currently already being captured", null); - return; - } + // Only take one 1 picture at a time. + if (pictureCaptureRequest != null && !pictureCaptureRequest.isFinished()) { + result.error("captureAlreadyActive", "Picture is currently already being captured", null); + return; + } - // Create temporary file - final File outputDir = applicationContext.getCacheDir(); - try { - final File file = File.createTempFile("CAP", ".jpg", outputDir); + // Create temporary file + final File outputDir = applicationContext.getCacheDir(); + try { + final File file = File.createTempFile("CAP", ".jpg", outputDir); - // Start a new capture - pictureCaptureRequest = new PictureCaptureRequest(result, file); - } catch (IOException | SecurityException e) { - pictureCaptureRequest.error("cannotCreateFile", e.getMessage(), null); - return; - } + // Start a new capture + pictureCaptureRequest = new PictureCaptureRequest(result, file); + } catch (IOException | SecurityException e) { + pictureCaptureRequest.error("cannotCreateFile", e.getMessage(), null); + return; + } - // Listen for picture being taken - pictureImageReader.setOnImageAvailableListener(mOnImageAvailableListener, mBackgroundHandler); + // Listen for picture being taken + pictureImageReader.setOnImageAvailableListener(mOnImageAvailableListener, mBackgroundHandler); - if (useAutoFocus) { - runPictureAutoFocus(); - } else { - runPrecaptureSequence(); + if (useAutoFocus) { + runPictureAutoFocus(); + } else { + runPrecaptureSequence(); + } } - } - - /** - * Run the precapture sequence for capturing a still image. This method should be called when - * we get a response in {@link #mCaptureCallback} from lockFocus(). - */ - private void runPrecaptureSequence() { - Log.i(TAG, "runPrecaptureSequence"); - try { - // This is how to tell the camera to trigger. - mPreviewRequestBuilder.set(CaptureRequest.CONTROL_AE_PRECAPTURE_TRIGGER, - CaptureRequest.CONTROL_AE_PRECAPTURE_TRIGGER_START); - // Tell #mCaptureCallback to wait for the precapture sequence to be set. - cameraState = CameraState.STATE_WAITING_PRECAPTURE; - mPreviewSession.capture(mPreviewRequestBuilder.build(), mCaptureCallback, - mBackgroundHandler); - } catch (CameraAccessException e) { - pictureCaptureRequest.error("cameraAccess", e.getMessage(), null); + + /** + * Run the precapture sequence for capturing a still image. This method should be called when + * we get a response in {@link #mCaptureCallback} from lockFocus(). + */ + private void runPrecaptureSequence() { + Log.i(TAG, "runPrecaptureSequence"); + try { + +// // First set precapture state to idle or else it can hang in STATE_WAITING_PRECAPTURE +// mPreviewRequestBuilder.set(CaptureRequest.CONTROL_AE_PRECAPTURE_TRIGGER, +// CaptureRequest.CONTROL_AE_PRECAPTURE_TRIGGER_IDLE); +// mPreviewSession.capture(mPreviewRequestBuilder.build(), mCaptureCallback, +// mBackgroundHandler); +// refreshPreviewCaptureSession( +// null, (code, message) -> pictureCaptureRequest.error(code, message, null)); + + // Start precapture now + cameraState = CameraState.STATE_WAITING_PRECAPTURE_START; + + mPreviewRequestBuilder.set(CaptureRequest.CONTROL_AE_PRECAPTURE_TRIGGER, + CaptureRequest.CONTROL_AE_PRECAPTURE_TRIGGER_START); + mPreviewSession.capture(mPreviewRequestBuilder.build(), mCaptureCallback, + mBackgroundHandler); + } catch (CameraAccessException e) { + pictureCaptureRequest.error("cameraAccess", e.getMessage(), null); + } } - } - - /** - * Capture a still picture. This method should be called when we get a response in - * {@link #mCaptureCallback} from both lockFocus(). - */ - private void captureStillPicture() { - Log.i(TAG, "captureStillPicture"); - try { - if (null == cameraDevice) { - return; - } - // This is the CaptureRequest.Builder that we use to take a picture. - final CaptureRequest.Builder captureBuilder = - cameraDevice.createCaptureRequest(CameraDevice.TEMPLATE_STILL_CAPTURE); - captureBuilder.addTarget(pictureImageReader.getSurface()); - - // Zoom - captureBuilder.set( - CaptureRequest.SCALER_CROP_REGION, - mPreviewRequestBuilder.get(CaptureRequest.SCALER_CROP_REGION)); - - // Set focus / flash from preview mode - updateFlash(captureBuilder); - updateFocus(captureBuilder); - - // Orientation - int rotation = activity.getWindowManager().getDefaultDisplay().getRotation(); - captureBuilder.set(CaptureRequest.JPEG_ORIENTATION, getOrientation(rotation)); - - CameraCaptureSession.CaptureCallback CaptureCallback - = new CameraCaptureSession.CaptureCallback() { - @Override - public void onCaptureCompleted(@NonNull CameraCaptureSession session, - @NonNull CaptureRequest request, - @NonNull TotalCaptureResult result) { - unlockAutoFocus(); + /** + * Capture a still picture. This method should be called when we get a response in + * {@link #mCaptureCallback} from both lockFocus(). + */ + private void captureStillPicture() { + Log.i(TAG, "captureStillPicture"); + try { + if (null == cameraDevice) { + return; + } + // This is the CaptureRequest.Builder that we use to take a picture. + final CaptureRequest.Builder captureBuilder = + cameraDevice.createCaptureRequest(CameraDevice.TEMPLATE_STILL_CAPTURE); + captureBuilder.addTarget(pictureImageReader.getSurface()); + + // Zoom + captureBuilder.set( + CaptureRequest.SCALER_CROP_REGION, + mPreviewRequestBuilder.get(CaptureRequest.SCALER_CROP_REGION)); + + // Set focus / flash from preview mode + updateFlash(captureBuilder); + updateFocus(captureBuilder); + updateExposureMode(captureBuilder); + + // Orientation + int rotation = activity.getWindowManager().getDefaultDisplay().getRotation(); + captureBuilder.set(CaptureRequest.JPEG_ORIENTATION, getOrientation(rotation)); + + CameraCaptureSession.CaptureCallback CaptureCallback + = new CameraCaptureSession.CaptureCallback() { + + @Override + public void onCaptureCompleted(@NonNull CameraCaptureSession session, + @NonNull CaptureRequest request, + @NonNull TotalCaptureResult result) { + Log.i(TAG, "onCaptureCompleted"); + unlockAutoFocus(); + } + }; + + mPreviewSession.stopRepeating(); + mPreviewSession.abortCaptures(); + mPreviewSession.capture(captureBuilder.build(), CaptureCallback, null); + } catch (CameraAccessException e) { + pictureCaptureRequest.error("cameraAccess", e.getMessage(), null); } - }; + } - mPreviewSession.stopRepeating(); - mPreviewSession.abortCaptures(); - mPreviewSession.capture(captureBuilder.build(), CaptureCallback, null); - } catch (CameraAccessException e) { - pictureCaptureRequest.error("cameraAccess", e.getMessage(), null); + /** + * Starts a background thread and its {@link Handler}. + * TODO: call when activity resumed + */ + private void startBackgroundThread() { + mBackgroundThread = new HandlerThread("CameraBackground"); + mBackgroundThread.start(); + mBackgroundHandler = new Handler(mBackgroundThread.getLooper()); } - } - - /** - * Starts a background thread and its {@link Handler}. - * TODO: call when activity resumed - */ - private void startBackgroundThread() { - mBackgroundThread = new HandlerThread("CameraBackground"); - mBackgroundThread.start(); - mBackgroundHandler = new Handler(mBackgroundThread.getLooper()); - } - - /** - * Stops the background thread and its {@link Handler}. - * TODO: call when activity paused - */ - private void stopBackgroundThread() { - mBackgroundThread.quitSafely(); - try { - mBackgroundThread.join(); - mBackgroundThread = null; - mBackgroundHandler = null; - } catch (InterruptedException e) { - pictureCaptureRequest.error("cameraAccess", e.getMessage(), null); + + /** + * Stops the background thread and its {@link Handler}. + * TODO: call when activity paused + */ + private void stopBackgroundThread() { + mBackgroundThread.quitSafely(); + try { + mBackgroundThread.join(); + mBackgroundThread = null; + mBackgroundHandler = null; + } catch (InterruptedException e) { + pictureCaptureRequest.error("cameraAccess", e.getMessage(), null); + } } - } + /** + * Sync the requestBuilder exposure mode setting ot the current exposure mode setting of the camera. + */ + void updateExposureMode(CaptureRequest.Builder requestBuilder) { + Log.i(TAG, "updateExposureMode"); + // Applying auto exposure + MeteringRectangle aeRect = cameraRegions.getAEMeteringRectangle(); + mPreviewRequestBuilder.set( + CaptureRequest.CONTROL_AE_REGIONS, + aeRect == null ? null : new MeteringRectangle[]{cameraRegions.getAEMeteringRectangle()}); + + switch (exposureMode) { + case locked: + requestBuilder.set(CaptureRequest.CONTROL_AE_LOCK, true); + break; + case auto: + default: + requestBuilder.set(CaptureRequest.CONTROL_AE_LOCK, false); + break; + } - /** - * Sync the requestBuilder flash setting to the current flash mode setting of the camera. - */ - void updateFlash(CaptureRequest.Builder requestBuilder) { - if (!mFlashSupported) { - return; + // TODO: move this to its own setting (exposure offset) + requestBuilder.set(CaptureRequest.CONTROL_AE_EXPOSURE_COMPENSATION, exposureOffset); } - switch (currentFlashMode) { - case off: - requestBuilder.set(CaptureRequest.CONTROL_AE_MODE, - CaptureRequest.CONTROL_AE_MODE_ON); - requestBuilder.set(CaptureRequest.FLASH_MODE, - CaptureRequest.FLASH_MODE_OFF); - break; - - case always: - requestBuilder.set(CaptureRequest.CONTROL_AE_MODE, - CaptureRequest.CONTROL_AE_MODE_ON_ALWAYS_FLASH); - requestBuilder.set(CaptureRequest.FLASH_MODE, - CaptureRequest.FLASH_MODE_OFF); - break; - - case torch: - requestBuilder.set(CaptureRequest.CONTROL_AE_MODE, - CaptureRequest.CONTROL_AE_MODE_ON); - requestBuilder.set(CaptureRequest.FLASH_MODE, - CaptureRequest.FLASH_MODE_TORCH); - break; - - case auto: - requestBuilder.set(CaptureRequest.CONTROL_AE_MODE, - CaptureRequest.CONTROL_AE_MODE_ON_AUTO_FLASH); - requestBuilder.set(CaptureRequest.FLASH_MODE, - CaptureRequest.FLASH_MODE_OFF); - break; - - // TODO: to be implemented someday. Need to add it to dart and iOS. + /** + * Sync the requestBuilder flash setting to the current flash mode setting of the camera. + */ + void updateFlash(CaptureRequest.Builder requestBuilder) { + Log.i(TAG, "updateFlash"); + + if (!mFlashSupported) { + return; + } + + switch (currentFlashMode) { + case off: + requestBuilder.set(CaptureRequest.CONTROL_AE_MODE, + CaptureRequest.CONTROL_AE_MODE_ON); + requestBuilder.set(CaptureRequest.FLASH_MODE, + CaptureRequest.FLASH_MODE_OFF); + break; + + case always: + requestBuilder.set(CaptureRequest.CONTROL_AE_MODE, + CaptureRequest.CONTROL_AE_MODE_ON_ALWAYS_FLASH); + requestBuilder.set(CaptureRequest.FLASH_MODE, + CaptureRequest.FLASH_MODE_OFF); + break; + + case torch: + requestBuilder.set(CaptureRequest.CONTROL_AE_MODE, + CaptureRequest.CONTROL_AE_MODE_ON); + requestBuilder.set(CaptureRequest.FLASH_MODE, + CaptureRequest.FLASH_MODE_TORCH); + break; + + case auto: + requestBuilder.set(CaptureRequest.CONTROL_AE_MODE, + CaptureRequest.CONTROL_AE_MODE_ON_AUTO_FLASH); + requestBuilder.set(CaptureRequest.FLASH_MODE, + CaptureRequest.FLASH_MODE_OFF); + break; + + // TODO: to be implemented someday. Need to add it to dart and iOS. // case autoRedEye: // requestBuilder.set(CaptureRequest.CONTROL_AE_MODE, // CaptureRequest.CONTROL_AE_MODE_ON_AUTO_FLASH_REDEYE); // requestBuilder.set(CaptureRequest.FLASH_MODE, // CaptureRequest.FLASH_MODE_OFF); // break; - } - } - - /** - * Retrieves the JPEG orientation from the specified screen rotation. - * - * @param rotation The screen rotation. - * @return The JPEG orientation (one of 0, 90, 270, and 360) - */ - private int getOrientation(int rotation) { - // Sensor orientation is 90 for most devices, or 270 for some devices (eg. Nexus 5X) - // We have to take that into account and rotate JPEG properly. - // For devices with orientation of 90, we simply return our mapping from ORIENTATIONS. - // For devices with orientation of 270, we need to rotate the JPEG 180 degrees. - return (ORIENTATIONS.get(rotation) + sensorOrientation + 270) % 360; - } - - /** - * A {@link CameraCaptureSession.CaptureCallback} that handles events related to JPEG capture. - */ - private final CameraCaptureSession.CaptureCallback mCaptureCallback - = new CameraCaptureSession.CaptureCallback() { - - private void process(CaptureResult result) { - if (pictureCaptureRequest == null) { - return; - } - - Integer aeState = result.get(CaptureResult.CONTROL_AE_STATE); - Integer afState = result.get(CaptureResult.CONTROL_AF_STATE); - - if (cameraState != CameraState.STATE_PREVIEW) { - Log.i(TAG, "mCaptureCallback | state: " + cameraState + " | afState: " + afState + " | aeState: " + aeState); - } - - switch (cameraState) { - case STATE_PREVIEW: { - // We have nothing to do when the camera preview is working normally. - break; } + } - case STATE_WAITING_FOCUS: { - if (afState == null) { - cameraState = CameraState.STATE_CAPTURING; - captureStillPicture(); - } else if (afState == CaptureResult.CONTROL_AF_STATE_FOCUSED_LOCKED || - afState == CaptureResult.CONTROL_AF_STATE_NOT_FOCUSED_LOCKED) { - // CONTROL_AE_STATE can be null on some devices - - if (aeState == null || - aeState == CaptureResult.CONTROL_AE_STATE_CONVERGED) { - cameraState = CameraState.STATE_CAPTURING; - captureStillPicture(); - } else { - runPrecaptureSequence(); + /** + * Retrieves the JPEG orientation from the specified screen rotation. + * + * @param rotation The screen rotation. + * @return The JPEG orientation (one of 0, 90, 270, and 360) + */ + private int getOrientation(int rotation) { + // Sensor orientation is 90 for most devices, or 270 for some devices (eg. Nexus 5X) + // We have to take that into account and rotate JPEG properly. + // For devices with orientation of 90, we simply return our mapping from ORIENTATIONS. + // For devices with orientation of 270, we need to rotate the JPEG 180 degrees. + return (ORIENTATIONS.get(rotation) + sensorOrientation + 270) % 360; + } + + /** + * A {@link CameraCaptureSession.CaptureCallback} that handles events related to JPEG capture. + */ + private final CameraCaptureSession.CaptureCallback mCaptureCallback + = new CameraCaptureSession.CaptureCallback() { + + private void process(CaptureResult result) { + if (pictureCaptureRequest == null) { + return; + } + + Integer aeState = result.get(CaptureResult.CONTROL_AE_STATE); + Integer afState = result.get(CaptureResult.CONTROL_AF_STATE); + + if (cameraState != CameraState.STATE_PREVIEW) { + Log.i(TAG, "mCaptureCallback | state: " + cameraState + " | afState: " + afState + " | aeState: " + aeState); + } + + switch (cameraState) { + case STATE_PREVIEW: { + // We have nothing to do when the camera preview is working normally. + break; + } + + case STATE_WAITING_FOCUS: { + if (afState == null) { + cameraState = CameraState.STATE_CAPTURING; + captureStillPicture(); + } else if (afState == CaptureResult.CONTROL_AF_STATE_FOCUSED_LOCKED || + afState == CaptureResult.CONTROL_AF_STATE_NOT_FOCUSED_LOCKED) { + // CONTROL_AE_STATE can be null on some devices + + if (aeState == null || + aeState == CaptureResult.CONTROL_AE_STATE_CONVERGED) { + cameraState = CameraState.STATE_CAPTURING; + captureStillPicture(); + } else { + runPrecaptureSequence(); + } + } + break; + } + + case STATE_WAITING_PRECAPTURE_START: { + // CONTROL_AE_STATE can be null on some devices + if (aeState == null || + aeState == CaptureResult.CONTROL_AE_STATE_CONVERGED || + aeState == CaptureResult.CONTROL_AE_STATE_PRECAPTURE || + aeState == CaptureResult.CONTROL_AE_STATE_FLASH_REQUIRED) { + cameraState = CameraState.STATE_WAITING_PRECAPTURE_DONE; + pictureCaptureRequest.setPreCaptureStartTime(); + } + break; + } + + case STATE_WAITING_PRECAPTURE_DONE: { + // CONTROL_AE_STATE can be null on some devices + if (aeState == null || aeState != CaptureResult.CONTROL_AE_STATE_PRECAPTURE) { + cameraState = CameraState.STATE_CAPTURING; + captureStillPicture(); + } else { + if (pictureCaptureRequest.hitPreCaptureTimeout()) { + unlockAutoFocus(); + } + } + break; + } } - } - break; } - case STATE_WAITING_PRECAPTURE: { - // CONTROL_AE_STATE can be null on some devices - if (aeState == null || - aeState == CaptureResult.CONTROL_AE_STATE_CONVERGED || - aeState == CaptureResult.CONTROL_AE_STATE_PRECAPTURE || - aeState == CaptureResult.CONTROL_AE_STATE_FLASH_REQUIRED) { - cameraState = CameraState.STATE_WAITING_PRECAPTURE_READY; - pictureCaptureRequest.setPreCaptureStartTime(); - } - break; + @Override + public void onCaptureProgressed(@NonNull CameraCaptureSession session, + @NonNull CaptureRequest request, + @NonNull CaptureResult partialResult) { + process(partialResult); } - case STATE_WAITING_PRECAPTURE_READY: { - // CONTROL_AE_STATE can be null on some devices - if (aeState == null || aeState != CaptureResult.CONTROL_AE_STATE_PRECAPTURE) { - cameraState = CameraState.STATE_CAPTURING; - captureStillPicture(); - } else { - if (pictureCaptureRequest.hitPreCaptureTimeout()) { - unlockAutoFocus(); - } - } - break; + @Override + public void onCaptureCompleted(@NonNull CameraCaptureSession session, + @NonNull CaptureRequest request, + @NonNull TotalCaptureResult result) { + process(result); } - } - } + }; - @Override - public void onCaptureProgressed(@NonNull CameraCaptureSession session, - @NonNull CaptureRequest request, - @NonNull CaptureResult partialResult) { - process(partialResult); - } + /** + * Trigger auto focus to start and refresh preview capture session. + */ + private void startAutoFocus() { + Log.i(TAG, "startAutoFocus"); + mPreviewRequestBuilder.set( + CaptureRequest.CONTROL_AF_TRIGGER, CaptureRequest.CONTROL_AF_TRIGGER_START); - @Override - public void onCaptureCompleted(@NonNull CameraCaptureSession session, - @NonNull CaptureRequest request, - @NonNull TotalCaptureResult result) { - process(result); - } - }; - - /** - * Trigger auto focus to start and refresh preview capture session. - */ - private void startAutoFocus() { - Log.i(TAG, "startAutoFocus"); - mPreviewRequestBuilder.set( - CaptureRequest.CONTROL_AF_TRIGGER, CaptureRequest.CONTROL_AF_TRIGGER_START); - - try { - mPreviewSession.capture(mPreviewRequestBuilder.build(), mCaptureCallback, - mBackgroundHandler); - } catch (CameraAccessException e) { - pictureCaptureRequest.error("cannotStartAutofocus", e.getMessage(), null); + try { + mPreviewSession.capture(mPreviewRequestBuilder.build(), mCaptureCallback, + mBackgroundHandler); + } catch (CameraAccessException e) { + pictureCaptureRequest.error("cannotStartAutofocus", e.getMessage(), null); + } } - } - - /** - * Start capturing a picture, doing autofocus first. - */ - private void runPictureAutoFocus() { - Log.i(TAG, "runPictureAutoFocus"); - assert (pictureCaptureRequest != null); - - cameraState = CameraState.STATE_WAITING_FOCUS; - lockAutoFocus(); - } - - /** - * Start the autofocus routine. - */ - private void lockAutoFocus() { - Log.i(TAG, "lockAutoFocus"); - mPreviewRequestBuilder.set( - CaptureRequest.CONTROL_AF_TRIGGER, CaptureRequest.CONTROL_AF_TRIGGER_START); - - refreshPreviewCaptureSession( - null, (code, message) -> pictureCaptureRequest.error(code, message, null)); - } - - /** - * Cancel and reset auto focus state and refresh the preview session. - */ - private void unlockAutoFocus() { - Log.i(TAG, "unlockAutoFocus"); - - mPreviewRequestBuilder.set( - CaptureRequest.CONTROL_AF_TRIGGER, CameraMetadata.CONTROL_AF_TRIGGER_CANCEL); - - updateFocus(mPreviewRequestBuilder); - updateFlash(mPreviewRequestBuilder); - - try { - mPreviewSession.capture(mPreviewRequestBuilder.build(), null, null); - } catch (CameraAccessException ignored) { + + /** + * Start capturing a picture, doing autofocus first. + */ + private void runPictureAutoFocus() { + Log.i(TAG, "runPictureAutoFocus"); + assert (pictureCaptureRequest != null); + + cameraState = CameraState.STATE_WAITING_FOCUS; + lockAutoFocus(); } - refreshPreviewCaptureSession( - null, - (errorCode, errorMessage) -> pictureCaptureRequest.error(errorCode, errorMessage, null)); - } - - public void startVideoRecording(Result result) { - final File outputDir = applicationContext.getCacheDir(); - try { - videoRecordingFile = File.createTempFile("REC", ".mp4", outputDir); - } catch (IOException | SecurityException e) { - result.error("cannotCreateFile", e.getMessage(), null); - return; + /** + * Start the autofocus routine. + */ + private void lockAutoFocus() { + Log.i(TAG, "lockAutoFocus"); + mPreviewRequestBuilder.set( + CaptureRequest.CONTROL_AF_TRIGGER, CaptureRequest.CONTROL_AF_TRIGGER_START); + + refreshPreviewCaptureSession( + null, (code, message) -> pictureCaptureRequest.error(code, message, null)); } - try { - prepareMediaRecorder(videoRecordingFile.getAbsolutePath()); - recordingVideo = true; - createCaptureSession( - CameraDevice.TEMPLATE_RECORD, () -> mediaRecorder.start(), mediaRecorder.getSurface()); - result.success(null); - } catch (CameraAccessException | IOException e) { - recordingVideo = false; - videoRecordingFile = null; - result.error("videoRecordingFailed", e.getMessage(), null); + /** + * Cancel and reset auto focus state and refresh the preview session. + */ + private void unlockAutoFocus() { + Log.i(TAG, "unlockAutoFocus"); + + // Reset AF state + mPreviewRequestBuilder.set( + CaptureRequest.CONTROL_AF_TRIGGER, CameraMetadata.CONTROL_AF_TRIGGER_CANCEL); + + // Reset AE state. If we don't call this then the preview won't show AE again. + mPreviewRequestBuilder.set(CaptureRequest.CONTROL_AE_PRECAPTURE_TRIGGER, + CaptureRequest.CONTROL_AE_PRECAPTURE_TRIGGER_IDLE); + + updateFocus(mPreviewRequestBuilder); + updateFlash(mPreviewRequestBuilder); + updateExposureMode(mPreviewRequestBuilder); + + try { + mPreviewSession.capture(mPreviewRequestBuilder.build(), null, null); + } catch (CameraAccessException ignored) { + } + + refreshPreviewCaptureSession( + null, + (errorCode, errorMessage) -> pictureCaptureRequest.error(errorCode, errorMessage, null)); } - } - public void stopVideoRecording(@NonNull final Result result) { - if (!recordingVideo) { - result.success(null); - return; + public void startVideoRecording(Result result) { + final File outputDir = applicationContext.getCacheDir(); + try { + videoRecordingFile = File.createTempFile("REC", ".mp4", outputDir); + } catch (IOException | SecurityException e) { + result.error("cannotCreateFile", e.getMessage(), null); + return; + } + + try { + prepareMediaRecorder(videoRecordingFile.getAbsolutePath()); + recordingVideo = true; + createCaptureSession( + CameraDevice.TEMPLATE_RECORD, () -> mediaRecorder.start(), mediaRecorder.getSurface()); + result.success(null); + } catch (CameraAccessException | IOException e) { + recordingVideo = false; + videoRecordingFile = null; + result.error("videoRecordingFailed", e.getMessage(), null); + } } - try { - recordingVideo = false; - - try { - mPreviewSession.abortCaptures(); - mediaRecorder.stop(); - } catch (CameraAccessException | IllegalStateException e) { - // Ignore exceptions and try to continue (changes are camera session already aborted capture) - } - - mediaRecorder.reset(); - startPreview(); - result.success(videoRecordingFile.getAbsolutePath()); - videoRecordingFile = null; - } catch (CameraAccessException | IllegalStateException e) { - result.error("videoRecordingFailed", e.getMessage(), null); + public void stopVideoRecording(@NonNull final Result result) { + if (!recordingVideo) { + result.success(null); + return; + } + + try { + recordingVideo = false; + + try { + mPreviewSession.abortCaptures(); + mediaRecorder.stop(); + } catch (CameraAccessException | IllegalStateException e) { + // Ignore exceptions and try to continue (changes are camera session already aborted capture) + } + + mediaRecorder.reset(); + startPreview(); + result.success(videoRecordingFile.getAbsolutePath()); + videoRecordingFile = null; + } catch (CameraAccessException | IllegalStateException e) { + result.error("videoRecordingFailed", e.getMessage(), null); + } } - } - public void pauseVideoRecording(@NonNull final Result result) { - if (!recordingVideo) { - result.success(null); - return; + public void pauseVideoRecording(@NonNull final Result result) { + if (!recordingVideo) { + result.success(null); + return; + } + + try { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { + mediaRecorder.pause(); + } else { + result.error("videoRecordingFailed", "pauseVideoRecording requires Android API +24.", null); + return; + } + } catch (IllegalStateException e) { + result.error("videoRecordingFailed", e.getMessage(), null); + return; + } + + result.success(null); } - try { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { - mediaRecorder.pause(); - } else { - result.error("videoRecordingFailed", "pauseVideoRecording requires Android API +24.", null); - return; - } - } catch (IllegalStateException e) { - result.error("videoRecordingFailed", e.getMessage(), null); - return; + public void resumeVideoRecording(@NonNull final Result result) { + if (!recordingVideo) { + result.success(null); + return; + } + + try { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { + mediaRecorder.resume(); + } else { + result.error( + "videoRecordingFailed", "resumeVideoRecording requires Android API +24.", null); + return; + } + } catch (IllegalStateException e) { + result.error("videoRecordingFailed", e.getMessage(), null); + return; + } + + result.success(null); } - result.success(null); - } + /** + * Dart handler when it's time to set a new flash mode. This will try to set a new + * flash mode to the current camera. + * + * @param result + * @param newMode + * @throws CameraAccessException + */ + public void setFlashMode(@NonNull final Result result, FlashMode newMode) + throws CameraAccessException { + // Check if the current camera can modify the flash mode. + Boolean flashAvailable = + cameraManager + .getCameraCharacteristics(cameraDevice.getId()) + .get(CameraCharacteristics.FLASH_INFO_AVAILABLE); + + // Check if flash is available. + if (flashAvailable == null || !flashAvailable) { + result.error("setFlashModeFailed", "Device does not have flash capabilities", null); + return; + } - public void resumeVideoRecording(@NonNull final Result result) { - if (!recordingVideo) { - result.success(null); - return; + // Save the new flash mode setting + final FlashMode oldFlashMode = currentFlashMode; + currentFlashMode = newMode; + updateFlash(mPreviewRequestBuilder); + + // If switching directly from torch to auto or on, make sure we turn off the torch. + if (oldFlashMode == FlashMode.torch && newMode != FlashMode.torch && newMode != FlashMode.off) { + // TODO: why cant we just call refresh preview here? + mPreviewSession.setRepeatingRequest( + mPreviewRequestBuilder.build(), + new CaptureCallback() { + private boolean isFinished = false; + + @Override + public void onCaptureCompleted( + @NonNull CameraCaptureSession session, + @NonNull CaptureRequest request, + @NonNull TotalCaptureResult captureResult) { + if (isFinished) { + return; + } + + updateFlash(mPreviewRequestBuilder); + refreshPreviewCaptureSession( + () -> { + result.success(null); + isFinished = true; + }, + (code, message) -> + result.error("setFlashModeFailed", "Could not set flash mode.", null)); + } + + @Override + public void onCaptureFailed( + @NonNull CameraCaptureSession session, + @NonNull CaptureRequest request, + @NonNull CaptureFailure failure) { + if (isFinished) { + return; + } + + result.error("setFlashModeFailed", "Could not set flash mode.", null); + isFinished = true; + } + }, + null); + } else { + refreshPreviewCaptureSession( + () -> result.success(null), + (code, message) -> result.error("setFlashModeFailed", "Could not set flash mode.", null)); + } } - try { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { - mediaRecorder.resume(); - } else { - result.error( - "videoRecordingFailed", "resumeVideoRecording requires Android API +24.", null); - return; - } - } catch (IllegalStateException e) { - result.error("videoRecordingFailed", e.getMessage(), null); - return; + /** + * Dart handler for setting new exposure mode setting. + * + * @param result + * @param newMode + * @throws CameraAccessException + */ + public void setExposureMode(@NonNull final Result result, ExposureMode newMode) + throws CameraAccessException { + exposureMode = newMode; + + refreshPreviewCaptureSession( + () -> result.success(null), + (code, message) -> result.error("setExposureModeFailed", "Could not set exposure mode.", null)); } - result.success(null); - } - - /** - * Dart handler when it's time to set a new flash mode. This will try to set a new - * flash mode to the current camera. - * @param result - * @param newMode - * @throws CameraAccessException - */ - public void setFlashMode(@NonNull final Result result, FlashMode newMode) - throws CameraAccessException { - // Check if the current camera can modify the flash mode. - Boolean flashAvailable = - cameraManager - .getCameraCharacteristics(cameraDevice.getId()) - .get(CameraCharacteristics.FLASH_INFO_AVAILABLE); + public void setExposurePoint(@NonNull final Result result, Double x, Double y) + throws CameraAccessException { + // Check if exposure point functionality is available. + if (!isExposurePointSupported()) { + result.error( + "setExposurePointFailed", "Device does not have exposure point capabilities", null); + return; + } + // Check if the current region boundaries are known + if (cameraRegions.getMaxBoundaries() == null) { + result.error("setExposurePointFailed", "Could not determine max region boundaries", null); + return; + } + // Set the metering rectangle + if (x == null || y == null) cameraRegions.resetAutoExposureMeteringRectangle(); + else cameraRegions.setAutoExposureMeteringRectangleFromPoint(y, 1 - x); + // Apply it + updateExposureMode(mPreviewRequestBuilder); + refreshPreviewCaptureSession( + () -> result.success(null), (code, message) -> result.error("CameraAccess", message, null)); + } - // Check if flash is available. - if (flashAvailable == null || !flashAvailable) { - result.error("setFlashModeFailed", "Device does not have flash capabilities", null); - return; + /** + * Set new focus mode from dart. + * + * @param result + * @param newMode + * @throws CameraAccessException + */ + public void setFocusMode(@NonNull final Result result, FocusMode newMode) + throws CameraAccessException { + currentFocusMode = newMode; + updateFocus(mPreviewRequestBuilder); + + switch (newMode) { + case auto: + refreshPreviewCaptureSession( + null, (code, message) -> result.error("setFocusMode", message, null)); + break; + case locked: + startAutoFocus(); + break; + } + result.success(null); } - // Save the new flash mode setting - final FlashMode oldFlashMode = currentFlashMode; - currentFlashMode = newMode; - updateFlash(mPreviewRequestBuilder); + public void setFocusPoint(@NonNull final Result result, Double x, Double y) + throws CameraAccessException { + // Check if focus point functionality is available. + if (!isFocusPointSupported()) { + result.error("setFocusPointFailed", "Device does not have focus point capabilities", null); + return; + } + + // Check if the current region boundaries are known + if (cameraRegions.getMaxBoundaries() == null) { + result.error("setFocusPointFailed", "Could not determine max region boundaries", null); + return; + } - // If switching directly from torch to auto or on, make sure we turn off the torch. - if (oldFlashMode == FlashMode.torch && newMode != FlashMode.torch && newMode != FlashMode.off) { - // TODO: why cant we just call refresh preview here? - mPreviewSession.setRepeatingRequest( - mPreviewRequestBuilder.build(), - new CaptureCallback() { - private boolean isFinished = false; + // Set the metering rectangle + if (x == null || y == null) { + cameraRegions.resetAutoFocusMeteringRectangle(); + } else { + cameraRegions.setAutoFocusMeteringRectangleFromPoint(y, 1 - x); + } - @Override - public void onCaptureCompleted( - @NonNull CameraCaptureSession session, - @NonNull CaptureRequest request, - @NonNull TotalCaptureResult captureResult) { - if (isFinished) { - return; - } - - updateFlash(mPreviewRequestBuilder); - refreshPreviewCaptureSession( - () -> { - result.success(null); - isFinished = true; - }, - (code, message) -> - result.error("setFlashModeFailed", "Could not set flash mode.", null)); - } + // Apply the new metering rectangle + setFocusMode(result, currentFocusMode); + } - @Override - public void onCaptureFailed( - @NonNull CameraCaptureSession session, - @NonNull CaptureRequest request, - @NonNull CaptureFailure failure) { - if (isFinished) { - return; - } - - result.error("setFlashModeFailed", "Could not set flash mode.", null); - isFinished = true; - } - }, - null); - } else { - refreshPreviewCaptureSession( - () -> result.success(null), - (code, message) -> result.error("setFlashModeFailed", "Could not set flash mode.", null)); + @TargetApi(VERSION_CODES.P) + private boolean supportsDistortionCorrection() throws CameraAccessException { + int[] availableDistortionCorrectionModes = + cameraManager + .getCameraCharacteristics(cameraDevice.getId()) + .get(CameraCharacteristics.DISTORTION_CORRECTION_AVAILABLE_MODES); + if (availableDistortionCorrectionModes == null) + availableDistortionCorrectionModes = new int[0]; + long nonOffModesSupported = + Arrays.stream(availableDistortionCorrectionModes) + .filter((value) -> value != CaptureRequest.DISTORTION_CORRECTION_MODE_OFF) + .count(); + return nonOffModesSupported > 0; } - } - - public void setExposureMode(@NonNull final Result result, ExposureMode mode) - throws CameraAccessException { - updateExposure(mode); - mPreviewSession.setRepeatingRequest(mPreviewRequestBuilder.build(), null, null); - result.success(null); - } - - public void setExposurePoint(@NonNull final Result result, Double x, Double y) - throws CameraAccessException { - // Check if exposure point functionality is available. - if (!isExposurePointSupported()) { - result.error( - "setExposurePointFailed", "Device does not have exposure point capabilities", null); - return; + + private Size getRegionBoundaries() throws CameraAccessException { + // No distortion correction support + if (android.os.Build.VERSION.SDK_INT < VERSION_CODES.P || !supportsDistortionCorrection()) { + return cameraManager + .getCameraCharacteristics(cameraDevice.getId()) + .get(CameraCharacteristics.SENSOR_INFO_PIXEL_ARRAY_SIZE); + } + // Get the current distortion correction mode + Integer distortionCorrectionMode = + mPreviewRequestBuilder.get(CaptureRequest.DISTORTION_CORRECTION_MODE); + // Return the correct boundaries depending on the mode + android.graphics.Rect rect; + if (distortionCorrectionMode == null + || distortionCorrectionMode == CaptureRequest.DISTORTION_CORRECTION_MODE_OFF) { + rect = + cameraManager + .getCameraCharacteristics(cameraDevice.getId()) + .get(CameraCharacteristics.SENSOR_INFO_PRE_CORRECTION_ACTIVE_ARRAY_SIZE); + } else { + rect = + cameraManager + .getCameraCharacteristics(cameraDevice.getId()) + .get(CameraCharacteristics.SENSOR_INFO_ACTIVE_ARRAY_SIZE); + } + return rect == null ? null : new Size(rect.width(), rect.height()); } - // Check if the current region boundaries are known - if (cameraRegions.getMaxBoundaries() == null) { - result.error("setExposurePointFailed", "Could not determine max region boundaries", null); - return; + + private boolean isExposurePointSupported() throws CameraAccessException { + Integer supportedRegions = + cameraManager + .getCameraCharacteristics(cameraDevice.getId()) + .get(CameraCharacteristics.CONTROL_MAX_REGIONS_AE); + return supportedRegions != null && supportedRegions > 0; } - // Set the metering rectangle - if (x == null || y == null) cameraRegions.resetAutoExposureMeteringRectangle(); - else cameraRegions.setAutoExposureMeteringRectangleFromPoint(y, 1 - x); - // Apply it - updateExposure(exposureMode); - refreshPreviewCaptureSession( - () -> result.success(null), (code, message) -> result.error("CameraAccess", message, null)); - } - - /** - * Set new focus mode from dart. - * @param result - * @param newMode - * @throws CameraAccessException - */ - public void setFocusMode(@NonNull final Result result, FocusMode newMode) - throws CameraAccessException { - currentFocusMode = newMode; - updateFocus(mPreviewRequestBuilder); - - switch (newMode) { - case auto: - refreshPreviewCaptureSession( - null, (code, message) -> result.error("setFocusMode", message, null)); - break; - case locked: - startAutoFocus(); - break; + + private boolean isFocusPointSupported() throws CameraAccessException { + Integer supportedRegions = + cameraManager + .getCameraCharacteristics(cameraDevice.getId()) + .get(CameraCharacteristics.CONTROL_MAX_REGIONS_AF); + return supportedRegions != null && supportedRegions > 0; } - result.success(null); - } - - public void setFocusPoint(@NonNull final Result result, Double x, Double y) - throws CameraAccessException { - // Check if focus point functionality is available. - if (!isFocusPointSupported()) { - result.error("setFocusPointFailed", "Device does not have focus point capabilities", null); - return; + + public double getMinExposureOffset() throws CameraAccessException { + Range range = + cameraManager + .getCameraCharacteristics(cameraDevice.getId()) + .get(CameraCharacteristics.CONTROL_AE_COMPENSATION_RANGE); + double minStepped = range == null ? 0 : range.getLower(); + double stepSize = getExposureOffsetStepSize(); + return minStepped * stepSize; } - // Check if the current region boundaries are known - if (cameraRegions.getMaxBoundaries() == null) { - result.error("setFocusPointFailed", "Could not determine max region boundaries", null); - return; + public double getMaxExposureOffset() throws CameraAccessException { + Range range = + cameraManager + .getCameraCharacteristics(cameraDevice.getId()) + .get(CameraCharacteristics.CONTROL_AE_COMPENSATION_RANGE); + double maxStepped = range == null ? 0 : range.getUpper(); + double stepSize = getExposureOffsetStepSize(); + return maxStepped * stepSize; } - // Set the metering rectangle - if (x == null || y == null) { - cameraRegions.resetAutoFocusMeteringRectangle(); - } else { - cameraRegions.setAutoFocusMeteringRectangleFromPoint(y, 1 - x); + public double getExposureOffsetStepSize() throws CameraAccessException { + Rational stepSize = + cameraManager + .getCameraCharacteristics(cameraDevice.getId()) + .get(CameraCharacteristics.CONTROL_AE_COMPENSATION_STEP); + return stepSize == null ? 0.0 : stepSize.doubleValue(); } - // Apply the new metering rectangle - setFocusMode(result, currentFocusMode); - } + public void setExposureOffset(@NonNull final Result result, double offset) + throws CameraAccessException { + // Set the exposure offset + double stepSize = getExposureOffsetStepSize(); + exposureOffset = (int) (offset / stepSize); + // Apply it + updateExposureMode(mPreviewRequestBuilder); + // TODO: refresh preview session? + this.mPreviewSession.setRepeatingRequest(mPreviewRequestBuilder.build(), null, null); + result.success(offset); + } - @TargetApi(VERSION_CODES.P) - private boolean supportsDistortionCorrection() throws CameraAccessException { - int[] availableDistortionCorrectionModes = - cameraManager - .getCameraCharacteristics(cameraDevice.getId()) - .get(CameraCharacteristics.DISTORTION_CORRECTION_AVAILABLE_MODES); - if (availableDistortionCorrectionModes == null) - availableDistortionCorrectionModes = new int[0]; - long nonOffModesSupported = - Arrays.stream(availableDistortionCorrectionModes) - .filter((value) -> value != CaptureRequest.DISTORTION_CORRECTION_MODE_OFF) - .count(); - return nonOffModesSupported > 0; - } - - private Size getRegionBoundaries() throws CameraAccessException { - // No distortion correction support - if (android.os.Build.VERSION.SDK_INT < VERSION_CODES.P || !supportsDistortionCorrection()) { - return cameraManager - .getCameraCharacteristics(cameraDevice.getId()) - .get(CameraCharacteristics.SENSOR_INFO_PIXEL_ARRAY_SIZE); + public float getMaxZoomLevel() { + return cameraZoom.maxZoom; } - // Get the current distortion correction mode - Integer distortionCorrectionMode = - mPreviewRequestBuilder.get(CaptureRequest.DISTORTION_CORRECTION_MODE); - // Return the correct boundaries depending on the mode - android.graphics.Rect rect; - if (distortionCorrectionMode == null - || distortionCorrectionMode == CaptureRequest.DISTORTION_CORRECTION_MODE_OFF) { - rect = - cameraManager - .getCameraCharacteristics(cameraDevice.getId()) - .get(CameraCharacteristics.SENSOR_INFO_PRE_CORRECTION_ACTIVE_ARRAY_SIZE); - } else { - rect = - cameraManager - .getCameraCharacteristics(cameraDevice.getId()) - .get(CameraCharacteristics.SENSOR_INFO_ACTIVE_ARRAY_SIZE); + + public float getMinZoomLevel() { + return CameraZoom.DEFAULT_ZOOM_FACTOR; } - return rect == null ? null : new Size(rect.width(), rect.height()); - } - private boolean isExposurePointSupported() throws CameraAccessException { - Integer supportedRegions = - cameraManager - .getCameraCharacteristics(cameraDevice.getId()) - .get(CameraCharacteristics.CONTROL_MAX_REGIONS_AE); - return supportedRegions != null && supportedRegions > 0; - } + public void setZoomLevel(@NonNull final Result result, float zoom) throws CameraAccessException { + float maxZoom = cameraZoom.maxZoom; + float minZoom = CameraZoom.DEFAULT_ZOOM_FACTOR; + + if (zoom > maxZoom || zoom < minZoom) { + String errorMessage = + String.format( + Locale.ENGLISH, + "Zoom level out of bounds (zoom level should be between %f and %f).", + minZoom, + maxZoom); + result.error("ZOOM_ERROR", errorMessage, null); + return; + } - private boolean isFocusPointSupported() throws CameraAccessException { - Integer supportedRegions = - cameraManager - .getCameraCharacteristics(cameraDevice.getId()) - .get(CameraCharacteristics.CONTROL_MAX_REGIONS_AF); - return supportedRegions != null && supportedRegions > 0; - } + //Zoom area is calculated relative to sensor area (activeRect) + if (mPreviewRequestBuilder != null) { + final Rect computedZoom = cameraZoom.computeZoom(zoom); + mPreviewRequestBuilder.set(CaptureRequest.SCALER_CROP_REGION, computedZoom); + mPreviewSession.setRepeatingRequest(mPreviewRequestBuilder.build(), null, null); + } - public double getMinExposureOffset() throws CameraAccessException { - Range range = - cameraManager - .getCameraCharacteristics(cameraDevice.getId()) - .get(CameraCharacteristics.CONTROL_AE_COMPENSATION_RANGE); - double minStepped = range == null ? 0 : range.getLower(); - double stepSize = getExposureOffsetStepSize(); - return minStepped * stepSize; - } - - public double getMaxExposureOffset() throws CameraAccessException { - Range range = - cameraManager - .getCameraCharacteristics(cameraDevice.getId()) - .get(CameraCharacteristics.CONTROL_AE_COMPENSATION_RANGE); - double maxStepped = range == null ? 0 : range.getUpper(); - double stepSize = getExposureOffsetStepSize(); - return maxStepped * stepSize; - } - - public double getExposureOffsetStepSize() throws CameraAccessException { - Rational stepSize = - cameraManager - .getCameraCharacteristics(cameraDevice.getId()) - .get(CameraCharacteristics.CONTROL_AE_COMPENSATION_STEP); - return stepSize == null ? 0.0 : stepSize.doubleValue(); - } - - public void setExposureOffset(@NonNull final Result result, double offset) - throws CameraAccessException { - // Set the exposure offset - double stepSize = getExposureOffsetStepSize(); - exposureOffset = (int) (offset / stepSize); - // Apply it - updateExposure(exposureMode); - this.mPreviewSession.setRepeatingRequest(mPreviewRequestBuilder.build(), null, null); - result.success(offset); - } - - public float getMaxZoomLevel() { - return cameraZoom.maxZoom; - } - - public float getMinZoomLevel() { - return CameraZoom.DEFAULT_ZOOM_FACTOR; - } - - public void setZoomLevel(@NonNull final Result result, float zoom) throws CameraAccessException { - float maxZoom = cameraZoom.maxZoom; - float minZoom = CameraZoom.DEFAULT_ZOOM_FACTOR; - - if (zoom > maxZoom || zoom < minZoom) { - String errorMessage = - String.format( - Locale.ENGLISH, - "Zoom level out of bounds (zoom level should be between %f and %f).", - minZoom, - maxZoom); - result.error("ZOOM_ERROR", errorMessage, null); - return; + result.success(null); } - //Zoom area is calculated relative to sensor area (activeRect) - if (mPreviewRequestBuilder != null) { - final Rect computedZoom = cameraZoom.computeZoom(zoom); - mPreviewRequestBuilder.set(CaptureRequest.SCALER_CROP_REGION, computedZoom); - mPreviewSession.setRepeatingRequest(mPreviewRequestBuilder.build(), null, null); + public void lockCaptureOrientation(PlatformChannel.DeviceOrientation orientation) { + this.lockedCaptureOrientation = orientation; } - result.success(null); - } - - public void lockCaptureOrientation(PlatformChannel.DeviceOrientation orientation) { - this.lockedCaptureOrientation = orientation; - } + public void unlockCaptureOrientation() { + this.lockedCaptureOrientation = null; + } - public void unlockCaptureOrientation() { - this.lockedCaptureOrientation = null; - } + /** + * Set current fps range setting to the current preview request builder + */ + private void updateFpsRange() { + if (fpsRange == null) { + return; + } - /** - * Set current fps range setting to the current preview request builder - */ - private void updateFpsRange() { - if (fpsRange == null) { - return; + mPreviewRequestBuilder.set(CaptureRequest.CONTROL_AE_TARGET_FPS_RANGE, fpsRange); } - mPreviewRequestBuilder.set(CaptureRequest.CONTROL_AE_TARGET_FPS_RANGE, fpsRange); - } - - private void updateFocus(CaptureRequest.Builder requestBuilder) { - if (useAutoFocus) { - int[] modes = cameraCharacteristics.get(CameraCharacteristics.CONTROL_AF_AVAILABLE_MODES); - // Auto focus is not supported - if (modes == null - || modes.length == 0 - || (modes.length == 1 && modes[0] == CameraCharacteristics.CONTROL_AF_MODE_OFF)) { - useAutoFocus = false; - mPreviewRequestBuilder.set( - CaptureRequest.CONTROL_AF_MODE, CaptureRequest.CONTROL_AF_MODE_OFF); - } else { - // Applying auto focus - switch (currentFocusMode) { - case locked: - requestBuilder.set( - CaptureRequest.CONTROL_AF_MODE, CaptureRequest.CONTROL_AF_MODE_AUTO); - break; - case auto: - mPreviewRequestBuilder.set( - CaptureRequest.CONTROL_AF_MODE, - recordingVideo - ? CaptureRequest.CONTROL_AF_MODE_CONTINUOUS_VIDEO - : CaptureRequest.CONTROL_AF_MODE_CONTINUOUS_PICTURE); - default: - break; + private void updateFocus(CaptureRequest.Builder requestBuilder) { + if (useAutoFocus) { + int[] modes = cameraCharacteristics.get(CameraCharacteristics.CONTROL_AF_AVAILABLE_MODES); + // Auto focus is not supported + if (modes == null + || modes.length == 0 + || (modes.length == 1 && modes[0] == CameraCharacteristics.CONTROL_AF_MODE_OFF)) { + useAutoFocus = false; + mPreviewRequestBuilder.set( + CaptureRequest.CONTROL_AF_MODE, CaptureRequest.CONTROL_AF_MODE_OFF); + } else { + // Applying auto focus + switch (currentFocusMode) { + case locked: + requestBuilder.set( + CaptureRequest.CONTROL_AF_MODE, CaptureRequest.CONTROL_AF_MODE_AUTO); + break; + case auto: + mPreviewRequestBuilder.set( + CaptureRequest.CONTROL_AF_MODE, + recordingVideo + ? CaptureRequest.CONTROL_AF_MODE_CONTINUOUS_VIDEO + : CaptureRequest.CONTROL_AF_MODE_CONTINUOUS_PICTURE); + default: + break; + } + MeteringRectangle afRect = cameraRegions.getAFMeteringRectangle(); + mPreviewRequestBuilder.set( + CaptureRequest.CONTROL_AF_REGIONS, + afRect == null ? null : new MeteringRectangle[]{afRect}); + } + } else { + mPreviewRequestBuilder.set(CaptureRequest.CONTROL_AF_MODE, CaptureRequest.CONTROL_AF_MODE_OFF); } - MeteringRectangle afRect = cameraRegions.getAFMeteringRectangle(); - mPreviewRequestBuilder.set( - CaptureRequest.CONTROL_AF_REGIONS, - afRect == null ? null : new MeteringRectangle[]{afRect}); - } - } else { - mPreviewRequestBuilder.set(CaptureRequest.CONTROL_AF_MODE, CaptureRequest.CONTROL_AF_MODE_OFF); - } - } - - private void updateExposure(ExposureMode mode) { - exposureMode = mode; - - // Applying auto exposure - MeteringRectangle aeRect = cameraRegions.getAEMeteringRectangle(); - mPreviewRequestBuilder.set( - CaptureRequest.CONTROL_AE_REGIONS, - aeRect == null ? null : new MeteringRectangle[]{cameraRegions.getAEMeteringRectangle()}); - - switch (mode) { - case locked: - mPreviewRequestBuilder.set(CaptureRequest.CONTROL_AE_LOCK, true); - break; - case auto: - default: - mPreviewRequestBuilder.set(CaptureRequest.CONTROL_AE_LOCK, false); - break; } - mPreviewRequestBuilder.set(CaptureRequest.CONTROL_AE_EXPOSURE_COMPENSATION, exposureOffset); - } - - public void startPreview() throws CameraAccessException { - if (pictureImageReader == null || pictureImageReader.getSurface() == null) return; - Log.i(TAG, "startPreview"); - - createCaptureSession(CameraDevice.TEMPLATE_PREVIEW, pictureImageReader.getSurface()); - } - - public void startPreviewWithImageStream(EventChannel imageStreamChannel) - throws CameraAccessException { - createCaptureSession(CameraDevice.TEMPLATE_RECORD, imageStreamReader.getSurface()); - Log.i(TAG, "startPreviewWithImageStream"); - - imageStreamChannel.setStreamHandler( - new EventChannel.StreamHandler() { - @Override - public void onListen(Object o, EventChannel.EventSink imageStreamSink) { - setImageStreamImageAvailableListener(imageStreamSink); - } - - @Override - public void onCancel(Object o) { - imageStreamReader.setOnImageAvailableListener(null, null); - } - }); - } - - private void setImageStreamImageAvailableListener(final EventChannel.EventSink imageStreamSink) { - imageStreamReader.setOnImageAvailableListener( - reader -> { - Image img = reader.acquireLatestImage(); - if (img == null) return; - - List> planes = new ArrayList<>(); - for (Image.Plane plane : img.getPlanes()) { - ByteBuffer buffer = plane.getBuffer(); - - byte[] bytes = new byte[buffer.remaining()]; - buffer.get(bytes, 0, bytes.length); - - Map planeBuffer = new HashMap<>(); - planeBuffer.put("bytesPerRow", plane.getRowStride()); - planeBuffer.put("bytesPerPixel", plane.getPixelStride()); - planeBuffer.put("bytes", bytes); - - planes.add(planeBuffer); - } - - Map imageBuffer = new HashMap<>(); - imageBuffer.put("width", img.getWidth()); - imageBuffer.put("height", img.getHeight()); - imageBuffer.put("format", img.getFormat()); - imageBuffer.put("planes", planes); - - imageStreamSink.success(imageBuffer); - img.close(); - }, - null); - } - - public void stopImageStream() throws CameraAccessException { - if (imageStreamReader != null) { - imageStreamReader.setOnImageAvailableListener(null, null); + public void startPreview() throws CameraAccessException { + if (pictureImageReader == null || pictureImageReader.getSurface() == null) return; + Log.i(TAG, "startPreview"); + + createCaptureSession(CameraDevice.TEMPLATE_PREVIEW, pictureImageReader.getSurface()); } - startPreview(); - } + public void startPreviewWithImageStream(EventChannel imageStreamChannel) + throws CameraAccessException { + createCaptureSession(CameraDevice.TEMPLATE_RECORD, imageStreamReader.getSurface()); + Log.i(TAG, "startPreviewWithImageStream"); + + imageStreamChannel.setStreamHandler( + new EventChannel.StreamHandler() { + @Override + public void onListen(Object o, EventChannel.EventSink imageStreamSink) { + setImageStreamImageAvailableListener(imageStreamSink); + } + + @Override + public void onCancel(Object o) { + imageStreamReader.setOnImageAvailableListener(null, null); + } + }); + } + private void setImageStreamImageAvailableListener(final EventChannel.EventSink imageStreamSink) { + imageStreamReader.setOnImageAvailableListener( + reader -> { + Image img = reader.acquireLatestImage(); + if (img == null) return; + + List> planes = new ArrayList<>(); + for (Image.Plane plane : img.getPlanes()) { + ByteBuffer buffer = plane.getBuffer(); + + byte[] bytes = new byte[buffer.remaining()]; + buffer.get(bytes, 0, bytes.length); + + Map planeBuffer = new HashMap<>(); + planeBuffer.put("bytesPerRow", plane.getRowStride()); + planeBuffer.put("bytesPerPixel", plane.getPixelStride()); + planeBuffer.put("bytes", bytes); + + planes.add(planeBuffer); + } + + Map imageBuffer = new HashMap<>(); + imageBuffer.put("width", img.getWidth()); + imageBuffer.put("height", img.getHeight()); + imageBuffer.put("format", img.getFormat()); + imageBuffer.put("planes", planes); + + imageStreamSink.success(imageBuffer); + img.close(); + }, + null); + } + public void stopImageStream() throws CameraAccessException { + if (imageStreamReader != null) { + imageStreamReader.setOnImageAvailableListener(null, null); + } + startPreview(); + } - private void closeCaptureSession() { - if (mPreviewSession != null) { - Log.i(TAG, "closeCaptureSession"); + private void closeCaptureSession() { + if (mPreviewSession != null) { + Log.i(TAG, "closeCaptureSession"); - mPreviewSession.close(); - mPreviewSession = null; + mPreviewSession.close(); + mPreviewSession = null; + } } - } - public void close() { - Log.i(TAG, "close"); + public void close() { + Log.i(TAG, "close"); - closeCaptureSession(); + closeCaptureSession(); - if (cameraDevice != null) { - cameraDevice.close(); - cameraDevice = null; - } - if (pictureImageReader != null) { - pictureImageReader.close(); - pictureImageReader = null; - } - if (imageStreamReader != null) { - imageStreamReader.close(); - imageStreamReader = null; - } - if (mediaRecorder != null) { - mediaRecorder.reset(); - mediaRecorder.release(); - mediaRecorder = null; + if (cameraDevice != null) { + cameraDevice.close(); + cameraDevice = null; + } + if (pictureImageReader != null) { + pictureImageReader.close(); + pictureImageReader = null; + } + if (imageStreamReader != null) { + imageStreamReader.close(); + imageStreamReader = null; + } + if (mediaRecorder != null) { + mediaRecorder.reset(); + mediaRecorder.release(); + mediaRecorder = null; + } } - } - public void dispose() { - Log.i(TAG, "dispose"); + public void dispose() { + Log.i(TAG, "dispose"); - close(); - flutterTexture.release(); - deviceOrientationListener.stop(); - } + close(); + flutterTexture.release(); + deviceOrientationListener.stop(); + } } diff --git a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/CameraState.java b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/CameraState.java index fca1e0099e40..0bb9c0c2cee0 100644 --- a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/CameraState.java +++ b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/CameraState.java @@ -20,12 +20,12 @@ public enum CameraState { /** * Start performing autoexposure. */ - STATE_WAITING_PRECAPTURE, + STATE_WAITING_PRECAPTURE_START, /** * waiting for autoexposure to complete. */ - STATE_WAITING_PRECAPTURE_READY, + STATE_WAITING_PRECAPTURE_DONE, /** * Capturing an image. diff --git a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/PictureCaptureRequest.java b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/PictureCaptureRequest.java index de60c68d37eb..d6f3ee40b546 100644 --- a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/PictureCaptureRequest.java +++ b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/PictureCaptureRequest.java @@ -67,6 +67,7 @@ class PictureCaptureRequest { * @param mFile */ public PictureCaptureRequest(MethodChannel.Result result, File mFile) { + Log.i("Camera", "PictureCaptureRequest constructor"); this.result = result; this.timeoutHandler = new TimeoutHandler(); this.mFile = mFile; From 0f23f3a2587defe5adf4e6daef2b6306354b37b4 Mon Sep 17 00:00:00 2001 From: Andrew Coutts Date: Mon, 1 Mar 2021 07:27:09 -0500 Subject: [PATCH 033/114] Reduce capture delay and preview lag while capturing by providing preview texture to capture request --- .../io/flutter/plugins/camera/Camera.java | 320 ++++++++++-------- .../io/flutter/plugins/camera/ImageSaver.java | 7 + 2 files changed, 177 insertions(+), 150 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 9058c1112235..55d38e5dd9e2 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 @@ -111,101 +111,175 @@ public class Camera { private final CameraZoom cameraZoom; private final CameraCharacteristics cameraCharacteristics; private final Activity activity; - + /** + * Whether the current camera device supports auto focus or not. + */ + private final boolean mAutoFocusSupported = false; + /** + * This manages the state of the camera and the current capture request. + */ + PictureCaptureRequest pictureCaptureRequest; /** * The state of the camera. By default we are in the preview state. */ private CameraState cameraState = CameraState.STATE_PREVIEW; - /** * A {@link Handler} for running tasks in the background. */ private Handler mBackgroundHandler; + /** + * The preview surface which will be provided to still capture requests to keep the + * preview going during capture. + */ + private Surface previewSurface; + + /** + * This a callback object for the {@link ImageReader}. "onImageAvailable" will be called when a + * still image is ready to be saved. + */ + private final ImageReader.OnImageAvailableListener mOnImageAvailableListener + = new ImageReader.OnImageAvailableListener() { + + @Override + public void onImageAvailable(ImageReader reader) { + Log.i(TAG, "onImageAvailable"); + mBackgroundHandler.post(new ImageSaver(reader.acquireNextImage(), pictureCaptureRequest.mFile, pictureCaptureRequest)); + cameraState = CameraState.STATE_PREVIEW; + } + + }; /** * An additional thread for running tasks that shouldn't block the UI. */ private HandlerThread mBackgroundThread; - private CameraDevice cameraDevice; - private CameraCaptureSession mPreviewSession; + private CameraCaptureSession captureSession; private ImageReader pictureImageReader; private ImageReader imageStreamReader; - /** * {@link CaptureRequest.Builder} for the camera preview */ private CaptureRequest.Builder mPreviewRequestBuilder; - /** * {@link CaptureRequest} generated by {@link #mPreviewRequestBuilder} */ private CaptureRequest mPreviewRequest; - private MediaRecorder mediaRecorder; private boolean recordingVideo; private File videoRecordingFile; - /** * Flash mode setting of the current camera. Initialize to off because * we don't know if the current camera supports flash yet. */ private FlashMode currentFlashMode = FlashMode.off; - /** * Exposure mode setting of the current camera. Initialize to auto * because all cameras support autoexposure by default. */ private ExposureMode exposureMode = ExposureMode.auto; - /** * Focus mode setting of the current camera. Initialize to locked because * we don't know if the current camera supports autofocus yet. */ private FocusMode currentFocusMode = FocusMode.locked; - - /** - * Whether the current camera device supports auto focus or not. - */ - private boolean mAutoFocusSupported = false; - /** * Whether or not to use autofocus. */ private boolean useAutoFocus = false; - /** * Whether the current camera device supports Flash or not. */ private boolean mFlashSupported = false; - + private CameraRegions cameraRegions; + private int exposureOffset; /** - * This manages the state of the camera and the current capture request. + * A {@link CameraCaptureSession.CaptureCallback} that handles events related to JPEG capture. */ - PictureCaptureRequest pictureCaptureRequest; + private final CameraCaptureSession.CaptureCallback mCaptureCallback + = new CameraCaptureSession.CaptureCallback() { - /** - * This a callback object for the {@link ImageReader}. "onImageAvailable" will be called when a - * still image is ready to be saved. - */ - private final ImageReader.OnImageAvailableListener mOnImageAvailableListener - = new ImageReader.OnImageAvailableListener() { + private void process(CaptureResult result) { + if (pictureCaptureRequest == null) { + return; + } + + Integer aeState = result.get(CaptureResult.CONTROL_AE_STATE); + Integer afState = result.get(CaptureResult.CONTROL_AF_STATE); + + if (cameraState != CameraState.STATE_PREVIEW) { + Log.i(TAG, "mCaptureCallback | state: " + cameraState + " | afState: " + afState + " | aeState: " + aeState); + } + + switch (cameraState) { + case STATE_PREVIEW: { + // We have nothing to do when the camera preview is working normally. + break; + } + + case STATE_WAITING_FOCUS: { + if (afState == null) { + cameraState = CameraState.STATE_CAPTURING; + takePictureAfterPrecapture(); + } else if (afState == CaptureResult.CONTROL_AF_STATE_FOCUSED_LOCKED || + afState == CaptureResult.CONTROL_AF_STATE_NOT_FOCUSED_LOCKED) { + // CONTROL_AE_STATE can be null on some devices + + if (aeState == null || + aeState == CaptureResult.CONTROL_AE_STATE_CONVERGED) { + cameraState = CameraState.STATE_CAPTURING; + takePictureAfterPrecapture(); + } else { + runPrecaptureSequence(); + } + } + break; + } + + case STATE_WAITING_PRECAPTURE_START: { + // CONTROL_AE_STATE can be null on some devices + if (aeState == null || + aeState == CaptureResult.CONTROL_AE_STATE_CONVERGED || + aeState == CaptureResult.CONTROL_AE_STATE_PRECAPTURE || + aeState == CaptureResult.CONTROL_AE_STATE_FLASH_REQUIRED) { + cameraState = CameraState.STATE_WAITING_PRECAPTURE_DONE; + pictureCaptureRequest.setPreCaptureStartTime(); + } + break; + } + + case STATE_WAITING_PRECAPTURE_DONE: { + // CONTROL_AE_STATE can be null on some devices + if (aeState == null || aeState != CaptureResult.CONTROL_AE_STATE_PRECAPTURE) { + cameraState = CameraState.STATE_CAPTURING; + takePictureAfterPrecapture(); + } else { + if (pictureCaptureRequest.hitPreCaptureTimeout()) { + unlockAutoFocus(); + } + } + break; + } + } + } @Override - public void onImageAvailable(ImageReader reader) { - Log.i(TAG, "onImageAvailable"); - mBackgroundHandler.post(new ImageSaver(reader.acquireNextImage(), pictureCaptureRequest.mFile, pictureCaptureRequest)); - cameraState = CameraState.STATE_PREVIEW; + public void onCaptureProgressed(@NonNull CameraCaptureSession session, + @NonNull CaptureRequest request, + @NonNull CaptureResult partialResult) { + process(partialResult); } + @Override + public void onCaptureCompleted(@NonNull CameraCaptureSession session, + @NonNull CaptureRequest request, + @NonNull TotalCaptureResult result) { + process(result); + } }; - private CameraRegions cameraRegions; - private int exposureOffset; - private Range fpsRange; private PlatformChannel.DeviceOrientation lockedCaptureOrientation; - public Camera( final Activity activity, final SurfaceTextureEntry flutterTexture, @@ -386,7 +460,8 @@ public void onError(@NonNull CameraDevice cameraDevice, int errorCode) { dartMessenger.sendCameraErrorEvent(errorDescription); } }, - null); + mBackgroundHandler + ); } private void createCaptureSession(int templateType, Surface... surfaces) @@ -411,6 +486,11 @@ private void createCaptureSession( Surface flutterSurface = new Surface(surfaceTexture); mPreviewRequestBuilder.addTarget(flutterSurface); + // Save surface if it's preview + if (templateType == CameraDevice.TEMPLATE_PREVIEW) { + previewSurface =flutterSurface; + } + List remainingSurfaces = Arrays.asList(surfaces); if (templateType != CameraDevice.TEMPLATE_PREVIEW) { // If it is not preview mode, add all surfaces as targets. @@ -431,7 +511,7 @@ public void onConfigured(@NonNull CameraCaptureSession session) { dartMessenger.sendCameraErrorEvent("The camera was closed during configuration."); return; } - mPreviewSession = session; + captureSession = session; updateFpsRange(); updateFocus(mPreviewRequestBuilder); @@ -483,13 +563,13 @@ private void createCaptureSessionWithSessionConfig( private void createCaptureSession( List surfaces, CameraCaptureSession.StateCallback callback) throws CameraAccessException { - cameraDevice.createCaptureSession(surfaces, callback, null); + cameraDevice.createCaptureSession(surfaces, callback, mBackgroundHandler); } private void refreshPreviewCaptureSession( @Nullable Runnable onSuccessCallback, @NonNull ErrorCallback onErrorCallback) { Log.i(TAG, "refreshPreviewCaptureSession"); - if (mPreviewSession == null) { + if (captureSession == null) { Log.i(TAG, "[refreshPreviewCaptureSession] mPreviewSession null, returning"); return; } @@ -497,7 +577,7 @@ private void refreshPreviewCaptureSession( try { // mPreviewRequestBuilder.set(CaptureRequest.CONTROL_AE_PRECAPTURE_TRIGGER, // CaptureRequest.CONTROL_AE_PRECAPTURE_TRIGGER_START); - mPreviewSession.setRepeatingRequest(mPreviewRequestBuilder.build(), + captureSession.setRepeatingRequest(mPreviewRequestBuilder.build(), mCaptureCallback, mBackgroundHandler); if (onSuccessCallback != null) { @@ -560,7 +640,7 @@ private void runPrecaptureSequence() { mPreviewRequestBuilder.set(CaptureRequest.CONTROL_AE_PRECAPTURE_TRIGGER, CaptureRequest.CONTROL_AE_PRECAPTURE_TRIGGER_START); - mPreviewSession.capture(mPreviewRequestBuilder.build(), mCaptureCallback, + captureSession.capture(mPreviewRequestBuilder.build(), mCaptureCallback, mBackgroundHandler); } catch (CameraAccessException e) { pictureCaptureRequest.error("cameraAccess", e.getMessage(), null); @@ -571,34 +651,52 @@ private void runPrecaptureSequence() { * Capture a still picture. This method should be called when we get a response in * {@link #mCaptureCallback} from both lockFocus(). */ - private void captureStillPicture() { + private void takePictureAfterPrecapture() { Log.i(TAG, "captureStillPicture"); try { if (null == cameraDevice) { return; } // This is the CaptureRequest.Builder that we use to take a picture. - final CaptureRequest.Builder captureBuilder = + final CaptureRequest.Builder stillBuilder = cameraDevice.createCaptureRequest(CameraDevice.TEMPLATE_STILL_CAPTURE); - captureBuilder.addTarget(pictureImageReader.getSurface()); + stillBuilder.addTarget(pictureImageReader.getSurface()); + + // Add the preview surface to reduce delay when capturing + stillBuilder.addTarget(previewSurface); // Zoom - captureBuilder.set( + stillBuilder.set( CaptureRequest.SCALER_CROP_REGION, mPreviewRequestBuilder.get(CaptureRequest.SCALER_CROP_REGION)); // Set focus / flash from preview mode - updateFlash(captureBuilder); - updateFocus(captureBuilder); - updateExposureMode(captureBuilder); + updateFlash(stillBuilder); + updateFocus(stillBuilder); + updateExposureMode(stillBuilder); // Orientation int rotation = activity.getWindowManager().getDefaultDisplay().getRotation(); - captureBuilder.set(CaptureRequest.JPEG_ORIENTATION, getOrientation(rotation)); + stillBuilder.set(CaptureRequest.JPEG_ORIENTATION, getOrientation(rotation)); CameraCaptureSession.CaptureCallback CaptureCallback = new CameraCaptureSession.CaptureCallback() { + @Override + public void onCaptureStarted(@NonNull CameraCaptureSession session, + @NonNull CaptureRequest request, + long timestamp, + long frameNumber) { + Log.i(TAG, "onCaptureStarted"); + } + + @Override + public void onCaptureProgressed(@NonNull CameraCaptureSession session, + @NonNull CaptureRequest request, + @NonNull CaptureResult partialResult) { + Log.i(TAG, "onCaptureProgressed"); + } + @Override public void onCaptureCompleted(@NonNull CameraCaptureSession session, @NonNull CaptureRequest request, @@ -608,9 +706,15 @@ public void onCaptureCompleted(@NonNull CameraCaptureSession session, } }; - mPreviewSession.stopRepeating(); - mPreviewSession.abortCaptures(); - mPreviewSession.capture(captureBuilder.build(), CaptureCallback, null); + Log.i(TAG, "stopRepeating"); + captureSession.stopRepeating(); + + Log.i(TAG, "abortCaptures"); + captureSession.abortCaptures(); + + Log.i(TAG, "sending capture request"); + captureSession.capture(stillBuilder.build(), CaptureCallback, mBackgroundHandler); +// captureSession.setRepeatingRequest(mPreviewRequestBuilder.build(), null, mBackgroundHandler); } catch (CameraAccessException e) { pictureCaptureRequest.error("cameraAccess", e.getMessage(), null); } @@ -730,91 +834,6 @@ private int getOrientation(int rotation) { return (ORIENTATIONS.get(rotation) + sensorOrientation + 270) % 360; } - /** - * A {@link CameraCaptureSession.CaptureCallback} that handles events related to JPEG capture. - */ - private final CameraCaptureSession.CaptureCallback mCaptureCallback - = new CameraCaptureSession.CaptureCallback() { - - private void process(CaptureResult result) { - if (pictureCaptureRequest == null) { - return; - } - - Integer aeState = result.get(CaptureResult.CONTROL_AE_STATE); - Integer afState = result.get(CaptureResult.CONTROL_AF_STATE); - - if (cameraState != CameraState.STATE_PREVIEW) { - Log.i(TAG, "mCaptureCallback | state: " + cameraState + " | afState: " + afState + " | aeState: " + aeState); - } - - switch (cameraState) { - case STATE_PREVIEW: { - // We have nothing to do when the camera preview is working normally. - break; - } - - case STATE_WAITING_FOCUS: { - if (afState == null) { - cameraState = CameraState.STATE_CAPTURING; - captureStillPicture(); - } else if (afState == CaptureResult.CONTROL_AF_STATE_FOCUSED_LOCKED || - afState == CaptureResult.CONTROL_AF_STATE_NOT_FOCUSED_LOCKED) { - // CONTROL_AE_STATE can be null on some devices - - if (aeState == null || - aeState == CaptureResult.CONTROL_AE_STATE_CONVERGED) { - cameraState = CameraState.STATE_CAPTURING; - captureStillPicture(); - } else { - runPrecaptureSequence(); - } - } - break; - } - - case STATE_WAITING_PRECAPTURE_START: { - // CONTROL_AE_STATE can be null on some devices - if (aeState == null || - aeState == CaptureResult.CONTROL_AE_STATE_CONVERGED || - aeState == CaptureResult.CONTROL_AE_STATE_PRECAPTURE || - aeState == CaptureResult.CONTROL_AE_STATE_FLASH_REQUIRED) { - cameraState = CameraState.STATE_WAITING_PRECAPTURE_DONE; - pictureCaptureRequest.setPreCaptureStartTime(); - } - break; - } - - case STATE_WAITING_PRECAPTURE_DONE: { - // CONTROL_AE_STATE can be null on some devices - if (aeState == null || aeState != CaptureResult.CONTROL_AE_STATE_PRECAPTURE) { - cameraState = CameraState.STATE_CAPTURING; - captureStillPicture(); - } else { - if (pictureCaptureRequest.hitPreCaptureTimeout()) { - unlockAutoFocus(); - } - } - break; - } - } - } - - @Override - public void onCaptureProgressed(@NonNull CameraCaptureSession session, - @NonNull CaptureRequest request, - @NonNull CaptureResult partialResult) { - process(partialResult); - } - - @Override - public void onCaptureCompleted(@NonNull CameraCaptureSession session, - @NonNull CaptureRequest request, - @NonNull TotalCaptureResult result) { - process(result); - } - }; - /** * Trigger auto focus to start and refresh preview capture session. */ @@ -824,7 +843,7 @@ private void startAutoFocus() { CaptureRequest.CONTROL_AF_TRIGGER, CaptureRequest.CONTROL_AF_TRIGGER_START); try { - mPreviewSession.capture(mPreviewRequestBuilder.build(), mCaptureCallback, + captureSession.capture(mPreviewRequestBuilder.build(), mCaptureCallback, mBackgroundHandler); } catch (CameraAccessException e) { pictureCaptureRequest.error("cannotStartAutofocus", e.getMessage(), null); @@ -865,15 +884,15 @@ private void unlockAutoFocus() { CaptureRequest.CONTROL_AF_TRIGGER, CameraMetadata.CONTROL_AF_TRIGGER_CANCEL); // Reset AE state. If we don't call this then the preview won't show AE again. - mPreviewRequestBuilder.set(CaptureRequest.CONTROL_AE_PRECAPTURE_TRIGGER, - CaptureRequest.CONTROL_AE_PRECAPTURE_TRIGGER_IDLE); + mPreviewRequestBuilder.set(CaptureRequest.CONTROL_AE_PRECAPTURE_TRIGGER, + CaptureRequest.CONTROL_AE_PRECAPTURE_TRIGGER_IDLE); updateFocus(mPreviewRequestBuilder); updateFlash(mPreviewRequestBuilder); updateExposureMode(mPreviewRequestBuilder); try { - mPreviewSession.capture(mPreviewRequestBuilder.build(), null, null); + captureSession.capture(mPreviewRequestBuilder.build(), null, mBackgroundHandler); } catch (CameraAccessException ignored) { } @@ -914,7 +933,7 @@ public void stopVideoRecording(@NonNull final Result result) { recordingVideo = false; try { - mPreviewSession.abortCaptures(); + captureSession.abortCaptures(); mediaRecorder.stop(); } catch (CameraAccessException | IllegalStateException e) { // Ignore exceptions and try to continue (changes are camera session already aborted capture) @@ -1002,7 +1021,7 @@ public void setFlashMode(@NonNull final Result result, FlashMode newMode) // If switching directly from torch to auto or on, make sure we turn off the torch. if (oldFlashMode == FlashMode.torch && newMode != FlashMode.torch && newMode != FlashMode.off) { // TODO: why cant we just call refresh preview here? - mPreviewSession.setRepeatingRequest( + captureSession.setRepeatingRequest( mPreviewRequestBuilder.build(), new CaptureCallback() { private boolean isFinished = false; @@ -1228,7 +1247,7 @@ public void setExposureOffset(@NonNull final Result result, double offset) // Apply it updateExposureMode(mPreviewRequestBuilder); // TODO: refresh preview session? - this.mPreviewSession.setRepeatingRequest(mPreviewRequestBuilder.build(), null, null); + this.captureSession.setRepeatingRequest(mPreviewRequestBuilder.build(), null, mBackgroundHandler); result.success(offset); } @@ -1259,7 +1278,7 @@ public void setZoomLevel(@NonNull final Result result, float zoom) throws Camera if (mPreviewRequestBuilder != null) { final Rect computedZoom = cameraZoom.computeZoom(zoom); mPreviewRequestBuilder.set(CaptureRequest.SCALER_CROP_REGION, computedZoom); - mPreviewSession.setRepeatingRequest(mPreviewRequestBuilder.build(), null, null); + captureSession.setRepeatingRequest(mPreviewRequestBuilder.build(), null, mBackgroundHandler); } result.success(null); @@ -1341,7 +1360,7 @@ public void onListen(Object o, EventChannel.EventSink imageStreamSink) { @Override public void onCancel(Object o) { - imageStreamReader.setOnImageAvailableListener(null, null); + imageStreamReader.setOnImageAvailableListener(null, mBackgroundHandler); } }); } @@ -1376,23 +1395,24 @@ private void setImageStreamImageAvailableListener(final EventChannel.EventSink i imageStreamSink.success(imageBuffer); img.close(); }, - null); + mBackgroundHandler + ); } public void stopImageStream() throws CameraAccessException { if (imageStreamReader != null) { - imageStreamReader.setOnImageAvailableListener(null, null); + imageStreamReader.setOnImageAvailableListener(null, mBackgroundHandler); } startPreview(); } private void closeCaptureSession() { - if (mPreviewSession != null) { + if (captureSession != null) { Log.i(TAG, "closeCaptureSession"); - mPreviewSession.close(); - mPreviewSession = null; + captureSession.close(); + captureSession = null; } } diff --git a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/ImageSaver.java b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/ImageSaver.java index 12d2accce199..954818606db9 100644 --- a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/ImageSaver.java +++ b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/ImageSaver.java @@ -24,6 +24,12 @@ public class ImageSaver implements Runnable { */ private final File mFile; + /** + * For running background tasks + */ + private Handler mBackgroundHandler; + + /** * * Used to finish the picture capture request @@ -34,6 +40,7 @@ public class ImageSaver implements Runnable { mImage = image; mFile = file; mPictureCaptureRequest = pictureCaptureRequest; + mBackgroundHandler = mBackgroundHandler; } @Override From 5dbd500c39df912fe5720bb32740110ed6f21a12 Mon Sep 17 00:00:00 2001 From: Andrew Coutts Date: Mon, 1 Mar 2021 08:06:46 -0500 Subject: [PATCH 034/114] Can't fix delay on pixel yet --- .../io/flutter/plugins/camera/Camera.java | 25 ++++++++++--------- .../example/ios/Flutter/Flutter.podspec | 18 +++++++++++++ .../xcshareddata/IDEWorkspaceChecks.plist | 8 ++++++ 3 files changed, 39 insertions(+), 12 deletions(-) create mode 100644 packages/camera/camera/example/ios/Flutter/Flutter.podspec create mode 100644 packages/camera/camera/example/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist 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 55d38e5dd9e2..e9a0d4857f85 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 @@ -127,14 +127,7 @@ public class Camera { * A {@link Handler} for running tasks in the background. */ private Handler mBackgroundHandler; - - /** - * The preview surface which will be provided to still capture requests to keep the - * preview going during capture. - */ - private Surface previewSurface; - - /** + /** * This a callback object for the {@link ImageReader}. "onImageAvailable" will be called when a * still image is ready to be saved. */ @@ -149,6 +142,11 @@ public void onImageAvailable(ImageReader reader) { } }; + /** + * The preview surface which will be provided to still capture requests to keep the + * preview going during capture. + */ + private Surface previewSurface; /** * An additional thread for running tasks that shouldn't block the UI. */ @@ -380,7 +378,9 @@ private void prepareMediaRecorder(String outputFilePath) throws IOException { public void open(String imageFormatGroup) throws CameraAccessException { pictureImageReader = ImageReader.newInstance( - captureSize.getWidth(), captureSize.getHeight(), ImageFormat.JPEG, 2); + captureSize.getWidth(), + captureSize.getHeight(), + ImageFormat.JPEG, 2); Integer imageFormat = supportedImageFormats.get(imageFormatGroup); if (imageFormat == null) { @@ -488,7 +488,7 @@ private void createCaptureSession( // Save surface if it's preview if (templateType == CameraDevice.TEMPLATE_PREVIEW) { - previewSurface =flutterSurface; + previewSurface = flutterSurface; } List remainingSurfaces = Arrays.asList(surfaces); @@ -662,8 +662,8 @@ private void takePictureAfterPrecapture() { cameraDevice.createCaptureRequest(CameraDevice.TEMPLATE_STILL_CAPTURE); stillBuilder.addTarget(pictureImageReader.getSurface()); - // Add the preview surface to reduce delay when capturing - stillBuilder.addTarget(previewSurface); + // Add the preview surface to show image after it's captured +// stillBuilder.addTarget(previewSurface); // Zoom stillBuilder.set( @@ -715,6 +715,7 @@ public void onCaptureCompleted(@NonNull CameraCaptureSession session, Log.i(TAG, "sending capture request"); captureSession.capture(stillBuilder.build(), CaptureCallback, mBackgroundHandler); // captureSession.setRepeatingRequest(mPreviewRequestBuilder.build(), null, mBackgroundHandler); +// pictureCaptureRequest.finish("a"); } catch (CameraAccessException e) { pictureCaptureRequest.error("cameraAccess", e.getMessage(), null); } diff --git a/packages/camera/camera/example/ios/Flutter/Flutter.podspec b/packages/camera/camera/example/ios/Flutter/Flutter.podspec new file mode 100644 index 000000000000..2c4421cfe51e --- /dev/null +++ b/packages/camera/camera/example/ios/Flutter/Flutter.podspec @@ -0,0 +1,18 @@ +# +# NOTE: This podspec is NOT to be published. It is only used as a local source! +# This is a generated file; do not edit or check into version control. +# + +Pod::Spec.new do |s| + s.name = 'Flutter' + s.version = '1.0.0' + s.summary = 'High-performance, high-fidelity mobile apps.' + s.homepage = 'https://flutter.io' + s.license = { :type => 'MIT' } + s.author = { 'Flutter Dev Team' => 'flutter-dev@googlegroups.com' } + s.source = { :git => 'https://github.com/flutter/engine', :tag => s.version.to_s } + s.ios.deployment_target = '8.0' + # Framework linking is handled by Flutter tooling, not CocoaPods. + # Add a placeholder to satisfy `s.dependency 'Flutter'` plugin podspecs. + s.vendored_frameworks = 'path/to/nothing' +end diff --git a/packages/camera/camera/example/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/packages/camera/camera/example/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 000000000000..18d981003d68 --- /dev/null +++ b/packages/camera/camera/example/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + From 04d047699f6ae67eba7ab06fdae0e0a40130e2c8 Mon Sep 17 00:00:00 2001 From: Andrew Coutts Date: Mon, 1 Mar 2021 11:04:23 -0500 Subject: [PATCH 035/114] Focus lock works now for image capture --- .../io/flutter/plugins/camera/Camera.java | 399 ++++++++++-------- packages/camera/camera/example/lib/main.dart | 2 +- 2 files changed, 233 insertions(+), 168 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 e9a0d4857f85..15a04d446250 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 @@ -13,12 +13,10 @@ import android.graphics.SurfaceTexture; import android.hardware.camera2.CameraAccessException; import android.hardware.camera2.CameraCaptureSession; -import android.hardware.camera2.CameraCaptureSession.CaptureCallback; import android.hardware.camera2.CameraCharacteristics; import android.hardware.camera2.CameraDevice; import android.hardware.camera2.CameraManager; import android.hardware.camera2.CameraMetadata; -import android.hardware.camera2.CaptureFailure; import android.hardware.camera2.CaptureRequest; import android.hardware.camera2.CaptureResult; import android.hardware.camera2.TotalCaptureResult; @@ -111,14 +109,14 @@ public class Camera { private final CameraZoom cameraZoom; private final CameraCharacteristics cameraCharacteristics; private final Activity activity; - /** - * Whether the current camera device supports auto focus or not. - */ - private final boolean mAutoFocusSupported = false; /** * This manages the state of the camera and the current capture request. */ PictureCaptureRequest pictureCaptureRequest; + /** + * Whether the current camera device supports auto focus or not. + */ + private boolean mAutoFocusSupported = true; /** * The state of the camera. By default we are in the preview state. */ @@ -142,11 +140,7 @@ public void onImageAvailable(ImageReader reader) { } }; - /** - * The preview surface which will be provided to still capture requests to keep the - * preview going during capture. - */ - private Surface previewSurface; + /** * An additional thread for running tasks that shouldn't block the UI. */ @@ -162,7 +156,6 @@ public void onImageAvailable(ImageReader reader) { /** * {@link CaptureRequest} generated by {@link #mPreviewRequestBuilder} */ - private CaptureRequest mPreviewRequest; private MediaRecorder mediaRecorder; private boolean recordingVideo; private File videoRecordingFile; @@ -170,17 +163,17 @@ public void onImageAvailable(ImageReader reader) { * Flash mode setting of the current camera. Initialize to off because * we don't know if the current camera supports flash yet. */ - private FlashMode currentFlashMode = FlashMode.off; + private FlashMode currentFlashMode; /** * Exposure mode setting of the current camera. Initialize to auto * because all cameras support autoexposure by default. */ - private ExposureMode exposureMode = ExposureMode.auto; + private ExposureMode exposureMode; /** * Focus mode setting of the current camera. Initialize to locked because * we don't know if the current camera supports autofocus yet. */ - private FocusMode currentFocusMode = FocusMode.locked; + private FocusMode currentFocusMode; /** * Whether or not to use autofocus. */ @@ -198,10 +191,6 @@ public void onImageAvailable(ImageReader reader) { = new CameraCaptureSession.CaptureCallback() { private void process(CaptureResult result) { - if (pictureCaptureRequest == null) { - return; - } - Integer aeState = result.get(CaptureResult.CONTROL_AE_STATE); Integer afState = result.get(CaptureResult.CONTROL_AF_STATE); @@ -219,12 +208,14 @@ private void process(CaptureResult result) { if (afState == null) { cameraState = CameraState.STATE_CAPTURING; takePictureAfterPrecapture(); - } else if (afState == CaptureResult.CONTROL_AF_STATE_FOCUSED_LOCKED || - afState == CaptureResult.CONTROL_AF_STATE_NOT_FOCUSED_LOCKED) { + } else if ( + afState == CaptureRequest.CONTROL_AF_STATE_PASSIVE_SCAN || + afState == CaptureRequest.CONTROL_AF_STATE_FOCUSED_LOCKED || + afState == CaptureRequest.CONTROL_AF_STATE_NOT_FOCUSED_LOCKED) { // CONTROL_AE_STATE can be null on some devices if (aeState == null || - aeState == CaptureResult.CONTROL_AE_STATE_CONVERGED) { + aeState == CaptureRequest.CONTROL_AE_STATE_CONVERGED) { cameraState = CameraState.STATE_CAPTURING; takePictureAfterPrecapture(); } else { @@ -303,32 +294,69 @@ public Camera( this.currentFocusMode = FocusMode.auto; this.exposureOffset = 0; + // Get camera characteristics and check for supported features cameraCharacteristics = cameraManager.getCameraCharacteristics(cameraName); getAvailableFpsRange(cameraCharacteristics); + checkAutoFocusSupported(); + checkFlashSupported(); + + // Setup orientation sensorOrientation = cameraCharacteristics.get(CameraCharacteristics.SENSOR_ORIENTATION); isFrontFacing = cameraCharacteristics.get(CameraCharacteristics.LENS_FACING) == CameraMetadata.LENS_FACING_FRONT; + deviceOrientationListener = + new DeviceOrientationManager(activity, dartMessenger, isFrontFacing, sensorOrientation); + deviceOrientationListener.start(); + + // Resolution configuration ResolutionPreset preset = ResolutionPreset.valueOf(resolutionPreset); recordingProfile = CameraUtils.getBestAvailableCamcorderProfileForResolutionPreset(cameraName, preset); captureSize = new Size(recordingProfile.videoFrameWidth, recordingProfile.videoFrameHeight); previewSize = computeBestPreviewSize(cameraName, preset); + + // Zoom setup cameraZoom = new CameraZoom( cameraCharacteristics.get(CameraCharacteristics.SENSOR_INFO_ACTIVE_ARRAY_SIZE), cameraCharacteristics.get(CameraCharacteristics.SCALER_AVAILABLE_MAX_DIGITAL_ZOOM)); - deviceOrientationListener = - new DeviceOrientationManager(activity, dartMessenger, isFrontFacing, sensorOrientation); - deviceOrientationListener.start(); - - // Check if the flash is supported. - Boolean flashAvailable = cameraCharacteristics.get(CameraCharacteristics.FLASH_INFO_AVAILABLE); - mFlashSupported = flashAvailable == null ? false : flashAvailable; + // Start background thread. startBackgroundThread(); } + /** + * Check if the auto focus is supported. + */ + private void checkAutoFocusSupported() { + int[] modes = cameraCharacteristics.get(CameraCharacteristics.CONTROL_AF_AVAILABLE_MODES); + Log.i(TAG, "checkAutoFocusSupported | modes:"); + for (int mode : modes) { + Log.i(TAG, "checkAutoFocusSupported | ==> " + mode); + } + + // Check if fixed focal length lens. If LENS_INFO_MINIMUM_FOCUS_DISTANCE=0, then this is fixed. + // Can be null on some devices. + float minFocus = cameraCharacteristics.get(CameraCharacteristics.LENS_INFO_MINIMUM_FOCUS_DISTANCE); + float maxFocus = cameraCharacteristics.get(CameraCharacteristics.LENS_INFO_HYPERFOCAL_DISTANCE); + final boolean isFixedLength = minFocus == 0; + Log.i(TAG, "checkAutoFocusSupported | minFocus " + minFocus + " | maxFocus: " + maxFocus); + + + mAutoFocusSupported = !(modes == null || modes.length == 0 || + (modes.length == 1 && modes[0] == CameraCharacteristics.CONTROL_AF_MODE_OFF)); + Log.i(TAG, "checkAutoFocusSupported: " + mAutoFocusSupported); + } + + /** + * Check if the flash is supported. + */ + private void checkFlashSupported() { + Boolean available = cameraCharacteristics.get(CameraCharacteristics.FLASH_INFO_AVAILABLE); + mFlashSupported = available == null ? false : available; + } + /** * Load available FPS range for the current camera and update the available fps range with it. * @@ -486,11 +514,6 @@ private void createCaptureSession( Surface flutterSurface = new Surface(surfaceTexture); mPreviewRequestBuilder.addTarget(flutterSurface); - // Save surface if it's preview - if (templateType == CameraDevice.TEMPLATE_PREVIEW) { - previewSurface = flutterSurface; - } - List remainingSurfaces = Arrays.asList(surfaces); if (templateType != CameraDevice.TEMPLATE_PREVIEW) { // If it is not preview mode, add all surfaces as targets. @@ -514,7 +537,7 @@ public void onConfigured(@NonNull CameraCaptureSession session) { captureSession = session; updateFpsRange(); - updateFocus(mPreviewRequestBuilder); + updateFocusMode(mPreviewRequestBuilder); updateFlash(mPreviewRequestBuilder); updateExposureMode(mPreviewRequestBuilder); @@ -566,6 +589,7 @@ private void createCaptureSession( cameraDevice.createCaptureSession(surfaces, callback, mBackgroundHandler); } + // Send a repeating request to refresh our capture session. private void refreshPreviewCaptureSession( @Nullable Runnable onSuccessCallback, @NonNull ErrorCallback onErrorCallback) { Log.i(TAG, "refreshPreviewCaptureSession"); @@ -575,14 +599,19 @@ private void refreshPreviewCaptureSession( } try { -// mPreviewRequestBuilder.set(CaptureRequest.CONTROL_AE_PRECAPTURE_TRIGGER, -// CaptureRequest.CONTROL_AE_PRECAPTURE_TRIGGER_START); captureSession.setRepeatingRequest(mPreviewRequestBuilder.build(), - mCaptureCallback, mBackgroundHandler); + new CameraCaptureSession.CaptureCallback() { + @Override + public void onCaptureCompleted(@NonNull CameraCaptureSession session, + @NonNull CaptureRequest request, + @NonNull TotalCaptureResult result) { + if (onSuccessCallback != null) { + onSuccessCallback.run(); + } + } + }, mBackgroundHandler); + - if (onSuccessCallback != null) { - onSuccessCallback.run(); - } } catch (CameraAccessException | IllegalStateException | IllegalArgumentException e) { onErrorCallback.onError("cameraAccess", e.getMessage()); } @@ -662,9 +691,6 @@ private void takePictureAfterPrecapture() { cameraDevice.createCaptureRequest(CameraDevice.TEMPLATE_STILL_CAPTURE); stillBuilder.addTarget(pictureImageReader.getSurface()); - // Add the preview surface to show image after it's captured -// stillBuilder.addTarget(previewSurface); - // Zoom stillBuilder.set( CaptureRequest.SCALER_CROP_REGION, @@ -672,7 +698,7 @@ private void takePictureAfterPrecapture() { // Set focus / flash from preview mode updateFlash(stillBuilder); - updateFocus(stillBuilder); + updateFocusMode(stillBuilder); updateExposureMode(stillBuilder); // Orientation @@ -811,7 +837,7 @@ void updateFlash(CaptureRequest.Builder requestBuilder) { CaptureRequest.FLASH_MODE_OFF); break; - // TODO: to be implemented someday. Need to add it to dart and iOS. + // TODO: to be implemented someday. Need to add it to dart/iOS as another flash mode setting. // case autoRedEye: // requestBuilder.set(CaptureRequest.CONTROL_AE_MODE, // CaptureRequest.CONTROL_AE_MODE_ON_AUTO_FLASH_REDEYE); @@ -835,22 +861,6 @@ private int getOrientation(int rotation) { return (ORIENTATIONS.get(rotation) + sensorOrientation + 270) % 360; } - /** - * Trigger auto focus to start and refresh preview capture session. - */ - private void startAutoFocus() { - Log.i(TAG, "startAutoFocus"); - mPreviewRequestBuilder.set( - CaptureRequest.CONTROL_AF_TRIGGER, CaptureRequest.CONTROL_AF_TRIGGER_START); - - try { - captureSession.capture(mPreviewRequestBuilder.build(), mCaptureCallback, - mBackgroundHandler); - } catch (CameraAccessException e) { - pictureCaptureRequest.error("cannotStartAutofocus", e.getMessage(), null); - } - } - /** * Start capturing a picture, doing autofocus first. */ @@ -859,19 +869,22 @@ private void runPictureAutoFocus() { assert (pictureCaptureRequest != null); cameraState = CameraState.STATE_WAITING_FOCUS; - lockAutoFocus(); + lockAutoFocus(null); } /** * Start the autofocus routine. */ - private void lockAutoFocus() { + private void lockAutoFocus(Runnable callback) { Log.i(TAG, "lockAutoFocus"); mPreviewRequestBuilder.set( CaptureRequest.CONTROL_AF_TRIGGER, CaptureRequest.CONTROL_AF_TRIGGER_START); - refreshPreviewCaptureSession( - null, (code, message) -> pictureCaptureRequest.error(code, message, null)); + try { + captureSession.capture(mPreviewRequestBuilder.build(), mCaptureCallback, + mBackgroundHandler); + } catch (CameraAccessException e) { + } } /** @@ -879,20 +892,25 @@ private void lockAutoFocus() { */ private void unlockAutoFocus() { Log.i(TAG, "unlockAutoFocus"); + try { + // Cancel existing AF state + mPreviewRequestBuilder.set( + CaptureRequest.CONTROL_AF_TRIGGER, CameraMetadata.CONTROL_AF_TRIGGER_CANCEL); + captureSession.capture(mPreviewRequestBuilder.build(), null, + mBackgroundHandler); - // Reset AF state - mPreviewRequestBuilder.set( - CaptureRequest.CONTROL_AF_TRIGGER, CameraMetadata.CONTROL_AF_TRIGGER_CANCEL); + // Set AE state to idle again + mPreviewRequestBuilder.set(CaptureRequest.CONTROL_AE_PRECAPTURE_TRIGGER, + CaptureRequest.CONTROL_AE_PRECAPTURE_TRIGGER_IDLE); - // Reset AE state. If we don't call this then the preview won't show AE again. - mPreviewRequestBuilder.set(CaptureRequest.CONTROL_AE_PRECAPTURE_TRIGGER, - CaptureRequest.CONTROL_AE_PRECAPTURE_TRIGGER_IDLE); + // Set AF state to idle again + mPreviewRequestBuilder.set( + CaptureRequest.CONTROL_AF_TRIGGER, CameraMetadata.CONTROL_AF_TRIGGER_CANCEL); - updateFocus(mPreviewRequestBuilder); - updateFlash(mPreviewRequestBuilder); - updateExposureMode(mPreviewRequestBuilder); + updateFocusMode(mPreviewRequestBuilder); + updateFlash(mPreviewRequestBuilder); + updateExposureMode(mPreviewRequestBuilder); - try { captureSession.capture(mPreviewRequestBuilder.build(), null, mBackgroundHandler); } catch (CameraAccessException ignored) { } @@ -1000,17 +1018,8 @@ public void resumeVideoRecording(@NonNull final Result result) { * @param newMode * @throws CameraAccessException */ - public void setFlashMode(@NonNull final Result result, FlashMode newMode) - throws CameraAccessException { - // Check if the current camera can modify the flash mode. - Boolean flashAvailable = - cameraManager - .getCameraCharacteristics(cameraDevice.getId()) - .get(CameraCharacteristics.FLASH_INFO_AVAILABLE); - - // Check if flash is available. - if (flashAvailable == null || !flashAvailable) { - result.error("setFlashModeFailed", "Device does not have flash capabilities", null); + public void setFlashMode(@NonNull final Result result, FlashMode newMode) { + if (currentFlashMode == newMode) { return; } @@ -1019,52 +1028,56 @@ public void setFlashMode(@NonNull final Result result, FlashMode newMode) currentFlashMode = newMode; updateFlash(mPreviewRequestBuilder); - // If switching directly from torch to auto or on, make sure we turn off the torch. - if (oldFlashMode == FlashMode.torch && newMode != FlashMode.torch && newMode != FlashMode.off) { - // TODO: why cant we just call refresh preview here? - captureSession.setRepeatingRequest( - mPreviewRequestBuilder.build(), - new CaptureCallback() { - private boolean isFinished = false; - - @Override - public void onCaptureCompleted( - @NonNull CameraCaptureSession session, - @NonNull CaptureRequest request, - @NonNull TotalCaptureResult captureResult) { - if (isFinished) { - return; - } - - updateFlash(mPreviewRequestBuilder); - refreshPreviewCaptureSession( - () -> { - result.success(null); - isFinished = true; - }, - (code, message) -> - result.error("setFlashModeFailed", "Could not set flash mode.", null)); - } - - @Override - public void onCaptureFailed( - @NonNull CameraCaptureSession session, - @NonNull CaptureRequest request, - @NonNull CaptureFailure failure) { - if (isFinished) { - return; - } - - result.error("setFlashModeFailed", "Could not set flash mode.", null); - isFinished = true; - } - }, - null); - } else { - refreshPreviewCaptureSession( - () -> result.success(null), - (code, message) -> result.error("setFlashModeFailed", "Could not set flash mode.", null)); - } + refreshPreviewCaptureSession( + () -> result.success(null), + (code, message) -> result.error("setFlashModeFailed", "Could not set flash mode.", null)); + +// // If switching directly from torch to auto or on, make sure we turn off the torch. +// if (oldFlashMode == FlashMode.torch && newMode != FlashMode.torch && newMode != FlashMode.off) { +// // TODO: why cant we just call refresh preview here? +// captureSession.setRepeatingRequest( +// mPreviewRequestBuilder.build(), +// new CaptureCallback() { +// private boolean isFinished = false; +// +// @Override +// public void onCaptureCompleted( +// @NonNull CameraCaptureSession session, +// @NonNull CaptureRequest request, +// @NonNull TotalCaptureResult captureResult) { +// if (isFinished) { +// return; +// } +// +// updateFlash(mPreviewRequestBuilder); +// refreshPreviewCaptureSession( +// () -> { +// result.success(null); +// isFinished = true; +// }, +// (code, message) -> +// result.error("setFlashModeFailed", "Could not set flash mode.", null)); +// } +// +// @Override +// public void onCaptureFailed( +// @NonNull CameraCaptureSession session, +// @NonNull CaptureRequest request, +// @NonNull CaptureFailure failure) { +// if (isFinished) { +// return; +// } +// +// result.error("setFlashModeFailed", "Could not set flash mode.", null); +// isFinished = true; +// } +// }, +// null); +// } else { +// refreshPreviewCaptureSession( +// () -> result.success(null), +// (code, message) -> result.error("setFlashModeFailed", "Could not set flash mode.", null)); +// } } /** @@ -1114,21 +1127,65 @@ public void setExposurePoint(@NonNull final Result result, Double x, Double y) */ public void setFocusMode(@NonNull final Result result, FocusMode newMode) throws CameraAccessException { + if (currentFocusMode == newMode) { + return; + } + + Log.i(TAG, "setFocusMode: " + newMode); + + // Set new focus mode currentFocusMode = newMode; - updateFocus(mPreviewRequestBuilder); + // Sync new focus mode to the current capture request builder + updateFocusMode(mPreviewRequestBuilder); + + // Now depending on the new mode we either want to restart the AF routine (if setting to auto) + // or we want to trigger a one-time focus and then set AF to idle (locked mode). switch (newMode) { case auto: + Log.i(TAG, "Triggering AF start with mode " + currentFocusMode); + // Reset state of autofocus so it goes back to passive scanning. + mPreviewRequestBuilder.set( + CaptureRequest.CONTROL_AF_TRIGGER, CaptureRequest.CONTROL_AF_TRIGGER_CANCEL); + + // Refresh preview session using repeating request as it will be in CONTROL_AF_MODE_CONTINUOUS_PICTURE refreshPreviewCaptureSession( null, (code, message) -> result.error("setFocusMode", message, null)); break; + case locked: - startAutoFocus(); + // AF mode will be in Auto so we just want to perform one AF routine + mPreviewRequestBuilder.set( + CaptureRequest.CONTROL_AF_TRIGGER, CaptureRequest.CONTROL_AF_TRIGGER_START); + + // Refresh the AF once. When the AF start is completed triggering then we will set it to idle mode. + // If we don't wait for the callback like this, then setting it to idle just resets the focus to infinity + // on some devices like Sony XZ. + try { + captureSession.capture(mPreviewRequestBuilder.build(), new CameraCaptureSession.CaptureCallback() { + @Override + public void onCaptureCompleted(@NonNull CameraCaptureSession session, + @NonNull CaptureRequest request, + @NonNull TotalCaptureResult _result) { + Log.i(TAG, "Success after triggering AF start for locked focus"); + + mPreviewRequestBuilder.set( + CaptureRequest.CONTROL_AF_TRIGGER, CaptureRequest.CONTROL_AF_TRIGGER_IDLE); + refreshPreviewCaptureSession( + null, (code, message) -> result.error("setFocusMode", message, null)); + } + }, + mBackgroundHandler); + } catch (CameraAccessException e) { + result.error("setFocusMode", e.getMessage(), null); + } break; } + result.success(null); } + public void setFocusPoint(@NonNull final Result result, Double x, Double y) throws CameraAccessException { // Check if focus point functionality is available. @@ -1304,40 +1361,48 @@ private void updateFpsRange() { mPreviewRequestBuilder.set(CaptureRequest.CONTROL_AE_TARGET_FPS_RANGE, fpsRange); } - private void updateFocus(CaptureRequest.Builder requestBuilder) { - if (useAutoFocus) { - int[] modes = cameraCharacteristics.get(CameraCharacteristics.CONTROL_AF_AVAILABLE_MODES); - // Auto focus is not supported - if (modes == null - || modes.length == 0 - || (modes.length == 1 && modes[0] == CameraCharacteristics.CONTROL_AF_MODE_OFF)) { - useAutoFocus = false; - mPreviewRequestBuilder.set( - CaptureRequest.CONTROL_AF_MODE, CaptureRequest.CONTROL_AF_MODE_OFF); - } else { - // Applying auto focus - switch (currentFocusMode) { - case locked: - requestBuilder.set( - CaptureRequest.CONTROL_AF_MODE, CaptureRequest.CONTROL_AF_MODE_AUTO); - break; - case auto: - mPreviewRequestBuilder.set( - CaptureRequest.CONTROL_AF_MODE, - recordingVideo - ? CaptureRequest.CONTROL_AF_MODE_CONTINUOUS_VIDEO - : CaptureRequest.CONTROL_AF_MODE_CONTINUOUS_PICTURE); - default: - break; - } - MeteringRectangle afRect = cameraRegions.getAFMeteringRectangle(); - mPreviewRequestBuilder.set( - CaptureRequest.CONTROL_AF_REGIONS, - afRect == null ? null : new MeteringRectangle[]{afRect}); - } + /** + * Sync the focus mode setting to the provided capture request builder. + * + * @param requestBuilder + */ + private void updateFocusMode(CaptureRequest.Builder requestBuilder) { + Log.i(TAG, "updateFocusMode"); + if (!mAutoFocusSupported) { + useAutoFocus = false; + requestBuilder.set( + CaptureRequest.CONTROL_AF_MODE, CaptureRequest.CONTROL_AF_MODE_OFF); } else { - mPreviewRequestBuilder.set(CaptureRequest.CONTROL_AF_MODE, CaptureRequest.CONTROL_AF_MODE_OFF); + switch (currentFocusMode) { + case locked: + useAutoFocus = false; + requestBuilder.set( + CaptureRequest.CONTROL_AF_MODE, CaptureRequest.CONTROL_AF_MODE_AUTO); + break; + + case auto: + useAutoFocus = true; + requestBuilder.set( + CaptureRequest.CONTROL_AF_MODE, + recordingVideo + ? CaptureRequest.CONTROL_AF_MODE_CONTINUOUS_VIDEO + : CaptureRequest.CONTROL_AF_MODE_CONTINUOUS_PICTURE); + default: + break; + } } + + // Some devices use an extremely high noise reduction setting by default (pixel 4 selfie mode), which + // causes the preview/capture to look blurry and out of focus. To fix this we set NR to off. + // TODO: we should add a noise reduction setting in dart/ios in the future. + requestBuilder.set(CaptureRequest.NOISE_REDUCTION_MODE, CaptureRequest.NOISE_REDUCTION_MODE_OFF); + + + // Update metering + MeteringRectangle afRect = cameraRegions.getAFMeteringRectangle(); + requestBuilder.set( + CaptureRequest.CONTROL_AF_REGIONS, + afRect == null ? null : new MeteringRectangle[]{afRect}); } public void startPreview() throws CameraAccessException { diff --git a/packages/camera/camera/example/lib/main.dart b/packages/camera/camera/example/lib/main.dart index 5eebc9a379ca..2bfeda1ca60e 100644 --- a/packages/camera/camera/example/lib/main.dart +++ b/packages/camera/camera/example/lib/main.dart @@ -599,7 +599,7 @@ class _CameraExampleHomeState extends State } final CameraController cameraController = CameraController( cameraDescription, - ResolutionPreset.medium, + ResolutionPreset.max, enableAudio: enableAudio, imageFormatGroup: ImageFormatGroup.jpeg, ); From d0514b541b0b044f2610ea76a379c1d5649c3e47 Mon Sep 17 00:00:00 2001 From: Andrew Coutts Date: Mon, 1 Mar 2021 11:48:21 -0500 Subject: [PATCH 036/114] Photo capture works in focus auto/locked state now --- .../io/flutter/plugins/camera/Camera.java | 61 +++++++++---------- 1 file changed, 29 insertions(+), 32 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 15a04d446250..cbef9a3028c2 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 @@ -195,7 +195,7 @@ private void process(CaptureResult result) { Integer afState = result.get(CaptureResult.CONTROL_AF_STATE); if (cameraState != CameraState.STATE_PREVIEW) { - Log.i(TAG, "mCaptureCallback | state: " + cameraState + " | afState: " + afState + " | aeState: " + aeState); + Log.i(TAG, "mCaptureCallback | state: " + cameraState + " | afState: " + afState + " | aeState: " + aeState); } switch (cameraState) { @@ -599,17 +599,10 @@ private void refreshPreviewCaptureSession( } try { - captureSession.setRepeatingRequest(mPreviewRequestBuilder.build(), - new CameraCaptureSession.CaptureCallback() { - @Override - public void onCaptureCompleted(@NonNull CameraCaptureSession session, - @NonNull CaptureRequest request, - @NonNull TotalCaptureResult result) { - if (onSuccessCallback != null) { - onSuccessCallback.run(); - } - } - }, mBackgroundHandler); + captureSession.setRepeatingRequest( + mPreviewRequestBuilder.build(), + mCaptureCallback, + mBackgroundHandler); } catch (CameraAccessException | IllegalStateException | IllegalArgumentException e) { @@ -655,24 +648,29 @@ public void takePicture(@NonNull final Result result) { private void runPrecaptureSequence() { Log.i(TAG, "runPrecaptureSequence"); try { + // First set precapture state to idle or else it can hang in STATE_WAITING_PRECAPTURE + mPreviewRequestBuilder.set(CaptureRequest.CONTROL_AE_PRECAPTURE_TRIGGER, + CaptureRequest.CONTROL_AE_PRECAPTURE_TRIGGER_IDLE); + captureSession.capture(mPreviewRequestBuilder.build(), mCaptureCallback, + mBackgroundHandler); -// // First set precapture state to idle or else it can hang in STATE_WAITING_PRECAPTURE -// mPreviewRequestBuilder.set(CaptureRequest.CONTROL_AE_PRECAPTURE_TRIGGER, -// CaptureRequest.CONTROL_AE_PRECAPTURE_TRIGGER_IDLE); -// mPreviewSession.capture(mPreviewRequestBuilder.build(), mCaptureCallback, -// mBackgroundHandler); -// refreshPreviewCaptureSession( -// null, (code, message) -> pictureCaptureRequest.error(code, message, null)); + // Repeating request to refresh preview session + refreshPreviewCaptureSession( + null, + (code, message) -> pictureCaptureRequest.error("cameraAccess", message, null)); // Start precapture now cameraState = CameraState.STATE_WAITING_PRECAPTURE_START; mPreviewRequestBuilder.set(CaptureRequest.CONTROL_AE_PRECAPTURE_TRIGGER, CaptureRequest.CONTROL_AE_PRECAPTURE_TRIGGER_START); + + // Trigger one capture to start AE sequence captureSession.capture(mPreviewRequestBuilder.build(), mCaptureCallback, mBackgroundHandler); + } catch (CameraAccessException e) { - pictureCaptureRequest.error("cameraAccess", e.getMessage(), null); + e.printStackTrace(); } } @@ -869,22 +867,24 @@ private void runPictureAutoFocus() { assert (pictureCaptureRequest != null); cameraState = CameraState.STATE_WAITING_FOCUS; - lockAutoFocus(null); + lockAutoFocus(); } /** * Start the autofocus routine. */ - private void lockAutoFocus(Runnable callback) { + private void lockAutoFocus() { Log.i(TAG, "lockAutoFocus"); mPreviewRequestBuilder.set( CaptureRequest.CONTROL_AF_TRIGGER, CaptureRequest.CONTROL_AF_TRIGGER_START); - try { - captureSession.capture(mPreviewRequestBuilder.build(), mCaptureCallback, - mBackgroundHandler); - } catch (CameraAccessException e) { - } + refreshPreviewCaptureSession( + null, (code, message) -> pictureCaptureRequest.error(code, message, null)); +// try { +// captureSession.capture(mPreviewRequestBuilder.build(), mCaptureCallback, +// mBackgroundHandler); +// } catch (CameraAccessException e) { +// } } /** @@ -1127,10 +1127,6 @@ public void setExposurePoint(@NonNull final Result result, Double x, Double y) */ public void setFocusMode(@NonNull final Result result, FocusMode newMode) throws CameraAccessException { - if (currentFocusMode == newMode) { - return; - } - Log.i(TAG, "setFocusMode: " + newMode); // Set new focus mode @@ -1367,7 +1363,8 @@ private void updateFpsRange() { * @param requestBuilder */ private void updateFocusMode(CaptureRequest.Builder requestBuilder) { - Log.i(TAG, "updateFocusMode"); + Log.i(TAG, "updateFocusMode currentFocusMode: " + currentFocusMode); + if (!mAutoFocusSupported) { useAutoFocus = false; requestBuilder.set( From e1ebde4c7c8361a13d0f94e19f8b055a2e83000e Mon Sep 17 00:00:00 2001 From: Andrew Coutts Date: Mon, 1 Mar 2021 13:08:58 -0500 Subject: [PATCH 037/114] Setting exposure mode works now --- .../java/io/flutter/plugins/camera/Camera.java | 17 ++++++++++------- .../plugins/camera/PictureCaptureRequest.java | 2 ++ 2 files changed, 12 insertions(+), 7 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 cbef9a3028c2..7a31b90ebfe9 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 @@ -168,7 +168,7 @@ public void onImageAvailable(ImageReader reader) { * Exposure mode setting of the current camera. Initialize to auto * because all cameras support autoexposure by default. */ - private ExposureMode exposureMode; + private ExposureMode currentExposureMode; /** * Focus mode setting of the current camera. Initialize to locked because * we don't know if the current camera supports autofocus yet. @@ -290,7 +290,7 @@ public Camera( this.cameraManager = (CameraManager) activity.getSystemService(Context.CAMERA_SERVICE); this.applicationContext = activity.getApplicationContext(); this.currentFlashMode = FlashMode.off; - this.exposureMode = ExposureMode.auto; + this.currentExposureMode = ExposureMode.auto; this.currentFocusMode = FocusMode.auto; this.exposureOffset = 0; @@ -434,7 +434,7 @@ public void onOpened(@NonNull CameraDevice device) { dartMessenger.sendCameraInitializedEvent( previewSize.getWidth(), previewSize.getHeight(), - exposureMode, + currentExposureMode, currentFocusMode, isExposurePointSupported(), isFocusPointSupported()); @@ -778,11 +778,11 @@ void updateExposureMode(CaptureRequest.Builder requestBuilder) { // Applying auto exposure MeteringRectangle aeRect = cameraRegions.getAEMeteringRectangle(); - mPreviewRequestBuilder.set( + requestBuilder.set( CaptureRequest.CONTROL_AE_REGIONS, aeRect == null ? null : new MeteringRectangle[]{cameraRegions.getAEMeteringRectangle()}); - switch (exposureMode) { + switch (currentExposureMode) { case locked: requestBuilder.set(CaptureRequest.CONTROL_AE_LOCK, true); break; @@ -1089,11 +1089,14 @@ public void setFlashMode(@NonNull final Result result, FlashMode newMode) { */ public void setExposureMode(@NonNull final Result result, ExposureMode newMode) throws CameraAccessException { - exposureMode = newMode; + currentExposureMode = newMode; + updateExposureMode(mPreviewRequestBuilder); refreshPreviewCaptureSession( - () -> result.success(null), + null, (code, message) -> result.error("setExposureModeFailed", "Could not set exposure mode.", null)); + + result.success(null); } public void setExposurePoint(@NonNull final Result result, Double x, Double y) diff --git a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/PictureCaptureRequest.java b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/PictureCaptureRequest.java index d6f3ee40b546..2debeda8be57 100644 --- a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/PictureCaptureRequest.java +++ b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/PictureCaptureRequest.java @@ -56,6 +56,7 @@ class PictureCaptureRequest { * The state of this picture capture request. */ private PictureCaptureRequestState state = PictureCaptureRequestState.STATE_IDLE; + private final Runnable timeoutCallback = () -> error("captureTimeout", "Picture capture request timed out", state.toString()); @@ -128,6 +129,7 @@ public void error( * @return true if the timeout is reached; otherwise false is returned. */ public boolean hitPreCaptureTimeout() { + Log.i("Camera", "hitPreCaptureTimeout | Time elapsed: " + (SystemClock.elapsedRealtime() - preCaptureStartTime)); return (SystemClock.elapsedRealtime() - preCaptureStartTime) > PRECAPTURE_TIMEOUT_MS; } From 56fd09ad328aee68b7f4c7ff52ef53450c35943d Mon Sep 17 00:00:00 2001 From: Andrew Coutts Date: Mon, 1 Mar 2021 13:23:10 -0500 Subject: [PATCH 038/114] Cleanup --- .../java/io/flutter/plugins/camera/Camera.java | 17 ++++------------- .../plugins/camera/PictureCaptureRequest.java | 6 ------ 2 files changed, 4 insertions(+), 19 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 7a31b90ebfe9..997c85ed832f 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 @@ -206,8 +206,7 @@ private void process(CaptureResult result) { case STATE_WAITING_FOCUS: { if (afState == null) { - cameraState = CameraState.STATE_CAPTURING; - takePictureAfterPrecapture(); + return; } else if ( afState == CaptureRequest.CONTROL_AF_STATE_PASSIVE_SCAN || afState == CaptureRequest.CONTROL_AF_STATE_FOCUSED_LOCKED || @@ -216,7 +215,6 @@ private void process(CaptureResult result) { if (aeState == null || aeState == CaptureRequest.CONTROL_AE_STATE_CONVERGED) { - cameraState = CameraState.STATE_CAPTURING; takePictureAfterPrecapture(); } else { runPrecaptureSequence(); @@ -240,7 +238,6 @@ private void process(CaptureResult result) { case STATE_WAITING_PRECAPTURE_DONE: { // CONTROL_AE_STATE can be null on some devices if (aeState == null || aeState != CaptureResult.CONTROL_AE_STATE_PRECAPTURE) { - cameraState = CameraState.STATE_CAPTURING; takePictureAfterPrecapture(); } else { if (pictureCaptureRequest.hitPreCaptureTimeout()) { @@ -680,6 +677,9 @@ private void runPrecaptureSequence() { */ private void takePictureAfterPrecapture() { Log.i(TAG, "captureStillPicture"); + + cameraState = CameraState.STATE_CAPTURING; + try { if (null == cameraDevice) { return; @@ -730,12 +730,8 @@ public void onCaptureCompleted(@NonNull CameraCaptureSession session, } }; - Log.i(TAG, "stopRepeating"); captureSession.stopRepeating(); - - Log.i(TAG, "abortCaptures"); captureSession.abortCaptures(); - Log.i(TAG, "sending capture request"); captureSession.capture(stillBuilder.build(), CaptureCallback, mBackgroundHandler); // captureSession.setRepeatingRequest(mPreviewRequestBuilder.build(), null, mBackgroundHandler); @@ -880,11 +876,6 @@ private void lockAutoFocus() { refreshPreviewCaptureSession( null, (code, message) -> pictureCaptureRequest.error(code, message, null)); -// try { -// captureSession.capture(mPreviewRequestBuilder.build(), mCaptureCallback, -// mBackgroundHandler); -// } catch (CameraAccessException e) { -// } } /** diff --git a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/PictureCaptureRequest.java b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/PictureCaptureRequest.java index 2debeda8be57..ca6194e6ee65 100644 --- a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/PictureCaptureRequest.java +++ b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/PictureCaptureRequest.java @@ -68,7 +68,6 @@ class PictureCaptureRequest { * @param mFile */ public PictureCaptureRequest(MethodChannel.Result result, File mFile) { - Log.i("Camera", "PictureCaptureRequest constructor"); this.result = result; this.timeoutHandler = new TimeoutHandler(); this.mFile = mFile; @@ -155,14 +154,9 @@ private void onStateChange(PictureCaptureRequestState oldState) { break; case STATE_FINISHED: - timeoutHandler.clearTimeout(timeoutCallback); - break; - case STATE_ERROR: timeoutHandler.clearTimeout(timeoutCallback); - setState(PictureCaptureRequestState.STATE_FINISHED); break; - } } From 8892fb971a701b47f8b8d333df7886f1950d0559 Mon Sep 17 00:00:00 2001 From: Andrew Coutts Date: Mon, 1 Mar 2021 13:42:00 -0500 Subject: [PATCH 039/114] Flash working now --- .../io/flutter/plugins/camera/Camera.java | 76 ++++--------------- 1 file changed, 16 insertions(+), 60 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 997c85ed832f..bee5a31caca3 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 @@ -195,7 +195,7 @@ private void process(CaptureResult result) { Integer afState = result.get(CaptureResult.CONTROL_AF_STATE); if (cameraState != CameraState.STATE_PREVIEW) { - Log.i(TAG, "mCaptureCallback | state: " + cameraState + " | afState: " + afState + " | aeState: " + aeState); + Log.i(TAG, "mCaptureCallback | state: " + cameraState + " | afState: " + afState + " | aeState: " + aeState); } switch (cameraState) { @@ -208,7 +208,7 @@ private void process(CaptureResult result) { if (afState == null) { return; } else if ( - afState == CaptureRequest.CONTROL_AF_STATE_PASSIVE_SCAN || +// afState == CaptureRequest.CONTROL_AF_STATE_PASSIVE_SCAN || afState == CaptureRequest.CONTROL_AF_STATE_FOCUSED_LOCKED || afState == CaptureRequest.CONTROL_AF_STATE_NOT_FOCUSED_LOCKED) { // CONTROL_AE_STATE can be null on some devices @@ -601,6 +601,10 @@ private void refreshPreviewCaptureSession( mCaptureCallback, mBackgroundHandler); + if (onSuccessCallback != null) { + onSuccessCallback.run(); + } + } catch (CameraAccessException | IllegalStateException | IllegalArgumentException e) { onErrorCallback.onError("cameraAccess", e.getMessage()); @@ -867,7 +871,7 @@ private void runPictureAutoFocus() { } /** - * Start the autofocus routine. + * Start the autofocus routine on the current capture request. */ private void lockAutoFocus() { Log.i(TAG, "lockAutoFocus"); @@ -1010,65 +1014,14 @@ public void resumeVideoRecording(@NonNull final Result result) { * @throws CameraAccessException */ public void setFlashMode(@NonNull final Result result, FlashMode newMode) { - if (currentFlashMode == newMode) { - return; - } - // Save the new flash mode setting final FlashMode oldFlashMode = currentFlashMode; currentFlashMode = newMode; updateFlash(mPreviewRequestBuilder); refreshPreviewCaptureSession( - () -> result.success(null), + () -> result.success(null), (code, message) -> result.error("setFlashModeFailed", "Could not set flash mode.", null)); - -// // If switching directly from torch to auto or on, make sure we turn off the torch. -// if (oldFlashMode == FlashMode.torch && newMode != FlashMode.torch && newMode != FlashMode.off) { -// // TODO: why cant we just call refresh preview here? -// captureSession.setRepeatingRequest( -// mPreviewRequestBuilder.build(), -// new CaptureCallback() { -// private boolean isFinished = false; -// -// @Override -// public void onCaptureCompleted( -// @NonNull CameraCaptureSession session, -// @NonNull CaptureRequest request, -// @NonNull TotalCaptureResult captureResult) { -// if (isFinished) { -// return; -// } -// -// updateFlash(mPreviewRequestBuilder); -// refreshPreviewCaptureSession( -// () -> { -// result.success(null); -// isFinished = true; -// }, -// (code, message) -> -// result.error("setFlashModeFailed", "Could not set flash mode.", null)); -// } -// -// @Override -// public void onCaptureFailed( -// @NonNull CameraCaptureSession session, -// @NonNull CaptureRequest request, -// @NonNull CaptureFailure failure) { -// if (isFinished) { -// return; -// } -// -// result.error("setFlashModeFailed", "Could not set flash mode.", null); -// isFinished = true; -// } -// }, -// null); -// } else { -// refreshPreviewCaptureSession( -// () -> result.success(null), -// (code, message) -> result.error("setFlashModeFailed", "Could not set flash mode.", null)); -// } } /** @@ -1140,7 +1093,7 @@ public void setFocusMode(@NonNull final Result result, FocusMode newMode) // Refresh preview session using repeating request as it will be in CONTROL_AF_MODE_CONTINUOUS_PICTURE refreshPreviewCaptureSession( - null, (code, message) -> result.error("setFocusMode", message, null)); + () -> result.success(null), (code, message) -> result.error("setFocusMode", message, null)); break; case locked: @@ -1166,13 +1119,15 @@ public void onCaptureCompleted(@NonNull CameraCaptureSession session, } }, mBackgroundHandler); + + result.success(null); } catch (CameraAccessException e) { result.error("setFocusMode", e.getMessage(), null); } break; } - result.success(null); + } @@ -1294,9 +1249,10 @@ public void setExposureOffset(@NonNull final Result result, double offset) exposureOffset = (int) (offset / stepSize); // Apply it updateExposureMode(mPreviewRequestBuilder); - // TODO: refresh preview session? - this.captureSession.setRepeatingRequest(mPreviewRequestBuilder.build(), null, mBackgroundHandler); - result.success(offset); + + // Refresh capture session + refreshPreviewCaptureSession(() -> result.success(null), + (code, message) -> result.error("setExposureModeFailed", "Could not set flash mode.", null)); } public float getMaxZoomLevel() { From 5a0f41b13194bd76b8b91a789676fa10652e651c Mon Sep 17 00:00:00 2001 From: Andrew Coutts Date: Mon, 1 Mar 2021 13:56:58 -0500 Subject: [PATCH 040/114] Stop background thread when camera is closed --- .../android/src/main/java/io/flutter/plugins/camera/Camera.java | 1 + 1 file changed, 1 insertion(+) 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 bee5a31caca3..a198429c0299 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 @@ -1432,6 +1432,7 @@ private void closeCaptureSession() { public void close() { Log.i(TAG, "close"); + stopBackgroundThread();; closeCaptureSession(); if (cameraDevice != null) { From 4ad0bfa82df4cf0d83b68a80d103f57d90d25444 Mon Sep 17 00:00:00 2001 From: Andrew Coutts Date: Mon, 1 Mar 2021 14:21:23 -0500 Subject: [PATCH 041/114] Dont cleanup background thread until cameras topped --- .../io/flutter/plugins/camera/Camera.java | 25 ++++++++----------- 1 file changed, 11 insertions(+), 14 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 a198429c0299..e82eaaf8810a 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 @@ -32,6 +32,7 @@ import android.os.Build.VERSION_CODES; import android.os.Handler; import android.os.HandlerThread; +import android.os.Looper; import android.util.Log; import android.util.Range; import android.util.Rational; @@ -760,10 +761,13 @@ private void startBackgroundThread() { * TODO: call when activity paused */ private void stopBackgroundThread() { - mBackgroundThread.quitSafely(); try { - mBackgroundThread.join(); - mBackgroundThread = null; + if (mBackgroundThread != null) { + mBackgroundThread.quitSafely(); + mBackgroundThread.join(); + mBackgroundThread = null; + } + mBackgroundHandler = null; } catch (InterruptedException e) { pictureCaptureRequest.error("cameraAccess", e.getMessage(), null); @@ -1405,21 +1409,14 @@ private void setImageStreamImageAvailableListener(final EventChannel.EventSink i imageBuffer.put("format", img.getFormat()); imageBuffer.put("planes", planes); - imageStreamSink.success(imageBuffer); + final Handler handler = new Handler(Looper.getMainLooper()); + handler.post(() -> imageStreamSink.success(imageBuffer)); img.close(); }, mBackgroundHandler ); } - public void stopImageStream() throws CameraAccessException { - if (imageStreamReader != null) { - imageStreamReader.setOnImageAvailableListener(null, mBackgroundHandler); - } - startPreview(); - } - - private void closeCaptureSession() { if (captureSession != null) { Log.i(TAG, "closeCaptureSession"); @@ -1431,8 +1428,6 @@ private void closeCaptureSession() { public void close() { Log.i(TAG, "close"); - - stopBackgroundThread();; closeCaptureSession(); if (cameraDevice != null) { @@ -1452,6 +1447,8 @@ public void close() { mediaRecorder.release(); mediaRecorder = null; } + + stopBackgroundThread(); } public void dispose() { From 3098bd4868677b3fea4e3819a6a449be46db9ccc Mon Sep 17 00:00:00 2001 From: Andrew Coutts Date: Mon, 1 Mar 2021 15:09:52 -0500 Subject: [PATCH 042/114] Make sure to set focus mode idle --- .../java/io/flutter/plugins/camera/Camera.java | 16 ++++++++-------- .../plugins/camera/PictureCaptureRequest.java | 11 +++++++++-- 2 files changed, 17 insertions(+), 10 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 e82eaaf8810a..9a348b6f2053 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 @@ -209,7 +209,7 @@ private void process(CaptureResult result) { if (afState == null) { return; } else if ( -// afState == CaptureRequest.CONTROL_AF_STATE_PASSIVE_SCAN || + afState == CaptureRequest.CONTROL_AF_STATE_PASSIVE_SCAN || afState == CaptureRequest.CONTROL_AF_STATE_FOCUSED_LOCKED || afState == CaptureRequest.CONTROL_AF_STATE_NOT_FOCUSED_LOCKED) { // CONTROL_AE_STATE can be null on some devices @@ -242,6 +242,7 @@ private void process(CaptureResult result) { takePictureAfterPrecapture(); } else { if (pictureCaptureRequest.hitPreCaptureTimeout()) { + Log.i(TAG, "===> Hit precapture timeout"); unlockAutoFocus(); } } @@ -627,7 +628,7 @@ public void takePicture(@NonNull final Result result) { final File file = File.createTempFile("CAP", ".jpg", outputDir); // Start a new capture - pictureCaptureRequest = new PictureCaptureRequest(result, file); + pictureCaptureRequest = new PictureCaptureRequest(result, file, dartMessenger); } catch (IOException | SecurityException e) { pictureCaptureRequest.error("cannotCreateFile", e.getMessage(), null); return; @@ -904,14 +905,13 @@ private void unlockAutoFocus() { // Set AF state to idle again mPreviewRequestBuilder.set( - CaptureRequest.CONTROL_AF_TRIGGER, CameraMetadata.CONTROL_AF_TRIGGER_CANCEL); - - updateFocusMode(mPreviewRequestBuilder); - updateFlash(mPreviewRequestBuilder); - updateExposureMode(mPreviewRequestBuilder); + CaptureRequest.CONTROL_AF_TRIGGER, CameraMetadata.CONTROL_AF_TRIGGER_IDLE); captureSession.capture(mPreviewRequestBuilder.build(), null, mBackgroundHandler); - } catch (CameraAccessException ignored) { + } catch (CameraAccessException e) { + Log.i(TAG, "Error unlocking focus: " + e.getMessage()); + dartMessenger.sendCameraErrorEvent(e.getMessage()); + return; } refreshPreviewCaptureSession( diff --git a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/PictureCaptureRequest.java b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/PictureCaptureRequest.java index ca6194e6ee65..74ac848030ab 100644 --- a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/PictureCaptureRequest.java +++ b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/PictureCaptureRequest.java @@ -52,6 +52,11 @@ class PictureCaptureRequest { */ private long preCaptureStartTime; + /** + * To send errors back to dart + */ + private final DartMessenger dartMessenger; + /** * The state of this picture capture request. */ @@ -67,10 +72,11 @@ class PictureCaptureRequest { * @param result * @param mFile */ - public PictureCaptureRequest(MethodChannel.Result result, File mFile) { + public PictureCaptureRequest(MethodChannel.Result result, File mFile, DartMessenger dartMessenger) { this.result = result; this.timeoutHandler = new TimeoutHandler(); this.mFile = mFile; + this.dartMessenger = dartMessenger; } /** @@ -92,7 +98,8 @@ public void setState(PictureCaptureRequestState newState) { // Once a request is finished, that's it for its lifecycle. if (state == PictureCaptureRequestState.STATE_FINISHED) { - throw new IllegalStateException("Request has already been finished"); + dartMessenger.sendCameraErrorEvent("Request has already been finished"); + return; } final PictureCaptureRequestState oldState = state; From 772d3f1c0f157fa748fe1833fd3140af7c6a163d Mon Sep 17 00:00:00 2001 From: Andrew Coutts Date: Mon, 1 Mar 2021 16:38:08 -0500 Subject: [PATCH 043/114] Add nv21 image format for android --- .../io/flutter/plugins/camera/Camera.java | 5 ++-- .../lib/src/types/image_format_group.dart | 25 +++++++++++++++++++ 2 files changed, 28 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 9a348b6f2053..8752abbcef45 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 @@ -91,8 +91,9 @@ public class Camera { // Current supported outputs static { supportedImageFormats = new HashMap<>(); - supportedImageFormats.put("yuv420", 35); - supportedImageFormats.put("jpeg", 256); + supportedImageFormats.put("yuv420", ImageFormat.YUV_420_888); + supportedImageFormats.put("jpeg", ImageFormat.JPEG); + supportedImageFormats.put("nv21", ImageFormat.NV21); } private final SurfaceTextureEntry flutterTexture; diff --git a/packages/camera/camera_platform_interface/lib/src/types/image_format_group.dart b/packages/camera/camera_platform_interface/lib/src/types/image_format_group.dart index 3d2c0180fe65..b07f7af79e82 100644 --- a/packages/camera/camera_platform_interface/lib/src/types/image_format_group.dart +++ b/packages/camera/camera_platform_interface/lib/src/types/image_format_group.dart @@ -14,8 +14,33 @@ enum ImageFormatGroup { /// /// On iOS, this is `kCVPixelFormatType_420YpCbCr8BiPlanarVideoRange`. See /// https://developer.apple.com/documentation/corevideo/1563591-pixel_format_identifiers/kcvpixelformattype_420ypcbcr8biplanarvideorange?language=objc + /// + /// Note: if you are using YUV420 with Firebase ML Vision you need to concatenate the planes + /// using the following function: + /// ```dart + /// Uint8List concatenatePlanes(List planes) { + /// var totalBytes = 0; + /// for (var i = 0; i < planes.length; ++i) { + /// totalBytes += planes[i].bytes.length; + /// } + /// + /// final bytes = Uint8List(totalBytes); + /// + /// var byteOffset = 0; + /// for (var i = 0; i < planes.length; ++i) { + /// final length = planes[i].bytes.length; + /// bytes.setRange(byteOffset, byteOffset += length, planes[i].bytes); + /// } + /// return bytes; + /// } + /// ``` yuv420, + /// NV21 is only valid in Android and should be used when feeding images to Firebase ML Vision. If you + /// don't use this format then you need to concatenate the planes from YUV420 which is costly both in + /// memory and CPU. It's most efficient to just feed it NV21. + nv21, + /// 32-bit BGRA. /// /// On iOS, this is `kCVPixelFormatType_32BGRA`. See From 9aee09c1bacc0f3b913d842edf8755a12fec8800 Mon Sep 17 00:00:00 2001 From: Andrew Coutts Date: Mon, 1 Mar 2021 17:40:45 -0500 Subject: [PATCH 044/114] Unit tests passing and capture timeout works again --- .../io/flutter/plugins/camera/Camera.java | 12 +- .../plugins/camera/PictureCaptureRequest.java | 55 +++- .../camera/PictureCaptureRequestState.java | 15 + .../camera/PictureCaptureRequestTest.java | 269 +++++++++--------- 4 files changed, 197 insertions(+), 154 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 8752abbcef45..a4d0e1b3e76a 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 @@ -232,7 +232,7 @@ private void process(CaptureResult result) { aeState == CaptureResult.CONTROL_AE_STATE_PRECAPTURE || aeState == CaptureResult.CONTROL_AE_STATE_FLASH_REQUIRED) { cameraState = CameraState.STATE_WAITING_PRECAPTURE_DONE; - pictureCaptureRequest.setPreCaptureStartTime(); + pictureCaptureRequest.setState(PictureCaptureRequestState.STATE_WAITING_PRECAPTURE_DONE); } break; } @@ -684,8 +684,9 @@ private void runPrecaptureSequence() { */ private void takePictureAfterPrecapture() { Log.i(TAG, "captureStillPicture"); - cameraState = CameraState.STATE_CAPTURING; + pictureCaptureRequest.setState(PictureCaptureRequestState.STATE_CAPTURING); + try { if (null == cameraDevice) { @@ -741,8 +742,6 @@ public void onCaptureCompleted(@NonNull CameraCaptureSession session, captureSession.abortCaptures(); Log.i(TAG, "sending capture request"); captureSession.capture(stillBuilder.build(), CaptureCallback, mBackgroundHandler); -// captureSession.setRepeatingRequest(mPreviewRequestBuilder.build(), null, mBackgroundHandler); -// pictureCaptureRequest.finish("a"); } catch (CameraAccessException e) { pictureCaptureRequest.error("cameraAccess", e.getMessage(), null); } @@ -873,6 +872,7 @@ private void runPictureAutoFocus() { assert (pictureCaptureRequest != null); cameraState = CameraState.STATE_WAITING_FOCUS; + pictureCaptureRequest.setState(PictureCaptureRequestState.STATE_WAITING_FOCUS); lockAutoFocus(); } @@ -881,6 +881,8 @@ private void runPictureAutoFocus() { */ private void lockAutoFocus() { Log.i(TAG, "lockAutoFocus"); + pictureCaptureRequest.setState(PictureCaptureRequestState.STATE_WAITING_PRECAPTURE_START); + mPreviewRequestBuilder.set( CaptureRequest.CONTROL_AF_TRIGGER, CaptureRequest.CONTROL_AF_TRIGGER_START); @@ -1025,7 +1027,7 @@ public void setFlashMode(@NonNull final Result result, FlashMode newMode) { updateFlash(mPreviewRequestBuilder); refreshPreviewCaptureSession( - () -> result.success(null), + () -> result.success(null), (code, message) -> result.error("setFlashModeFailed", "Could not set flash mode.", null)); } diff --git a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/PictureCaptureRequest.java b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/PictureCaptureRequest.java index 74ac848030ab..5788f8e97aa2 100644 --- a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/PictureCaptureRequest.java +++ b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/PictureCaptureRequest.java @@ -45,25 +45,25 @@ class PictureCaptureRequest { * Timeout handler. */ private final TimeoutHandler timeoutHandler; - + /** + * To send errors back to dart + */ + private final DartMessenger dartMessenger; /** * The time that the most recent capture started at. Used to check if * the current capture request has timed out. */ private long preCaptureStartTime; - - /** - * To send errors back to dart - */ - private final DartMessenger dartMessenger; - /** * The state of this picture capture request. */ private PictureCaptureRequestState state = PictureCaptureRequestState.STATE_IDLE; private final Runnable timeoutCallback = - () -> error("captureTimeout", "Picture capture request timed out", state.toString()); + () -> { + error("captureTimeout", "Picture capture request timed out", state.toString()); + setState(PictureCaptureRequestState.STATE_ERROR); + }; /** @@ -79,6 +79,16 @@ public PictureCaptureRequest(MethodChannel.Result result, File mFile, DartMessen this.dartMessenger = dartMessenger; } + /** + * Constructor for unit tests where we can mock the timeout handler + */ + public PictureCaptureRequest(MethodChannel.Result result, File mFile, DartMessenger dartMessenger, TimeoutHandler timeoutHandler) { + this.result = result; + this.timeoutHandler = timeoutHandler; + this.mFile = mFile; + this.dartMessenger = dartMessenger; + } + /** * Return the current state of this picture capture request. * @@ -94,9 +104,9 @@ public PictureCaptureRequestState getState() { * @param newState */ public void setState(PictureCaptureRequestState newState) { - Log.i("Camera", "PictureCaptureRequest setState: " + newState); + Log.i("Camera", "====> PictureCaptureRequest setState: " + newState); - // Once a request is finished, that's it for its lifecycle. + // Once a request is finished, that's it for its lifecycle. if (state == PictureCaptureRequestState.STATE_FINISHED) { dartMessenger.sendCameraErrorEvent("Request has already been finished"); return; @@ -108,7 +118,8 @@ public void setState(PictureCaptureRequestState newState) { } public boolean isFinished() { - return state == PictureCaptureRequestState.STATE_FINISHED; + return state == PictureCaptureRequestState.STATE_FINISHED || + state == PictureCaptureRequestState.STATE_ERROR; } /** @@ -117,6 +128,11 @@ public boolean isFinished() { * @param absolutePath */ public void finish(String absolutePath) { + if (state == PictureCaptureRequestState.STATE_ERROR) { + return; + } + + if (isFinished()) throw new IllegalStateException("Request has already been finished"); setState(PictureCaptureRequestState.STATE_FINISHED); Log.i("Camera", "PictureCaptureRequest finish"); result.success(absolutePath); @@ -125,6 +141,11 @@ public void finish(String absolutePath) { public void error( String errorCode, @Nullable String errorMessage, @Nullable Object errorDetails) { + if (state == PictureCaptureRequestState.STATE_ERROR) { + return; + } + + if (isFinished()) throw new IllegalStateException("Request has already been finished"); setState(PictureCaptureRequestState.STATE_ERROR); result.error(errorCode, errorMessage, errorDetails); } @@ -151,15 +172,18 @@ public void setPreCaptureStartTime() { */ private void onStateChange(PictureCaptureRequestState oldState) { switch (state) { - case STATE_IDLE: - // Nothing to do in idle state. + case STATE_CAPTURING: + case STATE_WAITING_FOCUS: + case STATE_WAITING_PRECAPTURE_START: + timeoutHandler.resetTimeout(timeoutCallback); break; - case STATE_CAPTURING: - // Started an image capture. + case STATE_WAITING_PRECAPTURE_DONE: + setPreCaptureStartTime(); timeoutHandler.resetTimeout(timeoutCallback); break; + case STATE_IDLE: case STATE_FINISHED: case STATE_ERROR: timeoutHandler.clearTimeout(timeoutCallback); @@ -180,6 +204,7 @@ static class TimeoutHandler { } public void resetTimeout(Runnable runnable) { + Log.i("Camear", "PictureCaptureRequest | resetting timeout"); clearTimeout(runnable); handler.postDelayed(runnable, REQUEST_TIMEOUT); } diff --git a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/PictureCaptureRequestState.java b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/PictureCaptureRequestState.java index fb8c2c7503bd..c506e2489e1c 100644 --- a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/PictureCaptureRequestState.java +++ b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/PictureCaptureRequestState.java @@ -16,6 +16,21 @@ public enum PictureCaptureRequestState { */ STATE_IDLE, + /** + * Starting and waiting for autofocus to complete. + */ + STATE_WAITING_FOCUS, + + /** + * Start performing autoexposure. + */ + STATE_WAITING_PRECAPTURE_START, + + /** + * waiting for autoexposure to complete. + */ + STATE_WAITING_PRECAPTURE_DONE, + /** * Picture is being captured. */ diff --git a/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/PictureCaptureRequestTest.java b/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/PictureCaptureRequestTest.java index 3252b3e111c4..ef81028e2bc6 100644 --- a/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/PictureCaptureRequestTest.java +++ b/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/PictureCaptureRequestTest.java @@ -4,6 +4,10 @@ package io.flutter.plugins.camera; +import org.junit.Test; + +import io.flutter.plugin.common.MethodChannel; + import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertTrue; @@ -13,140 +17,137 @@ import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; -import io.flutter.plugin.common.MethodChannel; -import org.junit.Test; - public class PictureCaptureRequestTest { - @Test - public void state_is_idle_by_default() { - PictureCaptureRequest req = new PictureCaptureRequest(null); - assertEquals("Default state is idle", req.getState(), PictureCaptureRequest.State.idle); - } - - @Test - public void setState_sets_state() { - PictureCaptureRequest req = new PictureCaptureRequest(null); - req.setState(PictureCaptureRequest.State.focusing); - assertEquals("State is focusing", req.getState(), PictureCaptureRequest.State.focusing); - req.setState(PictureCaptureRequest.State.preCapture); - assertEquals("State is preCapture", req.getState(), PictureCaptureRequest.State.preCapture); - req.setState(PictureCaptureRequest.State.waitingPreCaptureReady); - assertEquals( - "State is waitingPreCaptureReady", - req.getState(), - PictureCaptureRequest.State.waitingPreCaptureReady); - req.setState(PictureCaptureRequest.State.capturing); - assertEquals( - "State is awaitingPreCapture", req.getState(), PictureCaptureRequest.State.capturing); - } - - @Test - public void setState_resets_timeout() { - PictureCaptureRequest.TimeoutHandler mockTimeoutHandler = - mock(PictureCaptureRequest.TimeoutHandler.class); - PictureCaptureRequest req = new PictureCaptureRequest(null, mockTimeoutHandler); - req.setState(PictureCaptureRequest.State.focusing); - req.setState(PictureCaptureRequest.State.preCapture); - req.setState(PictureCaptureRequest.State.waitingPreCaptureReady); - req.setState(PictureCaptureRequest.State.capturing); - verify(mockTimeoutHandler, times(4)).resetTimeout(any()); - verify(mockTimeoutHandler, never()).clearTimeout(any()); - } - - @Test - public void setState_clears_timeout() { - PictureCaptureRequest.TimeoutHandler mockTimeoutHandler = - mock(PictureCaptureRequest.TimeoutHandler.class); - PictureCaptureRequest req = new PictureCaptureRequest(null, mockTimeoutHandler); - req.setState(PictureCaptureRequest.State.idle); - req.setState(PictureCaptureRequest.State.finished); - req = new PictureCaptureRequest(null, mockTimeoutHandler); - req.setState(PictureCaptureRequest.State.error); - verify(mockTimeoutHandler, never()).resetTimeout(any()); - verify(mockTimeoutHandler, times(3)).clearTimeout(any()); - } - - @Test - public void finish_sets_result_and_state() { - // Setup - MethodChannel.Result mockResult = mock(MethodChannel.Result.class); - PictureCaptureRequest req = new PictureCaptureRequest(mockResult); - // Act - req.finish("/test/path"); - // Test - verify(mockResult).success("/test/path"); - assertEquals("State is finished", req.getState(), PictureCaptureRequest.State.finished); - } - - @Test - public void finish_clears_timeout() { - PictureCaptureRequest.TimeoutHandler mockTimeoutHandler = - mock(PictureCaptureRequest.TimeoutHandler.class); - MethodChannel.Result mockResult = mock(MethodChannel.Result.class); - PictureCaptureRequest req = new PictureCaptureRequest(mockResult, mockTimeoutHandler); - req.finish("/test/path"); - verify(mockTimeoutHandler, never()).resetTimeout(any()); - verify(mockTimeoutHandler).clearTimeout(any()); - } - - @Test - public void isFinished_is_true_When_state_is_finished_or_error() { - // Setup - PictureCaptureRequest req = new PictureCaptureRequest(null); - // Test false states - req.setState(PictureCaptureRequest.State.idle); - assertFalse(req.isFinished()); - req.setState(PictureCaptureRequest.State.preCapture); - assertFalse(req.isFinished()); - req.setState(PictureCaptureRequest.State.capturing); - assertFalse(req.isFinished()); - // Test true states - req.setState(PictureCaptureRequest.State.finished); - assertTrue(req.isFinished()); - req = new PictureCaptureRequest(null); // Refresh - req.setState(PictureCaptureRequest.State.error); - assertTrue(req.isFinished()); - } - - @Test(expected = IllegalStateException.class) - public void finish_throws_When_already_finished() { - // Setup - PictureCaptureRequest req = new PictureCaptureRequest(null); - req.setState(PictureCaptureRequest.State.finished); - // Act - req.finish("/test/path"); - } - - @Test - public void error_sets_result_and_state() { - // Setup - MethodChannel.Result mockResult = mock(MethodChannel.Result.class); - PictureCaptureRequest req = new PictureCaptureRequest(mockResult); - // Act - req.error("ERROR_CODE", "Error Message", null); - // Test - verify(mockResult).error("ERROR_CODE", "Error Message", null); - assertEquals("State is error", req.getState(), PictureCaptureRequest.State.error); - } - - @Test - public void error_clears_timeout() { - PictureCaptureRequest.TimeoutHandler mockTimeoutHandler = - mock(PictureCaptureRequest.TimeoutHandler.class); - MethodChannel.Result mockResult = mock(MethodChannel.Result.class); - PictureCaptureRequest req = new PictureCaptureRequest(mockResult, mockTimeoutHandler); - req.error("ERROR_CODE", "Error Message", null); - verify(mockTimeoutHandler, never()).resetTimeout(any()); - verify(mockTimeoutHandler).clearTimeout(any()); - } - - @Test(expected = IllegalStateException.class) - public void error_throws_When_already_finished() { - // Setup - PictureCaptureRequest req = new PictureCaptureRequest(null); - req.setState(PictureCaptureRequest.State.finished); - // Act - req.error(null, null, null); - } + @Test + public void state_is_idle_by_default() { + PictureCaptureRequest req = new PictureCaptureRequest(null, null, null); + assertEquals("Default state is idle", req.getState(), PictureCaptureRequestState.STATE_IDLE); + } + + @Test + public void setState_sets_state() { + PictureCaptureRequest req = new PictureCaptureRequest(null, null, null); + req.setState(PictureCaptureRequestState.STATE_WAITING_FOCUS); + assertEquals("State is focusing", req.getState(), PictureCaptureRequestState.STATE_WAITING_FOCUS); + req.setState(PictureCaptureRequestState.STATE_WAITING_PRECAPTURE_START); + assertEquals("State is preCapture", req.getState(), PictureCaptureRequestState.STATE_WAITING_PRECAPTURE_START); + req.setState(PictureCaptureRequestState.STATE_WAITING_PRECAPTURE_DONE); + assertEquals( + "State is waitingPreCaptureReady", + req.getState(), + PictureCaptureRequestState.STATE_WAITING_PRECAPTURE_DONE); + req.setState(PictureCaptureRequestState.STATE_CAPTURING); + assertEquals( + "State is awaitingPreCapture", req.getState(), PictureCaptureRequestState.STATE_CAPTURING); + } + + @Test + public void setState_resets_timeout() { + PictureCaptureRequest.TimeoutHandler mockTimeoutHandler = + mock(PictureCaptureRequest.TimeoutHandler.class); + PictureCaptureRequest req = new PictureCaptureRequest(null, null, null, mockTimeoutHandler); + req.setState(PictureCaptureRequestState.STATE_WAITING_FOCUS); + req.setState(PictureCaptureRequestState.STATE_WAITING_PRECAPTURE_START); + req.setState(PictureCaptureRequestState.STATE_WAITING_PRECAPTURE_DONE); + req.setState(PictureCaptureRequestState.STATE_CAPTURING); + verify(mockTimeoutHandler, times(4)).resetTimeout(any()); + verify(mockTimeoutHandler, never()).clearTimeout(any()); + } + + @Test + public void setState_clears_timeout() { + PictureCaptureRequest.TimeoutHandler mockTimeoutHandler = + mock(PictureCaptureRequest.TimeoutHandler.class); + PictureCaptureRequest req = new PictureCaptureRequest(null, null, null, mockTimeoutHandler); + req.setState(PictureCaptureRequestState.STATE_IDLE); + req.setState(PictureCaptureRequestState.STATE_FINISHED); + req = new PictureCaptureRequest(null, null, null, mockTimeoutHandler); + req.setState(PictureCaptureRequestState.STATE_ERROR); + verify(mockTimeoutHandler, never()).resetTimeout(any()); + verify(mockTimeoutHandler, times(3)).clearTimeout(any()); + } + + @Test + public void finish_sets_result_and_state() { + // Setup + MethodChannel.Result mockResult = mock(MethodChannel.Result.class); + PictureCaptureRequest req = new PictureCaptureRequest(mockResult, null, null); + // Act + req.finish("/test/path"); + // Test + verify(mockResult).success("/test/path"); + assertEquals("State is finished", req.getState(), PictureCaptureRequestState.STATE_FINISHED); + } + + @Test + public void finish_clears_timeout() { + PictureCaptureRequest.TimeoutHandler mockTimeoutHandler = + mock(PictureCaptureRequest.TimeoutHandler.class); + MethodChannel.Result mockResult = mock(MethodChannel.Result.class); + PictureCaptureRequest req = new PictureCaptureRequest(mockResult, null, null, mockTimeoutHandler); + req.finish("/test/path"); + verify(mockTimeoutHandler, never()).resetTimeout(any()); + verify(mockTimeoutHandler).clearTimeout(any()); + } + + @Test + public void isFinished_is_true_When_state_is_finished_or_error() { + // Setup + PictureCaptureRequest req = new PictureCaptureRequest(null, null, null); + // Test false states + req.setState(PictureCaptureRequestState.STATE_IDLE); + assertFalse(req.isFinished()); + req.setState(PictureCaptureRequestState.STATE_WAITING_PRECAPTURE_START); + assertFalse(req.isFinished()); + req.setState(PictureCaptureRequestState.STATE_WAITING_PRECAPTURE_DONE); + assertFalse(req.isFinished()); + // Test true states + req.setState(PictureCaptureRequestState.STATE_FINISHED); + assertTrue(req.isFinished()); + req = new PictureCaptureRequest(null, null, null); // Refresh + req.setState(PictureCaptureRequestState.STATE_ERROR); + assertTrue(req.isFinished()); + } + + @Test(expected = IllegalStateException.class) + public void finish_throws_When_already_finished() { + // Setup + PictureCaptureRequest req = new PictureCaptureRequest(null, null, null); + req.setState(PictureCaptureRequestState.STATE_FINISHED); + // Act + req.finish("/test/path"); + } + + @Test + public void error_sets_result_and_state() { + // Setup + MethodChannel.Result mockResult = mock(MethodChannel.Result.class); + PictureCaptureRequest req = new PictureCaptureRequest(mockResult, null, null); + // Act + req.error("ERROR_CODE", "Error Message", null); + // Test + verify(mockResult).error("ERROR_CODE", "Error Message", null); + assertEquals("State is error", req.getState(), PictureCaptureRequestState.STATE_ERROR); + } + + @Test + public void error_clears_timeout() { + PictureCaptureRequest.TimeoutHandler mockTimeoutHandler = + mock(PictureCaptureRequest.TimeoutHandler.class); + MethodChannel.Result mockResult = mock(MethodChannel.Result.class); + PictureCaptureRequest req = new PictureCaptureRequest(mockResult, null, null, mockTimeoutHandler); + req.error("ERROR_CODE", "Error Message", null); + verify(mockTimeoutHandler, never()).resetTimeout(any()); + verify(mockTimeoutHandler).clearTimeout(any()); + } + + @Test(expected = IllegalStateException.class) + public void error_throws_When_already_finished() { + // Setup + PictureCaptureRequest req = new PictureCaptureRequest(null, null, null); + req.setState(PictureCaptureRequestState.STATE_FINISHED); + // Act + req.error(null, null, null); + } } From 03ccc2270e2a43bd1bb2dbae1bb46a09c2694e1b Mon Sep 17 00:00:00 2001 From: Andrew Coutts Date: Mon, 1 Mar 2021 17:48:46 -0500 Subject: [PATCH 045/114] Remove comments and clean things up --- packages/camera/camera/CHANGELOG.md | 7 ++ .../io/flutter/plugins/camera/Camera.java | 82 +++++++++---------- .../plugins/camera/PictureCaptureRequest.java | 8 +- packages/camera/camera/pubspec.yaml | 5 +- 4 files changed, 54 insertions(+), 48 deletions(-) diff --git a/packages/camera/camera/CHANGELOG.md b/packages/camera/camera/CHANGELOG.md index f1d771496dde..d03e810e07b3 100644 --- a/packages/camera/camera/CHANGELOG.md +++ b/packages/camera/camera/CHANGELOG.md @@ -1,3 +1,10 @@ +## 0.8.1 + +* Complete rewrite of Android plugin to fix all capture, focus, flash, and exposure issues. +* Fixed crash when opening front-facing cameras on some legacy android devices like Sony XZ. +* Android Flash mode works with full precapture sequence. +* Added support for NV21 image stream format on Android. + ## 0.8.0 * Stable null safety release. 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 a4d0e1b3e76a..6b400a17e392 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 @@ -136,7 +136,7 @@ public class Camera { @Override public void onImageAvailable(ImageReader reader) { - Log.i(TAG, "onImageAvailable"); + // Log.i(TAG, "onImageAvailable"); mBackgroundHandler.post(new ImageSaver(reader.acquireNextImage(), pictureCaptureRequest.mFile, pictureCaptureRequest)); cameraState = CameraState.STATE_PREVIEW; } @@ -197,7 +197,7 @@ private void process(CaptureResult result) { Integer afState = result.get(CaptureResult.CONTROL_AF_STATE); if (cameraState != CameraState.STATE_PREVIEW) { - Log.i(TAG, "mCaptureCallback | state: " + cameraState + " | afState: " + afState + " | aeState: " + aeState); + // Log.i(TAG, "mCaptureCallback | state: " + cameraState + " | afState: " + afState + " | aeState: " + aeState); } switch (cameraState) { @@ -243,7 +243,7 @@ private void process(CaptureResult result) { takePictureAfterPrecapture(); } else { if (pictureCaptureRequest.hitPreCaptureTimeout()) { - Log.i(TAG, "===> Hit precapture timeout"); + // Log.i(TAG, "===> Hit precapture timeout"); unlockAutoFocus(); } } @@ -277,7 +277,7 @@ public Camera( final String resolutionPreset, final boolean enableAudio) throws CameraAccessException { - Log.i(TAG, "Camear constructor"); + // Log.i(TAG, "Camear constructor"); if (activity == null) { throw new IllegalStateException("No activity available!"); @@ -331,9 +331,9 @@ public Camera( */ private void checkAutoFocusSupported() { int[] modes = cameraCharacteristics.get(CameraCharacteristics.CONTROL_AF_AVAILABLE_MODES); - Log.i(TAG, "checkAutoFocusSupported | modes:"); + // Log.i(TAG, "checkAutoFocusSupported | modes:"); for (int mode : modes) { - Log.i(TAG, "checkAutoFocusSupported | ==> " + mode); + // Log.i(TAG, "checkAutoFocusSupported | ==> " + mode); } // Check if fixed focal length lens. If LENS_INFO_MINIMUM_FOCUS_DISTANCE=0, then this is fixed. @@ -341,12 +341,12 @@ private void checkAutoFocusSupported() { float minFocus = cameraCharacteristics.get(CameraCharacteristics.LENS_INFO_MINIMUM_FOCUS_DISTANCE); float maxFocus = cameraCharacteristics.get(CameraCharacteristics.LENS_INFO_HYPERFOCAL_DISTANCE); final boolean isFixedLength = minFocus == 0; - Log.i(TAG, "checkAutoFocusSupported | minFocus " + minFocus + " | maxFocus: " + maxFocus); + // Log.i(TAG, "checkAutoFocusSupported | minFocus " + minFocus + " | maxFocus: " + maxFocus); mAutoFocusSupported = !(modes == null || modes.length == 0 || (modes.length == 1 && modes[0] == CameraCharacteristics.CONTROL_AF_MODE_OFF)); - Log.i(TAG, "checkAutoFocusSupported: " + mAutoFocusSupported); + // Log.i(TAG, "checkAutoFocusSupported: " + mAutoFocusSupported); } /** @@ -363,7 +363,7 @@ private void checkFlashSupported() { * @param cameraCharacteristics */ private void getAvailableFpsRange(CameraCharacteristics cameraCharacteristics) { - Log.i(TAG, "getAvailableFpsRange"); + // Log.i(TAG, "getAvailableFpsRange"); try { Range[] ranges = @@ -371,7 +371,7 @@ private void getAvailableFpsRange(CameraCharacteristics cameraCharacteristics) { if (ranges != null) { for (Range range : ranges) { int upper = range.getUpper(); - Log.i("Camera", "[FPS Range Available] is:" + range); + // Log.i("Camera", "[FPS Range Available] is:" + range); if (upper >= 10) { if (fpsRange == null || upper > fpsRange.getUpper()) { fpsRange = range; @@ -382,11 +382,11 @@ private void getAvailableFpsRange(CameraCharacteristics cameraCharacteristics) { } catch (Exception e) { pictureCaptureRequest.error("cameraAccess", e.getMessage(), null); } - Log.i("Camera", "[FPS Range] is:" + fpsRange); + // Log.i("Camera", "[FPS Range] is:" + fpsRange); } private void prepareMediaRecorder(String outputFilePath) throws IOException { - Log.i(TAG, "prepareMediaRecorder"); + // Log.i(TAG, "prepareMediaRecorder"); if (mediaRecorder != null) { mediaRecorder.release(); @@ -425,7 +425,7 @@ public void open(String imageFormatGroup) throws CameraAccessException { new CameraDevice.StateCallback() { @Override public void onOpened(@NonNull CameraDevice device) { - Log.i(TAG, "open | onOpened"); + // Log.i(TAG, "open | onOpened"); cameraDevice = device; @@ -446,7 +446,7 @@ public void onOpened(@NonNull CameraDevice device) { @Override public void onClosed(@NonNull CameraDevice camera) { - Log.i(TAG, "open | onClosed"); + // Log.i(TAG, "open | onClosed"); dartMessenger.sendCameraClosingEvent(); super.onClosed(camera); @@ -454,7 +454,7 @@ public void onClosed(@NonNull CameraDevice camera) { @Override public void onDisconnected(@NonNull CameraDevice cameraDevice) { - Log.i(TAG, "open | onDisconnected"); + // Log.i(TAG, "open | onDisconnected"); close(); dartMessenger.sendCameraErrorEvent("The camera was disconnected."); @@ -462,7 +462,7 @@ public void onDisconnected(@NonNull CameraDevice cameraDevice) { @Override public void onError(@NonNull CameraDevice cameraDevice, int errorCode) { - Log.i(TAG, "open | onError"); + // Log.i(TAG, "open | onError"); close(); String errorDescription; @@ -500,7 +500,7 @@ private void createCaptureSession(int templateType, Surface... surfaces) private void createCaptureSession( int templateType, Runnable onSuccessCallback, Surface... surfaces) throws CameraAccessException { - Log.i(TAG, "createCaptureSession"); + // Log.i(TAG, "createCaptureSession"); // Close any existing capture session. closeCaptureSession(); @@ -592,9 +592,9 @@ private void createCaptureSession( // Send a repeating request to refresh our capture session. private void refreshPreviewCaptureSession( @Nullable Runnable onSuccessCallback, @NonNull ErrorCallback onErrorCallback) { - Log.i(TAG, "refreshPreviewCaptureSession"); + // Log.i(TAG, "refreshPreviewCaptureSession"); if (captureSession == null) { - Log.i(TAG, "[refreshPreviewCaptureSession] mPreviewSession null, returning"); + // Log.i(TAG, "[refreshPreviewCaptureSession] mPreviewSession null, returning"); return; } @@ -615,7 +615,7 @@ private void refreshPreviewCaptureSession( } public void takePicture(@NonNull final Result result) { - Log.i(TAG, "takePicture | useAutoFocus: " + useAutoFocus); + // Log.i(TAG, "takePicture | useAutoFocus: " + useAutoFocus); // Only take one 1 picture at a time. if (pictureCaptureRequest != null && !pictureCaptureRequest.isFinished()) { @@ -650,7 +650,7 @@ public void takePicture(@NonNull final Result result) { * we get a response in {@link #mCaptureCallback} from lockFocus(). */ private void runPrecaptureSequence() { - Log.i(TAG, "runPrecaptureSequence"); + // Log.i(TAG, "runPrecaptureSequence"); try { // First set precapture state to idle or else it can hang in STATE_WAITING_PRECAPTURE mPreviewRequestBuilder.set(CaptureRequest.CONTROL_AE_PRECAPTURE_TRIGGER, @@ -683,7 +683,7 @@ private void runPrecaptureSequence() { * {@link #mCaptureCallback} from both lockFocus(). */ private void takePictureAfterPrecapture() { - Log.i(TAG, "captureStillPicture"); + // Log.i(TAG, "captureStillPicture"); cameraState = CameraState.STATE_CAPTURING; pictureCaptureRequest.setState(PictureCaptureRequestState.STATE_CAPTURING); @@ -719,28 +719,28 @@ public void onCaptureStarted(@NonNull CameraCaptureSession session, @NonNull CaptureRequest request, long timestamp, long frameNumber) { - Log.i(TAG, "onCaptureStarted"); + // Log.i(TAG, "onCaptureStarted"); } @Override public void onCaptureProgressed(@NonNull CameraCaptureSession session, @NonNull CaptureRequest request, @NonNull CaptureResult partialResult) { - Log.i(TAG, "onCaptureProgressed"); + // Log.i(TAG, "onCaptureProgressed"); } @Override public void onCaptureCompleted(@NonNull CameraCaptureSession session, @NonNull CaptureRequest request, @NonNull TotalCaptureResult result) { - Log.i(TAG, "onCaptureCompleted"); + // Log.i(TAG, "onCaptureCompleted"); unlockAutoFocus(); } }; captureSession.stopRepeating(); captureSession.abortCaptures(); - Log.i(TAG, "sending capture request"); + // Log.i(TAG, "sending capture request"); captureSession.capture(stillBuilder.build(), CaptureCallback, mBackgroundHandler); } catch (CameraAccessException e) { pictureCaptureRequest.error("cameraAccess", e.getMessage(), null); @@ -779,7 +779,7 @@ private void stopBackgroundThread() { * Sync the requestBuilder exposure mode setting ot the current exposure mode setting of the camera. */ void updateExposureMode(CaptureRequest.Builder requestBuilder) { - Log.i(TAG, "updateExposureMode"); + // Log.i(TAG, "updateExposureMode"); // Applying auto exposure MeteringRectangle aeRect = cameraRegions.getAEMeteringRectangle(); @@ -805,7 +805,7 @@ void updateExposureMode(CaptureRequest.Builder requestBuilder) { * Sync the requestBuilder flash setting to the current flash mode setting of the camera. */ void updateFlash(CaptureRequest.Builder requestBuilder) { - Log.i(TAG, "updateFlash"); + // Log.i(TAG, "updateFlash"); if (!mFlashSupported) { return; @@ -868,7 +868,7 @@ private int getOrientation(int rotation) { * Start capturing a picture, doing autofocus first. */ private void runPictureAutoFocus() { - Log.i(TAG, "runPictureAutoFocus"); + // Log.i(TAG, "runPictureAutoFocus"); assert (pictureCaptureRequest != null); cameraState = CameraState.STATE_WAITING_FOCUS; @@ -880,7 +880,7 @@ private void runPictureAutoFocus() { * Start the autofocus routine on the current capture request. */ private void lockAutoFocus() { - Log.i(TAG, "lockAutoFocus"); + // Log.i(TAG, "lockAutoFocus"); pictureCaptureRequest.setState(PictureCaptureRequestState.STATE_WAITING_PRECAPTURE_START); mPreviewRequestBuilder.set( @@ -894,7 +894,7 @@ private void lockAutoFocus() { * Cancel and reset auto focus state and refresh the preview session. */ private void unlockAutoFocus() { - Log.i(TAG, "unlockAutoFocus"); + // Log.i(TAG, "unlockAutoFocus"); try { // Cancel existing AF state mPreviewRequestBuilder.set( @@ -912,7 +912,7 @@ private void unlockAutoFocus() { captureSession.capture(mPreviewRequestBuilder.build(), null, mBackgroundHandler); } catch (CameraAccessException e) { - Log.i(TAG, "Error unlocking focus: " + e.getMessage()); + // Log.i(TAG, "Error unlocking focus: " + e.getMessage()); dartMessenger.sendCameraErrorEvent(e.getMessage()); return; } @@ -1081,7 +1081,7 @@ public void setExposurePoint(@NonNull final Result result, Double x, Double y) */ public void setFocusMode(@NonNull final Result result, FocusMode newMode) throws CameraAccessException { - Log.i(TAG, "setFocusMode: " + newMode); + // Log.i(TAG, "setFocusMode: " + newMode); // Set new focus mode currentFocusMode = newMode; @@ -1093,7 +1093,7 @@ public void setFocusMode(@NonNull final Result result, FocusMode newMode) // or we want to trigger a one-time focus and then set AF to idle (locked mode). switch (newMode) { case auto: - Log.i(TAG, "Triggering AF start with mode " + currentFocusMode); + // Log.i(TAG, "Triggering AF start with mode " + currentFocusMode); // Reset state of autofocus so it goes back to passive scanning. mPreviewRequestBuilder.set( CaptureRequest.CONTROL_AF_TRIGGER, CaptureRequest.CONTROL_AF_TRIGGER_CANCEL); @@ -1117,7 +1117,7 @@ public void setFocusMode(@NonNull final Result result, FocusMode newMode) public void onCaptureCompleted(@NonNull CameraCaptureSession session, @NonNull CaptureRequest request, @NonNull TotalCaptureResult _result) { - Log.i(TAG, "Success after triggering AF start for locked focus"); + // Log.i(TAG, "Success after triggering AF start for locked focus"); mPreviewRequestBuilder.set( CaptureRequest.CONTROL_AF_TRIGGER, CaptureRequest.CONTROL_AF_TRIGGER_IDLE); @@ -1320,7 +1320,7 @@ private void updateFpsRange() { * @param requestBuilder */ private void updateFocusMode(CaptureRequest.Builder requestBuilder) { - Log.i(TAG, "updateFocusMode currentFocusMode: " + currentFocusMode); + // Log.i(TAG, "updateFocusMode currentFocusMode: " + currentFocusMode); if (!mAutoFocusSupported) { useAutoFocus = false; @@ -1361,7 +1361,7 @@ private void updateFocusMode(CaptureRequest.Builder requestBuilder) { public void startPreview() throws CameraAccessException { if (pictureImageReader == null || pictureImageReader.getSurface() == null) return; - Log.i(TAG, "startPreview"); + // Log.i(TAG, "startPreview"); createCaptureSession(CameraDevice.TEMPLATE_PREVIEW, pictureImageReader.getSurface()); } @@ -1369,7 +1369,7 @@ public void startPreview() throws CameraAccessException { public void startPreviewWithImageStream(EventChannel imageStreamChannel) throws CameraAccessException { createCaptureSession(CameraDevice.TEMPLATE_RECORD, imageStreamReader.getSurface()); - Log.i(TAG, "startPreviewWithImageStream"); + // Log.i(TAG, "startPreviewWithImageStream"); imageStreamChannel.setStreamHandler( new EventChannel.StreamHandler() { @@ -1422,7 +1422,7 @@ private void setImageStreamImageAvailableListener(final EventChannel.EventSink i private void closeCaptureSession() { if (captureSession != null) { - Log.i(TAG, "closeCaptureSession"); + // Log.i(TAG, "closeCaptureSession"); captureSession.close(); captureSession = null; @@ -1430,7 +1430,7 @@ private void closeCaptureSession() { } public void close() { - Log.i(TAG, "close"); + // Log.i(TAG, "close"); closeCaptureSession(); if (cameraDevice != null) { @@ -1455,7 +1455,7 @@ public void close() { } public void dispose() { - Log.i(TAG, "dispose"); + // Log.i(TAG, "dispose"); close(); flutterTexture.release(); diff --git a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/PictureCaptureRequest.java b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/PictureCaptureRequest.java index 5788f8e97aa2..5e5c75b05d25 100644 --- a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/PictureCaptureRequest.java +++ b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/PictureCaptureRequest.java @@ -104,7 +104,7 @@ public PictureCaptureRequestState getState() { * @param newState */ public void setState(PictureCaptureRequestState newState) { - Log.i("Camera", "====> PictureCaptureRequest setState: " + newState); + // Log.i("Camera", "====> PictureCaptureRequest setState: " + newState); // Once a request is finished, that's it for its lifecycle. if (state == PictureCaptureRequestState.STATE_FINISHED) { @@ -134,7 +134,7 @@ public void finish(String absolutePath) { if (isFinished()) throw new IllegalStateException("Request has already been finished"); setState(PictureCaptureRequestState.STATE_FINISHED); - Log.i("Camera", "PictureCaptureRequest finish"); + // Log.i("Camera", "PictureCaptureRequest finish"); result.success(absolutePath); } @@ -156,7 +156,7 @@ public void error( * @return true if the timeout is reached; otherwise false is returned. */ public boolean hitPreCaptureTimeout() { - Log.i("Camera", "hitPreCaptureTimeout | Time elapsed: " + (SystemClock.elapsedRealtime() - preCaptureStartTime)); + // Log.i("Camera", "hitPreCaptureTimeout | Time elapsed: " + (SystemClock.elapsedRealtime() - preCaptureStartTime)); return (SystemClock.elapsedRealtime() - preCaptureStartTime) > PRECAPTURE_TIMEOUT_MS; } @@ -204,7 +204,7 @@ static class TimeoutHandler { } public void resetTimeout(Runnable runnable) { - Log.i("Camear", "PictureCaptureRequest | resetting timeout"); + // Log.i("Camear", "PictureCaptureRequest | resetting timeout"); clearTimeout(runnable); handler.postDelayed(runnable, REQUEST_TIMEOUT); } diff --git a/packages/camera/camera/pubspec.yaml b/packages/camera/camera/pubspec.yaml index 53f9b3b40ad1..4ccb5c8b3919 100644 --- a/packages/camera/camera/pubspec.yaml +++ b/packages/camera/camera/pubspec.yaml @@ -2,15 +2,14 @@ name: camera description: A Flutter plugin for getting information about and controlling the camera on Android and iOS. Supports previewing the camera feed, capturing images, capturing video, and streaming image buffers to dart. -version: 0.8.0 +version: 0.8.1 homepage: https://github.com/flutter/plugins/tree/master/packages/camera/camera dependencies: flutter: sdk: flutter - camera_platform_interface: ^2.0.0 - + camera_platform_interface: ^2.0.1 pedantic: ^1.10.0 quiver: ^3.0.0 From 01edeeb977ac24b1162d186f5b617fe1159933b0 Mon Sep 17 00:00:00 2001 From: Andrew Coutts Date: Mon, 1 Mar 2021 17:49:47 -0500 Subject: [PATCH 046/114] Cleanup --- .../android/src/main/java/io/flutter/plugins/camera/Camera.java | 2 -- .../java/io/flutter/plugins/camera/PictureCaptureRequest.java | 1 - 2 files changed, 3 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 6b400a17e392..37d7659ed985 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 @@ -133,14 +133,12 @@ public class Camera { */ private final ImageReader.OnImageAvailableListener mOnImageAvailableListener = new ImageReader.OnImageAvailableListener() { - @Override public void onImageAvailable(ImageReader reader) { // Log.i(TAG, "onImageAvailable"); mBackgroundHandler.post(new ImageSaver(reader.acquireNextImage(), pictureCaptureRequest.mFile, pictureCaptureRequest)); cameraState = CameraState.STATE_PREVIEW; } - }; /** diff --git a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/PictureCaptureRequest.java b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/PictureCaptureRequest.java index 5e5c75b05d25..fd139cf16f43 100644 --- a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/PictureCaptureRequest.java +++ b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/PictureCaptureRequest.java @@ -12,7 +12,6 @@ import java.io.File; -import io.flutter.Log; import io.flutter.plugin.common.MethodChannel; /** From 8d05deb03cdb38b110171cebaea6ea765132cb7d Mon Sep 17 00:00:00 2001 From: Andrew Coutts Date: Mon, 1 Mar 2021 18:14:51 -0500 Subject: [PATCH 047/114] Cleanup unused --- .../flutter/plugins/camera/AspectRatio.java | 175 ------------------ .../plugins/camera/CameraConstants.java | 26 --- 2 files changed, 201 deletions(-) delete mode 100644 packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/AspectRatio.java delete mode 100644 packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/CameraConstants.java diff --git a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/AspectRatio.java b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/AspectRatio.java deleted file mode 100644 index 455dd7ceaf62..000000000000 --- a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/AspectRatio.java +++ /dev/null @@ -1,175 +0,0 @@ -package io.flutter.plugins.camera; - -import android.os.Parcel; -import android.os.Parcelable; -import android.util.Size; - -import androidx.annotation.NonNull; -import androidx.collection.SparseArrayCompat; - -/** - * Immutable class for describing proportional relationship between width and height. - */ -public class AspectRatio implements Comparable, Parcelable { - - private final static SparseArrayCompat> sCache - = new SparseArrayCompat<>(16); - - private final int mX; - private final int mY; - - /** - * Returns an instance of {@link AspectRatio} specified by {@code x} and {@code y} values. - * The values {@code x} and {@code} will be reduced by their greatest common divider. - * - * @param x The width - * @param y The height - * @return An instance of {@link AspectRatio} - */ - public static AspectRatio of(int x, int y) { - int gcd = gcd(x, y); - x /= gcd; - y /= gcd; - SparseArrayCompat arrayX = sCache.get(x); - if (arrayX == null) { - AspectRatio ratio = new AspectRatio(x, y); - arrayX = new SparseArrayCompat<>(); - arrayX.put(y, ratio); - sCache.put(x, arrayX); - return ratio; - } else { - AspectRatio ratio = arrayX.get(y); - if (ratio == null) { - ratio = new AspectRatio(x, y); - arrayX.put(y, ratio); - } - return ratio; - } - } - - /** - * Parse an {@link AspectRatio} from a {@link String} formatted like "4:3". - * - * @param s The string representation of the aspect ratio - * @return The aspect ratio - * @throws IllegalArgumentException when the format is incorrect. - */ - public static AspectRatio parse(String s) { - int position = s.indexOf(':'); - if (position == -1) { - throw new IllegalArgumentException("Malformed aspect ratio: " + s); - } - try { - int x = Integer.parseInt(s.substring(0, position)); - int y = Integer.parseInt(s.substring(position + 1)); - return AspectRatio.of(x, y); - } catch (NumberFormatException e) { - throw new IllegalArgumentException("Malformed aspect ratio: " + s, e); - } - } - - private AspectRatio(int x, int y) { - mX = x; - mY = y; - } - - public int getX() { - return mX; - } - - public int getY() { - return mY; - } - - public boolean matches(Size size) { - int gcd = gcd(size.getWidth(), size.getHeight()); - int x = size.getWidth() / gcd; - int y = size.getHeight() / gcd; - return mX == x && mY == y; - } - - @Override - public boolean equals(Object o) { - if (o == null) { - return false; - } - if (this == o) { - return true; - } - if (o instanceof AspectRatio) { - AspectRatio ratio = (AspectRatio) o; - return mX == ratio.mX && mY == ratio.mY; - } - return false; - } - - @Override - public String toString() { - return mX + ":" + mY; - } - - public float toFloat() { - return (float) mX / mY; - } - - @Override - public int hashCode() { - // assuming most sizes are <2^16, doing a rotate will give us perfect hashing - return mY ^ ((mX << (Integer.SIZE / 2)) | (mX >>> (Integer.SIZE / 2))); - } - - @Override - public int compareTo(@NonNull AspectRatio another) { - if (equals(another)) { - return 0; - } else if (toFloat() - another.toFloat() > 0) { - return 1; - } - return -1; - } - - /** - * @return The inverse of this {@link AspectRatio}. - */ - public AspectRatio inverse() { - //noinspection SuspiciousNameCombination - return AspectRatio.of(mY, mX); - } - - private static int gcd(int a, int b) { - while (b != 0) { - int c = b; - b = a % b; - a = c; - } - return a; - } - - @Override - public int describeContents() { - return 0; - } - - @Override - public void writeToParcel(Parcel dest, int flags) { - dest.writeInt(mX); - dest.writeInt(mY); - } - - public static final Creator CREATOR - = new Creator() { - - @Override - public AspectRatio createFromParcel(Parcel source) { - int x = source.readInt(); - int y = source.readInt(); - return AspectRatio.of(x, y); - } - - @Override - public AspectRatio[] newArray(int size) { - return new AspectRatio[size]; - } - }; - -} diff --git a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/CameraConstants.java b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/CameraConstants.java deleted file mode 100644 index ad71203da028..000000000000 --- a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/CameraConstants.java +++ /dev/null @@ -1,26 +0,0 @@ -package io.flutter.plugins.camera; - -public class CameraConstants { - - public static final AspectRatio DEFAULT_ASPECT_RATIO = AspectRatio.of(16, 9); - - public static final long AUTO_FOCUS_TIMEOUT_MS = 800; //800ms timeout, Under normal circumstances need to a few hundred milliseconds - - public static final long OPEN_CAMERA_TIMEOUT_MS = 2500; //2.5s - - public static final int FOCUS_HOLD_MILLIS = 3000; - - public static final float METERING_REGION_FRACTION = 0.1225f; - - public static final int ZOOM_REGION_DEFAULT = 1; - - public static final int FLASH_OFF = 0; - public static final int FLASH_ON = 1; - public static final int FLASH_TORCH = 2; - public static final int FLASH_AUTO = 3; - public static final int FLASH_RED_EYE = 4; - - public static final int FACING_BACK = 0; - public static final int FACING_FRONT = 1; - -} From 5d0e1ac7d146e0e7c610ef6f101478fcce3f13c0 Mon Sep 17 00:00:00 2001 From: Andrew Coutts Date: Mon, 1 Mar 2021 18:21:59 -0500 Subject: [PATCH 048/114] Bump version of platform interface --- packages/camera/camera/pubspec.yaml | 2 +- packages/camera/camera_platform_interface/pubspec.yaml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/camera/camera/pubspec.yaml b/packages/camera/camera/pubspec.yaml index 4ccb5c8b3919..a2f0550967a2 100644 --- a/packages/camera/camera/pubspec.yaml +++ b/packages/camera/camera/pubspec.yaml @@ -9,7 +9,7 @@ dependencies: flutter: sdk: flutter - camera_platform_interface: ^2.0.1 + camera_platform_interface: ^2.0.2 pedantic: ^1.10.0 quiver: ^3.0.0 diff --git a/packages/camera/camera_platform_interface/pubspec.yaml b/packages/camera/camera_platform_interface/pubspec.yaml index 12c5bc48b9ec..dd93dc30b41f 100644 --- a/packages/camera/camera_platform_interface/pubspec.yaml +++ b/packages/camera/camera_platform_interface/pubspec.yaml @@ -3,7 +3,7 @@ description: A common platform interface for the camera plugin. homepage: https://github.com/flutter/plugins/tree/master/packages/camera/camera_platform_interface # NOTE: We strongly prefer non-breaking changes, even at the expense of a # less-clean API. See https://flutter.dev/go/platform-interface-breaking-changes -version: 2.0.1 +version: 2.0.2 dependencies: flutter: From 25e93cbc0d9f2267b10ace1442d14da321232e89 Mon Sep 17 00:00:00 2001 From: Andrew Coutts Date: Tue, 2 Mar 2021 02:26:14 -0500 Subject: [PATCH 049/114] Clean up imports --- .../io/flutter/plugins/camera/CameraPermissions.java | 1 + .../java/io/flutter/plugins/camera/CameraPlugin.java | 2 ++ .../java/io/flutter/plugins/camera/CameraUtils.java | 6 ++++-- .../java/io/flutter/plugins/camera/CameraZoom.java | 1 + .../java/io/flutter/plugins/camera/DartMessenger.java | 7 +++++-- .../plugins/camera/DeviceOrientationManager.java | 1 + .../java/io/flutter/plugins/camera/ImageSaver.java | 1 - .../flutter/plugins/camera/MethodCallHandlerImpl.java | 7 +++++-- .../plugins/camera/media/MediaRecorderBuilder.java | 2 ++ .../io/flutter/plugins/camera/types/FlashMode.java | 10 ---------- 10 files changed, 21 insertions(+), 17 deletions(-) diff --git a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/CameraPermissions.java b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/CameraPermissions.java index 3529e69a2b0b..b9ca5da3a1a7 100644 --- a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/CameraPermissions.java +++ b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/CameraPermissions.java @@ -8,6 +8,7 @@ import android.Manifest.permission; import android.app.Activity; import android.content.pm.PackageManager; + import androidx.annotation.VisibleForTesting; import androidx.core.app.ActivityCompat; import androidx.core.content.ContextCompat; diff --git a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/CameraPlugin.java b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/CameraPlugin.java index 93183bb7c0a7..85af292a211b 100644 --- a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/CameraPlugin.java +++ b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/CameraPlugin.java @@ -6,8 +6,10 @@ import android.app.Activity; import android.os.Build; + import androidx.annotation.NonNull; import androidx.annotation.Nullable; + import io.flutter.embedding.engine.plugins.FlutterPlugin; import io.flutter.embedding.engine.plugins.activity.ActivityAware; import io.flutter.embedding.engine.plugins.activity.ActivityPluginBinding; diff --git a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/CameraUtils.java b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/CameraUtils.java index 03993a3b51f9..2cd3a8be261a 100644 --- a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/CameraUtils.java +++ b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/CameraUtils.java @@ -14,8 +14,7 @@ import android.hardware.camera2.params.StreamConfigurationMap; import android.media.CamcorderProfile; import android.util.Size; -import io.flutter.embedding.engine.systemchannels.PlatformChannel; -import io.flutter.plugins.camera.types.ResolutionPreset; + import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; @@ -24,6 +23,9 @@ import java.util.List; import java.util.Map; +import io.flutter.embedding.engine.systemchannels.PlatformChannel; +import io.flutter.plugins.camera.types.ResolutionPreset; + /** Provides various utilities for camera. */ public final class CameraUtils { diff --git a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/CameraZoom.java b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/CameraZoom.java index 5eed9f4734b7..36aee3b94777 100644 --- a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/CameraZoom.java +++ b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/CameraZoom.java @@ -5,6 +5,7 @@ package io.flutter.plugins.camera; import android.graphics.Rect; + import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.core.math.MathUtils; diff --git a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/DartMessenger.java b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/DartMessenger.java index 3892452892d9..0068421015dc 100644 --- a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/DartMessenger.java +++ b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/DartMessenger.java @@ -7,14 +7,17 @@ import android.os.Handler; import android.os.Looper; import android.text.TextUtils; + import androidx.annotation.Nullable; + +import java.util.HashMap; +import java.util.Map; + import io.flutter.embedding.engine.systemchannels.PlatformChannel; import io.flutter.plugin.common.BinaryMessenger; import io.flutter.plugin.common.MethodChannel; import io.flutter.plugins.camera.types.ExposureMode; import io.flutter.plugins.camera.types.FocusMode; -import java.util.HashMap; -import java.util.Map; class DartMessenger { @Nullable private MethodChannel cameraChannel; diff --git a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/DeviceOrientationManager.java b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/DeviceOrientationManager.java index b2a504b629d6..46f31cdab2c5 100644 --- a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/DeviceOrientationManager.java +++ b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/DeviceOrientationManager.java @@ -16,6 +16,7 @@ import android.view.OrientationEventListener; import android.view.Surface; import android.view.WindowManager; + import io.flutter.embedding.engine.systemchannels.PlatformChannel; class DeviceOrientationManager { diff --git a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/ImageSaver.java b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/ImageSaver.java index 954818606db9..589cfd1f553a 100644 --- a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/ImageSaver.java +++ b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/ImageSaver.java @@ -1,7 +1,6 @@ package io.flutter.plugins.camera; import android.media.Image; -import android.media.ImageReader; import android.os.Handler; import android.os.Looper; diff --git a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/MethodCallHandlerImpl.java b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/MethodCallHandlerImpl.java index aa7483f55679..19307a2f6a78 100644 --- a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/MethodCallHandlerImpl.java +++ b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/MethodCallHandlerImpl.java @@ -6,8 +6,13 @@ import android.app.Activity; import android.hardware.camera2.CameraAccessException; + import androidx.annotation.NonNull; import androidx.annotation.Nullable; + +import java.util.HashMap; +import java.util.Map; + import io.flutter.embedding.engine.systemchannels.PlatformChannel; import io.flutter.plugin.common.BinaryMessenger; import io.flutter.plugin.common.EventChannel; @@ -19,8 +24,6 @@ import io.flutter.plugins.camera.types.FlashMode; import io.flutter.plugins.camera.types.FocusMode; import io.flutter.view.TextureRegistry; -import java.util.HashMap; -import java.util.Map; final class MethodCallHandlerImpl implements MethodChannel.MethodCallHandler { private final Activity activity; diff --git a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/media/MediaRecorderBuilder.java b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/media/MediaRecorderBuilder.java index 4c3fb3add230..5e5d0c44c115 100644 --- a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/media/MediaRecorderBuilder.java +++ b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/media/MediaRecorderBuilder.java @@ -6,7 +6,9 @@ import android.media.CamcorderProfile; import android.media.MediaRecorder; + import androidx.annotation.NonNull; + import java.io.IOException; public class MediaRecorderBuilder { diff --git a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/types/FlashMode.java b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/types/FlashMode.java index 69db79859b80..b8ff2fac6652 100644 --- a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/types/FlashMode.java +++ b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/types/FlashMode.java @@ -4,16 +4,6 @@ package io.flutter.plugins.camera.types; -import android.hardware.camera2.CameraCaptureSession; -import android.hardware.camera2.CameraCharacteristics; -import android.hardware.camera2.CaptureFailure; -import android.hardware.camera2.CaptureRequest; -import android.hardware.camera2.TotalCaptureResult; - -import androidx.annotation.NonNull; - -import io.flutter.plugin.common.MethodChannel; - // Mirrors flash_mode.dart public enum FlashMode { off("off"), From 2378b5efe33ad33b589058c16161a702ea9519b3 Mon Sep 17 00:00:00 2001 From: Andrew Coutts Date: Tue, 2 Mar 2021 02:36:47 -0500 Subject: [PATCH 050/114] Handle LENS_INFO_MINIMUM_FOCUS_DISTANCE being null --- .../io/flutter/plugins/camera/Camera.java | 22 +++++++++++++------ 1 file changed, 15 insertions(+), 7 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 37d7659ed985..d7b28ae38265 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 @@ -325,7 +325,8 @@ public Camera( } /** - * Check if the auto focus is supported. + * Check if the auto focus is supported by the current camera. We look at the available AF modes + * and the available lens focusing distance to determine if its' a fixed length lens or not as well. */ private void checkAutoFocusSupported() { int[] modes = cameraCharacteristics.get(CameraCharacteristics.CONTROL_AF_AVAILABLE_MODES); @@ -336,15 +337,22 @@ private void checkAutoFocusSupported() { // Check if fixed focal length lens. If LENS_INFO_MINIMUM_FOCUS_DISTANCE=0, then this is fixed. // Can be null on some devices. - float minFocus = cameraCharacteristics.get(CameraCharacteristics.LENS_INFO_MINIMUM_FOCUS_DISTANCE); - float maxFocus = cameraCharacteristics.get(CameraCharacteristics.LENS_INFO_HYPERFOCAL_DISTANCE); - final boolean isFixedLength = minFocus == 0; + final Float minFocus = cameraCharacteristics.get(CameraCharacteristics.LENS_INFO_MINIMUM_FOCUS_DISTANCE); + // final Float maxFocus = cameraCharacteristics.get(CameraCharacteristics.LENS_INFO_HYPERFOCAL_DISTANCE); + + // Value can be null on some devices: + // https://developer.android.com/reference/android/hardware/camera2/CameraCharacteristics#LENS_INFO_MINIMUM_FOCUS_DISTANCE + boolean isFixedLength; + if (minFocus == null) { + isFixedLength = true; + } else { + isFixedLength = minFocus == 0; + } // Log.i(TAG, "checkAutoFocusSupported | minFocus " + minFocus + " | maxFocus: " + maxFocus); - - mAutoFocusSupported = !(modes == null || modes.length == 0 || + mAutoFocusSupported = !isFixedLength && !(modes == null || modes.length == 0 || (modes.length == 1 && modes[0] == CameraCharacteristics.CONTROL_AF_MODE_OFF)); - // Log.i(TAG, "checkAutoFocusSupported: " + mAutoFocusSupported); + Log.i(TAG, "checkAutoFocusSupported: " + mAutoFocusSupported); } /** From 989971cd2e182f08973526bf11344ad1d1259566 Mon Sep 17 00:00:00 2001 From: Andrew Coutts Date: Tue, 2 Mar 2021 03:06:29 -0500 Subject: [PATCH 051/114] Cleanup --- .../io/flutter/plugins/camera/Camera.java | 2530 ++++++++--------- .../plugins/camera/CameraPermissions.java | 1 - .../flutter/plugins/camera/CameraPlugin.java | 2 - .../flutter/plugins/camera/CameraState.java | 37 +- .../flutter/plugins/camera/CameraUtils.java | 6 +- .../io/flutter/plugins/camera/CameraZoom.java | 1 - .../flutter/plugins/camera/DartMessenger.java | 7 +- .../camera/DeviceOrientationManager.java | 1 - .../io/flutter/plugins/camera/ImageSaver.java | 109 +- .../plugins/camera/MethodCallHandlerImpl.java | 7 +- .../plugins/camera/PictureCaptureRequest.java | 353 ++- .../camera/PictureCaptureRequestState.java | 54 +- .../camera/media/MediaRecorderBuilder.java | 2 - .../plugins/camera/types/FlashMode.java | 5 +- .../camera/PictureCaptureRequestTest.java | 275 +- 15 files changed, 1645 insertions(+), 1745 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 d7b28ae38265..74b365e3f381 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 @@ -4,6 +4,8 @@ package io.flutter.plugins.camera; +import static io.flutter.plugins.camera.CameraUtils.computeBestPreviewSize; + import android.annotation.SuppressLint; import android.annotation.TargetApi; import android.app.Activity; @@ -39,10 +41,17 @@ import android.util.Size; import android.util.SparseIntArray; import android.view.Surface; - import androidx.annotation.NonNull; import androidx.annotation.Nullable; - +import io.flutter.embedding.engine.systemchannels.PlatformChannel; +import io.flutter.plugin.common.EventChannel; +import io.flutter.plugin.common.MethodChannel.Result; +import io.flutter.plugins.camera.media.MediaRecorderBuilder; +import io.flutter.plugins.camera.types.ExposureMode; +import io.flutter.plugins.camera.types.FlashMode; +import io.flutter.plugins.camera.types.FocusMode; +import io.flutter.plugins.camera.types.ResolutionPreset; +import io.flutter.view.TextureRegistry.SurfaceTextureEntry; import java.io.File; import java.io.IOException; import java.nio.ByteBuffer; @@ -54,1417 +63,1372 @@ import java.util.Map; import java.util.concurrent.Executors; -import io.flutter.embedding.engine.systemchannels.PlatformChannel; -import io.flutter.plugin.common.EventChannel; -import io.flutter.plugin.common.MethodChannel.Result; -import io.flutter.plugins.camera.media.MediaRecorderBuilder; -import io.flutter.plugins.camera.types.ExposureMode; -import io.flutter.plugins.camera.types.FlashMode; -import io.flutter.plugins.camera.types.FocusMode; -import io.flutter.plugins.camera.types.ResolutionPreset; -import io.flutter.view.TextureRegistry.SurfaceTextureEntry; - -import static io.flutter.plugins.camera.CameraUtils.computeBestPreviewSize; - @FunctionalInterface interface ErrorCallback { - void onError(String errorCode, String errorMessage); + void onError(String errorCode, String errorMessage); } public class Camera { - private static final String TAG = "Camera"; - - - /** - * Conversion from screen rotation to JPEG orientation. - */ - private static final SparseIntArray ORIENTATIONS = new SparseIntArray(); - private static final HashMap supportedImageFormats; - - static { - ORIENTATIONS.append(Surface.ROTATION_0, 90); - ORIENTATIONS.append(Surface.ROTATION_90, 0); - ORIENTATIONS.append(Surface.ROTATION_180, 270); - ORIENTATIONS.append(Surface.ROTATION_270, 180); - } - - // Current supported outputs - static { - supportedImageFormats = new HashMap<>(); - supportedImageFormats.put("yuv420", ImageFormat.YUV_420_888); - supportedImageFormats.put("jpeg", ImageFormat.JPEG); - supportedImageFormats.put("nv21", ImageFormat.NV21); - } - - private final SurfaceTextureEntry flutterTexture; - private final CameraManager cameraManager; - private final DeviceOrientationManager deviceOrientationListener; - private final boolean isFrontFacing; - private final int sensorOrientation; - private final String cameraName; - private final Size captureSize; - private final Size previewSize; - private final boolean enableAudio; - private final Context applicationContext; - private final CamcorderProfile recordingProfile; - private final DartMessenger dartMessenger; - private final CameraZoom cameraZoom; - private final CameraCharacteristics cameraCharacteristics; - private final Activity activity; - /** - * This manages the state of the camera and the current capture request. - */ - PictureCaptureRequest pictureCaptureRequest; - /** - * Whether the current camera device supports auto focus or not. - */ - private boolean mAutoFocusSupported = true; - /** - * The state of the camera. By default we are in the preview state. - */ - private CameraState cameraState = CameraState.STATE_PREVIEW; - /** - * A {@link Handler} for running tasks in the background. - */ - private Handler mBackgroundHandler; - /** - * This a callback object for the {@link ImageReader}. "onImageAvailable" will be called when a - * still image is ready to be saved. - */ - private final ImageReader.OnImageAvailableListener mOnImageAvailableListener - = new ImageReader.OnImageAvailableListener() { + private static final String TAG = "Camera"; + + /** Conversion from screen rotation to JPEG orientation. */ + private static final SparseIntArray ORIENTATIONS = new SparseIntArray(); + + private static final HashMap supportedImageFormats; + + static { + ORIENTATIONS.append(Surface.ROTATION_0, 90); + ORIENTATIONS.append(Surface.ROTATION_90, 0); + ORIENTATIONS.append(Surface.ROTATION_180, 270); + ORIENTATIONS.append(Surface.ROTATION_270, 180); + } + + // Current supported outputs + static { + supportedImageFormats = new HashMap<>(); + supportedImageFormats.put("yuv420", ImageFormat.YUV_420_888); + supportedImageFormats.put("jpeg", ImageFormat.JPEG); + supportedImageFormats.put("nv21", ImageFormat.NV21); + } + + private final SurfaceTextureEntry flutterTexture; + private final CameraManager cameraManager; + private final DeviceOrientationManager deviceOrientationListener; + private final boolean isFrontFacing; + private final int sensorOrientation; + private final String cameraName; + private final Size captureSize; + private final Size previewSize; + private final boolean enableAudio; + private final Context applicationContext; + private final CamcorderProfile recordingProfile; + private final DartMessenger dartMessenger; + private final CameraZoom cameraZoom; + private final CameraCharacteristics cameraCharacteristics; + private final Activity activity; + /** This manages the state of the camera and the current capture request. */ + PictureCaptureRequest pictureCaptureRequest; + /** Whether the current camera device supports auto focus or not. */ + private boolean mAutoFocusSupported = true; + /** The state of the camera. By default we are in the preview state. */ + private CameraState cameraState = CameraState.STATE_PREVIEW; + /** A {@link Handler} for running tasks in the background. */ + private Handler mBackgroundHandler; + /** + * This a callback object for the {@link ImageReader}. "onImageAvailable" will be called when a + * still image is ready to be saved. + */ + private final ImageReader.OnImageAvailableListener mOnImageAvailableListener = + new ImageReader.OnImageAvailableListener() { @Override public void onImageAvailable(ImageReader reader) { - // Log.i(TAG, "onImageAvailable"); - mBackgroundHandler.post(new ImageSaver(reader.acquireNextImage(), pictureCaptureRequest.mFile, pictureCaptureRequest)); - cameraState = CameraState.STATE_PREVIEW; + // Log.i(TAG, "onImageAvailable"); + mBackgroundHandler.post( + new ImageSaver( + reader.acquireLatestImage(), pictureCaptureRequest.mFile, pictureCaptureRequest)); + cameraState = CameraState.STATE_PREVIEW; } - }; - - /** - * An additional thread for running tasks that shouldn't block the UI. - */ - private HandlerThread mBackgroundThread; - private CameraDevice cameraDevice; - private CameraCaptureSession captureSession; - private ImageReader pictureImageReader; - private ImageReader imageStreamReader; - /** - * {@link CaptureRequest.Builder} for the camera preview - */ - private CaptureRequest.Builder mPreviewRequestBuilder; - /** - * {@link CaptureRequest} generated by {@link #mPreviewRequestBuilder} - */ - private MediaRecorder mediaRecorder; - private boolean recordingVideo; - private File videoRecordingFile; - /** - * Flash mode setting of the current camera. Initialize to off because - * we don't know if the current camera supports flash yet. - */ - private FlashMode currentFlashMode; - /** - * Exposure mode setting of the current camera. Initialize to auto - * because all cameras support autoexposure by default. - */ - private ExposureMode currentExposureMode; - /** - * Focus mode setting of the current camera. Initialize to locked because - * we don't know if the current camera supports autofocus yet. - */ - private FocusMode currentFocusMode; - /** - * Whether or not to use autofocus. - */ - private boolean useAutoFocus = false; - /** - * Whether the current camera device supports Flash or not. - */ - private boolean mFlashSupported = false; - private CameraRegions cameraRegions; - private int exposureOffset; - /** - * A {@link CameraCaptureSession.CaptureCallback} that handles events related to JPEG capture. - */ - private final CameraCaptureSession.CaptureCallback mCaptureCallback - = new CameraCaptureSession.CaptureCallback() { + }; + + /** An additional thread for running tasks that shouldn't block the UI. */ + private HandlerThread mBackgroundThread; + + private CameraDevice cameraDevice; + private CameraCaptureSession captureSession; + private ImageReader pictureImageReader; + private ImageReader imageStreamReader; + /** {@link CaptureRequest.Builder} for the camera preview */ + private CaptureRequest.Builder mPreviewRequestBuilder; + /** {@link CaptureRequest} generated by {@link #mPreviewRequestBuilder} */ + private MediaRecorder mediaRecorder; + + private boolean recordingVideo; + private File videoRecordingFile; + /** + * Flash mode setting of the current camera. Initialize to off because we don't know if the + * current camera supports flash yet. + */ + private FlashMode currentFlashMode; + /** + * Exposure mode setting of the current camera. Initialize to auto because all cameras support + * autoexposure by default. + */ + private ExposureMode currentExposureMode; + /** + * Focus mode setting of the current camera. Initialize to locked because we don't know if the + * current camera supports autofocus yet. + */ + private FocusMode currentFocusMode; + /** Whether or not to use autofocus. */ + private boolean useAutoFocus = false; + /** Whether the current camera device supports Flash or not. */ + private boolean mFlashSupported = false; + + private CameraRegions cameraRegions; + private int exposureOffset; + /** A {@link CameraCaptureSession.CaptureCallback} that handles events related to JPEG capture. */ + private final CameraCaptureSession.CaptureCallback mCaptureCallback = + new CameraCaptureSession.CaptureCallback() { private void process(CaptureResult result) { - Integer aeState = result.get(CaptureResult.CONTROL_AE_STATE); - Integer afState = result.get(CaptureResult.CONTROL_AF_STATE); - - if (cameraState != CameraState.STATE_PREVIEW) { - // Log.i(TAG, "mCaptureCallback | state: " + cameraState + " | afState: " + afState + " | aeState: " + aeState); - } + Integer aeState = result.get(CaptureResult.CONTROL_AE_STATE); + Integer afState = result.get(CaptureResult.CONTROL_AF_STATE); - switch (cameraState) { - case STATE_PREVIEW: { - // We have nothing to do when the camera preview is working normally. - break; - } + if (cameraState != CameraState.STATE_PREVIEW) { + // Log.i(TAG, "mCaptureCallback | state: " + cameraState + " | afState: " + afState + " | aeState: " + aeState); + } - case STATE_WAITING_FOCUS: { - if (afState == null) { - return; - } else if ( - afState == CaptureRequest.CONTROL_AF_STATE_PASSIVE_SCAN || - afState == CaptureRequest.CONTROL_AF_STATE_FOCUSED_LOCKED || - afState == CaptureRequest.CONTROL_AF_STATE_NOT_FOCUSED_LOCKED) { - // CONTROL_AE_STATE can be null on some devices - - if (aeState == null || - aeState == CaptureRequest.CONTROL_AE_STATE_CONVERGED) { - takePictureAfterPrecapture(); - } else { - runPrecaptureSequence(); - } - } - break; + switch (cameraState) { + case STATE_PREVIEW: + { + // We have nothing to do when the camera preview is working normally. + break; + } + + case STATE_WAITING_FOCUS: + { + if (afState == null) { + return; + } else if (afState == CaptureRequest.CONTROL_AF_STATE_PASSIVE_SCAN + || afState == CaptureRequest.CONTROL_AF_STATE_FOCUSED_LOCKED + || afState == CaptureRequest.CONTROL_AF_STATE_NOT_FOCUSED_LOCKED) { + // CONTROL_AE_STATE can be null on some devices + + if (aeState == null || aeState == CaptureRequest.CONTROL_AE_STATE_CONVERGED) { + takePictureAfterPrecapture(); + } else { + runPrecaptureSequence(); + } } - - case STATE_WAITING_PRECAPTURE_START: { - // CONTROL_AE_STATE can be null on some devices - if (aeState == null || - aeState == CaptureResult.CONTROL_AE_STATE_CONVERGED || - aeState == CaptureResult.CONTROL_AE_STATE_PRECAPTURE || - aeState == CaptureResult.CONTROL_AE_STATE_FLASH_REQUIRED) { - cameraState = CameraState.STATE_WAITING_PRECAPTURE_DONE; - pictureCaptureRequest.setState(PictureCaptureRequestState.STATE_WAITING_PRECAPTURE_DONE); - } - break; + break; + } + + case STATE_WAITING_PRECAPTURE_START: + { + // CONTROL_AE_STATE can be null on some devices + if (aeState == null + || aeState == CaptureResult.CONTROL_AE_STATE_CONVERGED + || aeState == CaptureResult.CONTROL_AE_STATE_PRECAPTURE + || aeState == CaptureResult.CONTROL_AE_STATE_FLASH_REQUIRED) { + cameraState = CameraState.STATE_WAITING_PRECAPTURE_DONE; + pictureCaptureRequest.setState( + PictureCaptureRequestState.STATE_WAITING_PRECAPTURE_DONE); } - - case STATE_WAITING_PRECAPTURE_DONE: { - // CONTROL_AE_STATE can be null on some devices - if (aeState == null || aeState != CaptureResult.CONTROL_AE_STATE_PRECAPTURE) { - takePictureAfterPrecapture(); - } else { - if (pictureCaptureRequest.hitPreCaptureTimeout()) { - // Log.i(TAG, "===> Hit precapture timeout"); - unlockAutoFocus(); - } - } - break; + break; + } + + case STATE_WAITING_PRECAPTURE_DONE: + { + // CONTROL_AE_STATE can be null on some devices + if (aeState == null || aeState != CaptureResult.CONTROL_AE_STATE_PRECAPTURE) { + takePictureAfterPrecapture(); + } else { + if (pictureCaptureRequest.hitPreCaptureTimeout()) { + // Log.i(TAG, "===> Hit precapture timeout"); + unlockAutoFocus(); + } } - } + break; + } + } } @Override - public void onCaptureProgressed(@NonNull CameraCaptureSession session, - @NonNull CaptureRequest request, - @NonNull CaptureResult partialResult) { - process(partialResult); + public void onCaptureProgressed( + @NonNull CameraCaptureSession session, + @NonNull CaptureRequest request, + @NonNull CaptureResult partialResult) { + process(partialResult); } @Override - public void onCaptureCompleted(@NonNull CameraCaptureSession session, - @NonNull CaptureRequest request, - @NonNull TotalCaptureResult result) { - process(result); - } - }; - private Range fpsRange; - private PlatformChannel.DeviceOrientation lockedCaptureOrientation; - - public Camera( - final Activity activity, - final SurfaceTextureEntry flutterTexture, - final DartMessenger dartMessenger, - final String cameraName, - final String resolutionPreset, - final boolean enableAudio) - throws CameraAccessException { - // Log.i(TAG, "Camear constructor"); - - if (activity == null) { - throw new IllegalStateException("No activity available!"); + public void onCaptureCompleted( + @NonNull CameraCaptureSession session, + @NonNull CaptureRequest request, + @NonNull TotalCaptureResult result) { + process(result); } - this.activity = activity; - this.cameraName = cameraName; - this.enableAudio = enableAudio; - this.flutterTexture = flutterTexture; - this.dartMessenger = dartMessenger; - this.cameraManager = (CameraManager) activity.getSystemService(Context.CAMERA_SERVICE); - this.applicationContext = activity.getApplicationContext(); - this.currentFlashMode = FlashMode.off; - this.currentExposureMode = ExposureMode.auto; - this.currentFocusMode = FocusMode.auto; - this.exposureOffset = 0; - - // Get camera characteristics and check for supported features - cameraCharacteristics = cameraManager.getCameraCharacteristics(cameraName); - getAvailableFpsRange(cameraCharacteristics); - checkAutoFocusSupported(); - checkFlashSupported(); - - // Setup orientation - sensorOrientation = cameraCharacteristics.get(CameraCharacteristics.SENSOR_ORIENTATION); - isFrontFacing = - cameraCharacteristics.get(CameraCharacteristics.LENS_FACING) - == CameraMetadata.LENS_FACING_FRONT; - deviceOrientationListener = - new DeviceOrientationManager(activity, dartMessenger, isFrontFacing, sensorOrientation); - deviceOrientationListener.start(); - - // Resolution configuration - ResolutionPreset preset = ResolutionPreset.valueOf(resolutionPreset); - recordingProfile = - CameraUtils.getBestAvailableCamcorderProfileForResolutionPreset(cameraName, preset); - captureSize = new Size(recordingProfile.videoFrameWidth, recordingProfile.videoFrameHeight); - previewSize = computeBestPreviewSize(cameraName, preset); - - // Zoom setup - cameraZoom = - new CameraZoom( - cameraCharacteristics.get(CameraCharacteristics.SENSOR_INFO_ACTIVE_ARRAY_SIZE), - cameraCharacteristics.get(CameraCharacteristics.SCALER_AVAILABLE_MAX_DIGITAL_ZOOM)); - - // Start background thread. - startBackgroundThread(); + }; + + private Range fpsRange; + private PlatformChannel.DeviceOrientation lockedCaptureOrientation; + + public Camera( + final Activity activity, + final SurfaceTextureEntry flutterTexture, + final DartMessenger dartMessenger, + final String cameraName, + final String resolutionPreset, + final boolean enableAudio) + throws CameraAccessException { + // Log.i(TAG, "Camear constructor"); + + if (activity == null) { + throw new IllegalStateException("No activity available!"); } - - /** - * Check if the auto focus is supported by the current camera. We look at the available AF modes - * and the available lens focusing distance to determine if its' a fixed length lens or not as well. - */ - private void checkAutoFocusSupported() { - int[] modes = cameraCharacteristics.get(CameraCharacteristics.CONTROL_AF_AVAILABLE_MODES); - // Log.i(TAG, "checkAutoFocusSupported | modes:"); - for (int mode : modes) { - // Log.i(TAG, "checkAutoFocusSupported | ==> " + mode); - } - - // Check if fixed focal length lens. If LENS_INFO_MINIMUM_FOCUS_DISTANCE=0, then this is fixed. - // Can be null on some devices. - final Float minFocus = cameraCharacteristics.get(CameraCharacteristics.LENS_INFO_MINIMUM_FOCUS_DISTANCE); - // final Float maxFocus = cameraCharacteristics.get(CameraCharacteristics.LENS_INFO_HYPERFOCAL_DISTANCE); - - // Value can be null on some devices: - // https://developer.android.com/reference/android/hardware/camera2/CameraCharacteristics#LENS_INFO_MINIMUM_FOCUS_DISTANCE - boolean isFixedLength; - if (minFocus == null) { - isFixedLength = true; - } else { - isFixedLength = minFocus == 0; - } - // Log.i(TAG, "checkAutoFocusSupported | minFocus " + minFocus + " | maxFocus: " + maxFocus); - - mAutoFocusSupported = !isFixedLength && !(modes == null || modes.length == 0 || - (modes.length == 1 && modes[0] == CameraCharacteristics.CONTROL_AF_MODE_OFF)); - Log.i(TAG, "checkAutoFocusSupported: " + mAutoFocusSupported); + this.activity = activity; + this.cameraName = cameraName; + this.enableAudio = enableAudio; + this.flutterTexture = flutterTexture; + this.dartMessenger = dartMessenger; + this.cameraManager = (CameraManager) activity.getSystemService(Context.CAMERA_SERVICE); + this.applicationContext = activity.getApplicationContext(); + this.currentFlashMode = FlashMode.off; + this.currentExposureMode = ExposureMode.auto; + this.currentFocusMode = FocusMode.auto; + this.exposureOffset = 0; + + // Get camera characteristics and check for supported features + cameraCharacteristics = cameraManager.getCameraCharacteristics(cameraName); + getAvailableFpsRange(cameraCharacteristics); + checkAutoFocusSupported(); + checkFlashSupported(); + + // Setup orientation + sensorOrientation = cameraCharacteristics.get(CameraCharacteristics.SENSOR_ORIENTATION); + isFrontFacing = + cameraCharacteristics.get(CameraCharacteristics.LENS_FACING) + == CameraMetadata.LENS_FACING_FRONT; + deviceOrientationListener = + new DeviceOrientationManager(activity, dartMessenger, isFrontFacing, sensorOrientation); + deviceOrientationListener.start(); + + // Resolution configuration + ResolutionPreset preset = ResolutionPreset.valueOf(resolutionPreset); + recordingProfile = + CameraUtils.getBestAvailableCamcorderProfileForResolutionPreset(cameraName, preset); + captureSize = new Size(recordingProfile.videoFrameWidth, recordingProfile.videoFrameHeight); + Log.i(TAG, "captureSize: " + captureSize); + + previewSize = computeBestPreviewSize(cameraName, preset); + + // Zoom setup + cameraZoom = + new CameraZoom( + cameraCharacteristics.get(CameraCharacteristics.SENSOR_INFO_ACTIVE_ARRAY_SIZE), + cameraCharacteristics.get(CameraCharacteristics.SCALER_AVAILABLE_MAX_DIGITAL_ZOOM)); + + // Start background thread. + startBackgroundThread(); + } + + /** + * Check if the auto focus is supported by the current camera. We look at the available AF modes + * and the available lens focusing distance to determine if its' a fixed length lens or not as + * well. + */ + private void checkAutoFocusSupported() { + int[] modes = cameraCharacteristics.get(CameraCharacteristics.CONTROL_AF_AVAILABLE_MODES); + // Log.i(TAG, "checkAutoFocusSupported | modes:"); + for (int mode : modes) { + // Log.i(TAG, "checkAutoFocusSupported | ==> " + mode); } - /** - * Check if the flash is supported. - */ - private void checkFlashSupported() { - Boolean available = cameraCharacteristics.get(CameraCharacteristics.FLASH_INFO_AVAILABLE); - mFlashSupported = available == null ? false : available; + // Check if fixed focal length lens. If LENS_INFO_MINIMUM_FOCUS_DISTANCE=0, then this is fixed. + // Can be null on some devices. + final Float minFocus = + cameraCharacteristics.get(CameraCharacteristics.LENS_INFO_MINIMUM_FOCUS_DISTANCE); + // final Float maxFocus = cameraCharacteristics.get(CameraCharacteristics.LENS_INFO_HYPERFOCAL_DISTANCE); + + // Value can be null on some devices: + // https://developer.android.com/reference/android/hardware/camera2/CameraCharacteristics#LENS_INFO_MINIMUM_FOCUS_DISTANCE + boolean isFixedLength; + if (minFocus == null) { + isFixedLength = true; + } else { + isFixedLength = minFocus == 0; } - - /** - * Load available FPS range for the current camera and update the available fps range with it. - * - * @param cameraCharacteristics - */ - private void getAvailableFpsRange(CameraCharacteristics cameraCharacteristics) { - // Log.i(TAG, "getAvailableFpsRange"); - - try { - Range[] ranges = - cameraCharacteristics.get(CameraCharacteristics.CONTROL_AE_AVAILABLE_TARGET_FPS_RANGES); - if (ranges != null) { - for (Range range : ranges) { - int upper = range.getUpper(); - // Log.i("Camera", "[FPS Range Available] is:" + range); - if (upper >= 10) { - if (fpsRange == null || upper > fpsRange.getUpper()) { - fpsRange = range; - } - } - } + // Log.i(TAG, "checkAutoFocusSupported | minFocus " + minFocus + " | maxFocus: " + maxFocus); + + mAutoFocusSupported = + !isFixedLength + && !(modes == null + || modes.length == 0 + || (modes.length == 1 && modes[0] == CameraCharacteristics.CONTROL_AF_MODE_OFF)); + Log.i(TAG, "checkAutoFocusSupported: " + mAutoFocusSupported); + } + + /** Check if the flash is supported. */ + private void checkFlashSupported() { + Boolean available = cameraCharacteristics.get(CameraCharacteristics.FLASH_INFO_AVAILABLE); + mFlashSupported = available == null ? false : available; + } + + /** + * Load available FPS range for the current camera and update the available fps range with it. + * + * @param cameraCharacteristics + */ + private void getAvailableFpsRange(CameraCharacteristics cameraCharacteristics) { + // Log.i(TAG, "getAvailableFpsRange"); + + try { + Range[] ranges = + cameraCharacteristics.get(CameraCharacteristics.CONTROL_AE_AVAILABLE_TARGET_FPS_RANGES); + if (ranges != null) { + for (Range range : ranges) { + int upper = range.getUpper(); + // Log.i("Camera", "[FPS Range Available] is:" + range); + if (upper >= 10) { + if (fpsRange == null || upper > fpsRange.getUpper()) { + fpsRange = range; } - } catch (Exception e) { - pictureCaptureRequest.error("cameraAccess", e.getMessage(), null); + } } - // Log.i("Camera", "[FPS Range] is:" + fpsRange); + } + } catch (Exception e) { + pictureCaptureRequest.error("cameraAccess", e.getMessage(), null); } + // Log.i("Camera", "[FPS Range] is:" + fpsRange); + } - private void prepareMediaRecorder(String outputFilePath) throws IOException { - // Log.i(TAG, "prepareMediaRecorder"); + private void prepareMediaRecorder(String outputFilePath) throws IOException { + // Log.i(TAG, "prepareMediaRecorder"); - if (mediaRecorder != null) { - mediaRecorder.release(); - } - - mediaRecorder = - new MediaRecorderBuilder(recordingProfile, outputFilePath) - .setEnableAudio(enableAudio) - .setMediaOrientation( - lockedCaptureOrientation == null - ? deviceOrientationListener.getMediaOrientation() - : deviceOrientationListener.getMediaOrientation(lockedCaptureOrientation)) - .build(); + if (mediaRecorder != null) { + mediaRecorder.release(); } - @SuppressLint("MissingPermission") - public void open(String imageFormatGroup) throws CameraAccessException { - pictureImageReader = - ImageReader.newInstance( - captureSize.getWidth(), - captureSize.getHeight(), - ImageFormat.JPEG, 2); - - Integer imageFormat = supportedImageFormats.get(imageFormatGroup); - if (imageFormat == null) { - Log.w(TAG, "The selected imageFormatGroup is not supported by Android. Defaulting to yuv420"); - imageFormat = ImageFormat.YUV_420_888; - } - - // Used to steam image byte data to dart side. - imageStreamReader = - ImageReader.newInstance(previewSize.getWidth(), previewSize.getHeight(), imageFormat, 2); - - cameraManager.openCamera( - cameraName, - new CameraDevice.StateCallback() { - @Override - public void onOpened(@NonNull CameraDevice device) { - // Log.i(TAG, "open | onOpened"); - - - cameraDevice = device; - try { - startPreview(); - dartMessenger.sendCameraInitializedEvent( - previewSize.getWidth(), - previewSize.getHeight(), - currentExposureMode, - currentFocusMode, - isExposurePointSupported(), - isFocusPointSupported()); - } catch (CameraAccessException e) { - dartMessenger.sendCameraErrorEvent(e.getMessage()); - close(); - } - } - - @Override - public void onClosed(@NonNull CameraDevice camera) { - // Log.i(TAG, "open | onClosed"); - - dartMessenger.sendCameraClosingEvent(); - super.onClosed(camera); - } - - @Override - public void onDisconnected(@NonNull CameraDevice cameraDevice) { - // Log.i(TAG, "open | onDisconnected"); - - close(); - dartMessenger.sendCameraErrorEvent("The camera was disconnected."); - } - - @Override - public void onError(@NonNull CameraDevice cameraDevice, int errorCode) { - // Log.i(TAG, "open | onError"); - - close(); - String errorDescription; - switch (errorCode) { - case ERROR_CAMERA_IN_USE: - errorDescription = "The camera device is in use already."; - break; - case ERROR_MAX_CAMERAS_IN_USE: - errorDescription = "Max cameras in use"; - break; - case ERROR_CAMERA_DISABLED: - errorDescription = "The camera device could not be opened due to a device policy."; - break; - case ERROR_CAMERA_DEVICE: - errorDescription = "The camera device has encountered a fatal error"; - break; - case ERROR_CAMERA_SERVICE: - errorDescription = "The camera service has encountered a fatal error."; - break; - default: - errorDescription = "Unknown camera error"; - } - dartMessenger.sendCameraErrorEvent(errorDescription); - } - }, - mBackgroundHandler - ); + mediaRecorder = + new MediaRecorderBuilder(recordingProfile, outputFilePath) + .setEnableAudio(enableAudio) + .setMediaOrientation( + lockedCaptureOrientation == null + ? deviceOrientationListener.getMediaOrientation() + : deviceOrientationListener.getMediaOrientation(lockedCaptureOrientation)) + .build(); + } + + @SuppressLint("MissingPermission") + public void open(String imageFormatGroup) throws CameraAccessException { + pictureImageReader = + ImageReader.newInstance( + captureSize.getWidth(), captureSize.getHeight(), ImageFormat.JPEG, 2); + + Integer imageFormat = supportedImageFormats.get(imageFormatGroup); + if (imageFormat == null) { + Log.w(TAG, "The selected imageFormatGroup is not supported by Android. Defaulting to yuv420"); + imageFormat = ImageFormat.YUV_420_888; } - private void createCaptureSession(int templateType, Surface... surfaces) - throws CameraAccessException { - createCaptureSession(templateType, null, surfaces); - } - - private void createCaptureSession( - int templateType, Runnable onSuccessCallback, Surface... surfaces) - throws CameraAccessException { - // Log.i(TAG, "createCaptureSession"); - - // Close any existing capture session. - closeCaptureSession(); + // Used to steam image byte data to dart side. + imageStreamReader = + ImageReader.newInstance(previewSize.getWidth(), previewSize.getHeight(), imageFormat, 2); - // Create a new capture builder. - mPreviewRequestBuilder = cameraDevice.createCaptureRequest(templateType); + cameraManager.openCamera( + cameraName, + new CameraDevice.StateCallback() { + @Override + public void onOpened(@NonNull CameraDevice device) { + // Log.i(TAG, "open | onOpened"); - // Build Flutter surface to render to - SurfaceTexture surfaceTexture = flutterTexture.surfaceTexture(); - surfaceTexture.setDefaultBufferSize(previewSize.getWidth(), previewSize.getHeight()); - Surface flutterSurface = new Surface(surfaceTexture); - mPreviewRequestBuilder.addTarget(flutterSurface); - - List remainingSurfaces = Arrays.asList(surfaces); - if (templateType != CameraDevice.TEMPLATE_PREVIEW) { - // If it is not preview mode, add all surfaces as targets. - for (Surface surface : remainingSurfaces) { - mPreviewRequestBuilder.addTarget(surface); + cameraDevice = device; + try { + startPreview(); + dartMessenger.sendCameraInitializedEvent( + previewSize.getWidth(), + previewSize.getHeight(), + currentExposureMode, + currentFocusMode, + isExposurePointSupported(), + isFocusPointSupported()); + } catch (CameraAccessException e) { + dartMessenger.sendCameraErrorEvent(e.getMessage()); + close(); } - } + } - cameraRegions = new CameraRegions(getRegionBoundaries()); - - // Prepare the callback - CameraCaptureSession.StateCallback callback = - new CameraCaptureSession.StateCallback() { - @Override - public void onConfigured(@NonNull CameraCaptureSession session) { - // Camera was already closed. - if (cameraDevice == null) { - dartMessenger.sendCameraErrorEvent("The camera was closed during configuration."); - return; - } - captureSession = session; - - updateFpsRange(); - updateFocusMode(mPreviewRequestBuilder); - updateFlash(mPreviewRequestBuilder); - updateExposureMode(mPreviewRequestBuilder); - - refreshPreviewCaptureSession( - onSuccessCallback, (code, message) -> dartMessenger.sendCameraErrorEvent(message)); - } - - @Override - public void onConfigureFailed(@NonNull CameraCaptureSession cameraCaptureSession) { - dartMessenger.sendCameraErrorEvent("Failed to configure camera session."); - } - }; - - // Start the session - if (VERSION.SDK_INT >= VERSION_CODES.P) { - // Collect all surfaces we want to render to. - List configs = new ArrayList<>(); - configs.add(new OutputConfiguration(flutterSurface)); - for (Surface surface : remainingSurfaces) { - configs.add(new OutputConfiguration(surface)); - } - createCaptureSessionWithSessionConfig(configs, callback); - } else { - // Collect all surfaces we want to render to. - List surfaceList = new ArrayList<>(); - surfaceList.add(flutterSurface); - surfaceList.addAll(remainingSurfaces); - createCaptureSession(surfaceList, callback); - } - } + @Override + public void onClosed(@NonNull CameraDevice camera) { + // Log.i(TAG, "open | onClosed"); - @TargetApi(VERSION_CODES.P) - private void createCaptureSessionWithSessionConfig( - List outputConfigs, CameraCaptureSession.StateCallback callback) - throws CameraAccessException { - cameraDevice.createCaptureSession( - new SessionConfiguration( - SessionConfiguration.SESSION_REGULAR, - outputConfigs, - Executors.newSingleThreadExecutor(), - callback)); - } + dartMessenger.sendCameraClosingEvent(); + super.onClosed(camera); + } - @TargetApi(VERSION_CODES.LOLLIPOP) - @SuppressWarnings("deprecation") - private void createCaptureSession( - List surfaces, CameraCaptureSession.StateCallback callback) - throws CameraAccessException { - cameraDevice.createCaptureSession(surfaces, callback, mBackgroundHandler); - } + @Override + public void onDisconnected(@NonNull CameraDevice cameraDevice) { + // Log.i(TAG, "open | onDisconnected"); - // Send a repeating request to refresh our capture session. - private void refreshPreviewCaptureSession( - @Nullable Runnable onSuccessCallback, @NonNull ErrorCallback onErrorCallback) { - // Log.i(TAG, "refreshPreviewCaptureSession"); - if (captureSession == null) { - // Log.i(TAG, "[refreshPreviewCaptureSession] mPreviewSession null, returning"); - return; - } + close(); + dartMessenger.sendCameraErrorEvent("The camera was disconnected."); + } - try { - captureSession.setRepeatingRequest( - mPreviewRequestBuilder.build(), - mCaptureCallback, - mBackgroundHandler); + @Override + public void onError(@NonNull CameraDevice cameraDevice, int errorCode) { + // Log.i(TAG, "open | onError"); - if (onSuccessCallback != null) { - onSuccessCallback.run(); + close(); + String errorDescription; + switch (errorCode) { + case ERROR_CAMERA_IN_USE: + errorDescription = "The camera device is in use already."; + break; + case ERROR_MAX_CAMERAS_IN_USE: + errorDescription = "Max cameras in use"; + break; + case ERROR_CAMERA_DISABLED: + errorDescription = "The camera device could not be opened due to a device policy."; + break; + case ERROR_CAMERA_DEVICE: + errorDescription = "The camera device has encountered a fatal error"; + break; + case ERROR_CAMERA_SERVICE: + errorDescription = "The camera service has encountered a fatal error."; + break; + default: + errorDescription = "Unknown camera error"; } - - - } catch (CameraAccessException | IllegalStateException | IllegalArgumentException e) { - onErrorCallback.onError("cameraAccess", e.getMessage()); - } + dartMessenger.sendCameraErrorEvent(errorDescription); + } + }, + mBackgroundHandler); + } + + private void createCaptureSession(int templateType, Surface... surfaces) + throws CameraAccessException { + createCaptureSession(templateType, null, surfaces); + } + + private void createCaptureSession( + int templateType, Runnable onSuccessCallback, Surface... surfaces) + throws CameraAccessException { + // Log.i(TAG, "createCaptureSession"); + + // Close any existing capture session. + closeCaptureSession(); + + // Create a new capture builder. + mPreviewRequestBuilder = cameraDevice.createCaptureRequest(templateType); + + // Build Flutter surface to render to + SurfaceTexture surfaceTexture = flutterTexture.surfaceTexture(); + surfaceTexture.setDefaultBufferSize(previewSize.getWidth(), previewSize.getHeight()); + Surface flutterSurface = new Surface(surfaceTexture); + mPreviewRequestBuilder.addTarget(flutterSurface); + + List remainingSurfaces = Arrays.asList(surfaces); + if (templateType != CameraDevice.TEMPLATE_PREVIEW) { + // If it is not preview mode, add all surfaces as targets. + for (Surface surface : remainingSurfaces) { + mPreviewRequestBuilder.addTarget(surface); + } } - public void takePicture(@NonNull final Result result) { - // Log.i(TAG, "takePicture | useAutoFocus: " + useAutoFocus); - - // Only take one 1 picture at a time. - if (pictureCaptureRequest != null && !pictureCaptureRequest.isFinished()) { - result.error("captureAlreadyActive", "Picture is currently already being captured", null); - return; - } - - // Create temporary file - final File outputDir = applicationContext.getCacheDir(); - try { - final File file = File.createTempFile("CAP", ".jpg", outputDir); - - // Start a new capture - pictureCaptureRequest = new PictureCaptureRequest(result, file, dartMessenger); - } catch (IOException | SecurityException e) { - pictureCaptureRequest.error("cannotCreateFile", e.getMessage(), null); - return; - } - - // Listen for picture being taken - pictureImageReader.setOnImageAvailableListener(mOnImageAvailableListener, mBackgroundHandler); - - if (useAutoFocus) { - runPictureAutoFocus(); - } else { - runPrecaptureSequence(); - } - } + cameraRegions = new CameraRegions(getRegionBoundaries()); + + // Prepare the callback + CameraCaptureSession.StateCallback callback = + new CameraCaptureSession.StateCallback() { + @Override + public void onConfigured(@NonNull CameraCaptureSession session) { + // Camera was already closed. + if (cameraDevice == null) { + dartMessenger.sendCameraErrorEvent("The camera was closed during configuration."); + return; + } + captureSession = session; - /** - * Run the precapture sequence for capturing a still image. This method should be called when - * we get a response in {@link #mCaptureCallback} from lockFocus(). - */ - private void runPrecaptureSequence() { - // Log.i(TAG, "runPrecaptureSequence"); - try { - // First set precapture state to idle or else it can hang in STATE_WAITING_PRECAPTURE - mPreviewRequestBuilder.set(CaptureRequest.CONTROL_AE_PRECAPTURE_TRIGGER, - CaptureRequest.CONTROL_AE_PRECAPTURE_TRIGGER_IDLE); - captureSession.capture(mPreviewRequestBuilder.build(), mCaptureCallback, - mBackgroundHandler); + updateFpsRange(); + updateFocusMode(mPreviewRequestBuilder); + updateFlash(mPreviewRequestBuilder); + updateExposureMode(mPreviewRequestBuilder); - // Repeating request to refresh preview session refreshPreviewCaptureSession( - null, - (code, message) -> pictureCaptureRequest.error("cameraAccess", message, null)); - - // Start precapture now - cameraState = CameraState.STATE_WAITING_PRECAPTURE_START; - - mPreviewRequestBuilder.set(CaptureRequest.CONTROL_AE_PRECAPTURE_TRIGGER, - CaptureRequest.CONTROL_AE_PRECAPTURE_TRIGGER_START); - - // Trigger one capture to start AE sequence - captureSession.capture(mPreviewRequestBuilder.build(), mCaptureCallback, - mBackgroundHandler); - - } catch (CameraAccessException e) { - e.printStackTrace(); - } + onSuccessCallback, (code, message) -> dartMessenger.sendCameraErrorEvent(message)); + } + + @Override + public void onConfigureFailed(@NonNull CameraCaptureSession cameraCaptureSession) { + dartMessenger.sendCameraErrorEvent("Failed to configure camera session."); + } + }; + + // Start the session + if (VERSION.SDK_INT >= VERSION_CODES.P) { + // Collect all surfaces we want to render to. + List configs = new ArrayList<>(); + configs.add(new OutputConfiguration(flutterSurface)); + for (Surface surface : remainingSurfaces) { + configs.add(new OutputConfiguration(surface)); + } + createCaptureSessionWithSessionConfig(configs, callback); + } else { + // Collect all surfaces we want to render to. + List surfaceList = new ArrayList<>(); + surfaceList.add(flutterSurface); + surfaceList.addAll(remainingSurfaces); + createCaptureSession(surfaceList, callback); + } + } + + @TargetApi(VERSION_CODES.P) + private void createCaptureSessionWithSessionConfig( + List outputConfigs, CameraCaptureSession.StateCallback callback) + throws CameraAccessException { + cameraDevice.createCaptureSession( + new SessionConfiguration( + SessionConfiguration.SESSION_REGULAR, + outputConfigs, + Executors.newSingleThreadExecutor(), + callback)); + } + + @TargetApi(VERSION_CODES.LOLLIPOP) + @SuppressWarnings("deprecation") + private void createCaptureSession( + List surfaces, CameraCaptureSession.StateCallback callback) + throws CameraAccessException { + cameraDevice.createCaptureSession(surfaces, callback, mBackgroundHandler); + } + + // Send a repeating request to refresh our capture session. + private void refreshPreviewCaptureSession( + @Nullable Runnable onSuccessCallback, @NonNull ErrorCallback onErrorCallback) { + // Log.i(TAG, "refreshPreviewCaptureSession"); + if (captureSession == null) { + // Log.i(TAG, "[refreshPreviewCaptureSession] mPreviewSession null, returning"); + return; } - /** - * Capture a still picture. This method should be called when we get a response in - * {@link #mCaptureCallback} from both lockFocus(). - */ - private void takePictureAfterPrecapture() { - // Log.i(TAG, "captureStillPicture"); - cameraState = CameraState.STATE_CAPTURING; - pictureCaptureRequest.setState(PictureCaptureRequestState.STATE_CAPTURING); - - - try { - if (null == cameraDevice) { - return; - } - // This is the CaptureRequest.Builder that we use to take a picture. - final CaptureRequest.Builder stillBuilder = - cameraDevice.createCaptureRequest(CameraDevice.TEMPLATE_STILL_CAPTURE); - stillBuilder.addTarget(pictureImageReader.getSurface()); + try { + captureSession.setRepeatingRequest( + mPreviewRequestBuilder.build(), mCaptureCallback, mBackgroundHandler); - // Zoom - stillBuilder.set( - CaptureRequest.SCALER_CROP_REGION, - mPreviewRequestBuilder.get(CaptureRequest.SCALER_CROP_REGION)); + if (onSuccessCallback != null) { + onSuccessCallback.run(); + } - // Set focus / flash from preview mode - updateFlash(stillBuilder); - updateFocusMode(stillBuilder); - updateExposureMode(stillBuilder); + } catch (CameraAccessException | IllegalStateException | IllegalArgumentException e) { + onErrorCallback.onError("cameraAccess", e.getMessage()); + } + } - // Orientation - int rotation = activity.getWindowManager().getDefaultDisplay().getRotation(); - stillBuilder.set(CaptureRequest.JPEG_ORIENTATION, getOrientation(rotation)); + public void takePicture(@NonNull final Result result) { + // Log.i(TAG, "takePicture | useAutoFocus: " + useAutoFocus); - CameraCaptureSession.CaptureCallback CaptureCallback - = new CameraCaptureSession.CaptureCallback() { + // Only take one 1 picture at a time. + if (pictureCaptureRequest != null && !pictureCaptureRequest.isFinished()) { + result.error("captureAlreadyActive", "Picture is currently already being captured", null); + return; + } - @Override - public void onCaptureStarted(@NonNull CameraCaptureSession session, - @NonNull CaptureRequest request, - long timestamp, - long frameNumber) { - // Log.i(TAG, "onCaptureStarted"); - } + // Create temporary file + final File outputDir = applicationContext.getCacheDir(); + try { + final File file = File.createTempFile("CAP", ".jpg", outputDir); - @Override - public void onCaptureProgressed(@NonNull CameraCaptureSession session, - @NonNull CaptureRequest request, - @NonNull CaptureResult partialResult) { - // Log.i(TAG, "onCaptureProgressed"); - } + // Start a new capture + pictureCaptureRequest = new PictureCaptureRequest(result, file, dartMessenger); + } catch (IOException | SecurityException e) { + pictureCaptureRequest.error("cannotCreateFile", e.getMessage(), null); + return; + } - @Override - public void onCaptureCompleted(@NonNull CameraCaptureSession session, - @NonNull CaptureRequest request, - @NonNull TotalCaptureResult result) { - // Log.i(TAG, "onCaptureCompleted"); - unlockAutoFocus(); - } - }; + // Listen for picture being taken + pictureImageReader.setOnImageAvailableListener(mOnImageAvailableListener, mBackgroundHandler); - captureSession.stopRepeating(); - captureSession.abortCaptures(); - // Log.i(TAG, "sending capture request"); - captureSession.capture(stillBuilder.build(), CaptureCallback, mBackgroundHandler); - } catch (CameraAccessException e) { - pictureCaptureRequest.error("cameraAccess", e.getMessage(), null); - } + if (useAutoFocus) { + runPictureAutoFocus(); + } else { + runPrecaptureSequence(); } - - /** - * Starts a background thread and its {@link Handler}. - * TODO: call when activity resumed - */ - private void startBackgroundThread() { - mBackgroundThread = new HandlerThread("CameraBackground"); - mBackgroundThread.start(); - mBackgroundHandler = new Handler(mBackgroundThread.getLooper()); + } + + /** + * Run the precapture sequence for capturing a still image. This method should be called when we + * get a response in {@link #mCaptureCallback} from lockFocus(). + */ + private void runPrecaptureSequence() { + // Log.i(TAG, "runPrecaptureSequence"); + try { + // First set precapture state to idle or else it can hang in STATE_WAITING_PRECAPTURE + mPreviewRequestBuilder.set( + CaptureRequest.CONTROL_AE_PRECAPTURE_TRIGGER, + CaptureRequest.CONTROL_AE_PRECAPTURE_TRIGGER_IDLE); + captureSession.capture(mPreviewRequestBuilder.build(), mCaptureCallback, mBackgroundHandler); + + // Repeating request to refresh preview session + refreshPreviewCaptureSession( + null, (code, message) -> pictureCaptureRequest.error("cameraAccess", message, null)); + + // Start precapture now + cameraState = CameraState.STATE_WAITING_PRECAPTURE_START; + + mPreviewRequestBuilder.set( + CaptureRequest.CONTROL_AE_PRECAPTURE_TRIGGER, + CaptureRequest.CONTROL_AE_PRECAPTURE_TRIGGER_START); + + // Trigger one capture to start AE sequence + captureSession.capture(mPreviewRequestBuilder.build(), mCaptureCallback, mBackgroundHandler); + + } catch (CameraAccessException e) { + e.printStackTrace(); } + } + + /** + * Capture a still picture. This method should be called when we get a response in {@link + * #mCaptureCallback} from both lockFocus(). + */ + private void takePictureAfterPrecapture() { + // Log.i(TAG, "captureStillPicture"); + cameraState = CameraState.STATE_CAPTURING; + pictureCaptureRequest.setState(PictureCaptureRequestState.STATE_CAPTURING); + + try { + if (null == cameraDevice) { + return; + } + // This is the CaptureRequest.Builder that we use to take a picture. + final CaptureRequest.Builder stillBuilder = + cameraDevice.createCaptureRequest(CameraDevice.TEMPLATE_STILL_CAPTURE); + stillBuilder.addTarget(pictureImageReader.getSurface()); + + // Zoom + stillBuilder.set( + CaptureRequest.SCALER_CROP_REGION, + mPreviewRequestBuilder.get(CaptureRequest.SCALER_CROP_REGION)); + + // Set focus / flash from preview mode + updateFlash(stillBuilder); + updateFocusMode(stillBuilder); + updateExposureMode(stillBuilder); + + // Orientation + int rotation = activity.getWindowManager().getDefaultDisplay().getRotation(); + stillBuilder.set(CaptureRequest.JPEG_ORIENTATION, getOrientation(rotation)); + + CameraCaptureSession.CaptureCallback captureCallback = + new CameraCaptureSession.CaptureCallback() { + + @Override + public void onCaptureStarted( + @NonNull CameraCaptureSession session, + @NonNull CaptureRequest request, + long timestamp, + long frameNumber) { + // Log.i(TAG, "onCaptureStarted"); + } - /** - * Stops the background thread and its {@link Handler}. - * TODO: call when activity paused - */ - private void stopBackgroundThread() { - try { - if (mBackgroundThread != null) { - mBackgroundThread.quitSafely(); - mBackgroundThread.join(); - mBackgroundThread = null; + @Override + public void onCaptureProgressed( + @NonNull CameraCaptureSession session, + @NonNull CaptureRequest request, + @NonNull CaptureResult partialResult) { + // Log.i(TAG, "onCaptureProgressed"); } - mBackgroundHandler = null; - } catch (InterruptedException e) { - pictureCaptureRequest.error("cameraAccess", e.getMessage(), null); - } + @Override + public void onCaptureCompleted( + @NonNull CameraCaptureSession session, + @NonNull CaptureRequest request, + @NonNull TotalCaptureResult result) { + // Log.i(TAG, "onCaptureCompleted"); + unlockAutoFocus(); + } + }; + + captureSession.stopRepeating(); + captureSession.abortCaptures(); + // Log.i(TAG, "sending capture request"); + captureSession.capture(stillBuilder.build(), captureCallback, mBackgroundHandler); + } catch (CameraAccessException e) { + pictureCaptureRequest.error("cameraAccess", e.getMessage(), null); } - - /** - * Sync the requestBuilder exposure mode setting ot the current exposure mode setting of the camera. - */ - void updateExposureMode(CaptureRequest.Builder requestBuilder) { - // Log.i(TAG, "updateExposureMode"); - - // Applying auto exposure - MeteringRectangle aeRect = cameraRegions.getAEMeteringRectangle(); - requestBuilder.set( - CaptureRequest.CONTROL_AE_REGIONS, - aeRect == null ? null : new MeteringRectangle[]{cameraRegions.getAEMeteringRectangle()}); - - switch (currentExposureMode) { - case locked: - requestBuilder.set(CaptureRequest.CONTROL_AE_LOCK, true); - break; - case auto: - default: - requestBuilder.set(CaptureRequest.CONTROL_AE_LOCK, false); - break; - } - - // TODO: move this to its own setting (exposure offset) - requestBuilder.set(CaptureRequest.CONTROL_AE_EXPOSURE_COMPENSATION, exposureOffset); + } + + /** Starts a background thread and its {@link Handler}. TODO: call when activity resumed */ + private void startBackgroundThread() { + mBackgroundThread = new HandlerThread("CameraBackground"); + mBackgroundThread.start(); + mBackgroundHandler = new Handler(mBackgroundThread.getLooper()); + } + + /** Stops the background thread and its {@link Handler}. TODO: call when activity paused */ + private void stopBackgroundThread() { + try { + if (mBackgroundThread != null) { + mBackgroundThread.quitSafely(); + mBackgroundThread.join(); + mBackgroundThread = null; + } + + mBackgroundHandler = null; + } catch (InterruptedException e) { + pictureCaptureRequest.error("cameraAccess", e.getMessage(), null); + } + } + + /** + * Sync the requestBuilder exposure mode setting ot the current exposure mode setting of the + * camera. + */ + void updateExposureMode(CaptureRequest.Builder requestBuilder) { + // Log.i(TAG, "updateExposureMode"); + + // Applying auto exposure + MeteringRectangle aeRect = cameraRegions.getAEMeteringRectangle(); + requestBuilder.set( + CaptureRequest.CONTROL_AE_REGIONS, + aeRect == null ? null : new MeteringRectangle[] {cameraRegions.getAEMeteringRectangle()}); + + switch (currentExposureMode) { + case locked: + requestBuilder.set(CaptureRequest.CONTROL_AE_LOCK, true); + break; + case auto: + default: + requestBuilder.set(CaptureRequest.CONTROL_AE_LOCK, false); + break; } - /** - * Sync the requestBuilder flash setting to the current flash mode setting of the camera. - */ - void updateFlash(CaptureRequest.Builder requestBuilder) { - // Log.i(TAG, "updateFlash"); + // TODO: move this to its own setting (exposure offset) + requestBuilder.set(CaptureRequest.CONTROL_AE_EXPOSURE_COMPENSATION, exposureOffset); + } - if (!mFlashSupported) { - return; - } + /** Sync the requestBuilder flash setting to the current flash mode setting of the camera. */ + void updateFlash(CaptureRequest.Builder requestBuilder) { + // Log.i(TAG, "updateFlash"); - switch (currentFlashMode) { - case off: - requestBuilder.set(CaptureRequest.CONTROL_AE_MODE, - CaptureRequest.CONTROL_AE_MODE_ON); - requestBuilder.set(CaptureRequest.FLASH_MODE, - CaptureRequest.FLASH_MODE_OFF); - break; + if (!mFlashSupported) { + return; + } - case always: - requestBuilder.set(CaptureRequest.CONTROL_AE_MODE, - CaptureRequest.CONTROL_AE_MODE_ON_ALWAYS_FLASH); - requestBuilder.set(CaptureRequest.FLASH_MODE, - CaptureRequest.FLASH_MODE_OFF); - break; + switch (currentFlashMode) { + case off: + requestBuilder.set(CaptureRequest.CONTROL_AE_MODE, CaptureRequest.CONTROL_AE_MODE_ON); + requestBuilder.set(CaptureRequest.FLASH_MODE, CaptureRequest.FLASH_MODE_OFF); + break; - case torch: - requestBuilder.set(CaptureRequest.CONTROL_AE_MODE, - CaptureRequest.CONTROL_AE_MODE_ON); - requestBuilder.set(CaptureRequest.FLASH_MODE, - CaptureRequest.FLASH_MODE_TORCH); - break; + case always: + requestBuilder.set( + CaptureRequest.CONTROL_AE_MODE, CaptureRequest.CONTROL_AE_MODE_ON_ALWAYS_FLASH); + requestBuilder.set(CaptureRequest.FLASH_MODE, CaptureRequest.FLASH_MODE_OFF); + break; - case auto: - requestBuilder.set(CaptureRequest.CONTROL_AE_MODE, - CaptureRequest.CONTROL_AE_MODE_ON_AUTO_FLASH); - requestBuilder.set(CaptureRequest.FLASH_MODE, - CaptureRequest.FLASH_MODE_OFF); - break; + case torch: + requestBuilder.set(CaptureRequest.CONTROL_AE_MODE, CaptureRequest.CONTROL_AE_MODE_ON); + requestBuilder.set(CaptureRequest.FLASH_MODE, CaptureRequest.FLASH_MODE_TORCH); + break; - // TODO: to be implemented someday. Need to add it to dart/iOS as another flash mode setting. -// case autoRedEye: -// requestBuilder.set(CaptureRequest.CONTROL_AE_MODE, -// CaptureRequest.CONTROL_AE_MODE_ON_AUTO_FLASH_REDEYE); -// requestBuilder.set(CaptureRequest.FLASH_MODE, -// CaptureRequest.FLASH_MODE_OFF); -// break; - } + case auto: + requestBuilder.set( + CaptureRequest.CONTROL_AE_MODE, CaptureRequest.CONTROL_AE_MODE_ON_AUTO_FLASH); + requestBuilder.set(CaptureRequest.FLASH_MODE, CaptureRequest.FLASH_MODE_OFF); + break; + + // TODO: to be implemented someday. Need to add it to dart/iOS as another flash mode setting. + // case autoRedEye: + // requestBuilder.set(CaptureRequest.CONTROL_AE_MODE, + // CaptureRequest.CONTROL_AE_MODE_ON_AUTO_FLASH_REDEYE); + // requestBuilder.set(CaptureRequest.FLASH_MODE, + // CaptureRequest.FLASH_MODE_OFF); + // break; } - - /** - * Retrieves the JPEG orientation from the specified screen rotation. - * - * @param rotation The screen rotation. - * @return The JPEG orientation (one of 0, 90, 270, and 360) - */ - private int getOrientation(int rotation) { - // Sensor orientation is 90 for most devices, or 270 for some devices (eg. Nexus 5X) - // We have to take that into account and rotate JPEG properly. - // For devices with orientation of 90, we simply return our mapping from ORIENTATIONS. - // For devices with orientation of 270, we need to rotate the JPEG 180 degrees. - return (ORIENTATIONS.get(rotation) + sensorOrientation + 270) % 360; + } + + /** + * Retrieves the JPEG orientation from the specified screen rotation. + * + * @param rotation The screen rotation. + * @return The JPEG orientation (one of 0, 90, 270, and 360) + */ + private int getOrientation(int rotation) { + // Sensor orientation is 90 for most devices, or 270 for some devices (eg. Nexus 5X) + // We have to take that into account and rotate JPEG properly. + // For devices with orientation of 90, we simply return our mapping from ORIENTATIONS. + // For devices with orientation of 270, we need to rotate the JPEG 180 degrees. + return (ORIENTATIONS.get(rotation) + sensorOrientation + 270) % 360; + } + + /** Start capturing a picture, doing autofocus first. */ + private void runPictureAutoFocus() { + // Log.i(TAG, "runPictureAutoFocus"); + assert (pictureCaptureRequest != null); + + cameraState = CameraState.STATE_WAITING_FOCUS; + pictureCaptureRequest.setState(PictureCaptureRequestState.STATE_WAITING_FOCUS); + lockAutoFocus(); + } + + /** Start the autofocus routine on the current capture request. */ + private void lockAutoFocus() { + // Log.i(TAG, "lockAutoFocus"); + pictureCaptureRequest.setState(PictureCaptureRequestState.STATE_WAITING_PRECAPTURE_START); + + mPreviewRequestBuilder.set( + CaptureRequest.CONTROL_AF_TRIGGER, CaptureRequest.CONTROL_AF_TRIGGER_START); + + refreshPreviewCaptureSession( + null, (code, message) -> pictureCaptureRequest.error(code, message, null)); + } + + /** Cancel and reset auto focus state and refresh the preview session. */ + private void unlockAutoFocus() { + // Log.i(TAG, "unlockAutoFocus"); + try { + // Cancel existing AF state + mPreviewRequestBuilder.set( + CaptureRequest.CONTROL_AF_TRIGGER, CameraMetadata.CONTROL_AF_TRIGGER_CANCEL); + captureSession.capture(mPreviewRequestBuilder.build(), null, mBackgroundHandler); + + // Set AE state to idle again + mPreviewRequestBuilder.set( + CaptureRequest.CONTROL_AE_PRECAPTURE_TRIGGER, + CaptureRequest.CONTROL_AE_PRECAPTURE_TRIGGER_IDLE); + + // Set AF state to idle again + mPreviewRequestBuilder.set( + CaptureRequest.CONTROL_AF_TRIGGER, CameraMetadata.CONTROL_AF_TRIGGER_IDLE); + + captureSession.capture(mPreviewRequestBuilder.build(), null, mBackgroundHandler); + } catch (CameraAccessException e) { + // Log.i(TAG, "Error unlocking focus: " + e.getMessage()); + dartMessenger.sendCameraErrorEvent(e.getMessage()); + return; } - /** - * Start capturing a picture, doing autofocus first. - */ - private void runPictureAutoFocus() { - // Log.i(TAG, "runPictureAutoFocus"); - assert (pictureCaptureRequest != null); - - cameraState = CameraState.STATE_WAITING_FOCUS; - pictureCaptureRequest.setState(PictureCaptureRequestState.STATE_WAITING_FOCUS); - lockAutoFocus(); + refreshPreviewCaptureSession( + null, + (errorCode, errorMessage) -> pictureCaptureRequest.error(errorCode, errorMessage, null)); + } + + public void startVideoRecording(Result result) { + final File outputDir = applicationContext.getCacheDir(); + try { + videoRecordingFile = File.createTempFile("REC", ".mp4", outputDir); + } catch (IOException | SecurityException e) { + result.error("cannotCreateFile", e.getMessage(), null); + return; } - /** - * Start the autofocus routine on the current capture request. - */ - private void lockAutoFocus() { - // Log.i(TAG, "lockAutoFocus"); - pictureCaptureRequest.setState(PictureCaptureRequestState.STATE_WAITING_PRECAPTURE_START); - - mPreviewRequestBuilder.set( - CaptureRequest.CONTROL_AF_TRIGGER, CaptureRequest.CONTROL_AF_TRIGGER_START); - - refreshPreviewCaptureSession( - null, (code, message) -> pictureCaptureRequest.error(code, message, null)); + try { + prepareMediaRecorder(videoRecordingFile.getAbsolutePath()); + recordingVideo = true; + createCaptureSession( + CameraDevice.TEMPLATE_RECORD, () -> mediaRecorder.start(), mediaRecorder.getSurface()); + result.success(null); + } catch (CameraAccessException | IOException e) { + recordingVideo = false; + videoRecordingFile = null; + result.error("videoRecordingFailed", e.getMessage(), null); } + } - /** - * Cancel and reset auto focus state and refresh the preview session. - */ - private void unlockAutoFocus() { - // Log.i(TAG, "unlockAutoFocus"); - try { - // Cancel existing AF state - mPreviewRequestBuilder.set( - CaptureRequest.CONTROL_AF_TRIGGER, CameraMetadata.CONTROL_AF_TRIGGER_CANCEL); - captureSession.capture(mPreviewRequestBuilder.build(), null, - mBackgroundHandler); - - // Set AE state to idle again - mPreviewRequestBuilder.set(CaptureRequest.CONTROL_AE_PRECAPTURE_TRIGGER, - CaptureRequest.CONTROL_AE_PRECAPTURE_TRIGGER_IDLE); - - // Set AF state to idle again - mPreviewRequestBuilder.set( - CaptureRequest.CONTROL_AF_TRIGGER, CameraMetadata.CONTROL_AF_TRIGGER_IDLE); - - captureSession.capture(mPreviewRequestBuilder.build(), null, mBackgroundHandler); - } catch (CameraAccessException e) { - // Log.i(TAG, "Error unlocking focus: " + e.getMessage()); - dartMessenger.sendCameraErrorEvent(e.getMessage()); - return; - } - - refreshPreviewCaptureSession( - null, - (errorCode, errorMessage) -> pictureCaptureRequest.error(errorCode, errorMessage, null)); + public void stopVideoRecording(@NonNull final Result result) { + if (!recordingVideo) { + result.success(null); + return; } - public void startVideoRecording(Result result) { - final File outputDir = applicationContext.getCacheDir(); - try { - videoRecordingFile = File.createTempFile("REC", ".mp4", outputDir); - } catch (IOException | SecurityException e) { - result.error("cannotCreateFile", e.getMessage(), null); - return; - } - - try { - prepareMediaRecorder(videoRecordingFile.getAbsolutePath()); - recordingVideo = true; - createCaptureSession( - CameraDevice.TEMPLATE_RECORD, () -> mediaRecorder.start(), mediaRecorder.getSurface()); - result.success(null); - } catch (CameraAccessException | IOException e) { - recordingVideo = false; - videoRecordingFile = null; - result.error("videoRecordingFailed", e.getMessage(), null); - } + try { + recordingVideo = false; + + try { + captureSession.abortCaptures(); + mediaRecorder.stop(); + } catch (CameraAccessException | IllegalStateException e) { + // Ignore exceptions and try to continue (changes are camera session already aborted capture) + } + + mediaRecorder.reset(); + startPreview(); + result.success(videoRecordingFile.getAbsolutePath()); + videoRecordingFile = null; + } catch (CameraAccessException | IllegalStateException e) { + result.error("videoRecordingFailed", e.getMessage(), null); } + } - public void stopVideoRecording(@NonNull final Result result) { - if (!recordingVideo) { - result.success(null); - return; - } - - try { - recordingVideo = false; - - try { - captureSession.abortCaptures(); - mediaRecorder.stop(); - } catch (CameraAccessException | IllegalStateException e) { - // Ignore exceptions and try to continue (changes are camera session already aborted capture) - } - - mediaRecorder.reset(); - startPreview(); - result.success(videoRecordingFile.getAbsolutePath()); - videoRecordingFile = null; - } catch (CameraAccessException | IllegalStateException e) { - result.error("videoRecordingFailed", e.getMessage(), null); - } + public void pauseVideoRecording(@NonNull final Result result) { + if (!recordingVideo) { + result.success(null); + return; } - public void pauseVideoRecording(@NonNull final Result result) { - if (!recordingVideo) { - result.success(null); - return; - } - - try { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { - mediaRecorder.pause(); - } else { - result.error("videoRecordingFailed", "pauseVideoRecording requires Android API +24.", null); - return; - } - } catch (IllegalStateException e) { - result.error("videoRecordingFailed", e.getMessage(), null); - return; - } - - result.success(null); + try { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { + mediaRecorder.pause(); + } else { + result.error("videoRecordingFailed", "pauseVideoRecording requires Android API +24.", null); + return; + } + } catch (IllegalStateException e) { + result.error("videoRecordingFailed", e.getMessage(), null); + return; } - public void resumeVideoRecording(@NonNull final Result result) { - if (!recordingVideo) { - result.success(null); - return; - } - - try { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { - mediaRecorder.resume(); - } else { - result.error( - "videoRecordingFailed", "resumeVideoRecording requires Android API +24.", null); - return; - } - } catch (IllegalStateException e) { - result.error("videoRecordingFailed", e.getMessage(), null); - return; - } + result.success(null); + } - result.success(null); + public void resumeVideoRecording(@NonNull final Result result) { + if (!recordingVideo) { + result.success(null); + return; } - /** - * Dart handler when it's time to set a new flash mode. This will try to set a new - * flash mode to the current camera. - * - * @param result - * @param newMode - * @throws CameraAccessException - */ - public void setFlashMode(@NonNull final Result result, FlashMode newMode) { - // Save the new flash mode setting - final FlashMode oldFlashMode = currentFlashMode; - currentFlashMode = newMode; - updateFlash(mPreviewRequestBuilder); - - refreshPreviewCaptureSession( - () -> result.success(null), - (code, message) -> result.error("setFlashModeFailed", "Could not set flash mode.", null)); + try { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { + mediaRecorder.resume(); + } else { + result.error( + "videoRecordingFailed", "resumeVideoRecording requires Android API +24.", null); + return; + } + } catch (IllegalStateException e) { + result.error("videoRecordingFailed", e.getMessage(), null); + return; } - /** - * Dart handler for setting new exposure mode setting. - * - * @param result - * @param newMode - * @throws CameraAccessException - */ - public void setExposureMode(@NonNull final Result result, ExposureMode newMode) - throws CameraAccessException { - currentExposureMode = newMode; - updateExposureMode(mPreviewRequestBuilder); - - refreshPreviewCaptureSession( - null, - (code, message) -> result.error("setExposureModeFailed", "Could not set exposure mode.", null)); - - result.success(null); + result.success(null); + } + + /** + * Dart handler when it's time to set a new flash mode. This will try to set a new flash mode to + * the current camera. + * + * @param result + * @param newMode + * @throws CameraAccessException + */ + public void setFlashMode(@NonNull final Result result, FlashMode newMode) { + // Save the new flash mode setting + final FlashMode oldFlashMode = currentFlashMode; + currentFlashMode = newMode; + updateFlash(mPreviewRequestBuilder); + + refreshPreviewCaptureSession( + () -> result.success(null), + (code, message) -> result.error("setFlashModeFailed", "Could not set flash mode.", null)); + } + + /** + * Dart handler for setting new exposure mode setting. + * + * @param result + * @param newMode + * @throws CameraAccessException + */ + public void setExposureMode(@NonNull final Result result, ExposureMode newMode) + throws CameraAccessException { + currentExposureMode = newMode; + updateExposureMode(mPreviewRequestBuilder); + + refreshPreviewCaptureSession( + null, + (code, message) -> + result.error("setExposureModeFailed", "Could not set exposure mode.", null)); + + result.success(null); + } + + public void setExposurePoint(@NonNull final Result result, Double x, Double y) + throws CameraAccessException { + // Check if exposure point functionality is available. + if (!isExposurePointSupported()) { + result.error( + "setExposurePointFailed", "Device does not have exposure point capabilities", null); + return; + } + // Check if the current region boundaries are known + if (cameraRegions.getMaxBoundaries() == null) { + result.error("setExposurePointFailed", "Could not determine max region boundaries", null); + return; } + // Set the metering rectangle + if (x == null || y == null) cameraRegions.resetAutoExposureMeteringRectangle(); + else cameraRegions.setAutoExposureMeteringRectangleFromPoint(y, 1 - x); + // Apply it + updateExposureMode(mPreviewRequestBuilder); + refreshPreviewCaptureSession( + () -> result.success(null), (code, message) -> result.error("CameraAccess", message, null)); + } + + /** + * Set new focus mode from dart. + * + * @param result + * @param newMode + * @throws CameraAccessException + */ + public void setFocusMode(@NonNull final Result result, FocusMode newMode) + throws CameraAccessException { + // Log.i(TAG, "setFocusMode: " + newMode); + + // Set new focus mode + currentFocusMode = newMode; + + // Sync new focus mode to the current capture request builder + updateFocusMode(mPreviewRequestBuilder); + + // Now depending on the new mode we either want to restart the AF routine (if setting to auto) + // or we want to trigger a one-time focus and then set AF to idle (locked mode). + switch (newMode) { + case auto: + // Log.i(TAG, "Triggering AF start with mode " + currentFocusMode); + // Reset state of autofocus so it goes back to passive scanning. + mPreviewRequestBuilder.set( + CaptureRequest.CONTROL_AF_TRIGGER, CaptureRequest.CONTROL_AF_TRIGGER_CANCEL); - public void setExposurePoint(@NonNull final Result result, Double x, Double y) - throws CameraAccessException { - // Check if exposure point functionality is available. - if (!isExposurePointSupported()) { - result.error( - "setExposurePointFailed", "Device does not have exposure point capabilities", null); - return; - } - // Check if the current region boundaries are known - if (cameraRegions.getMaxBoundaries() == null) { - result.error("setExposurePointFailed", "Could not determine max region boundaries", null); - return; - } - // Set the metering rectangle - if (x == null || y == null) cameraRegions.resetAutoExposureMeteringRectangle(); - else cameraRegions.setAutoExposureMeteringRectangleFromPoint(y, 1 - x); - // Apply it - updateExposureMode(mPreviewRequestBuilder); + // Refresh preview session using repeating request as it will be in CONTROL_AF_MODE_CONTINUOUS_PICTURE refreshPreviewCaptureSession( - () -> result.success(null), (code, message) -> result.error("CameraAccess", message, null)); - } + () -> result.success(null), + (code, message) -> result.error("setFocusMode", message, null)); + break; - /** - * Set new focus mode from dart. - * - * @param result - * @param newMode - * @throws CameraAccessException - */ - public void setFocusMode(@NonNull final Result result, FocusMode newMode) - throws CameraAccessException { - // Log.i(TAG, "setFocusMode: " + newMode); - - // Set new focus mode - currentFocusMode = newMode; - - // Sync new focus mode to the current capture request builder - updateFocusMode(mPreviewRequestBuilder); - - // Now depending on the new mode we either want to restart the AF routine (if setting to auto) - // or we want to trigger a one-time focus and then set AF to idle (locked mode). - switch (newMode) { - case auto: - // Log.i(TAG, "Triggering AF start with mode " + currentFocusMode); - // Reset state of autofocus so it goes back to passive scanning. - mPreviewRequestBuilder.set( - CaptureRequest.CONTROL_AF_TRIGGER, CaptureRequest.CONTROL_AF_TRIGGER_CANCEL); - - // Refresh preview session using repeating request as it will be in CONTROL_AF_MODE_CONTINUOUS_PICTURE - refreshPreviewCaptureSession( - () -> result.success(null), (code, message) -> result.error("setFocusMode", message, null)); - break; + case locked: + // AF mode will be in Auto so we just want to perform one AF routine + mPreviewRequestBuilder.set( + CaptureRequest.CONTROL_AF_TRIGGER, CaptureRequest.CONTROL_AF_TRIGGER_START); - case locked: - // AF mode will be in Auto so we just want to perform one AF routine - mPreviewRequestBuilder.set( - CaptureRequest.CONTROL_AF_TRIGGER, CaptureRequest.CONTROL_AF_TRIGGER_START); - - // Refresh the AF once. When the AF start is completed triggering then we will set it to idle mode. - // If we don't wait for the callback like this, then setting it to idle just resets the focus to infinity - // on some devices like Sony XZ. - try { - captureSession.capture(mPreviewRequestBuilder.build(), new CameraCaptureSession.CaptureCallback() { - @Override - public void onCaptureCompleted(@NonNull CameraCaptureSession session, - @NonNull CaptureRequest request, - @NonNull TotalCaptureResult _result) { - // Log.i(TAG, "Success after triggering AF start for locked focus"); - - mPreviewRequestBuilder.set( - CaptureRequest.CONTROL_AF_TRIGGER, CaptureRequest.CONTROL_AF_TRIGGER_IDLE); - refreshPreviewCaptureSession( - null, (code, message) -> result.error("setFocusMode", message, null)); - } - }, - mBackgroundHandler); - - result.success(null); - } catch (CameraAccessException e) { - result.error("setFocusMode", e.getMessage(), null); + // Refresh the AF once. When the AF start is completed triggering then we will set it to idle mode. + // If we don't wait for the callback like this, then setting it to idle just resets the focus to infinity + // on some devices like Sony XZ. + try { + captureSession.capture( + mPreviewRequestBuilder.build(), + new CameraCaptureSession.CaptureCallback() { + @Override + public void onCaptureCompleted( + @NonNull CameraCaptureSession session, + @NonNull CaptureRequest request, + @NonNull TotalCaptureResult _result) { + // Log.i(TAG, "Success after triggering AF start for locked focus"); + + mPreviewRequestBuilder.set( + CaptureRequest.CONTROL_AF_TRIGGER, CaptureRequest.CONTROL_AF_TRIGGER_IDLE); + refreshPreviewCaptureSession( + null, (code, message) -> result.error("setFocusMode", message, null)); } - break; - } - - - } + }, + mBackgroundHandler); - - public void setFocusPoint(@NonNull final Result result, Double x, Double y) - throws CameraAccessException { - // Check if focus point functionality is available. - if (!isFocusPointSupported()) { - result.error("setFocusPointFailed", "Device does not have focus point capabilities", null); - return; - } - - // Check if the current region boundaries are known - if (cameraRegions.getMaxBoundaries() == null) { - result.error("setFocusPointFailed", "Could not determine max region boundaries", null); - return; - } - - // Set the metering rectangle - if (x == null || y == null) { - cameraRegions.resetAutoFocusMeteringRectangle(); - } else { - cameraRegions.setAutoFocusMeteringRectangleFromPoint(y, 1 - x); + result.success(null); + } catch (CameraAccessException e) { + result.error("setFocusMode", e.getMessage(), null); } - - // Apply the new metering rectangle - setFocusMode(result, currentFocusMode); + break; } - - @TargetApi(VERSION_CODES.P) - private boolean supportsDistortionCorrection() throws CameraAccessException { - int[] availableDistortionCorrectionModes = - cameraManager - .getCameraCharacteristics(cameraDevice.getId()) - .get(CameraCharacteristics.DISTORTION_CORRECTION_AVAILABLE_MODES); - if (availableDistortionCorrectionModes == null) - availableDistortionCorrectionModes = new int[0]; - long nonOffModesSupported = - Arrays.stream(availableDistortionCorrectionModes) - .filter((value) -> value != CaptureRequest.DISTORTION_CORRECTION_MODE_OFF) - .count(); - return nonOffModesSupported > 0; + } + + public void setFocusPoint(@NonNull final Result result, Double x, Double y) + throws CameraAccessException { + // Check if focus point functionality is available. + if (!isFocusPointSupported()) { + result.error("setFocusPointFailed", "Device does not have focus point capabilities", null); + return; } - private Size getRegionBoundaries() throws CameraAccessException { - // No distortion correction support - if (android.os.Build.VERSION.SDK_INT < VERSION_CODES.P || !supportsDistortionCorrection()) { - return cameraManager - .getCameraCharacteristics(cameraDevice.getId()) - .get(CameraCharacteristics.SENSOR_INFO_PIXEL_ARRAY_SIZE); - } - // Get the current distortion correction mode - Integer distortionCorrectionMode = - mPreviewRequestBuilder.get(CaptureRequest.DISTORTION_CORRECTION_MODE); - // Return the correct boundaries depending on the mode - android.graphics.Rect rect; - if (distortionCorrectionMode == null - || distortionCorrectionMode == CaptureRequest.DISTORTION_CORRECTION_MODE_OFF) { - rect = - cameraManager - .getCameraCharacteristics(cameraDevice.getId()) - .get(CameraCharacteristics.SENSOR_INFO_PRE_CORRECTION_ACTIVE_ARRAY_SIZE); - } else { - rect = - cameraManager - .getCameraCharacteristics(cameraDevice.getId()) - .get(CameraCharacteristics.SENSOR_INFO_ACTIVE_ARRAY_SIZE); - } - return rect == null ? null : new Size(rect.width(), rect.height()); + // Check if the current region boundaries are known + if (cameraRegions.getMaxBoundaries() == null) { + result.error("setFocusPointFailed", "Could not determine max region boundaries", null); + return; } - private boolean isExposurePointSupported() throws CameraAccessException { - Integer supportedRegions = - cameraManager - .getCameraCharacteristics(cameraDevice.getId()) - .get(CameraCharacteristics.CONTROL_MAX_REGIONS_AE); - return supportedRegions != null && supportedRegions > 0; + // Set the metering rectangle + if (x == null || y == null) { + cameraRegions.resetAutoFocusMeteringRectangle(); + } else { + cameraRegions.setAutoFocusMeteringRectangleFromPoint(y, 1 - x); } - private boolean isFocusPointSupported() throws CameraAccessException { - Integer supportedRegions = - cameraManager - .getCameraCharacteristics(cameraDevice.getId()) - .get(CameraCharacteristics.CONTROL_MAX_REGIONS_AF); - return supportedRegions != null && supportedRegions > 0; + // Apply the new metering rectangle + setFocusMode(result, currentFocusMode); + } + + @TargetApi(VERSION_CODES.P) + private boolean supportsDistortionCorrection() throws CameraAccessException { + int[] availableDistortionCorrectionModes = + cameraManager + .getCameraCharacteristics(cameraDevice.getId()) + .get(CameraCharacteristics.DISTORTION_CORRECTION_AVAILABLE_MODES); + if (availableDistortionCorrectionModes == null) availableDistortionCorrectionModes = new int[0]; + long nonOffModesSupported = + Arrays.stream(availableDistortionCorrectionModes) + .filter((value) -> value != CaptureRequest.DISTORTION_CORRECTION_MODE_OFF) + .count(); + return nonOffModesSupported > 0; + } + + private Size getRegionBoundaries() throws CameraAccessException { + // No distortion correction support + if (android.os.Build.VERSION.SDK_INT < VERSION_CODES.P || !supportsDistortionCorrection()) { + return cameraManager + .getCameraCharacteristics(cameraDevice.getId()) + .get(CameraCharacteristics.SENSOR_INFO_PIXEL_ARRAY_SIZE); } - - public double getMinExposureOffset() throws CameraAccessException { - Range range = - cameraManager - .getCameraCharacteristics(cameraDevice.getId()) - .get(CameraCharacteristics.CONTROL_AE_COMPENSATION_RANGE); - double minStepped = range == null ? 0 : range.getLower(); - double stepSize = getExposureOffsetStepSize(); - return minStepped * stepSize; + // Get the current distortion correction mode + Integer distortionCorrectionMode = + mPreviewRequestBuilder.get(CaptureRequest.DISTORTION_CORRECTION_MODE); + // Return the correct boundaries depending on the mode + android.graphics.Rect rect; + if (distortionCorrectionMode == null + || distortionCorrectionMode == CaptureRequest.DISTORTION_CORRECTION_MODE_OFF) { + rect = + cameraManager + .getCameraCharacteristics(cameraDevice.getId()) + .get(CameraCharacteristics.SENSOR_INFO_PRE_CORRECTION_ACTIVE_ARRAY_SIZE); + } else { + rect = + cameraManager + .getCameraCharacteristics(cameraDevice.getId()) + .get(CameraCharacteristics.SENSOR_INFO_ACTIVE_ARRAY_SIZE); } - - public double getMaxExposureOffset() throws CameraAccessException { - Range range = - cameraManager - .getCameraCharacteristics(cameraDevice.getId()) - .get(CameraCharacteristics.CONTROL_AE_COMPENSATION_RANGE); - double maxStepped = range == null ? 0 : range.getUpper(); - double stepSize = getExposureOffsetStepSize(); - return maxStepped * stepSize; + return rect == null ? null : new Size(rect.width(), rect.height()); + } + + private boolean isExposurePointSupported() throws CameraAccessException { + Integer supportedRegions = + cameraManager + .getCameraCharacteristics(cameraDevice.getId()) + .get(CameraCharacteristics.CONTROL_MAX_REGIONS_AE); + return supportedRegions != null && supportedRegions > 0; + } + + private boolean isFocusPointSupported() throws CameraAccessException { + Integer supportedRegions = + cameraManager + .getCameraCharacteristics(cameraDevice.getId()) + .get(CameraCharacteristics.CONTROL_MAX_REGIONS_AF); + return supportedRegions != null && supportedRegions > 0; + } + + public double getMinExposureOffset() throws CameraAccessException { + Range range = + cameraManager + .getCameraCharacteristics(cameraDevice.getId()) + .get(CameraCharacteristics.CONTROL_AE_COMPENSATION_RANGE); + double minStepped = range == null ? 0 : range.getLower(); + double stepSize = getExposureOffsetStepSize(); + return minStepped * stepSize; + } + + public double getMaxExposureOffset() throws CameraAccessException { + Range range = + cameraManager + .getCameraCharacteristics(cameraDevice.getId()) + .get(CameraCharacteristics.CONTROL_AE_COMPENSATION_RANGE); + double maxStepped = range == null ? 0 : range.getUpper(); + double stepSize = getExposureOffsetStepSize(); + return maxStepped * stepSize; + } + + public double getExposureOffsetStepSize() throws CameraAccessException { + Rational stepSize = + cameraManager + .getCameraCharacteristics(cameraDevice.getId()) + .get(CameraCharacteristics.CONTROL_AE_COMPENSATION_STEP); + return stepSize == null ? 0.0 : stepSize.doubleValue(); + } + + public void setExposureOffset(@NonNull final Result result, double offset) + throws CameraAccessException { + // Set the exposure offset + double stepSize = getExposureOffsetStepSize(); + exposureOffset = (int) (offset / stepSize); + // Apply it + updateExposureMode(mPreviewRequestBuilder); + + // Refresh capture session + refreshPreviewCaptureSession( + () -> result.success(null), + (code, message) -> + result.error("setExposureModeFailed", "Could not set flash mode.", null)); + } + + public float getMaxZoomLevel() { + return cameraZoom.maxZoom; + } + + public float getMinZoomLevel() { + return CameraZoom.DEFAULT_ZOOM_FACTOR; + } + + public void setZoomLevel(@NonNull final Result result, float zoom) throws CameraAccessException { + float maxZoom = cameraZoom.maxZoom; + float minZoom = CameraZoom.DEFAULT_ZOOM_FACTOR; + + if (zoom > maxZoom || zoom < minZoom) { + String errorMessage = + String.format( + Locale.ENGLISH, + "Zoom level out of bounds (zoom level should be between %f and %f).", + minZoom, + maxZoom); + result.error("ZOOM_ERROR", errorMessage, null); + return; } - public double getExposureOffsetStepSize() throws CameraAccessException { - Rational stepSize = - cameraManager - .getCameraCharacteristics(cameraDevice.getId()) - .get(CameraCharacteristics.CONTROL_AE_COMPENSATION_STEP); - return stepSize == null ? 0.0 : stepSize.doubleValue(); + //Zoom area is calculated relative to sensor area (activeRect) + if (mPreviewRequestBuilder != null) { + final Rect computedZoom = cameraZoom.computeZoom(zoom); + mPreviewRequestBuilder.set(CaptureRequest.SCALER_CROP_REGION, computedZoom); + captureSession.setRepeatingRequest(mPreviewRequestBuilder.build(), null, mBackgroundHandler); } - public void setExposureOffset(@NonNull final Result result, double offset) - throws CameraAccessException { - // Set the exposure offset - double stepSize = getExposureOffsetStepSize(); - exposureOffset = (int) (offset / stepSize); - // Apply it - updateExposureMode(mPreviewRequestBuilder); - - // Refresh capture session - refreshPreviewCaptureSession(() -> result.success(null), - (code, message) -> result.error("setExposureModeFailed", "Could not set flash mode.", null)); - } + result.success(null); + } - public float getMaxZoomLevel() { - return cameraZoom.maxZoom; - } + public void lockCaptureOrientation(PlatformChannel.DeviceOrientation orientation) { + this.lockedCaptureOrientation = orientation; + } - public float getMinZoomLevel() { - return CameraZoom.DEFAULT_ZOOM_FACTOR; - } + public void unlockCaptureOrientation() { + this.lockedCaptureOrientation = null; + } - public void setZoomLevel(@NonNull final Result result, float zoom) throws CameraAccessException { - float maxZoom = cameraZoom.maxZoom; - float minZoom = CameraZoom.DEFAULT_ZOOM_FACTOR; - - if (zoom > maxZoom || zoom < minZoom) { - String errorMessage = - String.format( - Locale.ENGLISH, - "Zoom level out of bounds (zoom level should be between %f and %f).", - minZoom, - maxZoom); - result.error("ZOOM_ERROR", errorMessage, null); - return; - } - - //Zoom area is calculated relative to sensor area (activeRect) - if (mPreviewRequestBuilder != null) { - final Rect computedZoom = cameraZoom.computeZoom(zoom); - mPreviewRequestBuilder.set(CaptureRequest.SCALER_CROP_REGION, computedZoom); - captureSession.setRepeatingRequest(mPreviewRequestBuilder.build(), null, mBackgroundHandler); - } - - result.success(null); + /** Set current fps range setting to the current preview request builder */ + private void updateFpsRange() { + if (fpsRange == null) { + return; } - public void lockCaptureOrientation(PlatformChannel.DeviceOrientation orientation) { - this.lockedCaptureOrientation = orientation; + mPreviewRequestBuilder.set(CaptureRequest.CONTROL_AE_TARGET_FPS_RANGE, fpsRange); + } + + /** + * Sync the focus mode setting to the provided capture request builder. + * + * @param requestBuilder + */ + private void updateFocusMode(CaptureRequest.Builder requestBuilder) { + // Log.i(TAG, "updateFocusMode currentFocusMode: " + currentFocusMode); + + if (!mAutoFocusSupported) { + useAutoFocus = false; + requestBuilder.set(CaptureRequest.CONTROL_AF_MODE, CaptureRequest.CONTROL_AF_MODE_OFF); + } else { + switch (currentFocusMode) { + case locked: + useAutoFocus = false; + requestBuilder.set(CaptureRequest.CONTROL_AF_MODE, CaptureRequest.CONTROL_AF_MODE_AUTO); + break; + + case auto: + useAutoFocus = true; + requestBuilder.set( + CaptureRequest.CONTROL_AF_MODE, + recordingVideo + ? CaptureRequest.CONTROL_AF_MODE_CONTINUOUS_VIDEO + : CaptureRequest.CONTROL_AF_MODE_CONTINUOUS_PICTURE); + default: + break; + } } - public void unlockCaptureOrientation() { - this.lockedCaptureOrientation = null; + // Some devices use an extremely high noise reduction setting by default (pixel 4 selfie mode), which + // causes the preview/capture to look blurry and out of focus. To fix this we set NR to off. + // TODO: we should add a noise reduction setting in dart/ios in the future. + requestBuilder.set( + CaptureRequest.NOISE_REDUCTION_MODE, CaptureRequest.NOISE_REDUCTION_MODE_OFF); + + // Update metering + MeteringRectangle afRect = cameraRegions.getAFMeteringRectangle(); + requestBuilder.set( + CaptureRequest.CONTROL_AF_REGIONS, + afRect == null ? null : new MeteringRectangle[] {afRect}); + } + + public void startPreview() throws CameraAccessException { + if (pictureImageReader == null || pictureImageReader.getSurface() == null) return; + // Log.i(TAG, "startPreview"); + + createCaptureSession(CameraDevice.TEMPLATE_PREVIEW, pictureImageReader.getSurface()); + } + + public void startPreviewWithImageStream(EventChannel imageStreamChannel) + throws CameraAccessException { + createCaptureSession(CameraDevice.TEMPLATE_RECORD, imageStreamReader.getSurface()); + // Log.i(TAG, "startPreviewWithImageStream"); + + imageStreamChannel.setStreamHandler( + new EventChannel.StreamHandler() { + @Override + public void onListen(Object o, EventChannel.EventSink imageStreamSink) { + setImageStreamImageAvailableListener(imageStreamSink); + } + + @Override + public void onCancel(Object o) { + imageStreamReader.setOnImageAvailableListener(null, mBackgroundHandler); + } + }); + } + + private void setImageStreamImageAvailableListener(final EventChannel.EventSink imageStreamSink) { + imageStreamReader.setOnImageAvailableListener( + reader -> { + Image img = reader.acquireLatestImage(); + if (img == null) return; + + List> planes = new ArrayList<>(); + for (Image.Plane plane : img.getPlanes()) { + ByteBuffer buffer = plane.getBuffer(); + + byte[] bytes = new byte[buffer.remaining()]; + buffer.get(bytes, 0, bytes.length); + + Map planeBuffer = new HashMap<>(); + planeBuffer.put("bytesPerRow", plane.getRowStride()); + planeBuffer.put("bytesPerPixel", plane.getPixelStride()); + planeBuffer.put("bytes", bytes); + + planes.add(planeBuffer); + } + + Map imageBuffer = new HashMap<>(); + imageBuffer.put("width", img.getWidth()); + imageBuffer.put("height", img.getHeight()); + imageBuffer.put("format", img.getFormat()); + imageBuffer.put("planes", planes); + + final Handler handler = new Handler(Looper.getMainLooper()); + handler.post(() -> imageStreamSink.success(imageBuffer)); + img.close(); + }, + mBackgroundHandler); + } + + private void closeCaptureSession() { + if (captureSession != null) { + // Log.i(TAG, "closeCaptureSession"); + + captureSession.close(); + captureSession = null; } + } - /** - * Set current fps range setting to the current preview request builder - */ - private void updateFpsRange() { - if (fpsRange == null) { - return; - } + public void close() { + // Log.i(TAG, "close"); + closeCaptureSession(); - mPreviewRequestBuilder.set(CaptureRequest.CONTROL_AE_TARGET_FPS_RANGE, fpsRange); + if (cameraDevice != null) { + cameraDevice.close(); + cameraDevice = null; } - - /** - * Sync the focus mode setting to the provided capture request builder. - * - * @param requestBuilder - */ - private void updateFocusMode(CaptureRequest.Builder requestBuilder) { - // Log.i(TAG, "updateFocusMode currentFocusMode: " + currentFocusMode); - - if (!mAutoFocusSupported) { - useAutoFocus = false; - requestBuilder.set( - CaptureRequest.CONTROL_AF_MODE, CaptureRequest.CONTROL_AF_MODE_OFF); - } else { - switch (currentFocusMode) { - case locked: - useAutoFocus = false; - requestBuilder.set( - CaptureRequest.CONTROL_AF_MODE, CaptureRequest.CONTROL_AF_MODE_AUTO); - break; - - case auto: - useAutoFocus = true; - requestBuilder.set( - CaptureRequest.CONTROL_AF_MODE, - recordingVideo - ? CaptureRequest.CONTROL_AF_MODE_CONTINUOUS_VIDEO - : CaptureRequest.CONTROL_AF_MODE_CONTINUOUS_PICTURE); - default: - break; - } - } - - // Some devices use an extremely high noise reduction setting by default (pixel 4 selfie mode), which - // causes the preview/capture to look blurry and out of focus. To fix this we set NR to off. - // TODO: we should add a noise reduction setting in dart/ios in the future. - requestBuilder.set(CaptureRequest.NOISE_REDUCTION_MODE, CaptureRequest.NOISE_REDUCTION_MODE_OFF); - - - // Update metering - MeteringRectangle afRect = cameraRegions.getAFMeteringRectangle(); - requestBuilder.set( - CaptureRequest.CONTROL_AF_REGIONS, - afRect == null ? null : new MeteringRectangle[]{afRect}); + if (pictureImageReader != null) { + pictureImageReader.close(); + pictureImageReader = null; } - - public void startPreview() throws CameraAccessException { - if (pictureImageReader == null || pictureImageReader.getSurface() == null) return; - // Log.i(TAG, "startPreview"); - - createCaptureSession(CameraDevice.TEMPLATE_PREVIEW, pictureImageReader.getSurface()); + if (imageStreamReader != null) { + imageStreamReader.close(); + imageStreamReader = null; } - - public void startPreviewWithImageStream(EventChannel imageStreamChannel) - throws CameraAccessException { - createCaptureSession(CameraDevice.TEMPLATE_RECORD, imageStreamReader.getSurface()); - // Log.i(TAG, "startPreviewWithImageStream"); - - imageStreamChannel.setStreamHandler( - new EventChannel.StreamHandler() { - @Override - public void onListen(Object o, EventChannel.EventSink imageStreamSink) { - setImageStreamImageAvailableListener(imageStreamSink); - } - - @Override - public void onCancel(Object o) { - imageStreamReader.setOnImageAvailableListener(null, mBackgroundHandler); - } - }); + if (mediaRecorder != null) { + mediaRecorder.reset(); + mediaRecorder.release(); + mediaRecorder = null; } - private void setImageStreamImageAvailableListener(final EventChannel.EventSink imageStreamSink) { - imageStreamReader.setOnImageAvailableListener( - reader -> { - Image img = reader.acquireLatestImage(); - if (img == null) return; - - List> planes = new ArrayList<>(); - for (Image.Plane plane : img.getPlanes()) { - ByteBuffer buffer = plane.getBuffer(); - - byte[] bytes = new byte[buffer.remaining()]; - buffer.get(bytes, 0, bytes.length); - - Map planeBuffer = new HashMap<>(); - planeBuffer.put("bytesPerRow", plane.getRowStride()); - planeBuffer.put("bytesPerPixel", plane.getPixelStride()); - planeBuffer.put("bytes", bytes); - - planes.add(planeBuffer); - } - - Map imageBuffer = new HashMap<>(); - imageBuffer.put("width", img.getWidth()); - imageBuffer.put("height", img.getHeight()); - imageBuffer.put("format", img.getFormat()); - imageBuffer.put("planes", planes); - - final Handler handler = new Handler(Looper.getMainLooper()); - handler.post(() -> imageStreamSink.success(imageBuffer)); - img.close(); - }, - mBackgroundHandler - ); - } - - private void closeCaptureSession() { - if (captureSession != null) { - // Log.i(TAG, "closeCaptureSession"); - - captureSession.close(); - captureSession = null; - } - } - - public void close() { - // Log.i(TAG, "close"); - closeCaptureSession(); - - if (cameraDevice != null) { - cameraDevice.close(); - cameraDevice = null; - } - if (pictureImageReader != null) { - pictureImageReader.close(); - pictureImageReader = null; - } - if (imageStreamReader != null) { - imageStreamReader.close(); - imageStreamReader = null; - } - if (mediaRecorder != null) { - mediaRecorder.reset(); - mediaRecorder.release(); - mediaRecorder = null; - } + stopBackgroundThread(); + } - stopBackgroundThread(); - } - - public void dispose() { - // Log.i(TAG, "dispose"); + public void dispose() { + // Log.i(TAG, "dispose"); - close(); - flutterTexture.release(); - deviceOrientationListener.stop(); - } + close(); + flutterTexture.release(); + deviceOrientationListener.stop(); + } } diff --git a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/CameraPermissions.java b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/CameraPermissions.java index b9ca5da3a1a7..3529e69a2b0b 100644 --- a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/CameraPermissions.java +++ b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/CameraPermissions.java @@ -8,7 +8,6 @@ import android.Manifest.permission; import android.app.Activity; import android.content.pm.PackageManager; - import androidx.annotation.VisibleForTesting; import androidx.core.app.ActivityCompat; import androidx.core.content.ContextCompat; diff --git a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/CameraPlugin.java b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/CameraPlugin.java index 85af292a211b..93183bb7c0a7 100644 --- a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/CameraPlugin.java +++ b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/CameraPlugin.java @@ -6,10 +6,8 @@ import android.app.Activity; import android.os.Build; - import androidx.annotation.NonNull; import androidx.annotation.Nullable; - import io.flutter.embedding.engine.plugins.FlutterPlugin; import io.flutter.embedding.engine.plugins.activity.ActivityAware; import io.flutter.embedding.engine.plugins.activity.ActivityPluginBinding; diff --git a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/CameraState.java b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/CameraState.java index 0bb9c0c2cee0..99dd3f0c6825 100644 --- a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/CameraState.java +++ b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/CameraState.java @@ -1,34 +1,23 @@ package io.flutter.plugins.camera; - /** - * These are the states that the camera can be in. The camera can only take one photo at a time - * so this state describes the state of the camera itself. The camera works like a pipeline where - * we feed it requests through. It can only process one tasks at a time. + * These are the states that the camera can be in. The camera can only take one photo at a time so + * this state describes the state of the camera itself. The camera works like a pipeline where we + * feed it requests through. It can only process one tasks at a time. */ public enum CameraState { - /** - * Idle, showing preview and not capturing anything. - */ - STATE_PREVIEW, + /** Idle, showing preview and not capturing anything. */ + STATE_PREVIEW, - /** - * Starting and waiting for autofocus to complete. - */ - STATE_WAITING_FOCUS, + /** Starting and waiting for autofocus to complete. */ + STATE_WAITING_FOCUS, - /** - * Start performing autoexposure. - */ - STATE_WAITING_PRECAPTURE_START, + /** Start performing autoexposure. */ + STATE_WAITING_PRECAPTURE_START, - /** - * waiting for autoexposure to complete. - */ - STATE_WAITING_PRECAPTURE_DONE, + /** waiting for autoexposure to complete. */ + STATE_WAITING_PRECAPTURE_DONE, - /** - * Capturing an image. - */ - STATE_CAPTURING, + /** Capturing an image. */ + STATE_CAPTURING, } diff --git a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/CameraUtils.java b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/CameraUtils.java index 2cd3a8be261a..03993a3b51f9 100644 --- a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/CameraUtils.java +++ b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/CameraUtils.java @@ -14,7 +14,8 @@ import android.hardware.camera2.params.StreamConfigurationMap; import android.media.CamcorderProfile; import android.util.Size; - +import io.flutter.embedding.engine.systemchannels.PlatformChannel; +import io.flutter.plugins.camera.types.ResolutionPreset; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; @@ -23,9 +24,6 @@ import java.util.List; import java.util.Map; -import io.flutter.embedding.engine.systemchannels.PlatformChannel; -import io.flutter.plugins.camera.types.ResolutionPreset; - /** Provides various utilities for camera. */ public final class CameraUtils { diff --git a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/CameraZoom.java b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/CameraZoom.java index 36aee3b94777..5eed9f4734b7 100644 --- a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/CameraZoom.java +++ b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/CameraZoom.java @@ -5,7 +5,6 @@ package io.flutter.plugins.camera; import android.graphics.Rect; - import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.core.math.MathUtils; diff --git a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/DartMessenger.java b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/DartMessenger.java index 0068421015dc..3892452892d9 100644 --- a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/DartMessenger.java +++ b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/DartMessenger.java @@ -7,17 +7,14 @@ import android.os.Handler; import android.os.Looper; import android.text.TextUtils; - import androidx.annotation.Nullable; - -import java.util.HashMap; -import java.util.Map; - import io.flutter.embedding.engine.systemchannels.PlatformChannel; import io.flutter.plugin.common.BinaryMessenger; import io.flutter.plugin.common.MethodChannel; import io.flutter.plugins.camera.types.ExposureMode; import io.flutter.plugins.camera.types.FocusMode; +import java.util.HashMap; +import java.util.Map; class DartMessenger { @Nullable private MethodChannel cameraChannel; diff --git a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/DeviceOrientationManager.java b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/DeviceOrientationManager.java index 46f31cdab2c5..b2a504b629d6 100644 --- a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/DeviceOrientationManager.java +++ b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/DeviceOrientationManager.java @@ -16,7 +16,6 @@ import android.view.OrientationEventListener; import android.view.Surface; import android.view.WindowManager; - import io.flutter.embedding.engine.systemchannels.PlatformChannel; class DeviceOrientationManager { diff --git a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/ImageSaver.java b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/ImageSaver.java index 589cfd1f553a..e3b93c15fcd0 100644 --- a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/ImageSaver.java +++ b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/ImageSaver.java @@ -3,82 +3,69 @@ import android.media.Image; import android.os.Handler; import android.os.Looper; - +import android.os.SystemClock; +import android.util.Log; import java.io.File; import java.io.FileOutputStream; import java.io.IOException; import java.nio.ByteBuffer; -/** - * Saves a JPEG {@link Image} into the specified {@link File}. - */ +/** Saves a JPEG {@link Image} into the specified {@link File}. */ public class ImageSaver implements Runnable { - /** - * The JPEG image - */ - private final Image mImage; - /** - * The file we save the image into. - */ - private final File mFile; + /** The JPEG image */ + private final Image mImage; - /** - * For running background tasks - */ - private Handler mBackgroundHandler; + /** The file we save the image into. */ + private final File mFile; + /** Used to finish the picture capture request */ + private final PictureCaptureRequest mPictureCaptureRequest; - /** - * - * Used to finish the picture capture request - */ - private PictureCaptureRequest mPictureCaptureRequest; + ImageSaver(Image image, File file, PictureCaptureRequest pictureCaptureRequest) { + mImage = image; + mFile = file; + mPictureCaptureRequest = pictureCaptureRequest; + } - ImageSaver(Image image, File file, PictureCaptureRequest pictureCaptureRequest) { - mImage = image; - mFile = file; - mPictureCaptureRequest = pictureCaptureRequest; - mBackgroundHandler = mBackgroundHandler; - } + @Override + public void run() { + // We need to call the method channel stuff on main thread + final Handler handler = new Handler(Looper.getMainLooper()); - @Override - public void run() { - final Handler handler = new Handler(Looper.getMainLooper()); - ByteBuffer buffer = mImage.getPlanes()[0].getBuffer(); - byte[] bytes = new byte[buffer.remaining()]; - buffer.get(bytes); - FileOutputStream output = null; - try { - output = new FileOutputStream(mFile); - output.write(bytes); + ByteBuffer buffer = mImage.getPlanes()[0].getBuffer(); + byte[] bytes = new byte[buffer.remaining()]; + buffer.get(bytes); + FileOutputStream output = null; + try { + output = new FileOutputStream(mFile); + output.write(bytes); - handler.post(new Runnable() { - @Override - public void run() { - mPictureCaptureRequest.finish(mFile.getAbsolutePath()); - } - }); + handler.post( + new Runnable() { + @Override + public void run() { + mPictureCaptureRequest.finish(mFile.getAbsolutePath()); + } + }); + } catch (IOException e) { + mPictureCaptureRequest.error("IOError", "Failed saving image", null); + } finally { + mImage.close(); + if (null != output) { + try { + output.close(); } catch (IOException e) { - mPictureCaptureRequest.error("IOError", "Failed saving image", null); - } finally { - mImage.close(); - if (null != output) { - try { - output.close(); - } catch (IOException e) { - handler.post(new Runnable() { - @Override - public void run() { - mPictureCaptureRequest.error("cameraAccess", e.getMessage(), null); - } - }); - - + handler.post( + new Runnable() { + @Override + public void run() { + mPictureCaptureRequest.error("cameraAccess", e.getMessage(), null); } - } + }); } + } } - -} \ No newline at end of file + } +} diff --git a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/MethodCallHandlerImpl.java b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/MethodCallHandlerImpl.java index 19307a2f6a78..aa7483f55679 100644 --- a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/MethodCallHandlerImpl.java +++ b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/MethodCallHandlerImpl.java @@ -6,13 +6,8 @@ import android.app.Activity; import android.hardware.camera2.CameraAccessException; - import androidx.annotation.NonNull; import androidx.annotation.Nullable; - -import java.util.HashMap; -import java.util.Map; - import io.flutter.embedding.engine.systemchannels.PlatformChannel; import io.flutter.plugin.common.BinaryMessenger; import io.flutter.plugin.common.EventChannel; @@ -24,6 +19,8 @@ import io.flutter.plugins.camera.types.FlashMode; import io.flutter.plugins.camera.types.FocusMode; import io.flutter.view.TextureRegistry; +import java.util.HashMap; +import java.util.Map; final class MethodCallHandlerImpl implements MethodChannel.MethodCallHandler { private final Activity activity; diff --git a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/PictureCaptureRequest.java b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/PictureCaptureRequest.java index fd139cf16f43..d5d49cec93eb 100644 --- a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/PictureCaptureRequest.java +++ b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/PictureCaptureRequest.java @@ -7,209 +7,192 @@ import android.os.Handler; import android.os.Looper; import android.os.SystemClock; - import androidx.annotation.Nullable; - -import java.io.File; - import io.flutter.plugin.common.MethodChannel; +import java.io.File; /** - * This is where we store the state of the camera. This conveniently - * allows us to handle capture results and send results back to flutter - * so we can handle errors. - *

- * It also handles a capture timeout so if a capture doesn't happen within - * 5 seconds it will return an error to dart. + * This is where we store the state of the camera. This conveniently allows us to handle capture + * results and send results back to flutter so we can handle errors. + * + *

It also handles a capture timeout so if a capture doesn't happen within 5 seconds it will + * return an error to dart. */ class PictureCaptureRequest { - /** - * Timeout for the pre-capture sequence. - */ - private static final long PRECAPTURE_TIMEOUT_MS = 1000; - - /** - * This is the output file for the curent capture. The file is created - * in Camera and passed here as reference to it. - */ - final File mFile; - - /** - * Dart method chanel result. - */ - private final MethodChannel.Result result; - - /** - * Timeout handler. - */ - private final TimeoutHandler timeoutHandler; - /** - * To send errors back to dart - */ - private final DartMessenger dartMessenger; - /** - * The time that the most recent capture started at. Used to check if - * the current capture request has timed out. - */ - private long preCaptureStartTime; - /** - * The state of this picture capture request. - */ - private PictureCaptureRequestState state = PictureCaptureRequestState.STATE_IDLE; - - private final Runnable timeoutCallback = - () -> { - error("captureTimeout", "Picture capture request timed out", state.toString()); - setState(PictureCaptureRequestState.STATE_ERROR); - }; - - - /** - * Constructor to create a picture capture request. - * - * @param result - * @param mFile - */ - public PictureCaptureRequest(MethodChannel.Result result, File mFile, DartMessenger dartMessenger) { - this.result = result; - this.timeoutHandler = new TimeoutHandler(); - this.mFile = mFile; - this.dartMessenger = dartMessenger; - } - - /** - * Constructor for unit tests where we can mock the timeout handler - */ - public PictureCaptureRequest(MethodChannel.Result result, File mFile, DartMessenger dartMessenger, TimeoutHandler timeoutHandler) { - this.result = result; - this.timeoutHandler = timeoutHandler; - this.mFile = mFile; - this.dartMessenger = dartMessenger; - } - - /** - * Return the current state of this picture capture request. - * - * @return - */ - public PictureCaptureRequestState getState() { - return state; - } - - /** - * Set the picture capture request to a new state. - * - * @param newState - */ - public void setState(PictureCaptureRequestState newState) { - // Log.i("Camera", "====> PictureCaptureRequest setState: " + newState); - - // Once a request is finished, that's it for its lifecycle. - if (state == PictureCaptureRequestState.STATE_FINISHED) { - dartMessenger.sendCameraErrorEvent("Request has already been finished"); - return; - } - - final PictureCaptureRequestState oldState = state; - state = newState; - onStateChange(oldState); - } - - public boolean isFinished() { - return state == PictureCaptureRequestState.STATE_FINISHED || - state == PictureCaptureRequestState.STATE_ERROR; + /** Timeout for the pre-capture sequence. */ + private static final long PRECAPTURE_TIMEOUT_MS = 1000; + + /** + * This is the output file for the curent capture. The file is created in Camera and passed here + * as reference to it. + */ + final File mFile; + + /** Dart method chanel result. */ + private final MethodChannel.Result result; + + /** Timeout handler. */ + private final TimeoutHandler timeoutHandler; + /** To send errors back to dart */ + private final DartMessenger dartMessenger; + /** + * The time that the most recent capture started at. Used to check if the current capture request + * has timed out. + */ + private long preCaptureStartTime; + /** The state of this picture capture request. */ + private PictureCaptureRequestState state = PictureCaptureRequestState.STATE_IDLE; + + private final Runnable timeoutCallback = + () -> { + error("captureTimeout", "Picture capture request timed out", state.toString()); + setState(PictureCaptureRequestState.STATE_ERROR); + }; + + /** + * Constructor to create a picture capture request. + * + * @param result + * @param mFile + */ + public PictureCaptureRequest( + MethodChannel.Result result, File mFile, DartMessenger dartMessenger) { + this.result = result; + this.timeoutHandler = new TimeoutHandler(); + this.mFile = mFile; + this.dartMessenger = dartMessenger; + } + + /** Constructor for unit tests where we can mock the timeout handler */ + public PictureCaptureRequest( + MethodChannel.Result result, + File mFile, + DartMessenger dartMessenger, + TimeoutHandler timeoutHandler) { + this.result = result; + this.timeoutHandler = timeoutHandler; + this.mFile = mFile; + this.dartMessenger = dartMessenger; + } + + /** + * Return the current state of this picture capture request. + * + * @return + */ + public PictureCaptureRequestState getState() { + return state; + } + + /** + * Set the picture capture request to a new state. + * + * @param newState + */ + public void setState(PictureCaptureRequestState newState) { + // Log.i("Camera", "====> PictureCaptureRequest setState: " + newState); + + // Once a request is finished, that's it for its lifecycle. + if (state == PictureCaptureRequestState.STATE_FINISHED) { + dartMessenger.sendCameraErrorEvent("Request has already been finished"); + return; } - /** - * Send the picture result back to Flutter. Returns the image path. - * - * @param absolutePath - */ - public void finish(String absolutePath) { - if (state == PictureCaptureRequestState.STATE_ERROR) { - return; - } - - if (isFinished()) throw new IllegalStateException("Request has already been finished"); - setState(PictureCaptureRequestState.STATE_FINISHED); - // Log.i("Camera", "PictureCaptureRequest finish"); - result.success(absolutePath); + final PictureCaptureRequestState oldState = state; + state = newState; + onStateChange(oldState); + } + + public boolean isFinished() { + return state == PictureCaptureRequestState.STATE_FINISHED + || state == PictureCaptureRequestState.STATE_ERROR; + } + + /** + * Send the picture result back to Flutter. Returns the image path. + * + * @param absolutePath + */ + public void finish(String absolutePath) { + if (state == PictureCaptureRequestState.STATE_ERROR) { + return; } - public void error( - String errorCode, @Nullable String errorMessage, - @Nullable Object errorDetails) { - if (state == PictureCaptureRequestState.STATE_ERROR) { - return; - } + if (isFinished()) throw new IllegalStateException("Request has already been finished"); + setState(PictureCaptureRequestState.STATE_FINISHED); + // Log.i("Camera", "PictureCaptureRequest finish"); + result.success(absolutePath); + } - if (isFinished()) throw new IllegalStateException("Request has already been finished"); - setState(PictureCaptureRequestState.STATE_ERROR); - result.error(errorCode, errorMessage, errorDetails); + public void error( + String errorCode, @Nullable String errorMessage, @Nullable Object errorDetails) { + if (state == PictureCaptureRequestState.STATE_ERROR) { + return; } - /** - * Check if the timeout for the pre-capture sequence has been reached. - * - * @return true if the timeout is reached; otherwise false is returned. - */ - public boolean hitPreCaptureTimeout() { - // Log.i("Camera", "hitPreCaptureTimeout | Time elapsed: " + (SystemClock.elapsedRealtime() - preCaptureStartTime)); - return (SystemClock.elapsedRealtime() - preCaptureStartTime) > PRECAPTURE_TIMEOUT_MS; + if (isFinished()) throw new IllegalStateException("Request has already been finished"); + setState(PictureCaptureRequestState.STATE_ERROR); + result.error(errorCode, errorMessage, errorDetails); + } + + /** + * Check if the timeout for the pre-capture sequence has been reached. + * + * @return true if the timeout is reached; otherwise false is returned. + */ + public boolean hitPreCaptureTimeout() { + // Log.i("Camera", "hitPreCaptureTimeout | Time elapsed: " + (SystemClock.elapsedRealtime() - preCaptureStartTime)); + return (SystemClock.elapsedRealtime() - preCaptureStartTime) > PRECAPTURE_TIMEOUT_MS; + } + + /** Sets the time the pre-capture sequence started. */ + public void setPreCaptureStartTime() { + preCaptureStartTime = SystemClock.elapsedRealtime(); + } + + /** Handle new state changes. */ + private void onStateChange(PictureCaptureRequestState oldState) { + switch (state) { + case STATE_CAPTURING: + case STATE_WAITING_FOCUS: + case STATE_WAITING_PRECAPTURE_START: + timeoutHandler.resetTimeout(timeoutCallback); + break; + + case STATE_WAITING_PRECAPTURE_DONE: + setPreCaptureStartTime(); + timeoutHandler.resetTimeout(timeoutCallback); + break; + + case STATE_IDLE: + case STATE_FINISHED: + case STATE_ERROR: + timeoutHandler.clearTimeout(timeoutCallback); + break; } - - /** - * Sets the time the pre-capture sequence started. - */ - public void setPreCaptureStartTime() { - preCaptureStartTime = SystemClock.elapsedRealtime(); + } + + /** + * This handles the timeout for capture requests so they return within a reasonable amount of + * time. + */ + static class TimeoutHandler { + private static final int REQUEST_TIMEOUT = 5000; + private final Handler handler; + + TimeoutHandler() { + this.handler = new Handler(Looper.getMainLooper()); } - /** - * Handle new state changes. - */ - private void onStateChange(PictureCaptureRequestState oldState) { - switch (state) { - case STATE_CAPTURING: - case STATE_WAITING_FOCUS: - case STATE_WAITING_PRECAPTURE_START: - timeoutHandler.resetTimeout(timeoutCallback); - break; - - case STATE_WAITING_PRECAPTURE_DONE: - setPreCaptureStartTime(); - timeoutHandler.resetTimeout(timeoutCallback); - break; - - case STATE_IDLE: - case STATE_FINISHED: - case STATE_ERROR: - timeoutHandler.clearTimeout(timeoutCallback); - break; - } + public void resetTimeout(Runnable runnable) { + // Log.i("Camear", "PictureCaptureRequest | resetting timeout"); + clearTimeout(runnable); + handler.postDelayed(runnable, REQUEST_TIMEOUT); } - /** - * This handles the timeout for capture requests so they return within a - * reasonable amount of time. - */ - static class TimeoutHandler { - private static final int REQUEST_TIMEOUT = 5000; - private final Handler handler; - - TimeoutHandler() { - this.handler = new Handler(Looper.getMainLooper()); - } - - public void resetTimeout(Runnable runnable) { - // Log.i("Camear", "PictureCaptureRequest | resetting timeout"); - clearTimeout(runnable); - handler.postDelayed(runnable, REQUEST_TIMEOUT); - } - - public void clearTimeout(Runnable runnable) { - handler.removeCallbacks(runnable); - } + public void clearTimeout(Runnable runnable) { + handler.removeCallbacks(runnable); } + } } diff --git a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/PictureCaptureRequestState.java b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/PictureCaptureRequestState.java index c506e2489e1c..979346c9bb09 100644 --- a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/PictureCaptureRequestState.java +++ b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/PictureCaptureRequestState.java @@ -1,48 +1,32 @@ package io.flutter.plugins.camera; /** - * This describes the state of the current picture capture request. - * This is different from the camera state because this simply says - * whether or not the current capture is finished and if there was - * an error. + * This describes the state of the current picture capture request. This is different from the + * camera state because this simply says whether or not the current capture is finished and if there + * was an error. * - * We have to separate this state because a picture capture request - * only exists in the context of a dart call where we have a result - * to return. + *

We have to separate this state because a picture capture request only exists in the context of + * a dart call where we have a result to return. */ public enum PictureCaptureRequestState { - /** - * Not doing anything yet. - */ - STATE_IDLE, + /** Not doing anything yet. */ + STATE_IDLE, - /** - * Starting and waiting for autofocus to complete. - */ - STATE_WAITING_FOCUS, + /** Starting and waiting for autofocus to complete. */ + STATE_WAITING_FOCUS, - /** - * Start performing autoexposure. - */ - STATE_WAITING_PRECAPTURE_START, + /** Start performing autoexposure. */ + STATE_WAITING_PRECAPTURE_START, - /** - * waiting for autoexposure to complete. - */ - STATE_WAITING_PRECAPTURE_DONE, + /** waiting for autoexposure to complete. */ + STATE_WAITING_PRECAPTURE_DONE, - /** - * Picture is being captured. - */ - STATE_CAPTURING, + /** Picture is being captured. */ + STATE_CAPTURING, - /** - * Picture capture is finished. - */ - STATE_FINISHED, + /** Picture capture is finished. */ + STATE_FINISHED, - /** - * An error occurred. - */ - STATE_ERROR, + /** An error occurred. */ + STATE_ERROR, } diff --git a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/media/MediaRecorderBuilder.java b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/media/MediaRecorderBuilder.java index 5e5d0c44c115..4c3fb3add230 100644 --- a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/media/MediaRecorderBuilder.java +++ b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/media/MediaRecorderBuilder.java @@ -6,9 +6,7 @@ import android.media.CamcorderProfile; import android.media.MediaRecorder; - import androidx.annotation.NonNull; - import java.io.IOException; public class MediaRecorderBuilder { diff --git a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/types/FlashMode.java b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/types/FlashMode.java index b8ff2fac6652..75437e80e1d4 100644 --- a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/types/FlashMode.java +++ b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/types/FlashMode.java @@ -4,6 +4,9 @@ package io.flutter.plugins.camera.types; + + + // Mirrors flash_mode.dart public enum FlashMode { off("off"), @@ -28,4 +31,4 @@ public static FlashMode getValueForString(String modeStr) { public String toString() { return strValue; } -} \ No newline at end of file +} diff --git a/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/PictureCaptureRequestTest.java b/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/PictureCaptureRequestTest.java index ef81028e2bc6..99d905180c96 100644 --- a/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/PictureCaptureRequestTest.java +++ b/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/PictureCaptureRequestTest.java @@ -4,10 +4,6 @@ package io.flutter.plugins.camera; -import org.junit.Test; - -import io.flutter.plugin.common.MethodChannel; - import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertTrue; @@ -17,137 +13,146 @@ import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; -public class PictureCaptureRequestTest { - - @Test - public void state_is_idle_by_default() { - PictureCaptureRequest req = new PictureCaptureRequest(null, null, null); - assertEquals("Default state is idle", req.getState(), PictureCaptureRequestState.STATE_IDLE); - } - - @Test - public void setState_sets_state() { - PictureCaptureRequest req = new PictureCaptureRequest(null, null, null); - req.setState(PictureCaptureRequestState.STATE_WAITING_FOCUS); - assertEquals("State is focusing", req.getState(), PictureCaptureRequestState.STATE_WAITING_FOCUS); - req.setState(PictureCaptureRequestState.STATE_WAITING_PRECAPTURE_START); - assertEquals("State is preCapture", req.getState(), PictureCaptureRequestState.STATE_WAITING_PRECAPTURE_START); - req.setState(PictureCaptureRequestState.STATE_WAITING_PRECAPTURE_DONE); - assertEquals( - "State is waitingPreCaptureReady", - req.getState(), - PictureCaptureRequestState.STATE_WAITING_PRECAPTURE_DONE); - req.setState(PictureCaptureRequestState.STATE_CAPTURING); - assertEquals( - "State is awaitingPreCapture", req.getState(), PictureCaptureRequestState.STATE_CAPTURING); - } - - @Test - public void setState_resets_timeout() { - PictureCaptureRequest.TimeoutHandler mockTimeoutHandler = - mock(PictureCaptureRequest.TimeoutHandler.class); - PictureCaptureRequest req = new PictureCaptureRequest(null, null, null, mockTimeoutHandler); - req.setState(PictureCaptureRequestState.STATE_WAITING_FOCUS); - req.setState(PictureCaptureRequestState.STATE_WAITING_PRECAPTURE_START); - req.setState(PictureCaptureRequestState.STATE_WAITING_PRECAPTURE_DONE); - req.setState(PictureCaptureRequestState.STATE_CAPTURING); - verify(mockTimeoutHandler, times(4)).resetTimeout(any()); - verify(mockTimeoutHandler, never()).clearTimeout(any()); - } - - @Test - public void setState_clears_timeout() { - PictureCaptureRequest.TimeoutHandler mockTimeoutHandler = - mock(PictureCaptureRequest.TimeoutHandler.class); - PictureCaptureRequest req = new PictureCaptureRequest(null, null, null, mockTimeoutHandler); - req.setState(PictureCaptureRequestState.STATE_IDLE); - req.setState(PictureCaptureRequestState.STATE_FINISHED); - req = new PictureCaptureRequest(null, null, null, mockTimeoutHandler); - req.setState(PictureCaptureRequestState.STATE_ERROR); - verify(mockTimeoutHandler, never()).resetTimeout(any()); - verify(mockTimeoutHandler, times(3)).clearTimeout(any()); - } - - @Test - public void finish_sets_result_and_state() { - // Setup - MethodChannel.Result mockResult = mock(MethodChannel.Result.class); - PictureCaptureRequest req = new PictureCaptureRequest(mockResult, null, null); - // Act - req.finish("/test/path"); - // Test - verify(mockResult).success("/test/path"); - assertEquals("State is finished", req.getState(), PictureCaptureRequestState.STATE_FINISHED); - } - - @Test - public void finish_clears_timeout() { - PictureCaptureRequest.TimeoutHandler mockTimeoutHandler = - mock(PictureCaptureRequest.TimeoutHandler.class); - MethodChannel.Result mockResult = mock(MethodChannel.Result.class); - PictureCaptureRequest req = new PictureCaptureRequest(mockResult, null, null, mockTimeoutHandler); - req.finish("/test/path"); - verify(mockTimeoutHandler, never()).resetTimeout(any()); - verify(mockTimeoutHandler).clearTimeout(any()); - } - - @Test - public void isFinished_is_true_When_state_is_finished_or_error() { - // Setup - PictureCaptureRequest req = new PictureCaptureRequest(null, null, null); - // Test false states - req.setState(PictureCaptureRequestState.STATE_IDLE); - assertFalse(req.isFinished()); - req.setState(PictureCaptureRequestState.STATE_WAITING_PRECAPTURE_START); - assertFalse(req.isFinished()); - req.setState(PictureCaptureRequestState.STATE_WAITING_PRECAPTURE_DONE); - assertFalse(req.isFinished()); - // Test true states - req.setState(PictureCaptureRequestState.STATE_FINISHED); - assertTrue(req.isFinished()); - req = new PictureCaptureRequest(null, null, null); // Refresh - req.setState(PictureCaptureRequestState.STATE_ERROR); - assertTrue(req.isFinished()); - } - - @Test(expected = IllegalStateException.class) - public void finish_throws_When_already_finished() { - // Setup - PictureCaptureRequest req = new PictureCaptureRequest(null, null, null); - req.setState(PictureCaptureRequestState.STATE_FINISHED); - // Act - req.finish("/test/path"); - } - - @Test - public void error_sets_result_and_state() { - // Setup - MethodChannel.Result mockResult = mock(MethodChannel.Result.class); - PictureCaptureRequest req = new PictureCaptureRequest(mockResult, null, null); - // Act - req.error("ERROR_CODE", "Error Message", null); - // Test - verify(mockResult).error("ERROR_CODE", "Error Message", null); - assertEquals("State is error", req.getState(), PictureCaptureRequestState.STATE_ERROR); - } +import io.flutter.plugin.common.MethodChannel; +import org.junit.Test; - @Test - public void error_clears_timeout() { - PictureCaptureRequest.TimeoutHandler mockTimeoutHandler = - mock(PictureCaptureRequest.TimeoutHandler.class); - MethodChannel.Result mockResult = mock(MethodChannel.Result.class); - PictureCaptureRequest req = new PictureCaptureRequest(mockResult, null, null, mockTimeoutHandler); - req.error("ERROR_CODE", "Error Message", null); - verify(mockTimeoutHandler, never()).resetTimeout(any()); - verify(mockTimeoutHandler).clearTimeout(any()); - } +public class PictureCaptureRequestTest { - @Test(expected = IllegalStateException.class) - public void error_throws_When_already_finished() { - // Setup - PictureCaptureRequest req = new PictureCaptureRequest(null, null, null); - req.setState(PictureCaptureRequestState.STATE_FINISHED); - // Act - req.error(null, null, null); - } + @Test + public void state_is_idle_by_default() { + PictureCaptureRequest req = new PictureCaptureRequest(null, null, null); + assertEquals("Default state is idle", req.getState(), PictureCaptureRequestState.STATE_IDLE); + } + + @Test + public void setState_sets_state() { + PictureCaptureRequest req = new PictureCaptureRequest(null, null, null); + req.setState(PictureCaptureRequestState.STATE_WAITING_FOCUS); + assertEquals( + "State is focusing", req.getState(), PictureCaptureRequestState.STATE_WAITING_FOCUS); + req.setState(PictureCaptureRequestState.STATE_WAITING_PRECAPTURE_START); + assertEquals( + "State is preCapture", + req.getState(), + PictureCaptureRequestState.STATE_WAITING_PRECAPTURE_START); + req.setState(PictureCaptureRequestState.STATE_WAITING_PRECAPTURE_DONE); + assertEquals( + "State is waitingPreCaptureReady", + req.getState(), + PictureCaptureRequestState.STATE_WAITING_PRECAPTURE_DONE); + req.setState(PictureCaptureRequestState.STATE_CAPTURING); + assertEquals( + "State is awaitingPreCapture", req.getState(), PictureCaptureRequestState.STATE_CAPTURING); + } + + @Test + public void setState_resets_timeout() { + PictureCaptureRequest.TimeoutHandler mockTimeoutHandler = + mock(PictureCaptureRequest.TimeoutHandler.class); + PictureCaptureRequest req = new PictureCaptureRequest(null, null, null, mockTimeoutHandler); + req.setState(PictureCaptureRequestState.STATE_WAITING_FOCUS); + req.setState(PictureCaptureRequestState.STATE_WAITING_PRECAPTURE_START); + req.setState(PictureCaptureRequestState.STATE_WAITING_PRECAPTURE_DONE); + req.setState(PictureCaptureRequestState.STATE_CAPTURING); + verify(mockTimeoutHandler, times(4)).resetTimeout(any()); + verify(mockTimeoutHandler, never()).clearTimeout(any()); + } + + @Test + public void setState_clears_timeout() { + PictureCaptureRequest.TimeoutHandler mockTimeoutHandler = + mock(PictureCaptureRequest.TimeoutHandler.class); + PictureCaptureRequest req = new PictureCaptureRequest(null, null, null, mockTimeoutHandler); + req.setState(PictureCaptureRequestState.STATE_IDLE); + req.setState(PictureCaptureRequestState.STATE_FINISHED); + req = new PictureCaptureRequest(null, null, null, mockTimeoutHandler); + req.setState(PictureCaptureRequestState.STATE_ERROR); + verify(mockTimeoutHandler, never()).resetTimeout(any()); + verify(mockTimeoutHandler, times(3)).clearTimeout(any()); + } + + @Test + public void finish_sets_result_and_state() { + // Setup + MethodChannel.Result mockResult = mock(MethodChannel.Result.class); + PictureCaptureRequest req = new PictureCaptureRequest(mockResult, null, null); + // Act + req.finish("/test/path"); + // Test + verify(mockResult).success("/test/path"); + assertEquals("State is finished", req.getState(), PictureCaptureRequestState.STATE_FINISHED); + } + + @Test + public void finish_clears_timeout() { + PictureCaptureRequest.TimeoutHandler mockTimeoutHandler = + mock(PictureCaptureRequest.TimeoutHandler.class); + MethodChannel.Result mockResult = mock(MethodChannel.Result.class); + PictureCaptureRequest req = + new PictureCaptureRequest(mockResult, null, null, mockTimeoutHandler); + req.finish("/test/path"); + verify(mockTimeoutHandler, never()).resetTimeout(any()); + verify(mockTimeoutHandler).clearTimeout(any()); + } + + @Test + public void isFinished_is_true_When_state_is_finished_or_error() { + // Setup + PictureCaptureRequest req = new PictureCaptureRequest(null, null, null); + // Test false states + req.setState(PictureCaptureRequestState.STATE_IDLE); + assertFalse(req.isFinished()); + req.setState(PictureCaptureRequestState.STATE_WAITING_PRECAPTURE_START); + assertFalse(req.isFinished()); + req.setState(PictureCaptureRequestState.STATE_WAITING_PRECAPTURE_DONE); + assertFalse(req.isFinished()); + // Test true states + req.setState(PictureCaptureRequestState.STATE_FINISHED); + assertTrue(req.isFinished()); + req = new PictureCaptureRequest(null, null, null); // Refresh + req.setState(PictureCaptureRequestState.STATE_ERROR); + assertTrue(req.isFinished()); + } + + @Test(expected = IllegalStateException.class) + public void finish_throws_When_already_finished() { + // Setup + PictureCaptureRequest req = new PictureCaptureRequest(null, null, null); + req.setState(PictureCaptureRequestState.STATE_FINISHED); + // Act + req.finish("/test/path"); + } + + @Test + public void error_sets_result_and_state() { + // Setup + MethodChannel.Result mockResult = mock(MethodChannel.Result.class); + PictureCaptureRequest req = new PictureCaptureRequest(mockResult, null, null); + // Act + req.error("ERROR_CODE", "Error Message", null); + // Test + verify(mockResult).error("ERROR_CODE", "Error Message", null); + assertEquals("State is error", req.getState(), PictureCaptureRequestState.STATE_ERROR); + } + + @Test + public void error_clears_timeout() { + PictureCaptureRequest.TimeoutHandler mockTimeoutHandler = + mock(PictureCaptureRequest.TimeoutHandler.class); + MethodChannel.Result mockResult = mock(MethodChannel.Result.class); + PictureCaptureRequest req = + new PictureCaptureRequest(mockResult, null, null, mockTimeoutHandler); + req.error("ERROR_CODE", "Error Message", null); + verify(mockTimeoutHandler, never()).resetTimeout(any()); + verify(mockTimeoutHandler).clearTimeout(any()); + } + + @Test(expected = IllegalStateException.class) + public void error_throws_When_already_finished() { + // Setup + PictureCaptureRequest req = new PictureCaptureRequest(null, null, null); + req.setState(PictureCaptureRequestState.STATE_FINISHED); + // Act + req.error(null, null, null); + } } From 5f3b70b07832252cc3cbb500b74b678538c7161d Mon Sep 17 00:00:00 2001 From: Andrew Coutts Date: Tue, 2 Mar 2021 03:11:49 -0500 Subject: [PATCH 052/114] Formatting --- .../src/main/java/io/flutter/plugins/camera/ImageSaver.java | 2 -- .../main/java/io/flutter/plugins/camera/types/FlashMode.java | 3 --- 2 files changed, 5 deletions(-) diff --git a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/ImageSaver.java b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/ImageSaver.java index e3b93c15fcd0..b467185c9096 100644 --- a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/ImageSaver.java +++ b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/ImageSaver.java @@ -3,8 +3,6 @@ import android.media.Image; import android.os.Handler; import android.os.Looper; -import android.os.SystemClock; -import android.util.Log; import java.io.File; import java.io.FileOutputStream; import java.io.IOException; diff --git a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/types/FlashMode.java b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/types/FlashMode.java index 75437e80e1d4..c4f0998c418a 100644 --- a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/types/FlashMode.java +++ b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/types/FlashMode.java @@ -4,9 +4,6 @@ package io.flutter.plugins.camera.types; - - - // Mirrors flash_mode.dart public enum FlashMode { off("off"), From c9250f0c6d9d6b9020def64d67d7b5cdd1597077 Mon Sep 17 00:00:00 2001 From: Andrew Coutts Date: Tue, 2 Mar 2021 03:34:32 -0500 Subject: [PATCH 053/114] Remove log --- .../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 74b365e3f381..c32e18d631e0 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 @@ -298,7 +298,7 @@ public Camera( recordingProfile = CameraUtils.getBestAvailableCamcorderProfileForResolutionPreset(cameraName, preset); captureSize = new Size(recordingProfile.videoFrameWidth, recordingProfile.videoFrameHeight); - Log.i(TAG, "captureSize: " + captureSize); + // Log.i(TAG, "captureSize: " + captureSize); previewSize = computeBestPreviewSize(cameraName, preset); From cfb7a2a4ecf166e0073b2ee776a61c823d04d238 Mon Sep 17 00:00:00 2001 From: Andrew Coutts Date: Tue, 2 Mar 2021 04:30:44 -0500 Subject: [PATCH 054/114] Reduce overhead for capture requests and streaming by only requesting an pictureImageReader for 1 image at a time to reduce memory usage --- .../java/io/flutter/plugins/camera/Camera.java | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 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 c32e18d631e0..a3b42cbfd013 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 @@ -123,9 +123,11 @@ public class Camera { @Override public void onImageAvailable(ImageReader reader) { // Log.i(TAG, "onImageAvailable"); + + // Use acquireNextImage since our image reader is only for 1 image. mBackgroundHandler.post( new ImageSaver( - reader.acquireLatestImage(), pictureCaptureRequest.mFile, pictureCaptureRequest)); + reader.acquireNextImage(), pictureCaptureRequest.mFile, pictureCaptureRequest)); cameraState = CameraState.STATE_PREVIEW; } }; @@ -345,7 +347,7 @@ private void checkAutoFocusSupported() { && !(modes == null || modes.length == 0 || (modes.length == 1 && modes[0] == CameraCharacteristics.CONTROL_AF_MODE_OFF)); - Log.i(TAG, "checkAutoFocusSupported: " + mAutoFocusSupported); + // Log.i(TAG, "checkAutoFocusSupported: " + mAutoFocusSupported); } /** Check if the flash is supported. */ @@ -401,20 +403,21 @@ private void prepareMediaRecorder(String outputFilePath) throws IOException { @SuppressLint("MissingPermission") public void open(String imageFormatGroup) throws CameraAccessException { + // We always capture using JPEG format. pictureImageReader = ImageReader.newInstance( - captureSize.getWidth(), captureSize.getHeight(), ImageFormat.JPEG, 2); + captureSize.getWidth(), captureSize.getHeight(), ImageFormat.JPEG, 1); + // For image streaming, we use the provided image format or fall back to YUV420. Integer imageFormat = supportedImageFormats.get(imageFormatGroup); if (imageFormat == null) { Log.w(TAG, "The selected imageFormatGroup is not supported by Android. Defaulting to yuv420"); imageFormat = ImageFormat.YUV_420_888; } - - // Used to steam image byte data to dart side. imageStreamReader = - ImageReader.newInstance(previewSize.getWidth(), previewSize.getHeight(), imageFormat, 2); + ImageReader.newInstance(previewSize.getWidth(), previewSize.getHeight(), imageFormat, 1); + // Open the camera now cameraManager.openCamera( cameraName, new CameraDevice.StateCallback() { @@ -1359,7 +1362,8 @@ public void onCancel(Object o) { private void setImageStreamImageAvailableListener(final EventChannel.EventSink imageStreamSink) { imageStreamReader.setOnImageAvailableListener( reader -> { - Image img = reader.acquireLatestImage(); + // Use acquireNextImage since our image reader is only for 1 image. + Image img = reader.acquireNextImage(); if (img == null) return; List> planes = new ArrayList<>(); From ae9e9825aea24f0dbf5f3f7b202d792aa4615ed4 Mon Sep 17 00:00:00 2001 From: Andrew Coutts Date: Tue, 2 Mar 2021 04:43:10 -0500 Subject: [PATCH 055/114] Cleanup --- .../android/src/main/java/io/flutter/plugins/camera/Camera.java | 1 - .../src/main/java/io/flutter/plugins/camera/CameraPlugin.java | 2 +- 2 files changed, 1 insertion(+), 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 a3b42cbfd013..4b53991b35c8 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 @@ -997,7 +997,6 @@ public void resumeVideoRecording(@NonNull final Result result) { */ public void setFlashMode(@NonNull final Result result, FlashMode newMode) { // Save the new flash mode setting - final FlashMode oldFlashMode = currentFlashMode; currentFlashMode = newMode; updateFlash(mPreviewRequestBuilder); diff --git a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/CameraPlugin.java b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/CameraPlugin.java index 93183bb7c0a7..036fcf1d4f30 100644 --- a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/CameraPlugin.java +++ b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/CameraPlugin.java @@ -26,7 +26,7 @@ */ public final class CameraPlugin implements FlutterPlugin, ActivityAware { - private static final String TAG = "CameraPlugin"; + // private static final String TAG = "CameraPlugin"; private @Nullable FlutterPluginBinding flutterPluginBinding; private @Nullable MethodCallHandlerImpl methodCallHandler; From f5c9146900d572198fec95e98f6cd7f09c11542f Mon Sep 17 00:00:00 2001 From: Andrew Coutts Date: Tue, 2 Mar 2021 07:26:58 -0500 Subject: [PATCH 056/114] Remove nv21 --- .../io/flutter/plugins/camera/Camera.java | 1 - packages/camera/camera/pubspec.yaml | 2 +- .../lib/src/types/image_format_group.dart | 25 ------------------- 3 files changed, 1 insertion(+), 27 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 4b53991b35c8..4912ff64d169 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 @@ -88,7 +88,6 @@ public class Camera { supportedImageFormats = new HashMap<>(); supportedImageFormats.put("yuv420", ImageFormat.YUV_420_888); supportedImageFormats.put("jpeg", ImageFormat.JPEG); - supportedImageFormats.put("nv21", ImageFormat.NV21); } private final SurfaceTextureEntry flutterTexture; diff --git a/packages/camera/camera/pubspec.yaml b/packages/camera/camera/pubspec.yaml index a2f0550967a2..a9b1fddc162b 100644 --- a/packages/camera/camera/pubspec.yaml +++ b/packages/camera/camera/pubspec.yaml @@ -9,7 +9,7 @@ dependencies: flutter: sdk: flutter - camera_platform_interface: ^2.0.2 + camera_platform_interface: ^2.0.0 pedantic: ^1.10.0 quiver: ^3.0.0 diff --git a/packages/camera/camera_platform_interface/lib/src/types/image_format_group.dart b/packages/camera/camera_platform_interface/lib/src/types/image_format_group.dart index b07f7af79e82..3d2c0180fe65 100644 --- a/packages/camera/camera_platform_interface/lib/src/types/image_format_group.dart +++ b/packages/camera/camera_platform_interface/lib/src/types/image_format_group.dart @@ -14,33 +14,8 @@ enum ImageFormatGroup { /// /// On iOS, this is `kCVPixelFormatType_420YpCbCr8BiPlanarVideoRange`. See /// https://developer.apple.com/documentation/corevideo/1563591-pixel_format_identifiers/kcvpixelformattype_420ypcbcr8biplanarvideorange?language=objc - /// - /// Note: if you are using YUV420 with Firebase ML Vision you need to concatenate the planes - /// using the following function: - /// ```dart - /// Uint8List concatenatePlanes(List planes) { - /// var totalBytes = 0; - /// for (var i = 0; i < planes.length; ++i) { - /// totalBytes += planes[i].bytes.length; - /// } - /// - /// final bytes = Uint8List(totalBytes); - /// - /// var byteOffset = 0; - /// for (var i = 0; i < planes.length; ++i) { - /// final length = planes[i].bytes.length; - /// bytes.setRange(byteOffset, byteOffset += length, planes[i].bytes); - /// } - /// return bytes; - /// } - /// ``` yuv420, - /// NV21 is only valid in Android and should be used when feeding images to Firebase ML Vision. If you - /// don't use this format then you need to concatenate the planes from YUV420 which is costly both in - /// memory and CPU. It's most efficient to just feed it NV21. - nv21, - /// 32-bit BGRA. /// /// On iOS, this is `kCVPixelFormatType_32BGRA`. See From 5015330bc3ac498eb17ae31ab5b22ea026815236 Mon Sep 17 00:00:00 2001 From: Andrew Coutts Date: Tue, 2 Mar 2021 12:37:02 -0500 Subject: [PATCH 057/114] Added a (not working yet) unit test trying to mock camera characteristics --- .../io/flutter/plugins/camera/Camera.java | 13 +++-- .../io/flutter/plugins/camera/CameraTest.java | 51 +++++++++++++++++++ 2 files changed, 61 insertions(+), 3 deletions(-) create mode 100644 packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/CameraTest.java 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 4912ff64d169..aa3806b4ef89 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 @@ -282,7 +282,7 @@ public Camera( // Get camera characteristics and check for supported features cameraCharacteristics = cameraManager.getCameraCharacteristics(cameraName); getAvailableFpsRange(cameraCharacteristics); - checkAutoFocusSupported(); + mAutoFocusSupported = checkAutoFocusSupported(cameraCharacteristics); checkFlashSupported(); // Setup orientation @@ -313,12 +313,19 @@ public Camera( startBackgroundThread(); } + /** + * Get the current camera state (use for testing). + */ + public CameraState getState() { + return this.cameraState; + } + /** * Check if the auto focus is supported by the current camera. We look at the available AF modes * and the available lens focusing distance to determine if its' a fixed length lens or not as * well. */ - private void checkAutoFocusSupported() { + public static boolean checkAutoFocusSupported(CameraCharacteristics cameraCharacteristics) { int[] modes = cameraCharacteristics.get(CameraCharacteristics.CONTROL_AF_AVAILABLE_MODES); // Log.i(TAG, "checkAutoFocusSupported | modes:"); for (int mode : modes) { @@ -341,7 +348,7 @@ private void checkAutoFocusSupported() { } // Log.i(TAG, "checkAutoFocusSupported | minFocus " + minFocus + " | maxFocus: " + maxFocus); - mAutoFocusSupported = + return !isFixedLength && !(modes == null || modes.length == 0 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 new file mode 100644 index 000000000000..73e1a6620c62 --- /dev/null +++ b/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/CameraTest.java @@ -0,0 +1,51 @@ +package io.flutter.plugins.camera; + +import android.app.Activity; +import android.content.Context; +import android.hardware.camera2.CameraAccessException; +import android.hardware.camera2.CameraCharacteristics; +import android.hardware.camera2.CameraManager; +import android.util.Range; + +import org.junit.Test; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.isNotNull; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import io.flutter.view.TextureRegistry; + +public class CameraTest { + @Test + public void should_create_camera_plugin() throws CameraAccessException { + final Activity mockActivity = mock(Activity.class); + final TextureRegistry.SurfaceTextureEntry flutterTextureMock = mock(TextureRegistry.SurfaceTextureEntry.class); + final DartMessenger dartMessengerMock = mock(DartMessenger.class); + final String cameraName = "camera1"; + final String resolutionPreset = "high"; + final boolean enableAudio = false; + final CameraCharacteristics mockCameraCharacteristics = mock(CameraCharacteristics.class); + + // Mocks + final CameraManager mockCameraManager = mock(CameraManager.class); + when(mockActivity.getSystemService(Context.CAMERA_SERVICE)).thenReturn(mockCameraManager); + when(mockCameraManager.getCameraCharacteristics(cameraName)).thenReturn(mockCameraCharacteristics); + when(mockCameraCharacteristics.get(CameraCharacteristics.CONTROL_AF_AVAILABLE_MODES)).thenReturn(new int[]{0, 1, 2}); + when(mockCameraCharacteristics.get(CameraCharacteristics.CONTROL_AE_AVAILABLE_TARGET_FPS_RANGES)).thenReturn(null); + + final Camera camera = new Camera(mockActivity, + flutterTextureMock, + dartMessengerMock, + cameraName, + resolutionPreset, + enableAudio); + + assertEquals("should create a camera", camera, isNotNull()); + assertEquals("should be in preview state from the start", camera.getState(), CameraState.STATE_PREVIEW); + } +} From 02d009ecb5a48d76a6533e6f4b6f8a2823793f0e Mon Sep 17 00:00:00 2001 From: Andrew Coutts Date: Tue, 2 Mar 2021 14:36:54 -0500 Subject: [PATCH 058/114] Update changelog to remove NV21 references. --- packages/camera/camera/CHANGELOG.md | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/camera/camera/CHANGELOG.md b/packages/camera/camera/CHANGELOG.md index d03e810e07b3..1e8789b9afda 100644 --- a/packages/camera/camera/CHANGELOG.md +++ b/packages/camera/camera/CHANGELOG.md @@ -3,7 +3,6 @@ * Complete rewrite of Android plugin to fix all capture, focus, flash, and exposure issues. * Fixed crash when opening front-facing cameras on some legacy android devices like Sony XZ. * Android Flash mode works with full precapture sequence. -* Added support for NV21 image stream format on Android. ## 0.8.0 From 2f8d774e28f207f7748e1ae927fed2831d77f97e Mon Sep 17 00:00:00 2001 From: Maurits van Beusekom Date: Wed, 3 Mar 2021 10:11:55 +0100 Subject: [PATCH 059/114] Removed unused parameter --- .../io/flutter/plugins/camera/PictureCaptureRequest.java | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/PictureCaptureRequest.java b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/PictureCaptureRequest.java index d5d49cec93eb..f5a7eefa3907 100644 --- a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/PictureCaptureRequest.java +++ b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/PictureCaptureRequest.java @@ -99,9 +99,8 @@ public void setState(PictureCaptureRequestState newState) { return; } - final PictureCaptureRequestState oldState = state; state = newState; - onStateChange(oldState); + onStateChange(newState); } public boolean isFinished() { @@ -152,8 +151,8 @@ public void setPreCaptureStartTime() { } /** Handle new state changes. */ - private void onStateChange(PictureCaptureRequestState oldState) { - switch (state) { + private void onStateChange(PictureCaptureRequestState newState) { + switch (newState) { case STATE_CAPTURING: case STATE_WAITING_FOCUS: case STATE_WAITING_PRECAPTURE_START: From 2e6337982c4876fda3b909538d2ae2d3180ec5b9 Mon Sep 17 00:00:00 2001 From: Maurits van Beusekom Date: Wed, 3 Mar 2021 10:12:48 +0100 Subject: [PATCH 060/114] Added missing test cases PictureCaptureRequest --- .../plugins/camera/DartMessengerTest.java | 3 ++ .../camera/PictureCaptureRequestTest.java | 46 +++++++++++++++++++ 2 files changed, 49 insertions(+) diff --git a/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/DartMessengerTest.java b/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/DartMessengerTest.java index e835b08f441a..44ba6040a467 100644 --- a/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/DartMessengerTest.java +++ b/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/DartMessengerTest.java @@ -19,7 +19,10 @@ import java.util.List; import org.junit.Before; import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.RobolectricTestRunner; +@RunWith(RobolectricTestRunner.class) public class DartMessengerTest { /** A {@link BinaryMessenger} implementation that does nothing but save its messages. */ private static class FakeBinaryMessenger implements BinaryMessenger { diff --git a/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/PictureCaptureRequestTest.java b/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/PictureCaptureRequestTest.java index 99d905180c96..54509857ba3a 100644 --- a/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/PictureCaptureRequestTest.java +++ b/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/PictureCaptureRequestTest.java @@ -15,7 +15,10 @@ import io.flutter.plugin.common.MethodChannel; import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.RobolectricTestRunner; +@RunWith(RobolectricTestRunner.class) public class PictureCaptureRequestTest { @Test @@ -45,6 +48,21 @@ public void setState_sets_state() { "State is awaitingPreCapture", req.getState(), PictureCaptureRequestState.STATE_CAPTURING); } + @Test + public void setState_sends_camera_error_event_When_already_finished() { + DartMessenger mockMessenger = mock(DartMessenger.class); + PictureCaptureRequest req = new PictureCaptureRequest(null, null, mockMessenger); + + // Make sure state is set to finished + req.setState(PictureCaptureRequestState.STATE_FINISHED); + + // Try to update state + req.setState(PictureCaptureRequestState.STATE_CAPTURING); + + verify(mockMessenger, times(1)).sendCameraErrorEvent("Request has already been finished"); + assertEquals("State is finished", req.getState(), PictureCaptureRequestState.STATE_FINISHED); + } + @Test public void setState_resets_timeout() { PictureCaptureRequest.TimeoutHandler mockTimeoutHandler = @@ -114,6 +132,20 @@ public void isFinished_is_true_When_state_is_finished_or_error() { assertTrue(req.isFinished()); } + @Test + public void finish_returns_When_in_error_state() { + MethodChannel.Result mockResult = mock(MethodChannel.Result.class); + PictureCaptureRequest req = new PictureCaptureRequest(mockResult, null, null); + + // Make sure state is set to error + req.setState(PictureCaptureRequestState.STATE_ERROR); + + req.finish("/test/path"); + + assertEquals("State is error", req.getState(), PictureCaptureRequestState.STATE_ERROR); + verify(mockResult, never()).success(any()); + } + @Test(expected = IllegalStateException.class) public void finish_throws_When_already_finished() { // Setup @@ -147,6 +179,20 @@ public void error_clears_timeout() { verify(mockTimeoutHandler).clearTimeout(any()); } + @Test + public void error_returns_When_in_error_state() { + MethodChannel.Result mockResult = mock(MethodChannel.Result.class); + PictureCaptureRequest req = new PictureCaptureRequest(mockResult, null, null); + + // Make sure state is set to error + req.setState(PictureCaptureRequestState.STATE_ERROR); + + req.error("ERROR_CODE", "Error Message", null); + + assertEquals("State is error", req.getState(), PictureCaptureRequestState.STATE_ERROR); + verify(mockResult, never()).error(any(), any(), any()); + } + @Test(expected = IllegalStateException.class) public void error_throws_When_already_finished() { // Setup From e552379b351b5664d883d0155764a50b9aa43e66 Mon Sep 17 00:00:00 2001 From: Maurits van Beusekom Date: Wed, 3 Mar 2021 11:34:28 +0100 Subject: [PATCH 061/114] Renamed mFile to file to match code style --- .../plugins/camera/PictureCaptureRequest.java | 22 +++++++++++-------- 1 file changed, 13 insertions(+), 9 deletions(-) diff --git a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/PictureCaptureRequest.java b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/PictureCaptureRequest.java index f5a7eefa3907..eb3cd6451316 100644 --- a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/PictureCaptureRequest.java +++ b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/PictureCaptureRequest.java @@ -27,7 +27,7 @@ class PictureCaptureRequest { * This is the output file for the curent capture. The file is created in Camera and passed here * as reference to it. */ - final File mFile; + final File file; /** Dart method chanel result. */ private final MethodChannel.Result result; @@ -54,25 +54,29 @@ class PictureCaptureRequest { * Constructor to create a picture capture request. * * @param result - * @param mFile + * @param file */ public PictureCaptureRequest( - MethodChannel.Result result, File mFile, DartMessenger dartMessenger) { - this.result = result; - this.timeoutHandler = new TimeoutHandler(); - this.mFile = mFile; - this.dartMessenger = dartMessenger; + MethodChannel.Result result, + File file, + DartMessenger dartMessenger) { + this( + result, + file, + dartMessenger, + new TimeoutHandler() + ); } /** Constructor for unit tests where we can mock the timeout handler */ public PictureCaptureRequest( MethodChannel.Result result, - File mFile, + File file, DartMessenger dartMessenger, TimeoutHandler timeoutHandler) { this.result = result; this.timeoutHandler = timeoutHandler; - this.mFile = mFile; + this.file = file; this.dartMessenger = dartMessenger; } From 081ba9c03e2fb466fa3d5a932b6a78abb9672c5e Mon Sep 17 00:00:00 2001 From: Maurits van Beusekom Date: Wed, 3 Mar 2021 16:02:09 +0100 Subject: [PATCH 062/114] Fixed all Android tests --- packages/camera/camera/android/build.gradle | 2 +- .../io/flutter/plugins/camera/Camera.java | 158 ++++++++---------- .../plugins/camera/CameraProperties.java | 139 +++++++++++++++ .../flutter/plugins/camera/CameraUtils.java | 4 + .../plugins/camera/MethodCallHandlerImpl.java | 10 +- .../io/flutter/plugins/camera/CameraTest.java | 100 ++++++----- .../plugins/camera/DartMessengerTest.java | 3 + 7 files changed, 289 insertions(+), 127 deletions(-) create mode 100644 packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/CameraProperties.java diff --git a/packages/camera/camera/android/build.gradle b/packages/camera/camera/android/build.gradle index 0b88fd10fb71..fa981d738015 100644 --- a/packages/camera/camera/android/build.gradle +++ b/packages/camera/camera/android/build.gradle @@ -49,7 +49,7 @@ android { dependencies { compileOnly 'androidx.annotation:annotation:1.1.0' testImplementation 'junit:junit:4.12' - testImplementation 'org.mockito:mockito-core:3.5.13' + testImplementation 'org.mockito:mockito-inline:3.5.13' testImplementation 'androidx.test:core:1.3.0' testImplementation 'org.robolectric:robolectric:4.3' } 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 aa3806b4ef89..dc0341af59b3 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 @@ -91,11 +91,9 @@ public class Camera { } private final SurfaceTextureEntry flutterTexture; - private final CameraManager cameraManager; private final DeviceOrientationManager deviceOrientationListener; private final boolean isFrontFacing; private final int sensorOrientation; - private final String cameraName; private final Size captureSize; private final Size previewSize; private final boolean enableAudio; @@ -103,7 +101,7 @@ public class Camera { private final CamcorderProfile recordingProfile; private final DartMessenger dartMessenger; private final CameraZoom cameraZoom; - private final CameraCharacteristics cameraCharacteristics; + private final CameraProperties cameraProperties; private final Activity activity; /** This manages the state of the camera and the current capture request. */ PictureCaptureRequest pictureCaptureRequest; @@ -258,64 +256,81 @@ public Camera( final Activity activity, final SurfaceTextureEntry flutterTexture, final DartMessenger dartMessenger, - final String cameraName, - final String resolutionPreset, - final boolean enableAudio) - throws CameraAccessException { - // Log.i(TAG, "Camear constructor"); + final CameraProperties cameraProperties, + final ResolutionPreset resolutionPreset, + final boolean enableAudio) { + this( + activity, + flutterTexture, + dartMessenger, + cameraProperties, + resolutionPreset, + enableAudio, + null); + } + + public Camera( + final Activity activity, + final SurfaceTextureEntry flutterTexture, + final DartMessenger dartMessenger, + final CameraProperties cameraProperties, + final ResolutionPreset resolutionPreset, + final boolean enableAudio, + @Nullable final DeviceOrientationManager deviceOrientationManager) { if (activity == null) { throw new IllegalStateException("No activity available!"); } + this.activity = activity; - this.cameraName = cameraName; this.enableAudio = enableAudio; this.flutterTexture = flutterTexture; this.dartMessenger = dartMessenger; - this.cameraManager = (CameraManager) activity.getSystemService(Context.CAMERA_SERVICE); this.applicationContext = activity.getApplicationContext(); + this.cameraProperties = cameraProperties; this.currentFlashMode = FlashMode.off; this.currentExposureMode = ExposureMode.auto; this.currentFocusMode = FocusMode.auto; this.exposureOffset = 0; // Get camera characteristics and check for supported features - cameraCharacteristics = cameraManager.getCameraCharacteristics(cameraName); - getAvailableFpsRange(cameraCharacteristics); - mAutoFocusSupported = checkAutoFocusSupported(cameraCharacteristics); + getAvailableFpsRange(cameraProperties); + mAutoFocusSupported = checkAutoFocusSupported(cameraProperties); checkFlashSupported(); // Setup orientation - sensorOrientation = cameraCharacteristics.get(CameraCharacteristics.SENSOR_ORIENTATION); - isFrontFacing = - cameraCharacteristics.get(CameraCharacteristics.LENS_FACING) - == CameraMetadata.LENS_FACING_FRONT; + sensorOrientation = cameraProperties.getSensorOrientation(); + isFrontFacing = cameraProperties.getLensFacing() == CameraMetadata.LENS_FACING_FRONT; + deviceOrientationListener = - new DeviceOrientationManager(activity, dartMessenger, isFrontFacing, sensorOrientation); + deviceOrientationManager != null + ? deviceOrientationManager + : new DeviceOrientationManager( + activity, dartMessenger, isFrontFacing, sensorOrientation); deviceOrientationListener.start(); + String cameraName = cameraProperties.getCameraName(); + // Resolution configuration - ResolutionPreset preset = ResolutionPreset.valueOf(resolutionPreset); recordingProfile = - CameraUtils.getBestAvailableCamcorderProfileForResolutionPreset(cameraName, preset); + CameraUtils.getBestAvailableCamcorderProfileForResolutionPreset( + cameraName, resolutionPreset); captureSize = new Size(recordingProfile.videoFrameWidth, recordingProfile.videoFrameHeight); // Log.i(TAG, "captureSize: " + captureSize); - previewSize = computeBestPreviewSize(cameraName, preset); + previewSize = computeBestPreviewSize(cameraName, resolutionPreset); // Zoom setup cameraZoom = new CameraZoom( - cameraCharacteristics.get(CameraCharacteristics.SENSOR_INFO_ACTIVE_ARRAY_SIZE), - cameraCharacteristics.get(CameraCharacteristics.SCALER_AVAILABLE_MAX_DIGITAL_ZOOM)); + cameraProperties.getSensorInfoActiveArraySize(), + cameraProperties.getScalerAvailableMaxDigitalZoom()); // Start background thread. startBackgroundThread(); } - /** - * Get the current camera state (use for testing). - */ + /** Get the current camera state (use for testing). */ public CameraState getState() { return this.cameraState; } @@ -325,8 +340,8 @@ public CameraState getState() { * and the available lens focusing distance to determine if its' a fixed length lens or not as * well. */ - public static boolean checkAutoFocusSupported(CameraCharacteristics cameraCharacteristics) { - int[] modes = cameraCharacteristics.get(CameraCharacteristics.CONTROL_AF_AVAILABLE_MODES); + public static boolean checkAutoFocusSupported(CameraProperties cameraProperties) { + int[] modes = cameraProperties.getControlAutoFocusAvailableModes(); // Log.i(TAG, "checkAutoFocusSupported | modes:"); for (int mode : modes) { // Log.i(TAG, "checkAutoFocusSupported | ==> " + mode); @@ -334,8 +349,7 @@ public static boolean checkAutoFocusSupported(CameraCharacteristics cameraCharac // Check if fixed focal length lens. If LENS_INFO_MINIMUM_FOCUS_DISTANCE=0, then this is fixed. // Can be null on some devices. - final Float minFocus = - cameraCharacteristics.get(CameraCharacteristics.LENS_INFO_MINIMUM_FOCUS_DISTANCE); + final Float minFocus = cameraProperties.getLensInfoMinimumFocusDistance(); // final Float maxFocus = cameraCharacteristics.get(CameraCharacteristics.LENS_INFO_HYPERFOCAL_DISTANCE); // Value can be null on some devices: @@ -348,31 +362,30 @@ public static boolean checkAutoFocusSupported(CameraCharacteristics cameraCharac } // Log.i(TAG, "checkAutoFocusSupported | minFocus " + minFocus + " | maxFocus: " + maxFocus); - return - !isFixedLength - && !(modes == null - || modes.length == 0 - || (modes.length == 1 && modes[0] == CameraCharacteristics.CONTROL_AF_MODE_OFF)); + return !isFixedLength + && !(modes == null + || modes.length == 0 + || (modes.length == 1 && modes[0] == CameraCharacteristics.CONTROL_AF_MODE_OFF)); // Log.i(TAG, "checkAutoFocusSupported: " + mAutoFocusSupported); } /** Check if the flash is supported. */ private void checkFlashSupported() { - Boolean available = cameraCharacteristics.get(CameraCharacteristics.FLASH_INFO_AVAILABLE); - mFlashSupported = available == null ? false : available; + Boolean available = cameraProperties.getFlashInfoAvailable(); + mFlashSupported = available != null && available; } /** * Load available FPS range for the current camera and update the available fps range with it. * - * @param cameraCharacteristics + * @param cameraProperties */ - private void getAvailableFpsRange(CameraCharacteristics cameraCharacteristics) { + private void getAvailableFpsRange(CameraProperties cameraProperties) { // Log.i(TAG, "getAvailableFpsRange"); try { - Range[] ranges = - cameraCharacteristics.get(CameraCharacteristics.CONTROL_AE_AVAILABLE_TARGET_FPS_RANGES); + Range[] ranges = cameraProperties.getControlAutoExposureAvailableTargetFpsRanges(); + if (ranges != null) { for (Range range : ranges) { int upper = range.getUpper(); @@ -424,8 +437,9 @@ public void open(String imageFormatGroup) throws CameraAccessException { ImageReader.newInstance(previewSize.getWidth(), previewSize.getHeight(), imageFormat, 1); // Open the camera now + CameraManager cameraManager = CameraUtils.getCameraManager(activity); cameraManager.openCamera( - cameraName, + cameraProperties.getCameraName(), new CameraDevice.StateCallback() { @Override public void onOpened(@NonNull CameraDevice device) { @@ -1146,11 +1160,9 @@ public void setFocusPoint(@NonNull final Result result, Double x, Double y) } @TargetApi(VERSION_CODES.P) - private boolean supportsDistortionCorrection() throws CameraAccessException { + private boolean supportsDistortionCorrection() { int[] availableDistortionCorrectionModes = - cameraManager - .getCameraCharacteristics(cameraDevice.getId()) - .get(CameraCharacteristics.DISTORTION_CORRECTION_AVAILABLE_MODES); + cameraProperties.getDistortionCorrectionAvailableModes(); if (availableDistortionCorrectionModes == null) availableDistortionCorrectionModes = new int[0]; long nonOffModesSupported = Arrays.stream(availableDistortionCorrectionModes) @@ -1159,12 +1171,10 @@ private boolean supportsDistortionCorrection() throws CameraAccessException { return nonOffModesSupported > 0; } - private Size getRegionBoundaries() throws CameraAccessException { + private Size getRegionBoundaries() { // No distortion correction support if (android.os.Build.VERSION.SDK_INT < VERSION_CODES.P || !supportsDistortionCorrection()) { - return cameraManager - .getCameraCharacteristics(cameraDevice.getId()) - .get(CameraCharacteristics.SENSOR_INFO_PIXEL_ARRAY_SIZE); + return cameraProperties.getSensorInfoPixelArraySize(); } // Get the current distortion correction mode Integer distortionCorrectionMode = @@ -1173,65 +1183,43 @@ private Size getRegionBoundaries() throws CameraAccessException { android.graphics.Rect rect; if (distortionCorrectionMode == null || distortionCorrectionMode == CaptureRequest.DISTORTION_CORRECTION_MODE_OFF) { - rect = - cameraManager - .getCameraCharacteristics(cameraDevice.getId()) - .get(CameraCharacteristics.SENSOR_INFO_PRE_CORRECTION_ACTIVE_ARRAY_SIZE); + rect = cameraProperties.getSensorInfoPreCorrectionActiveArraySize(); } else { - rect = - cameraManager - .getCameraCharacteristics(cameraDevice.getId()) - .get(CameraCharacteristics.SENSOR_INFO_ACTIVE_ARRAY_SIZE); + rect = cameraProperties.getSensorInfoActiveArraySize(); } return rect == null ? null : new Size(rect.width(), rect.height()); } - private boolean isExposurePointSupported() throws CameraAccessException { - Integer supportedRegions = - cameraManager - .getCameraCharacteristics(cameraDevice.getId()) - .get(CameraCharacteristics.CONTROL_MAX_REGIONS_AE); + private boolean isExposurePointSupported() { + Integer supportedRegions = cameraProperties.getControlMaxRegionsAutoExposure(); return supportedRegions != null && supportedRegions > 0; } - private boolean isFocusPointSupported() throws CameraAccessException { - Integer supportedRegions = - cameraManager - .getCameraCharacteristics(cameraDevice.getId()) - .get(CameraCharacteristics.CONTROL_MAX_REGIONS_AF); + private boolean isFocusPointSupported() { + Integer supportedRegions = cameraProperties.getControlMaxRegionsAutoFocus(); return supportedRegions != null && supportedRegions > 0; } - public double getMinExposureOffset() throws CameraAccessException { - Range range = - cameraManager - .getCameraCharacteristics(cameraDevice.getId()) - .get(CameraCharacteristics.CONTROL_AE_COMPENSATION_RANGE); + public double getMinExposureOffset() { + Range range = cameraProperties.getControlAutoExposureCompensationRange(); double minStepped = range == null ? 0 : range.getLower(); double stepSize = getExposureOffsetStepSize(); return minStepped * stepSize; } - public double getMaxExposureOffset() throws CameraAccessException { - Range range = - cameraManager - .getCameraCharacteristics(cameraDevice.getId()) - .get(CameraCharacteristics.CONTROL_AE_COMPENSATION_RANGE); + public double getMaxExposureOffset() { + Range range = cameraProperties.getControlAutoExposureCompensationRange(); double maxStepped = range == null ? 0 : range.getUpper(); double stepSize = getExposureOffsetStepSize(); return maxStepped * stepSize; } - public double getExposureOffsetStepSize() throws CameraAccessException { - Rational stepSize = - cameraManager - .getCameraCharacteristics(cameraDevice.getId()) - .get(CameraCharacteristics.CONTROL_AE_COMPENSATION_STEP); + public double getExposureOffsetStepSize() { + Rational stepSize = cameraProperties.getControlAutoExposureCompensationStep(); return stepSize == null ? 0.0 : stepSize.doubleValue(); } - public void setExposureOffset(@NonNull final Result result, double offset) - throws CameraAccessException { + public void setExposureOffset(@NonNull final Result result, double offset) { // Set the exposure offset double stepSize = getExposureOffsetStepSize(); exposureOffset = (int) (offset / stepSize); diff --git a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/CameraProperties.java b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/CameraProperties.java new file mode 100644 index 000000000000..1a7510883713 --- /dev/null +++ b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/CameraProperties.java @@ -0,0 +1,139 @@ +package io.flutter.plugins.camera; + +import android.graphics.Rect; +import android.hardware.camera2.CameraAccessException; +import android.hardware.camera2.CameraCharacteristics; +import android.hardware.camera2.CameraManager; +import android.os.Build.VERSION_CODES; +import android.util.Range; +import android.util.Rational; +import android.util.Size; +import androidx.annotation.RequiresApi; + +public interface CameraProperties { + String getCameraName(); + + Range[] getControlAutoExposureAvailableTargetFpsRanges(); + + Range getControlAutoExposureCompensationRange(); + + Rational getControlAutoExposureCompensationStep(); + + int[] getControlAutoFocusAvailableModes(); + + Integer getControlMaxRegionsAutoExposure(); + + Integer getControlMaxRegionsAutoFocus(); + + int[] getDistortionCorrectionAvailableModes(); + + Boolean getFlashInfoAvailable(); + + int getLensFacing(); + + Float getLensInfoMinimumFocusDistance(); + + Float getScalerAvailableMaxDigitalZoom(); + + Rect getSensorInfoActiveArraySize(); + + Size getSensorInfoPixelArraySize(); + + Rect getSensorInfoPreCorrectionActiveArraySize(); + + int getSensorOrientation(); +} + +class CameraPropertiesImpl implements CameraProperties { + private final CameraCharacteristics cameraCharacteristics; + private final String cameraName; + + public CameraPropertiesImpl(String cameraName, CameraManager cameraManager) + throws CameraAccessException { + this.cameraName = cameraName; + this.cameraCharacteristics = cameraManager.getCameraCharacteristics(cameraName); + } + + @Override + public String getCameraName() { + return cameraName; + } + + @Override + public Range[] getControlAutoExposureAvailableTargetFpsRanges() { + return cameraCharacteristics.get(CameraCharacteristics.CONTROL_AE_AVAILABLE_TARGET_FPS_RANGES); + } + + @Override + public Range getControlAutoExposureCompensationRange() { + return cameraCharacteristics.get(CameraCharacteristics.CONTROL_AE_COMPENSATION_RANGE); + } + + @Override + public Rational getControlAutoExposureCompensationStep() { + return cameraCharacteristics.get(CameraCharacteristics.CONTROL_AE_COMPENSATION_STEP); + } + + @Override + public int[] getControlAutoFocusAvailableModes() { + return cameraCharacteristics.get(CameraCharacteristics.CONTROL_AF_AVAILABLE_MODES); + } + + @Override + public Integer getControlMaxRegionsAutoExposure() { + return cameraCharacteristics.get(CameraCharacteristics.CONTROL_MAX_REGIONS_AE); + } + + @Override + public Integer getControlMaxRegionsAutoFocus() { + return cameraCharacteristics.get(CameraCharacteristics.CONTROL_MAX_REGIONS_AF); + } + + @RequiresApi(api = VERSION_CODES.P) + @Override + public int[] getDistortionCorrectionAvailableModes() { + return cameraCharacteristics.get(CameraCharacteristics.DISTORTION_CORRECTION_AVAILABLE_MODES); + } + + @Override + public Boolean getFlashInfoAvailable() { + return cameraCharacteristics.get(CameraCharacteristics.FLASH_INFO_AVAILABLE); + } + + @Override + public int getLensFacing() { + return cameraCharacteristics.get(CameraCharacteristics.LENS_FACING); + } + + @Override + public Float getLensInfoMinimumFocusDistance() { + return cameraCharacteristics.get(CameraCharacteristics.LENS_INFO_MINIMUM_FOCUS_DISTANCE); + } + + @Override + public Float getScalerAvailableMaxDigitalZoom() { + return cameraCharacteristics.get(CameraCharacteristics.SCALER_AVAILABLE_MAX_DIGITAL_ZOOM); + } + + @Override + public Rect getSensorInfoActiveArraySize() { + return cameraCharacteristics.get(CameraCharacteristics.SENSOR_INFO_ACTIVE_ARRAY_SIZE); + } + + @Override + public Size getSensorInfoPixelArraySize() { + return cameraCharacteristics.get(CameraCharacteristics.SENSOR_INFO_PIXEL_ARRAY_SIZE); + } + + @RequiresApi(api = VERSION_CODES.M) + @Override + public Rect getSensorInfoPreCorrectionActiveArraySize() { + return cameraCharacteristics.get( + CameraCharacteristics.SENSOR_INFO_PRE_CORRECTION_ACTIVE_ARRAY_SIZE); + } + + @Override + public int getSensorOrientation() { + return cameraCharacteristics.get(CameraCharacteristics.SENSOR_ORIENTATION); + } +} diff --git a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/CameraUtils.java b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/CameraUtils.java index 03993a3b51f9..3cd8313bf070 100644 --- a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/CameraUtils.java +++ b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/CameraUtils.java @@ -29,6 +29,10 @@ public final class CameraUtils { private CameraUtils() {} + static CameraManager getCameraManager(Context context) { + return (CameraManager) context.getSystemService(Context.CAMERA_SERVICE); + } + static PlatformChannel.DeviceOrientation getDeviceOrientationFromDegrees(int degrees) { // Round to the nearest 90 degrees. degrees = (int) (Math.round(degrees / 90.0) * 90) % 360; diff --git a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/MethodCallHandlerImpl.java b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/MethodCallHandlerImpl.java index aa7483f55679..4561b9eb73bc 100644 --- a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/MethodCallHandlerImpl.java +++ b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/MethodCallHandlerImpl.java @@ -18,6 +18,7 @@ import io.flutter.plugins.camera.types.ExposureMode; import io.flutter.plugins.camera.types.FlashMode; import io.flutter.plugins.camera.types.FocusMode; +import io.flutter.plugins.camera.types.ResolutionPreset; import io.flutter.view.TextureRegistry; import java.util.HashMap; import java.util.Map; @@ -349,17 +350,22 @@ void stopListening() { private void instantiateCamera(MethodCall call, Result result) throws CameraAccessException { String cameraName = call.argument("cameraName"); - String resolutionPreset = call.argument("resolutionPreset"); + String preset = call.argument("resolutionPreset"); boolean enableAudio = call.argument("enableAudio"); + TextureRegistry.SurfaceTextureEntry flutterSurfaceTexture = textureRegistry.createSurfaceTexture(); DartMessenger dartMessenger = new DartMessenger(messenger, flutterSurfaceTexture.id()); + CameraProperties cameraProperties = + new CameraPropertiesImpl(cameraName, CameraUtils.getCameraManager(activity)); + ResolutionPreset resolutionPreset = ResolutionPreset.valueOf(preset); + camera = new Camera( activity, flutterSurfaceTexture, dartMessenger, - cameraName, + cameraProperties, resolutionPreset, enableAudio); 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 73e1a6620c62..65c6c11e4dc4 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 @@ -1,51 +1,73 @@ package io.flutter.plugins.camera; -import android.app.Activity; -import android.content.Context; -import android.hardware.camera2.CameraAccessException; -import android.hardware.camera2.CameraCharacteristics; -import android.hardware.camera2.CameraManager; -import android.util.Range; - -import org.junit.Test; - import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertFalse; -import static org.junit.Assert.assertTrue; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.ArgumentMatchers.isNotNull; +import static org.junit.Assert.assertNotNull; import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.mockStatic; +import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; +import android.app.Activity; +import android.hardware.camera2.CameraAccessException; +import android.media.CamcorderProfile; +import io.flutter.plugins.camera.types.ResolutionPreset; import io.flutter.view.TextureRegistry; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.MockedStatic; +import org.robolectric.RobolectricTestRunner; +@RunWith(RobolectricTestRunner.class) public class CameraTest { - @Test - public void should_create_camera_plugin() throws CameraAccessException { - final Activity mockActivity = mock(Activity.class); - final TextureRegistry.SurfaceTextureEntry flutterTextureMock = mock(TextureRegistry.SurfaceTextureEntry.class); - final DartMessenger dartMessengerMock = mock(DartMessenger.class); - final String cameraName = "camera1"; - final String resolutionPreset = "high"; - final boolean enableAudio = false; - final CameraCharacteristics mockCameraCharacteristics = mock(CameraCharacteristics.class); - - // Mocks - final CameraManager mockCameraManager = mock(CameraManager.class); - when(mockActivity.getSystemService(Context.CAMERA_SERVICE)).thenReturn(mockCameraManager); - when(mockCameraManager.getCameraCharacteristics(cameraName)).thenReturn(mockCameraCharacteristics); - when(mockCameraCharacteristics.get(CameraCharacteristics.CONTROL_AF_AVAILABLE_MODES)).thenReturn(new int[]{0, 1, 2}); - when(mockCameraCharacteristics.get(CameraCharacteristics.CONTROL_AE_AVAILABLE_TARGET_FPS_RANGES)).thenReturn(null); - - final Camera camera = new Camera(mockActivity, - flutterTextureMock, - dartMessengerMock, - cameraName, - resolutionPreset, - enableAudio); - - assertEquals("should create a camera", camera, isNotNull()); - assertEquals("should be in preview state from the start", camera.getState(), CameraState.STATE_PREVIEW); + + @Test + public void should_create_camera_plugin() throws CameraAccessException { + final Activity mockActivity = mock(Activity.class); + final TextureRegistry.SurfaceTextureEntry flutterTextureMock = + mock(TextureRegistry.SurfaceTextureEntry.class); + final DartMessenger dartMessengerMock = mock(DartMessenger.class); + final String cameraName = "1"; + final ResolutionPreset resolutionPreset = ResolutionPreset.high; + final boolean enableAudio = false; + + // Mocks + final CamcorderProfile mockCamcorderProfile = mock(CamcorderProfile.class); + final CameraProperties mockCameraProperties = mock(CameraProperties.class); + final DeviceOrientationManager mockDeviceOrientationManager = + mock(DeviceOrientationManager.class); + + try (MockedStatic mockCameraUtils = mockStatic(CameraUtils.class)) { + mockCameraUtils + .when( + () -> + CameraUtils.getBestAvailableCamcorderProfileForResolutionPreset( + cameraName, resolutionPreset)) + .thenReturn(mockCamcorderProfile); + + mockCamcorderProfile.videoFrameHeight = 480; + mockCamcorderProfile.videoFrameWidth = 640; + when(mockCameraProperties.getCameraName()).thenReturn(cameraName); + when(mockCameraProperties.getControlAutoFocusAvailableModes()) + .thenReturn(new int[] {0, 1, 2}); + when(mockCameraProperties.getControlAutoExposureAvailableTargetFpsRanges()).thenReturn(null); + + final Camera camera = + new Camera( + mockActivity, + flutterTextureMock, + dartMessengerMock, + mockCameraProperties, + resolutionPreset, + enableAudio, + mockDeviceOrientationManager); + + assertNotNull("should create a camera", camera); + assertEquals( + "should be in preview state from the start", + camera.getState(), + CameraState.STATE_PREVIEW); + verify(mockDeviceOrientationManager, times(1)).start(); } + } } diff --git a/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/DartMessengerTest.java b/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/DartMessengerTest.java index e835b08f441a..44ba6040a467 100644 --- a/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/DartMessengerTest.java +++ b/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/DartMessengerTest.java @@ -19,7 +19,10 @@ import java.util.List; import org.junit.Before; import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.RobolectricTestRunner; +@RunWith(RobolectricTestRunner.class) public class DartMessengerTest { /** A {@link BinaryMessenger} implementation that does nothing but save its messages. */ private static class FakeBinaryMessenger implements BinaryMessenger { From 3481c1eeef91d4010cfab1d2ba01fffcd8a528b3 Mon Sep 17 00:00:00 2001 From: Maurits van Beusekom Date: Wed, 3 Mar 2021 16:36:34 +0100 Subject: [PATCH 063/114] Fix NRE when calling setExposureOffset --- .../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 aa3806b4ef89..00c8b1725dd5 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 @@ -1240,7 +1240,7 @@ public void setExposureOffset(@NonNull final Result result, double offset) // Refresh capture session refreshPreviewCaptureSession( - () -> result.success(null), + () -> result.success(offset), (code, message) -> result.error("setExposureModeFailed", "Could not set flash mode.", null)); } From 25cff82cadc285d62fae5f462f12e1494d6ad7ed Mon Sep 17 00:00:00 2001 From: Maurits van Beusekom Date: Thu, 4 Mar 2021 16:03:47 +0100 Subject: [PATCH 064/114] Fix compile error --- .../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 e510acc56fe1..048f23162eb5 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 @@ -124,7 +124,7 @@ public void onImageAvailable(ImageReader reader) { // Use acquireNextImage since our image reader is only for 1 image. mBackgroundHandler.post( new ImageSaver( - reader.acquireNextImage(), pictureCaptureRequest.mFile, pictureCaptureRequest)); + reader.acquireNextImage(), pictureCaptureRequest.file, pictureCaptureRequest)); cameraState = CameraState.STATE_PREVIEW; } }; From e7808a5fae47c3e6d8f65923f4aeec4c5aea423d Mon Sep 17 00:00:00 2001 From: Maurits van Beusekom Date: Fri, 5 Mar 2021 16:46:09 +0100 Subject: [PATCH 065/114] Extract CaptureCallback implementation --- .../io/flutter/plugins/camera/Camera.java | 131 +++--------------- .../plugins/camera/CameraCaptureCallback.java | 120 ++++++++++++++++ .../flutter/plugins/camera/DartMessenger.java | 2 +- .../camera/DeviceOrientationManager.java | 14 +- .../plugins/camera/PictureCaptureRequest.java | 32 +++-- .../camera/media/MediaRecorderBuilder.java | 14 +- .../io/flutter/plugins/camera/CameraTest.java | 27 ++-- .../camera/PictureCaptureRequestTest.java | 114 +++++++++------ .../media/MediaRecorderBuilderTest.java | 69 +++++---- 9 files changed, 304 insertions(+), 219 deletions(-) create mode 100644 packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/CameraCaptureCallback.java 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 048f23162eb5..7b47c844d856 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 @@ -68,7 +68,7 @@ interface ErrorCallback { void onError(String errorCode, String errorMessage); } -public class Camera { +class Camera implements CameraCaptureCallback.CameraCaptureStateListener { private static final String TAG = "Camera"; /** Conversion from screen rotation to JPEG orientation. */ @@ -92,7 +92,6 @@ public class Camera { private final SurfaceTextureEntry flutterTexture; private final DeviceOrientationManager deviceOrientationListener; - private final boolean isFrontFacing; private final int sensorOrientation; private final Size captureSize; private final Size previewSize; @@ -166,88 +165,7 @@ public void onImageAvailable(ImageReader reader) { private CameraRegions cameraRegions; private int exposureOffset; /** A {@link CameraCaptureSession.CaptureCallback} that handles events related to JPEG capture. */ - private final CameraCaptureSession.CaptureCallback mCaptureCallback = - new CameraCaptureSession.CaptureCallback() { - - private void process(CaptureResult result) { - Integer aeState = result.get(CaptureResult.CONTROL_AE_STATE); - Integer afState = result.get(CaptureResult.CONTROL_AF_STATE); - - if (cameraState != CameraState.STATE_PREVIEW) { - // Log.i(TAG, "mCaptureCallback | state: " + cameraState + " | afState: " + afState + " | aeState: " + aeState); - } - - switch (cameraState) { - case STATE_PREVIEW: - { - // We have nothing to do when the camera preview is working normally. - break; - } - - case STATE_WAITING_FOCUS: - { - if (afState == null) { - return; - } else if (afState == CaptureRequest.CONTROL_AF_STATE_PASSIVE_SCAN - || afState == CaptureRequest.CONTROL_AF_STATE_FOCUSED_LOCKED - || afState == CaptureRequest.CONTROL_AF_STATE_NOT_FOCUSED_LOCKED) { - // CONTROL_AE_STATE can be null on some devices - - if (aeState == null || aeState == CaptureRequest.CONTROL_AE_STATE_CONVERGED) { - takePictureAfterPrecapture(); - } else { - runPrecaptureSequence(); - } - } - break; - } - - case STATE_WAITING_PRECAPTURE_START: - { - // CONTROL_AE_STATE can be null on some devices - if (aeState == null - || aeState == CaptureResult.CONTROL_AE_STATE_CONVERGED - || aeState == CaptureResult.CONTROL_AE_STATE_PRECAPTURE - || aeState == CaptureResult.CONTROL_AE_STATE_FLASH_REQUIRED) { - cameraState = CameraState.STATE_WAITING_PRECAPTURE_DONE; - pictureCaptureRequest.setState( - PictureCaptureRequestState.STATE_WAITING_PRECAPTURE_DONE); - } - break; - } - - case STATE_WAITING_PRECAPTURE_DONE: - { - // CONTROL_AE_STATE can be null on some devices - if (aeState == null || aeState != CaptureResult.CONTROL_AE_STATE_PRECAPTURE) { - takePictureAfterPrecapture(); - } else { - if (pictureCaptureRequest.hitPreCaptureTimeout()) { - // Log.i(TAG, "===> Hit precapture timeout"); - unlockAutoFocus(); - } - } - break; - } - } - } - - @Override - public void onCaptureProgressed( - @NonNull CameraCaptureSession session, - @NonNull CaptureRequest request, - @NonNull CaptureResult partialResult) { - process(partialResult); - } - - @Override - public void onCaptureCompleted( - @NonNull CameraCaptureSession session, - @NonNull CaptureRequest request, - @NonNull TotalCaptureResult result) { - process(result); - } - }; + private final CameraCaptureCallback mCaptureCallback; private Range fpsRange; private PlatformChannel.DeviceOrientation lockedCaptureOrientation; @@ -259,24 +177,6 @@ public Camera( final CameraProperties cameraProperties, final ResolutionPreset resolutionPreset, final boolean enableAudio) { - this( - activity, - flutterTexture, - dartMessenger, - cameraProperties, - resolutionPreset, - enableAudio, - null); - } - - public Camera( - final Activity activity, - final SurfaceTextureEntry flutterTexture, - final DartMessenger dartMessenger, - final CameraProperties cameraProperties, - final ResolutionPreset resolutionPreset, - final boolean enableAudio, - @Nullable final DeviceOrientationManager deviceOrientationManager) { if (activity == null) { throw new IllegalStateException("No activity available!"); @@ -293,6 +193,8 @@ public Camera( this.currentFocusMode = FocusMode.auto; this.exposureOffset = 0; + mCaptureCallback = CameraCaptureCallback.create(this); + // Get camera characteristics and check for supported features getAvailableFpsRange(cameraProperties); mAutoFocusSupported = checkAutoFocusSupported(cameraProperties); @@ -300,12 +202,9 @@ public Camera( // Setup orientation sensorOrientation = cameraProperties.getSensorOrientation(); - isFrontFacing = cameraProperties.getLensFacing() == CameraMetadata.LENS_FACING_FRONT; + boolean isFrontFacing = cameraProperties.getLensFacing() == CameraMetadata.LENS_FACING_FRONT; - deviceOrientationListener = - deviceOrientationManager != null - ? deviceOrientationManager - : new DeviceOrientationManager( + deviceOrientationListener = DeviceOrientationManager.create( activity, dartMessenger, isFrontFacing, sensorOrientation); deviceOrientationListener.start(); @@ -330,6 +229,21 @@ public Camera( startBackgroundThread(); } + @Override + public void onConverged() { + takePictureAfterPrecapture(); + } + + @Override + public void onPrecapture() { + runPrecaptureSequence(); + } + + @Override + public void onPrecaptureTimeout() { + unlockAutoFocus(); + } + /** Get the current camera state (use for testing). */ public CameraState getState() { return this.cameraState; @@ -642,7 +556,8 @@ public void takePicture(@NonNull final Result result) { final File file = File.createTempFile("CAP", ".jpg", outputDir); // Start a new capture - pictureCaptureRequest = new PictureCaptureRequest(result, file, dartMessenger); + pictureCaptureRequest = PictureCaptureRequest.create(result, file, dartMessenger); + mCaptureCallback.setPictureCaptureRequest(pictureCaptureRequest); } catch (IOException | SecurityException e) { pictureCaptureRequest.error("cannotCreateFile", e.getMessage(), null); return; diff --git a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/CameraCaptureCallback.java b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/CameraCaptureCallback.java new file mode 100644 index 000000000000..67b872c01179 --- /dev/null +++ b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/CameraCaptureCallback.java @@ -0,0 +1,120 @@ +package io.flutter.plugins.camera; + +import android.hardware.camera2.CameraCaptureSession; +import android.hardware.camera2.CameraCaptureSession.CaptureCallback; +import android.hardware.camera2.CaptureRequest; +import android.hardware.camera2.CaptureResult; +import android.hardware.camera2.TotalCaptureResult; +import androidx.annotation.NonNull; + +class CameraCaptureCallback extends CaptureCallback { + interface CameraCaptureStateListener { + void onConverged(); + void onPrecapture(); + void onPrecaptureTimeout(); + } + + private final CameraCaptureStateListener cameraStateListener; + + private CameraState cameraState; + private PictureCaptureRequest pictureCaptureRequest; + + public static CameraCaptureCallback create( + @NonNull CameraCaptureStateListener cameraStateListener) { + return new CameraCaptureCallback(cameraStateListener); + } + + private CameraCaptureCallback( + @NonNull CameraCaptureStateListener cameraStateListener) { + cameraState = CameraState.STATE_PREVIEW; + this.cameraStateListener = cameraStateListener; + this.pictureCaptureRequest = pictureCaptureRequest; + } + + public CameraState getCameraState() { + return cameraState; + } + + public void setCameraState(@NonNull CameraState state) { + cameraState = state; + + if (pictureCaptureRequest != null && state == CameraState.STATE_WAITING_PRECAPTURE_DONE) { + pictureCaptureRequest.setState( + PictureCaptureRequestState.STATE_WAITING_PRECAPTURE_DONE); + } + } + + public void setPictureCaptureRequest(@NonNull PictureCaptureRequest pictureCaptureRequest) { + this.pictureCaptureRequest = pictureCaptureRequest; + } + + private void process(CaptureResult result) { + Integer aeState = result.get(CaptureResult.CONTROL_AE_STATE); + Integer afState = result.get(CaptureResult.CONTROL_AF_STATE); + + switch (cameraState) { + case STATE_PREVIEW: + { + // We have nothing to do when the camera preview is working normally. + break; + } + case STATE_WAITING_FOCUS: + { + if (afState == null) { + return; + } else if (afState == CaptureRequest.CONTROL_AF_STATE_PASSIVE_SCAN + || afState == CaptureRequest.CONTROL_AF_STATE_FOCUSED_LOCKED + || afState == CaptureRequest.CONTROL_AF_STATE_NOT_FOCUSED_LOCKED) { + // CONTROL_AE_STATE can be null on some devices + + if (aeState == null || aeState == CaptureRequest.CONTROL_AE_STATE_CONVERGED) { + cameraStateListener.onConverged(); + } else { + cameraStateListener.onPrecapture(); + } + } + break; + } + + case STATE_WAITING_PRECAPTURE_START: + { + // CONTROL_AE_STATE can be null on some devices + if (aeState == null + || aeState == CaptureResult.CONTROL_AE_STATE_CONVERGED + || aeState == CaptureResult.CONTROL_AE_STATE_PRECAPTURE + || aeState == CaptureResult.CONTROL_AE_STATE_FLASH_REQUIRED) { + setCameraState(CameraState.STATE_WAITING_PRECAPTURE_DONE); + } + break; + } + + case STATE_WAITING_PRECAPTURE_DONE: + { + // CONTROL_AE_STATE can be null on some devices + if (aeState == null || aeState != CaptureResult.CONTROL_AE_STATE_PRECAPTURE) { + cameraStateListener.onConverged(); + } else if (pictureCaptureRequest != null && pictureCaptureRequest.hitPreCaptureTimeout()) { + // Log.i(TAG, "===> Hit precapture timeout"); + cameraStateListener.onPrecaptureTimeout(); + } + break; + } + } + } + + @Override + public void onCaptureProgressed( + @NonNull CameraCaptureSession session, + @NonNull CaptureRequest request, + @NonNull CaptureResult partialResult) { + process(partialResult); + } + + @Override + public void onCaptureCompleted( + @NonNull CameraCaptureSession session, + @NonNull CaptureRequest request, + @NonNull TotalCaptureResult result) { + process(result); + } +} diff --git a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/DartMessenger.java b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/DartMessenger.java index 3892452892d9..59dfec9c43db 100644 --- a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/DartMessenger.java +++ b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/DartMessenger.java @@ -41,7 +41,7 @@ enum CameraEventType { } } - DartMessenger(BinaryMessenger messenger, long cameraId) { + public DartMessenger(BinaryMessenger messenger, long cameraId) { cameraChannel = new MethodChannel(messenger, "flutter.io/cameraPlugin/camera" + cameraId); deviceChannel = new MethodChannel(messenger, "flutter.io/cameraPlugin/device"); } diff --git a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/DeviceOrientationManager.java b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/DeviceOrientationManager.java index b2a504b629d6..a1852ce545de 100644 --- a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/DeviceOrientationManager.java +++ b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/DeviceOrientationManager.java @@ -31,7 +31,19 @@ class DeviceOrientationManager { private OrientationEventListener orientationEventListener; private BroadcastReceiver broadcastReceiver; - public DeviceOrientationManager( + /** + * Factory method to create a device orientation manager. + */ + public static DeviceOrientationManager create( + Activity activity, + DartMessenger messenger, + boolean isFrontFacing, + int sensorOrientation + ) { + return new DeviceOrientationManager(activity, messenger, isFrontFacing, sensorOrientation); + } + + private DeviceOrientationManager( Activity activity, DartMessenger messenger, boolean isFrontFacing, int sensorOrientation) { this.activity = activity; this.messenger = messenger; diff --git a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/PictureCaptureRequest.java b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/PictureCaptureRequest.java index eb3cd6451316..7613083bfd72 100644 --- a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/PictureCaptureRequest.java +++ b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/PictureCaptureRequest.java @@ -51,33 +51,33 @@ class PictureCaptureRequest { }; /** - * Constructor to create a picture capture request. + * Factory method to create a picture capture request. * * @param result * @param file */ - public PictureCaptureRequest( + static PictureCaptureRequest create( MethodChannel.Result result, File file, DartMessenger dartMessenger) { - this( - result, - file, - dartMessenger, - new TimeoutHandler() - ); + return new PictureCaptureRequest(result, file, dartMessenger); } - /** Constructor for unit tests where we can mock the timeout handler */ - public PictureCaptureRequest( + /** + * Private constructor to create a picture capture request. + * + * @param result + * @param file + */ + private PictureCaptureRequest( MethodChannel.Result result, File file, - DartMessenger dartMessenger, - TimeoutHandler timeoutHandler) { + DartMessenger dartMessenger) { + this.result = result; - this.timeoutHandler = timeoutHandler; this.file = file; this.dartMessenger = dartMessenger; + this.timeoutHandler = TimeoutHandler.create(); } /** @@ -184,7 +184,11 @@ static class TimeoutHandler { private static final int REQUEST_TIMEOUT = 5000; private final Handler handler; - TimeoutHandler() { + public static TimeoutHandler create() { + return new TimeoutHandler(); + } + + private TimeoutHandler() { this.handler = new Handler(Looper.getMainLooper()); } diff --git a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/media/MediaRecorderBuilder.java b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/media/MediaRecorderBuilder.java index 4c3fb3add230..19acc6f29b0c 100644 --- a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/media/MediaRecorderBuilder.java +++ b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/media/MediaRecorderBuilder.java @@ -11,30 +11,22 @@ public class MediaRecorderBuilder { static class MediaRecorderFactory { - MediaRecorder makeMediaRecorder() { + static MediaRecorder create() { return new MediaRecorder(); } } private final String outputFilePath; private final CamcorderProfile recordingProfile; - private final MediaRecorderFactory recorderFactory; private boolean enableAudio; private int mediaOrientation; public MediaRecorderBuilder( @NonNull CamcorderProfile recordingProfile, @NonNull String outputFilePath) { - this(recordingProfile, outputFilePath, new MediaRecorderFactory()); - } - MediaRecorderBuilder( - @NonNull CamcorderProfile recordingProfile, - @NonNull String outputFilePath, - MediaRecorderFactory helper) { - this.outputFilePath = outputFilePath; this.recordingProfile = recordingProfile; - this.recorderFactory = helper; + this.outputFilePath = outputFilePath; } public MediaRecorderBuilder setEnableAudio(boolean enableAudio) { @@ -48,7 +40,7 @@ public MediaRecorderBuilder setMediaOrientation(int orientation) { } public MediaRecorder build() throws IOException { - MediaRecorder mediaRecorder = recorderFactory.makeMediaRecorder(); + MediaRecorder mediaRecorder = MediaRecorderFactory.create(); // There's a fixed order that mediaRecorder expects. Only change these functions accordingly. // You can find the specifics here: https://developer.android.com/reference/android/media/MediaRecorder. 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 65c6c11e4dc4..54bb190bd43b 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 @@ -47,20 +47,29 @@ public void should_create_camera_plugin() throws CameraAccessException { mockCamcorderProfile.videoFrameHeight = 480; mockCamcorderProfile.videoFrameWidth = 640; + when(mockCameraProperties.getLensFacing()).thenReturn(0); + when(mockCameraProperties.getSensorOrientation()).thenReturn(0); when(mockCameraProperties.getCameraName()).thenReturn(cameraName); when(mockCameraProperties.getControlAutoFocusAvailableModes()) .thenReturn(new int[] {0, 1, 2}); when(mockCameraProperties.getControlAutoExposureAvailableTargetFpsRanges()).thenReturn(null); - final Camera camera = - new Camera( - mockActivity, - flutterTextureMock, - dartMessengerMock, - mockCameraProperties, - resolutionPreset, - enableAudio, - mockDeviceOrientationManager); + Camera camera = null; + try (MockedStatic mockOrientationManagerFactory = mockStatic(DeviceOrientationManager.class)) { + mockOrientationManagerFactory.when(() -> DeviceOrientationManager.create( + mockActivity, + dartMessengerMock, + true, + 0)).thenReturn(mockDeviceOrientationManager); + + camera = new Camera( + mockActivity, + flutterTextureMock, + dartMessengerMock, + mockCameraProperties, + resolutionPreset, + enableAudio); + } assertNotNull("should create a camera", camera); assertEquals( diff --git a/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/PictureCaptureRequestTest.java b/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/PictureCaptureRequestTest.java index 54509857ba3a..5f3a18a30f6f 100644 --- a/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/PictureCaptureRequestTest.java +++ b/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/PictureCaptureRequestTest.java @@ -9,13 +9,16 @@ import static org.junit.Assert.assertTrue; import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.mockStatic; import static org.mockito.Mockito.never; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import io.flutter.plugin.common.MethodChannel; +import io.flutter.plugins.camera.PictureCaptureRequest.TimeoutHandler; import org.junit.Test; import org.junit.runner.RunWith; +import org.mockito.MockedStatic; import org.robolectric.RobolectricTestRunner; @RunWith(RobolectricTestRunner.class) @@ -23,13 +26,13 @@ public class PictureCaptureRequestTest { @Test public void state_is_idle_by_default() { - PictureCaptureRequest req = new PictureCaptureRequest(null, null, null); + PictureCaptureRequest req = PictureCaptureRequest.create(null, null, null); assertEquals("Default state is idle", req.getState(), PictureCaptureRequestState.STATE_IDLE); } @Test public void setState_sets_state() { - PictureCaptureRequest req = new PictureCaptureRequest(null, null, null); + PictureCaptureRequest req = PictureCaptureRequest.create(null, null, null); req.setState(PictureCaptureRequestState.STATE_WAITING_FOCUS); assertEquals( "State is focusing", req.getState(), PictureCaptureRequestState.STATE_WAITING_FOCUS); @@ -51,7 +54,7 @@ public void setState_sets_state() { @Test public void setState_sends_camera_error_event_When_already_finished() { DartMessenger mockMessenger = mock(DartMessenger.class); - PictureCaptureRequest req = new PictureCaptureRequest(null, null, mockMessenger); + PictureCaptureRequest req = PictureCaptureRequest.create(null, null, mockMessenger); // Make sure state is set to finished req.setState(PictureCaptureRequestState.STATE_FINISHED); @@ -65,35 +68,46 @@ public void setState_sends_camera_error_event_When_already_finished() { @Test public void setState_resets_timeout() { - PictureCaptureRequest.TimeoutHandler mockTimeoutHandler = - mock(PictureCaptureRequest.TimeoutHandler.class); - PictureCaptureRequest req = new PictureCaptureRequest(null, null, null, mockTimeoutHandler); - req.setState(PictureCaptureRequestState.STATE_WAITING_FOCUS); - req.setState(PictureCaptureRequestState.STATE_WAITING_PRECAPTURE_START); - req.setState(PictureCaptureRequestState.STATE_WAITING_PRECAPTURE_DONE); - req.setState(PictureCaptureRequestState.STATE_CAPTURING); - verify(mockTimeoutHandler, times(4)).resetTimeout(any()); - verify(mockTimeoutHandler, never()).clearTimeout(any()); + try(MockedStatic mockTimeoutFactory = mockStatic(TimeoutHandler.class)) { + PictureCaptureRequest.TimeoutHandler mockTimeoutHandler = + mock(PictureCaptureRequest.TimeoutHandler.class); + + mockTimeoutFactory.when(TimeoutHandler::create).thenReturn(mockTimeoutHandler); + + PictureCaptureRequest req = PictureCaptureRequest + .create(null, null, null); + req.setState(PictureCaptureRequestState.STATE_WAITING_FOCUS); + req.setState(PictureCaptureRequestState.STATE_WAITING_PRECAPTURE_START); + req.setState(PictureCaptureRequestState.STATE_WAITING_PRECAPTURE_DONE); + req.setState(PictureCaptureRequestState.STATE_CAPTURING); + verify(mockTimeoutHandler, times(4)).resetTimeout(any()); + verify(mockTimeoutHandler, never()).clearTimeout(any()); + } } @Test public void setState_clears_timeout() { - PictureCaptureRequest.TimeoutHandler mockTimeoutHandler = - mock(PictureCaptureRequest.TimeoutHandler.class); - PictureCaptureRequest req = new PictureCaptureRequest(null, null, null, mockTimeoutHandler); - req.setState(PictureCaptureRequestState.STATE_IDLE); - req.setState(PictureCaptureRequestState.STATE_FINISHED); - req = new PictureCaptureRequest(null, null, null, mockTimeoutHandler); - req.setState(PictureCaptureRequestState.STATE_ERROR); - verify(mockTimeoutHandler, never()).resetTimeout(any()); - verify(mockTimeoutHandler, times(3)).clearTimeout(any()); + try(MockedStatic mockTimeoutFactory = mockStatic(TimeoutHandler.class)) { + PictureCaptureRequest.TimeoutHandler mockTimeoutHandler = + mock(PictureCaptureRequest.TimeoutHandler.class); + + mockTimeoutFactory.when(TimeoutHandler::create).thenReturn(mockTimeoutHandler); + + PictureCaptureRequest req = PictureCaptureRequest.create(null, null, null); + req.setState(PictureCaptureRequestState.STATE_IDLE); + req.setState(PictureCaptureRequestState.STATE_FINISHED); + req = PictureCaptureRequest.create(null, null, null); + req.setState(PictureCaptureRequestState.STATE_ERROR); + verify(mockTimeoutHandler, never()).resetTimeout(any()); + verify(mockTimeoutHandler, times(3)).clearTimeout(any()); + } } @Test public void finish_sets_result_and_state() { // Setup MethodChannel.Result mockResult = mock(MethodChannel.Result.class); - PictureCaptureRequest req = new PictureCaptureRequest(mockResult, null, null); + PictureCaptureRequest req = PictureCaptureRequest.create(mockResult, null, null); // Act req.finish("/test/path"); // Test @@ -103,20 +117,25 @@ public void finish_sets_result_and_state() { @Test public void finish_clears_timeout() { - PictureCaptureRequest.TimeoutHandler mockTimeoutHandler = - mock(PictureCaptureRequest.TimeoutHandler.class); - MethodChannel.Result mockResult = mock(MethodChannel.Result.class); - PictureCaptureRequest req = - new PictureCaptureRequest(mockResult, null, null, mockTimeoutHandler); - req.finish("/test/path"); - verify(mockTimeoutHandler, never()).resetTimeout(any()); - verify(mockTimeoutHandler).clearTimeout(any()); + try(MockedStatic mockTimeoutFactory = mockStatic(TimeoutHandler.class)) { + PictureCaptureRequest.TimeoutHandler mockTimeoutHandler = + mock(PictureCaptureRequest.TimeoutHandler.class); + + mockTimeoutFactory.when(TimeoutHandler::create).thenReturn(mockTimeoutHandler); + + MethodChannel.Result mockResult = mock(MethodChannel.Result.class); + PictureCaptureRequest req = + PictureCaptureRequest.create(mockResult, null, null); + req.finish("/test/path"); + verify(mockTimeoutHandler, never()).resetTimeout(any()); + verify(mockTimeoutHandler).clearTimeout(any()); + } } @Test public void isFinished_is_true_When_state_is_finished_or_error() { // Setup - PictureCaptureRequest req = new PictureCaptureRequest(null, null, null); + PictureCaptureRequest req = PictureCaptureRequest.create(null, null, null); // Test false states req.setState(PictureCaptureRequestState.STATE_IDLE); assertFalse(req.isFinished()); @@ -127,7 +146,7 @@ public void isFinished_is_true_When_state_is_finished_or_error() { // Test true states req.setState(PictureCaptureRequestState.STATE_FINISHED); assertTrue(req.isFinished()); - req = new PictureCaptureRequest(null, null, null); // Refresh + req = PictureCaptureRequest.create(null, null, null); // Refresh req.setState(PictureCaptureRequestState.STATE_ERROR); assertTrue(req.isFinished()); } @@ -135,7 +154,7 @@ public void isFinished_is_true_When_state_is_finished_or_error() { @Test public void finish_returns_When_in_error_state() { MethodChannel.Result mockResult = mock(MethodChannel.Result.class); - PictureCaptureRequest req = new PictureCaptureRequest(mockResult, null, null); + PictureCaptureRequest req = PictureCaptureRequest.create(mockResult, null, null); // Make sure state is set to error req.setState(PictureCaptureRequestState.STATE_ERROR); @@ -149,7 +168,7 @@ public void finish_returns_When_in_error_state() { @Test(expected = IllegalStateException.class) public void finish_throws_When_already_finished() { // Setup - PictureCaptureRequest req = new PictureCaptureRequest(null, null, null); + PictureCaptureRequest req = PictureCaptureRequest.create(null, null, null); req.setState(PictureCaptureRequestState.STATE_FINISHED); // Act req.finish("/test/path"); @@ -159,7 +178,7 @@ public void finish_throws_When_already_finished() { public void error_sets_result_and_state() { // Setup MethodChannel.Result mockResult = mock(MethodChannel.Result.class); - PictureCaptureRequest req = new PictureCaptureRequest(mockResult, null, null); + PictureCaptureRequest req = PictureCaptureRequest.create(mockResult, null, null); // Act req.error("ERROR_CODE", "Error Message", null); // Test @@ -169,20 +188,25 @@ public void error_sets_result_and_state() { @Test public void error_clears_timeout() { - PictureCaptureRequest.TimeoutHandler mockTimeoutHandler = - mock(PictureCaptureRequest.TimeoutHandler.class); - MethodChannel.Result mockResult = mock(MethodChannel.Result.class); - PictureCaptureRequest req = - new PictureCaptureRequest(mockResult, null, null, mockTimeoutHandler); - req.error("ERROR_CODE", "Error Message", null); - verify(mockTimeoutHandler, never()).resetTimeout(any()); - verify(mockTimeoutHandler).clearTimeout(any()); + try(MockedStatic mockTimeoutFactory = mockStatic(TimeoutHandler.class)) { + PictureCaptureRequest.TimeoutHandler mockTimeoutHandler = + mock(PictureCaptureRequest.TimeoutHandler.class); + + mockTimeoutFactory.when(TimeoutHandler::create).thenReturn(mockTimeoutHandler); + + MethodChannel.Result mockResult = mock(MethodChannel.Result.class); + PictureCaptureRequest req = PictureCaptureRequest + .create(mockResult, null, null); + req.error("ERROR_CODE", "Error Message", null); + verify(mockTimeoutHandler, never()).resetTimeout(any()); + verify(mockTimeoutHandler).clearTimeout(any()); + } } @Test public void error_returns_When_in_error_state() { MethodChannel.Result mockResult = mock(MethodChannel.Result.class); - PictureCaptureRequest req = new PictureCaptureRequest(mockResult, null, null); + PictureCaptureRequest req = PictureCaptureRequest.create(mockResult, null, null); // Make sure state is set to error req.setState(PictureCaptureRequestState.STATE_ERROR); @@ -196,7 +220,7 @@ public void error_returns_When_in_error_state() { @Test(expected = IllegalStateException.class) public void error_throws_When_already_finished() { // Setup - PictureCaptureRequest req = new PictureCaptureRequest(null, null, null); + PictureCaptureRequest req = PictureCaptureRequest.create(null, null, null); req.setState(PictureCaptureRequestState.STATE_FINISHED); // Act req.error(null, null, null); diff --git a/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/media/MediaRecorderBuilderTest.java b/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/media/MediaRecorderBuilderTest.java index 823975803994..2f018cbd4215 100644 --- a/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/media/MediaRecorderBuilderTest.java +++ b/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/media/MediaRecorderBuilderTest.java @@ -9,10 +9,12 @@ import android.media.CamcorderProfile; import android.media.MediaRecorder; +import io.flutter.plugins.camera.media.MediaRecorderBuilder.MediaRecorderFactory; import java.io.IOException; import java.lang.reflect.Constructor; import org.junit.Test; import org.mockito.InOrder; +import org.mockito.MockedStatic; public class MediaRecorderBuilderTest { @Test @@ -26,50 +28,57 @@ public void ctor_test() { @Test public void build_Should_set_values_in_correct_order_When_audio_is_disabled() throws IOException { CamcorderProfile recorderProfile = getEmptyCamcorderProfile(); - MediaRecorderBuilder.MediaRecorderFactory mockFactory = - mock(MediaRecorderBuilder.MediaRecorderFactory.class); - MediaRecorder mockMediaRecorder = mock(MediaRecorder.class); String outputFilePath = "mock_video_file_path"; int mediaOrientation = 1; - MediaRecorderBuilder builder = - new MediaRecorderBuilder(recorderProfile, outputFilePath, mockFactory) - .setEnableAudio(false) - .setMediaOrientation(mediaOrientation); - - when(mockFactory.makeMediaRecorder()).thenReturn(mockMediaRecorder); - - MediaRecorder recorder = builder.build(); + MediaRecorder recorder = null; + + try (MockedStatic mockMediaRecorderFactory = mockStatic(MediaRecorderFactory.class)) { + MediaRecorder mockMediaRecorder = mock(MediaRecorder.class); + mockMediaRecorderFactory.when(MediaRecorderFactory::create) + .thenReturn(mockMediaRecorder); + + MediaRecorderBuilder builder = + new MediaRecorderBuilder(recorderProfile, outputFilePath) + .setEnableAudio(false) + .setMediaOrientation(mediaOrientation); + recorder = builder.build(); + } - InOrder inOrder = inOrder(recorder); - inOrder.verify(recorder).setVideoSource(MediaRecorder.VideoSource.SURFACE); - inOrder.verify(recorder).setOutputFormat(recorderProfile.fileFormat); - inOrder.verify(recorder).setVideoEncoder(recorderProfile.videoCodec); - inOrder.verify(recorder).setVideoEncodingBitRate(recorderProfile.videoBitRate); - inOrder.verify(recorder).setVideoFrameRate(recorderProfile.videoFrameRate); - inOrder - .verify(recorder) - .setVideoSize(recorderProfile.videoFrameWidth, recorderProfile.videoFrameHeight); - inOrder.verify(recorder).setOutputFile(outputFilePath); - inOrder.verify(recorder).setOrientationHint(mediaOrientation); - inOrder.verify(recorder).prepare(); + InOrder inOrder = inOrder(recorder); + inOrder.verify(recorder).setVideoSource(MediaRecorder.VideoSource.SURFACE); + inOrder.verify(recorder).setOutputFormat(recorderProfile.fileFormat); + inOrder.verify(recorder).setVideoEncoder(recorderProfile.videoCodec); + inOrder.verify(recorder).setVideoEncodingBitRate(recorderProfile.videoBitRate); + inOrder.verify(recorder).setVideoFrameRate(recorderProfile.videoFrameRate); + inOrder + .verify(recorder) + .setVideoSize(recorderProfile.videoFrameWidth, recorderProfile.videoFrameHeight); + inOrder.verify(recorder).setOutputFile(outputFilePath); + inOrder.verify(recorder).setOrientationHint(mediaOrientation); + inOrder.verify(recorder).prepare(); } @Test public void build_Should_set_values_in_correct_order_When_audio_is_enabled() throws IOException { + MediaRecorder recorder = null; CamcorderProfile recorderProfile = getEmptyCamcorderProfile(); MediaRecorderBuilder.MediaRecorderFactory mockFactory = mock(MediaRecorderBuilder.MediaRecorderFactory.class); - MediaRecorder mockMediaRecorder = mock(MediaRecorder.class); String outputFilePath = "mock_video_file_path"; int mediaOrientation = 1; - MediaRecorderBuilder builder = - new MediaRecorderBuilder(recorderProfile, outputFilePath, mockFactory) - .setEnableAudio(true) - .setMediaOrientation(mediaOrientation); - when(mockFactory.makeMediaRecorder()).thenReturn(mockMediaRecorder); + try (MockedStatic mockMediaRecorderFactory = mockStatic(MediaRecorderFactory.class)) { + MediaRecorder mockMediaRecorder = mock(MediaRecorder.class); + mockMediaRecorderFactory.when(MediaRecorderFactory::create) + .thenReturn(mockMediaRecorder); - MediaRecorder recorder = builder.build(); + MediaRecorderBuilder builder = + new MediaRecorderBuilder(recorderProfile, outputFilePath) + .setEnableAudio(true) + .setMediaOrientation(mediaOrientation); + + recorder = builder.build(); + } InOrder inOrder = inOrder(recorder); inOrder.verify(recorder).setAudioSource(MediaRecorder.AudioSource.MIC); From 902e605f8cc8947c1fbbbbfb894068651cd59072 Mon Sep 17 00:00:00 2001 From: Andrew Coutts Date: Sat, 6 Mar 2021 08:12:11 -0500 Subject: [PATCH 066/114] Abstracted camera settings into new feature class --- .../io/flutter/plugins/camera/Camera.java | 292 ++++++------------ .../plugins/camera/CameraProperties.java | 11 + .../plugins/camera/features/AutoFocus.java | 85 +++++ .../camera/features/CameraFeature.java | 33 ++ .../camera/features/CameraFeatures.java | 34 ++ .../plugins/camera/features/ExposureLock.java | 67 ++++ .../camera/features/ExposureOffset.java | 46 +++ .../plugins/camera/features/Flash.java | 74 +++++ .../plugins/camera/features/FpsRange.java | 63 ++++ .../camera/features/NoiseReduction.java | 64 ++++ .../camera/types/NoiseReductionMode.java | 26 ++ 11 files changed, 600 insertions(+), 195 deletions(-) create mode 100644 packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/AutoFocus.java create mode 100644 packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/CameraFeature.java create mode 100644 packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/CameraFeatures.java create mode 100644 packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/ExposureLock.java create mode 100644 packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/ExposureOffset.java create mode 100644 packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/Flash.java create mode 100644 packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/FpsRange.java create mode 100644 packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/NoiseReduction.java create mode 100644 packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/types/NoiseReductionMode.java 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 7b47c844d856..c73e5719980c 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 @@ -4,8 +4,6 @@ package io.flutter.plugins.camera; -import static io.flutter.plugins.camera.CameraUtils.computeBestPreviewSize; - import android.annotation.SuppressLint; import android.annotation.TargetApi; import android.app.Activity; @@ -15,7 +13,6 @@ import android.graphics.SurfaceTexture; import android.hardware.camera2.CameraAccessException; import android.hardware.camera2.CameraCaptureSession; -import android.hardware.camera2.CameraCharacteristics; import android.hardware.camera2.CameraDevice; import android.hardware.camera2.CameraManager; import android.hardware.camera2.CameraMetadata; @@ -41,17 +38,10 @@ import android.util.Size; import android.util.SparseIntArray; import android.view.Surface; + import androidx.annotation.NonNull; import androidx.annotation.Nullable; -import io.flutter.embedding.engine.systemchannels.PlatformChannel; -import io.flutter.plugin.common.EventChannel; -import io.flutter.plugin.common.MethodChannel.Result; -import io.flutter.plugins.camera.media.MediaRecorderBuilder; -import io.flutter.plugins.camera.types.ExposureMode; -import io.flutter.plugins.camera.types.FlashMode; -import io.flutter.plugins.camera.types.FocusMode; -import io.flutter.plugins.camera.types.ResolutionPreset; -import io.flutter.view.TextureRegistry.SurfaceTextureEntry; + import java.io.File; import java.io.IOException; import java.nio.ByteBuffer; @@ -63,11 +53,46 @@ import java.util.Map; import java.util.concurrent.Executors; +import io.flutter.embedding.engine.systemchannels.PlatformChannel; +import io.flutter.plugin.common.EventChannel; +import io.flutter.plugin.common.MethodChannel.Result; +import io.flutter.plugins.camera.features.AutoFocus; +import io.flutter.plugins.camera.features.CameraFeature; +import io.flutter.plugins.camera.features.CameraFeatures; +import io.flutter.plugins.camera.features.ExposureLock; +import io.flutter.plugins.camera.features.ExposureOffset; +import io.flutter.plugins.camera.features.Flash; +import io.flutter.plugins.camera.features.FpsRange; +import io.flutter.plugins.camera.features.NoiseReduction; +import io.flutter.plugins.camera.media.MediaRecorderBuilder; +import io.flutter.plugins.camera.types.ExposureMode; +import io.flutter.plugins.camera.types.FlashMode; +import io.flutter.plugins.camera.types.FocusMode; +import io.flutter.plugins.camera.types.ResolutionPreset; +import io.flutter.view.TextureRegistry.SurfaceTextureEntry; + +import static io.flutter.plugins.camera.CameraUtils.computeBestPreviewSize; + @FunctionalInterface interface ErrorCallback { void onError(String errorCode, String errorMessage); } +/** + * Note: at this time we do not implement zero shutter lag (ZSL) capture. This is a potentail + * improvement we can use in the future. The idea is in a TEMPLATE_ZERO_SHUTTER_LAG capture + * session, the system maintains a ring buffer of images from the preview. It must be in full + * auto moved (flash, ae, focus, etc). When you capture an image, it simply picks one out of + * the ring buffer, thus capturing an image with zero shutter lag. + * + * This is a potential improvement for the future. A good example is the AOSP camera here: + * https://android.googlesource.com/platform/packages/apps/Camera2/+/9c94ab3/src/com/android/camera/one/v2/OneCameraZslImpl.java + * + * But one note- they mention sometimes ZSL captures can be very low quality so it might not + * be preferred on some devices. If we do add support for this in the future, we should allow + * it to be enabled from dart. + */ + class Camera implements CameraCaptureCallback.CameraCaptureStateListener { private static final String TAG = "Camera"; @@ -90,6 +115,12 @@ class Camera implements CameraCaptureCallback.CameraCaptureStateListener { supportedImageFormats.put("jpeg", ImageFormat.JPEG); } + /** + * Holds all of the camera features/settings and will be used to + * update the request builder when one changes. + */ + private final Map cameraFeatures; + private final SurfaceTextureEntry flutterTexture; private final DeviceOrientationManager deviceOrientationListener; private final int sensorOrientation; @@ -118,7 +149,7 @@ class Camera implements CameraCaptureCallback.CameraCaptureStateListener { new ImageReader.OnImageAvailableListener() { @Override public void onImageAvailable(ImageReader reader) { - // Log.i(TAG, "onImageAvailable"); + Log.i(TAG, "onImageAvailable"); // Use acquireNextImage since our image reader is only for 1 image. mBackgroundHandler.post( @@ -142,11 +173,7 @@ public void onImageAvailable(ImageReader reader) { private boolean recordingVideo; private File videoRecordingFile; - /** - * Flash mode setting of the current camera. Initialize to off because we don't know if the - * current camera supports flash yet. - */ - private FlashMode currentFlashMode; + /** * Exposure mode setting of the current camera. Initialize to auto because all cameras support * autoexposure by default. @@ -181,7 +208,6 @@ public Camera( if (activity == null) { throw new IllegalStateException("No activity available!"); } - this.activity = activity; this.enableAudio = enableAudio; this.flutterTexture = flutterTexture; @@ -193,12 +219,19 @@ public Camera( this.currentFocusMode = FocusMode.auto; this.exposureOffset = 0; - mCaptureCallback = CameraCaptureCallback.create(this); + // Setup camera features + this.cameraFeatures = new HashMap() {{ + put(CameraFeatures.autoFocus, new AutoFocus()); + put(CameraFeatures.flash, new Flash()); + put(CameraFeatures.noiseReduction, new NoiseReduction()); + put(CameraFeatures.fpsRange, new FpsRange(cameraProperties)); + put(CameraFeatures.exposureOffset, new ExposureOffset()); + + // TODO: cameraRegions is going to be null here + put(CameraFeatures.exposureLock, new ExposureLock(cameraRegions)); + }}; - // Get camera characteristics and check for supported features - getAvailableFpsRange(cameraProperties); - mAutoFocusSupported = checkAutoFocusSupported(cameraProperties); - checkFlashSupported(); + mCaptureCallback = CameraCaptureCallback.create(this); // Setup orientation sensorOrientation = cameraProperties.getSensorOrientation(); @@ -215,7 +248,7 @@ public Camera( CameraUtils.getBestAvailableCamcorderProfileForResolutionPreset( cameraName, resolutionPreset); captureSize = new Size(recordingProfile.videoFrameWidth, recordingProfile.videoFrameHeight); - // Log.i(TAG, "captureSize: " + captureSize); + Log.i(TAG, "captureSize: " + captureSize); previewSize = computeBestPreviewSize(cameraName, resolutionPreset); @@ -250,75 +283,17 @@ public CameraState getState() { } /** - * Check if the auto focus is supported by the current camera. We look at the available AF modes - * and the available lens focusing distance to determine if its' a fixed length lens or not as - * well. - */ - public static boolean checkAutoFocusSupported(CameraProperties cameraProperties) { - int[] modes = cameraProperties.getControlAutoFocusAvailableModes(); - // Log.i(TAG, "checkAutoFocusSupported | modes:"); - for (int mode : modes) { - // Log.i(TAG, "checkAutoFocusSupported | ==> " + mode); - } - - // Check if fixed focal length lens. If LENS_INFO_MINIMUM_FOCUS_DISTANCE=0, then this is fixed. - // Can be null on some devices. - final Float minFocus = cameraProperties.getLensInfoMinimumFocusDistance(); - // final Float maxFocus = cameraCharacteristics.get(CameraCharacteristics.LENS_INFO_HYPERFOCAL_DISTANCE); - - // Value can be null on some devices: - // https://developer.android.com/reference/android/hardware/camera2/CameraCharacteristics#LENS_INFO_MINIMUM_FOCUS_DISTANCE - boolean isFixedLength; - if (minFocus == null) { - isFixedLength = true; - } else { - isFixedLength = minFocus == 0; - } - // Log.i(TAG, "checkAutoFocusSupported | minFocus " + minFocus + " | maxFocus: " + maxFocus); - - return !isFixedLength - && !(modes == null - || modes.length == 0 - || (modes.length == 1 && modes[0] == CameraCharacteristics.CONTROL_AF_MODE_OFF)); - // Log.i(TAG, "checkAutoFocusSupported: " + mAutoFocusSupported); - } - - /** Check if the flash is supported. */ - private void checkFlashSupported() { - Boolean available = cameraProperties.getFlashInfoAvailable(); - mFlashSupported = available != null && available; - } - - /** - * Load available FPS range for the current camera and update the available fps range with it. - * - * @param cameraProperties + * Update the builder settings with all of our available features. + * @param requestBuilder */ - private void getAvailableFpsRange(CameraProperties cameraProperties) { - // Log.i(TAG, "getAvailableFpsRange"); - - try { - Range[] ranges = cameraProperties.getControlAutoExposureAvailableTargetFpsRanges(); - - if (ranges != null) { - for (Range range : ranges) { - int upper = range.getUpper(); - // Log.i("Camera", "[FPS Range Available] is:" + range); - if (upper >= 10) { - if (fpsRange == null || upper > fpsRange.getUpper()) { - fpsRange = range; - } - } - } - } - } catch (Exception e) { - pictureCaptureRequest.error("cameraAccess", e.getMessage(), null); + private void updateBuilderSettings(CaptureRequest.Builder requestBuilder) { + for (Map.Entry feature : cameraFeatures.entrySet()) { + feature.getValue().updateBuilder(requestBuilder); } - // Log.i("Camera", "[FPS Range] is:" + fpsRange); } private void prepareMediaRecorder(String outputFilePath) throws IOException { - // Log.i(TAG, "prepareMediaRecorder"); + Log.i(TAG, "prepareMediaRecorder"); if (mediaRecorder != null) { mediaRecorder.release(); @@ -357,7 +332,7 @@ public void open(String imageFormatGroup) throws CameraAccessException { new CameraDevice.StateCallback() { @Override public void onOpened(@NonNull CameraDevice device) { - // Log.i(TAG, "open | onOpened"); + Log.i(TAG, "open | onOpened"); cameraDevice = device; try { @@ -377,7 +352,7 @@ public void onOpened(@NonNull CameraDevice device) { @Override public void onClosed(@NonNull CameraDevice camera) { - // Log.i(TAG, "open | onClosed"); + Log.i(TAG, "open | onClosed"); dartMessenger.sendCameraClosingEvent(); super.onClosed(camera); @@ -385,7 +360,7 @@ public void onClosed(@NonNull CameraDevice camera) { @Override public void onDisconnected(@NonNull CameraDevice cameraDevice) { - // Log.i(TAG, "open | onDisconnected"); + Log.i(TAG, "open | onDisconnected"); close(); dartMessenger.sendCameraErrorEvent("The camera was disconnected."); @@ -393,7 +368,7 @@ public void onDisconnected(@NonNull CameraDevice cameraDevice) { @Override public void onError(@NonNull CameraDevice cameraDevice, int errorCode) { - // Log.i(TAG, "open | onError"); + Log.i(TAG, "open | onError"); close(); String errorDescription; @@ -430,7 +405,7 @@ private void createCaptureSession(int templateType, Surface... surfaces) private void createCaptureSession( int templateType, Runnable onSuccessCallback, Surface... surfaces) throws CameraAccessException { - // Log.i(TAG, "createCaptureSession"); + Log.i(TAG, "createCaptureSession"); // Close any existing capture session. closeCaptureSession(); @@ -466,10 +441,7 @@ public void onConfigured(@NonNull CameraCaptureSession session) { } captureSession = session; - updateFpsRange(); - updateFocusMode(mPreviewRequestBuilder); - updateFlash(mPreviewRequestBuilder); - updateExposureMode(mPreviewRequestBuilder); + updateBuilderSettings(mPreviewRequestBuilder); refreshPreviewCaptureSession( onSuccessCallback, (code, message) -> dartMessenger.sendCameraErrorEvent(message)); @@ -522,9 +494,9 @@ private void createCaptureSession( // Send a repeating request to refresh our capture session. private void refreshPreviewCaptureSession( @Nullable Runnable onSuccessCallback, @NonNull ErrorCallback onErrorCallback) { - // Log.i(TAG, "refreshPreviewCaptureSession"); + Log.i(TAG, "refreshPreviewCaptureSession"); if (captureSession == null) { - // Log.i(TAG, "[refreshPreviewCaptureSession] mPreviewSession null, returning"); + Log.i(TAG, "[refreshPreviewCaptureSession] mPreviewSession null, returning"); return; } @@ -542,7 +514,7 @@ private void refreshPreviewCaptureSession( } public void takePicture(@NonNull final Result result) { - // Log.i(TAG, "takePicture | useAutoFocus: " + useAutoFocus); + Log.i(TAG, "takePicture | useAutoFocus: " + useAutoFocus); // Only take one 1 picture at a time. if (pictureCaptureRequest != null && !pictureCaptureRequest.isFinished()) { @@ -578,7 +550,7 @@ public void takePicture(@NonNull final Result result) { * get a response in {@link #mCaptureCallback} from lockFocus(). */ private void runPrecaptureSequence() { - // Log.i(TAG, "runPrecaptureSequence"); + Log.i(TAG, "runPrecaptureSequence"); try { // First set precapture state to idle or else it can hang in STATE_WAITING_PRECAPTURE mPreviewRequestBuilder.set( @@ -610,7 +582,7 @@ private void runPrecaptureSequence() { * #mCaptureCallback} from both lockFocus(). */ private void takePictureAfterPrecapture() { - // Log.i(TAG, "captureStillPicture"); + Log.i(TAG, "captureStillPicture"); cameraState = CameraState.STATE_CAPTURING; pictureCaptureRequest.setState(PictureCaptureRequestState.STATE_CAPTURING); @@ -628,10 +600,8 @@ private void takePictureAfterPrecapture() { CaptureRequest.SCALER_CROP_REGION, mPreviewRequestBuilder.get(CaptureRequest.SCALER_CROP_REGION)); - // Set focus / flash from preview mode - updateFlash(stillBuilder); - updateFocusMode(stillBuilder); - updateExposureMode(stillBuilder); + // Update builder settings + updateBuilderSettings(stillBuilder); // Orientation int rotation = activity.getWindowManager().getDefaultDisplay().getRotation(); @@ -646,7 +616,7 @@ public void onCaptureStarted( @NonNull CaptureRequest request, long timestamp, long frameNumber) { - // Log.i(TAG, "onCaptureStarted"); + Log.i(TAG, "onCaptureStarted"); } @Override @@ -654,7 +624,7 @@ public void onCaptureProgressed( @NonNull CameraCaptureSession session, @NonNull CaptureRequest request, @NonNull CaptureResult partialResult) { - // Log.i(TAG, "onCaptureProgressed"); + Log.i(TAG, "onCaptureProgressed"); } @Override @@ -662,14 +632,14 @@ public void onCaptureCompleted( @NonNull CameraCaptureSession session, @NonNull CaptureRequest request, @NonNull TotalCaptureResult result) { - // Log.i(TAG, "onCaptureCompleted"); + Log.i(TAG, "onCaptureCompleted"); unlockAutoFocus(); } }; captureSession.stopRepeating(); captureSession.abortCaptures(); - // Log.i(TAG, "sending capture request"); + Log.i(TAG, "sending capture request"); captureSession.capture(stillBuilder.build(), captureCallback, mBackgroundHandler); } catch (CameraAccessException e) { pictureCaptureRequest.error("cameraAccess", e.getMessage(), null); @@ -698,74 +668,6 @@ private void stopBackgroundThread() { } } - /** - * Sync the requestBuilder exposure mode setting ot the current exposure mode setting of the - * camera. - */ - void updateExposureMode(CaptureRequest.Builder requestBuilder) { - // Log.i(TAG, "updateExposureMode"); - - // Applying auto exposure - MeteringRectangle aeRect = cameraRegions.getAEMeteringRectangle(); - requestBuilder.set( - CaptureRequest.CONTROL_AE_REGIONS, - aeRect == null ? null : new MeteringRectangle[] {cameraRegions.getAEMeteringRectangle()}); - - switch (currentExposureMode) { - case locked: - requestBuilder.set(CaptureRequest.CONTROL_AE_LOCK, true); - break; - case auto: - default: - requestBuilder.set(CaptureRequest.CONTROL_AE_LOCK, false); - break; - } - - // TODO: move this to its own setting (exposure offset) - requestBuilder.set(CaptureRequest.CONTROL_AE_EXPOSURE_COMPENSATION, exposureOffset); - } - - /** Sync the requestBuilder flash setting to the current flash mode setting of the camera. */ - void updateFlash(CaptureRequest.Builder requestBuilder) { - // Log.i(TAG, "updateFlash"); - - if (!mFlashSupported) { - return; - } - - switch (currentFlashMode) { - case off: - requestBuilder.set(CaptureRequest.CONTROL_AE_MODE, CaptureRequest.CONTROL_AE_MODE_ON); - requestBuilder.set(CaptureRequest.FLASH_MODE, CaptureRequest.FLASH_MODE_OFF); - break; - - case always: - requestBuilder.set( - CaptureRequest.CONTROL_AE_MODE, CaptureRequest.CONTROL_AE_MODE_ON_ALWAYS_FLASH); - requestBuilder.set(CaptureRequest.FLASH_MODE, CaptureRequest.FLASH_MODE_OFF); - break; - - case torch: - requestBuilder.set(CaptureRequest.CONTROL_AE_MODE, CaptureRequest.CONTROL_AE_MODE_ON); - requestBuilder.set(CaptureRequest.FLASH_MODE, CaptureRequest.FLASH_MODE_TORCH); - break; - - case auto: - requestBuilder.set( - CaptureRequest.CONTROL_AE_MODE, CaptureRequest.CONTROL_AE_MODE_ON_AUTO_FLASH); - requestBuilder.set(CaptureRequest.FLASH_MODE, CaptureRequest.FLASH_MODE_OFF); - break; - - // TODO: to be implemented someday. Need to add it to dart/iOS as another flash mode setting. - // case autoRedEye: - // requestBuilder.set(CaptureRequest.CONTROL_AE_MODE, - // CaptureRequest.CONTROL_AE_MODE_ON_AUTO_FLASH_REDEYE); - // requestBuilder.set(CaptureRequest.FLASH_MODE, - // CaptureRequest.FLASH_MODE_OFF); - // break; - } - } - /** * Retrieves the JPEG orientation from the specified screen rotation. * @@ -782,7 +684,7 @@ private int getOrientation(int rotation) { /** Start capturing a picture, doing autofocus first. */ private void runPictureAutoFocus() { - // Log.i(TAG, "runPictureAutoFocus"); + Log.i(TAG, "runPictureAutoFocus"); assert (pictureCaptureRequest != null); cameraState = CameraState.STATE_WAITING_FOCUS; @@ -792,7 +694,7 @@ private void runPictureAutoFocus() { /** Start the autofocus routine on the current capture request. */ private void lockAutoFocus() { - // Log.i(TAG, "lockAutoFocus"); + Log.i(TAG, "lockAutoFocus"); pictureCaptureRequest.setState(PictureCaptureRequestState.STATE_WAITING_PRECAPTURE_START); mPreviewRequestBuilder.set( @@ -804,7 +706,7 @@ private void lockAutoFocus() { /** Cancel and reset auto focus state and refresh the preview session. */ private void unlockAutoFocus() { - // Log.i(TAG, "unlockAutoFocus"); + Log.i(TAG, "unlockAutoFocus"); try { // Cancel existing AF state mPreviewRequestBuilder.set( @@ -822,7 +724,7 @@ private void unlockAutoFocus() { captureSession.capture(mPreviewRequestBuilder.build(), null, mBackgroundHandler); } catch (CameraAccessException e) { - // Log.i(TAG, "Error unlocking focus: " + e.getMessage()); + Log.i(TAG, "Error unlocking focus: " + e.getMessage()); dartMessenger.sendCameraErrorEvent(e.getMessage()); return; } @@ -932,8 +834,8 @@ public void resumeVideoRecording(@NonNull final Result result) { */ public void setFlashMode(@NonNull final Result result, FlashMode newMode) { // Save the new flash mode setting - currentFlashMode = newMode; - updateFlash(mPreviewRequestBuilder); + cameraFeatures.get(CameraFeatures.flash).setValue(newMode); + cameraFeatures.get(CameraFeatures.flash).updateBuilder(mPreviewRequestBuilder); refreshPreviewCaptureSession( () -> result.success(null), @@ -991,7 +893,7 @@ public void setExposurePoint(@NonNull final Result result, Double x, Double y) */ public void setFocusMode(@NonNull final Result result, FocusMode newMode) throws CameraAccessException { - // Log.i(TAG, "setFocusMode: " + newMode); + Log.i(TAG, "setFocusMode: " + newMode); // Set new focus mode currentFocusMode = newMode; @@ -1003,7 +905,7 @@ public void setFocusMode(@NonNull final Result result, FocusMode newMode) // or we want to trigger a one-time focus and then set AF to idle (locked mode). switch (newMode) { case auto: - // Log.i(TAG, "Triggering AF start with mode " + currentFocusMode); + Log.i(TAG, "Triggering AF start with mode " + currentFocusMode); // Reset state of autofocus so it goes back to passive scanning. mPreviewRequestBuilder.set( CaptureRequest.CONTROL_AF_TRIGGER, CaptureRequest.CONTROL_AF_TRIGGER_CANCEL); @@ -1031,7 +933,7 @@ public void onCaptureCompleted( @NonNull CameraCaptureSession session, @NonNull CaptureRequest request, @NonNull TotalCaptureResult _result) { - // Log.i(TAG, "Success after triggering AF start for locked focus"); + Log.i(TAG, "Success after triggering AF start for locked focus"); mPreviewRequestBuilder.set( CaptureRequest.CONTROL_AF_TRIGGER, CaptureRequest.CONTROL_AF_TRIGGER_IDLE); @@ -1200,11 +1102,11 @@ private void updateFpsRange() { /** * Sync the focus mode setting to the provided capture request builder. - * + *updateFps * @param requestBuilder */ private void updateFocusMode(CaptureRequest.Builder requestBuilder) { - // Log.i(TAG, "updateFocusMode currentFocusMode: " + currentFocusMode); + Log.i(TAG, "updateFocusMode currentFocusMode: " + currentFocusMode); if (!mAutoFocusSupported) { useAutoFocus = false; @@ -1243,7 +1145,7 @@ private void updateFocusMode(CaptureRequest.Builder requestBuilder) { public void startPreview() throws CameraAccessException { if (pictureImageReader == null || pictureImageReader.getSurface() == null) return; - // Log.i(TAG, "startPreview"); + Log.i(TAG, "startPreview"); createCaptureSession(CameraDevice.TEMPLATE_PREVIEW, pictureImageReader.getSurface()); } @@ -1251,7 +1153,7 @@ public void startPreview() throws CameraAccessException { public void startPreviewWithImageStream(EventChannel imageStreamChannel) throws CameraAccessException { createCaptureSession(CameraDevice.TEMPLATE_RECORD, imageStreamReader.getSurface()); - // Log.i(TAG, "startPreviewWithImageStream"); + Log.i(TAG, "startPreviewWithImageStream"); imageStreamChannel.setStreamHandler( new EventChannel.StreamHandler() { @@ -1304,7 +1206,7 @@ private void setImageStreamImageAvailableListener(final EventChannel.EventSink i private void closeCaptureSession() { if (captureSession != null) { - // Log.i(TAG, "closeCaptureSession"); + Log.i(TAG, "closeCaptureSession"); captureSession.close(); captureSession = null; @@ -1312,7 +1214,7 @@ private void closeCaptureSession() { } public void close() { - // Log.i(TAG, "close"); + Log.i(TAG, "close"); closeCaptureSession(); if (cameraDevice != null) { @@ -1337,7 +1239,7 @@ public void close() { } public void dispose() { - // Log.i(TAG, "dispose"); + Log.i(TAG, "dispose"); close(); flutterTexture.release(); diff --git a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/CameraProperties.java b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/CameraProperties.java index 1a7510883713..a3734effb504 100644 --- a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/CameraProperties.java +++ b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/CameraProperties.java @@ -42,6 +42,8 @@ public interface CameraProperties { Rect getSensorInfoPreCorrectionActiveArraySize(); int getSensorOrientation(); + + int getHardwareLevel(); } class CameraPropertiesImpl implements CameraProperties { @@ -136,4 +138,13 @@ public Rect getSensorInfoPreCorrectionActiveArraySize() { public int getSensorOrientation() { return cameraCharacteristics.get(CameraCharacteristics.SENSOR_ORIENTATION); } + + /** + * Returns the hardware level of the camera. + * @return + */ + @Override + public int getHardwareLevel() { + return cameraCharacteristics.get(CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL); + } } diff --git a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/AutoFocus.java b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/AutoFocus.java new file mode 100644 index 000000000000..65b57fe261b0 --- /dev/null +++ b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/AutoFocus.java @@ -0,0 +1,85 @@ +package io.flutter.plugins.camera.features; + +import android.hardware.camera2.CameraCharacteristics; +import android.hardware.camera2.CaptureRequest; +import android.util.Log; + +import io.flutter.plugins.camera.CameraProperties; +import io.flutter.plugins.camera.types.FocusMode; + +public class AutoFocus implements CameraFeature { +// private final boolean recordingVideo; + private boolean isSupported; + private FocusMode currentSetting; + +// public AutoFocus(boolean recordingVideo) { +// this.recordingVideo = recordingVideo; +// } + + @Override + public FocusMode getValue() { + return currentSetting; + } + + @Override + public void setValue(FocusMode value) { + this.currentSetting = value; + } + + @Override + public boolean isSupported(CameraProperties cameraProperties, CameraCharacteristics cameraCharacteristics) { + int[] modes = cameraProperties.getControlAutoFocusAvailableModes(); + Log.i("Camera", "checkAutoFocusSupported | modes:"); + for (int mode : modes) { + Log.i("Camera", "checkAutoFocusSupported | ==> " + mode); + } + + // Check if fixed focal length lens. If LENS_INFO_MINIMUM_FOCUS_DISTANCE=0, then this is fixed. + // Can be null on some devices. + final Float minFocus = cameraProperties.getLensInfoMinimumFocusDistance(); + // final Float maxFocus = cameraCharacteristics.get(CameraCharacteristics.LENS_INFO_HYPERFOCAL_DISTANCE); + + // Value can be null on some devices: + // https://developer.android.com/reference/android/hardware/camera2/CameraCharacteristics#LENS_INFO_MINIMUM_FOCUS_DISTANCE + boolean isFixedLength; + if (minFocus == null) { + isFixedLength = true; + } else { + isFixedLength = minFocus == 0; + } + Log.i("Camera", "checkAutoFocusSupported | minFocus " + minFocus); + + final boolean supported = !isFixedLength + && !(modes == null + || modes.length == 0 + || (modes.length == 1 && modes[0] == CameraCharacteristics.CONTROL_AF_MODE_OFF)); + isSupported = supported; + return supported; + } + + @Override + public void updateBuilder(CaptureRequest.Builder requestBuilder) { + Log.i("Camera", "updateFocusMode currentFocusMode: " + currentSetting); + + if (!isSupported) { + return; + } + + switch (currentSetting) { + case locked: + requestBuilder.set(CaptureRequest.CONTROL_AF_MODE, CaptureRequest.CONTROL_AF_MODE_AUTO); + break; + + case auto: + requestBuilder.set( + CaptureRequest.CONTROL_AF_MODE, +// recordingVideo +// ? CaptureRequest.CONTROL_AF_MODE_CONTINUOUS_VIDEO +// : + CaptureRequest.CONTROL_AF_MODE_CONTINUOUS_PICTURE); + default: + break; + } + + } +} diff --git a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/CameraFeature.java b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/CameraFeature.java new file mode 100644 index 000000000000..0a135ff99e9f --- /dev/null +++ b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/CameraFeature.java @@ -0,0 +1,33 @@ +package io.flutter.plugins.camera.features; + +import android.hardware.camera2.CameraCharacteristics; +import android.hardware.camera2.CaptureRequest; + +import io.flutter.plugins.camera.CameraProperties; + +public interface CameraFeature { + /** + * Get the current value of this feature's setting. + * @return + */ + public T getValue(); + + /** + * Set a new value for this feature's setting. + * @param value + */ + public void setValue(T value); + + /** + * Returns whether or not this feature is supported on the + * given camera properties. + * @return + */ + public boolean isSupported(CameraProperties cameraProperties, CameraCharacteristics cameraCharacteristics); + + /** + * Update the setting in a provided request builder. + * @param requestBuilder + */ + public void updateBuilder(CaptureRequest.Builder requestBuilder); +} diff --git a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/CameraFeatures.java b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/CameraFeatures.java new file mode 100644 index 000000000000..9fcd4cde3159 --- /dev/null +++ b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/CameraFeatures.java @@ -0,0 +1,34 @@ +package io.flutter.plugins.camera.features; + +/** + * This is all of our available features in the camera. Used in the features map + * of the camera to safely access feature class instances when we need to change + * their setting values. + */ +public enum CameraFeatures { + autoFocus("autoFocus"), + exposureLock("exposureLock"), + exposureOffset("exposureOffset"), + flash("flash"), + fpsRange("fpsRange"), + noiseReduction("noiseReduction"); + + private final String strValue; + + CameraFeatures(String strValue) { + this.strValue = strValue; + } + + public static CameraFeatures getValueForString(String modeStr) { + for (CameraFeatures value : values()) { + if (value.strValue.equals(modeStr)) return value; + } + return null; + } + + @Override + public String toString() { + return strValue; + } +} + diff --git a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/ExposureLock.java b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/ExposureLock.java new file mode 100644 index 000000000000..a32d41fb5d0f --- /dev/null +++ b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/ExposureLock.java @@ -0,0 +1,67 @@ +package io.flutter.plugins.camera.features; + +import android.hardware.camera2.CameraCharacteristics; +import android.hardware.camera2.CaptureRequest; +import android.hardware.camera2.params.MeteringRectangle; +import android.util.Log; + +import io.flutter.plugins.camera.CameraProperties; +import io.flutter.plugins.camera.CameraRegions; +import io.flutter.plugins.camera.types.ExposureMode; +import io.flutter.plugins.camera.types.FlashMode; + +/** + * Exposure lock controls whether or not exposure mode is currenty locked or + * automatically metering. + */ +public class ExposureLock implements CameraFeature { + private boolean isSupported; + private ExposureMode currentSetting; + private CameraRegions cameraRegions; + + public ExposureLock(CameraRegions cameraRegions) { + this.cameraRegions = cameraRegions; + } + + @Override + public ExposureMode getValue() { + return currentSetting; + } + + @Override + public void setValue(ExposureMode value) { + this.currentSetting = value; + } + + // Available on all devices. + @Override + public boolean isSupported(CameraProperties cameraProperties, CameraCharacteristics cameraCharacteristics) { + return true; + } + + @Override + public void updateBuilder(CaptureRequest.Builder requestBuilder) { + Log.i("Camera", "updateExposureMode"); + + // Don't try to set if the current camera doesn't support it. + if (!isSupported) { + return; + } + + // Applying auto exposure + MeteringRectangle aeRect = cameraRegions.getAEMeteringRectangle(); + requestBuilder.set( + CaptureRequest.CONTROL_AE_REGIONS, + aeRect == null ? null : new MeteringRectangle[] {cameraRegions.getAEMeteringRectangle()}); + + switch (currentSetting) { + case locked: + requestBuilder.set(CaptureRequest.CONTROL_AE_LOCK, true); + break; + case auto: + default: + requestBuilder.set(CaptureRequest.CONTROL_AE_LOCK, false); + break; + } + } +} diff --git a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/ExposureOffset.java b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/ExposureOffset.java new file mode 100644 index 000000000000..da01f9f45286 --- /dev/null +++ b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/ExposureOffset.java @@ -0,0 +1,46 @@ +package io.flutter.plugins.camera.features; + +import android.hardware.camera2.CameraCharacteristics; +import android.hardware.camera2.CaptureRequest; +import android.hardware.camera2.params.MeteringRectangle; +import android.util.Log; + +import io.flutter.plugins.camera.CameraProperties; +import io.flutter.plugins.camera.CameraRegions; +import io.flutter.plugins.camera.types.ExposureMode; + +/** + * Exposure offset makes the image brighter or darker. + */ +public class ExposureOffset implements CameraFeature { + private boolean isSupported; + private Integer currentSetting; + + @Override + public Integer getValue() { + return currentSetting; + } + + @Override + public void setValue(Integer value) { + this.currentSetting = value; + } + + // Available on all devices. + @Override + public boolean isSupported(CameraProperties cameraProperties, CameraCharacteristics cameraCharacteristics) { + return true; + } + + @Override + public void updateBuilder(CaptureRequest.Builder requestBuilder) { + Log.i("Camera", "updateExposureOffset"); + + // Don't try to set if the current camera doesn't support it. + if (!isSupported) { + return; + } + + requestBuilder.set(CaptureRequest.CONTROL_AE_EXPOSURE_COMPENSATION, currentSetting); + } +} diff --git a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/Flash.java b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/Flash.java new file mode 100644 index 000000000000..e594f27a6c40 --- /dev/null +++ b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/Flash.java @@ -0,0 +1,74 @@ +package io.flutter.plugins.camera.features; + +import android.hardware.camera2.CameraCharacteristics; +import android.hardware.camera2.CaptureRequest; +import android.util.Log; + +import io.flutter.plugins.camera.CameraProperties; +import io.flutter.plugins.camera.types.FlashMode; + +public class Flash implements CameraFeature { + private boolean isSupported; + private FlashMode currentSetting; + + @Override + public FlashMode getValue() { + return currentSetting; + } + + @Override + public void setValue(FlashMode value) { + this.currentSetting = value; + } + + @Override + public boolean isSupported(CameraProperties cameraProperties, CameraCharacteristics cameraCharacteristics) { + Boolean available = cameraProperties.getFlashInfoAvailable(); + final boolean supported = available != null && available; + isSupported = supported; + return supported; + } + + @Override + public void updateBuilder(CaptureRequest.Builder requestBuilder) { + Log.i("Camera", "updateFlash"); + + // Don't try to set flash enabled if the current camera doesn't + // support it. + if (!isSupported) { + return; + } + + switch (currentSetting) { + case off: + requestBuilder.set(CaptureRequest.CONTROL_AE_MODE, CaptureRequest.CONTROL_AE_MODE_ON); + requestBuilder.set(CaptureRequest.FLASH_MODE, CaptureRequest.FLASH_MODE_OFF); + break; + + case always: + requestBuilder.set( + CaptureRequest.CONTROL_AE_MODE, CaptureRequest.CONTROL_AE_MODE_ON_ALWAYS_FLASH); + requestBuilder.set(CaptureRequest.FLASH_MODE, CaptureRequest.FLASH_MODE_OFF); + break; + + case torch: + requestBuilder.set(CaptureRequest.CONTROL_AE_MODE, CaptureRequest.CONTROL_AE_MODE_ON); + requestBuilder.set(CaptureRequest.FLASH_MODE, CaptureRequest.FLASH_MODE_TORCH); + break; + + case auto: + requestBuilder.set( + CaptureRequest.CONTROL_AE_MODE, CaptureRequest.CONTROL_AE_MODE_ON_AUTO_FLASH); + requestBuilder.set(CaptureRequest.FLASH_MODE, CaptureRequest.FLASH_MODE_OFF); + break; + + // TODO: to be implemented someday. Need to add it to dart/iOS as another flash mode setting. + // case autoRedEye: + // requestBuilder.set(CaptureRequest.CONTROL_AE_MODE, + // CaptureRequest.CONTROL_AE_MODE_ON_AUTO_FLASH_REDEYE); + // requestBuilder.set(CaptureRequest.FLASH_MODE, + // CaptureRequest.FLASH_MODE_OFF); + // break; + } + } +} diff --git a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/FpsRange.java b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/FpsRange.java new file mode 100644 index 000000000000..2ba5a721500d --- /dev/null +++ b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/FpsRange.java @@ -0,0 +1,63 @@ +package io.flutter.plugins.camera.features; + +import android.hardware.camera2.CameraCharacteristics; +import android.hardware.camera2.CaptureRequest; +import android.util.Log; +import android.util.Range; + +import io.flutter.plugins.camera.CameraProperties; +import io.flutter.plugins.camera.types.FocusMode; + +public class FpsRange implements CameraFeature> { + private boolean isSupported; + private Range currentSetting; + + public FpsRange(CameraProperties cameraProperties) { + Log.i("Camera", "getAvailableFpsRange"); + + try { + Range[] ranges = cameraProperties.getControlAutoExposureAvailableTargetFpsRanges(); + + if (ranges != null) { + for (Range range : ranges) { + int upper = range.getUpper(); + Log.i("Camera", "[FPS Range Available] is:" + range); + if (upper >= 10) { + if (currentSetting == null || upper > currentSetting.getUpper()) { + currentSetting = range; + } + } + } + } + } catch (Exception e) { + // TODO: maybe just send a dart error back +// pictureCaptureRequest.error("cameraAccess", e.getMessage(), null); + } + Log.i("Camera", "[FPS Range] is:" + currentSetting); + } + + @Override + public Range getValue() { + return currentSetting; + } + + @Override + public void setValue(Range value) { + this.currentSetting = value; + } + + // Always supported + @Override + public boolean isSupported(CameraProperties cameraProperties, CameraCharacteristics cameraCharacteristics) { + return true; + } + + @Override + public void updateBuilder(CaptureRequest.Builder requestBuilder) { + if (currentSetting == null) { + return; + } + + requestBuilder.set(CaptureRequest.CONTROL_AE_TARGET_FPS_RANGE, currentSetting); + } +} diff --git a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/NoiseReduction.java b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/NoiseReduction.java new file mode 100644 index 000000000000..50b83c4cc695 --- /dev/null +++ b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/NoiseReduction.java @@ -0,0 +1,64 @@ +package io.flutter.plugins.camera.features; + +import android.hardware.camera2.CameraCharacteristics; +import android.hardware.camera2.CaptureRequest; +import android.util.Log; + +import io.flutter.plugins.camera.CameraProperties; +import io.flutter.plugins.camera.types.NoiseReductionMode; + +/** + * This can either be enabled or disabled. Only full capability devices + * can set this to off. Legacy and full support the fast mode. + * https://developer.android.com/reference/android/hardware/camera2/CameraCharacteristics#NOISE_REDUCTION_AVAILABLE_NOISE_REDUCTION_MODES + */ +public class NoiseReduction implements CameraFeature { + private boolean isSupported; + private NoiseReductionMode currentSetting; + + @Override + public NoiseReductionMode getValue() { + return currentSetting; + } + + @Override + public void setValue(NoiseReductionMode value) { + this.currentSetting = value; + } + + @Override + public boolean isSupported(CameraProperties cameraProperties, CameraCharacteristics cameraCharacteristics) { + /** + * Available settings: + * public static final int NOISE_REDUCTION_MODE_FAST = 1; + * public static final int NOISE_REDUCTION_MODE_HIGH_QUALITY = 2; + * public static final int NOISE_REDUCTION_MODE_MINIMAL = 3; + * public static final int NOISE_REDUCTION_MODE_OFF = 0; + * public static final int NOISE_REDUCTION_MODE_ZERO_SHUTTER_LAG = 4; + * + * Full-capability camera devices will always support OFF and FAST. + * Camera devices that support YUV_REPROCESSING or PRIVATE_REPROCESSING will support ZERO_SHUTTER_LAG. + * Legacy-capability camera devices will only support FAST mode. + */ + + // Can be null on some devices. + int[] modes = cameraCharacteristics.get(CameraCharacteristics.NOISE_REDUCTION_AVAILABLE_NOISE_REDUCTION_MODES); + + /// If there's at least one mode available then we are supported. + return modes != null && modes.length > 0; + } + + @Override + public void updateBuilder(CaptureRequest.Builder requestBuilder) { + Log.i("Camera", "updateFlash"); + + // Don't try to set if the current camera doesn't support it. + if (!isSupported) { + return; + } + + // Always use fast mode. + requestBuilder.set(CaptureRequest.NOISE_REDUCTION_MODE, CaptureRequest.NOISE_REDUCTION_MODE_FAST); + } +} + diff --git a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/types/NoiseReductionMode.java b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/types/NoiseReductionMode.java new file mode 100644 index 000000000000..b02bd49defed --- /dev/null +++ b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/types/NoiseReductionMode.java @@ -0,0 +1,26 @@ +package io.flutter.plugins.camera.types; + +/** + * Only supports fast mode for now. + */ +public enum NoiseReductionMode { + fast("fast"); + + private final String strValue; + + NoiseReductionMode(String strValue) { + this.strValue = strValue; + } + + public static NoiseReductionMode getValueForString(String modeStr) { + for (NoiseReductionMode value : values()) { + if (value.strValue.equals(modeStr)) return value; + } + return null; + } + + @Override + public String toString() { + return strValue; + } +} From 6cfd5ee43b12c7a95dbb0049083344a8f5b40a39 Mon Sep 17 00:00:00 2001 From: Andrew Coutts Date: Sat, 6 Mar 2021 11:37:50 -0500 Subject: [PATCH 067/114] More refactoring of camera features --- .../io/flutter/plugins/camera/Camera.java | 2036 ++++++++--------- .../plugins/camera/CameraProperties.java | 7 + .../plugins/camera/features/AutoFocus.java | 4 +- .../camera/features/CameraFeature.java | 2 +- .../camera/features/CameraFeatures.java | 2 + .../plugins/camera/features/ExposureLock.java | 4 +- .../camera/features/ExposureOffset.java | 68 +- .../camera/features/ExposureOffsetValue.java | 17 + .../camera/features/ExposurePoint.java | 76 + .../plugins/camera/features/Flash.java | 4 +- .../plugins/camera/features/FocusPoint.java | 60 + .../plugins/camera/features/FpsRange.java | 2 +- .../camera/features/NoiseReduction.java | 4 +- .../plugins/camera/features/Point.java | 14 + 14 files changed, 1214 insertions(+), 1086 deletions(-) create mode 100644 packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/ExposureOffsetValue.java create mode 100644 packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/ExposurePoint.java create mode 100644 packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/FocusPoint.java create mode 100644 packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/Point.java 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 c73e5719980c..ba3d7e194f6d 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 @@ -19,7 +19,6 @@ import android.hardware.camera2.CaptureRequest; import android.hardware.camera2.CaptureResult; import android.hardware.camera2.TotalCaptureResult; -import android.hardware.camera2.params.MeteringRectangle; import android.hardware.camera2.params.OutputConfiguration; import android.hardware.camera2.params.SessionConfiguration; import android.media.CamcorderProfile; @@ -33,8 +32,6 @@ import android.os.HandlerThread; import android.os.Looper; import android.util.Log; -import android.util.Range; -import android.util.Rational; import android.util.Size; import android.util.SparseIntArray; import android.view.Surface; @@ -61,9 +58,13 @@ import io.flutter.plugins.camera.features.CameraFeatures; import io.flutter.plugins.camera.features.ExposureLock; import io.flutter.plugins.camera.features.ExposureOffset; +import io.flutter.plugins.camera.features.ExposureOffsetValue; +import io.flutter.plugins.camera.features.ExposurePoint; import io.flutter.plugins.camera.features.Flash; +import io.flutter.plugins.camera.features.FocusPoint; import io.flutter.plugins.camera.features.FpsRange; import io.flutter.plugins.camera.features.NoiseReduction; +import io.flutter.plugins.camera.features.Point; import io.flutter.plugins.camera.media.MediaRecorderBuilder; import io.flutter.plugins.camera.types.ExposureMode; import io.flutter.plugins.camera.types.FlashMode; @@ -75,7 +76,7 @@ @FunctionalInterface interface ErrorCallback { - void onError(String errorCode, String errorMessage); + void onError(String errorCode, String errorMessage); } /** @@ -84,1165 +85,1062 @@ interface ErrorCallback { * session, the system maintains a ring buffer of images from the preview. It must be in full * auto moved (flash, ae, focus, etc). When you capture an image, it simply picks one out of * the ring buffer, thus capturing an image with zero shutter lag. - * + *

* This is a potential improvement for the future. A good example is the AOSP camera here: * https://android.googlesource.com/platform/packages/apps/Camera2/+/9c94ab3/src/com/android/camera/one/v2/OneCameraZslImpl.java - * + *

* But one note- they mention sometimes ZSL captures can be very low quality so it might not * be preferred on some devices. If we do add support for this in the future, we should allow * it to be enabled from dart. */ class Camera implements CameraCaptureCallback.CameraCaptureStateListener { - private static final String TAG = "Camera"; - - /** Conversion from screen rotation to JPEG orientation. */ - private static final SparseIntArray ORIENTATIONS = new SparseIntArray(); - - private static final HashMap supportedImageFormats; - - static { - ORIENTATIONS.append(Surface.ROTATION_0, 90); - ORIENTATIONS.append(Surface.ROTATION_90, 0); - ORIENTATIONS.append(Surface.ROTATION_180, 270); - ORIENTATIONS.append(Surface.ROTATION_270, 180); - } - - // Current supported outputs - static { - supportedImageFormats = new HashMap<>(); - supportedImageFormats.put("yuv420", ImageFormat.YUV_420_888); - supportedImageFormats.put("jpeg", ImageFormat.JPEG); - } - - /** - * Holds all of the camera features/settings and will be used to - * update the request builder when one changes. - */ - private final Map cameraFeatures; - - private final SurfaceTextureEntry flutterTexture; - private final DeviceOrientationManager deviceOrientationListener; - private final int sensorOrientation; - private final Size captureSize; - private final Size previewSize; - private final boolean enableAudio; - private final Context applicationContext; - private final CamcorderProfile recordingProfile; - private final DartMessenger dartMessenger; - private final CameraZoom cameraZoom; - private final CameraProperties cameraProperties; - private final Activity activity; - /** This manages the state of the camera and the current capture request. */ - PictureCaptureRequest pictureCaptureRequest; - /** Whether the current camera device supports auto focus or not. */ - private boolean mAutoFocusSupported = true; - /** The state of the camera. By default we are in the preview state. */ - private CameraState cameraState = CameraState.STATE_PREVIEW; - /** A {@link Handler} for running tasks in the background. */ - private Handler mBackgroundHandler; - /** - * This a callback object for the {@link ImageReader}. "onImageAvailable" will be called when a - * still image is ready to be saved. - */ - private final ImageReader.OnImageAvailableListener mOnImageAvailableListener = - new ImageReader.OnImageAvailableListener() { - @Override - public void onImageAvailable(ImageReader reader) { - Log.i(TAG, "onImageAvailable"); - - // Use acquireNextImage since our image reader is only for 1 image. - mBackgroundHandler.post( - new ImageSaver( - reader.acquireNextImage(), pictureCaptureRequest.file, pictureCaptureRequest)); - cameraState = CameraState.STATE_PREVIEW; - } - }; - - /** An additional thread for running tasks that shouldn't block the UI. */ - private HandlerThread mBackgroundThread; - - private CameraDevice cameraDevice; - private CameraCaptureSession captureSession; - private ImageReader pictureImageReader; - private ImageReader imageStreamReader; - /** {@link CaptureRequest.Builder} for the camera preview */ - private CaptureRequest.Builder mPreviewRequestBuilder; - /** {@link CaptureRequest} generated by {@link #mPreviewRequestBuilder} */ - private MediaRecorder mediaRecorder; - - private boolean recordingVideo; - private File videoRecordingFile; - - /** - * Exposure mode setting of the current camera. Initialize to auto because all cameras support - * autoexposure by default. - */ - private ExposureMode currentExposureMode; - /** - * Focus mode setting of the current camera. Initialize to locked because we don't know if the - * current camera supports autofocus yet. - */ - private FocusMode currentFocusMode; - /** Whether or not to use autofocus. */ - private boolean useAutoFocus = false; - /** Whether the current camera device supports Flash or not. */ - private boolean mFlashSupported = false; - - private CameraRegions cameraRegions; - private int exposureOffset; - /** A {@link CameraCaptureSession.CaptureCallback} that handles events related to JPEG capture. */ - private final CameraCaptureCallback mCaptureCallback; - - private Range fpsRange; - private PlatformChannel.DeviceOrientation lockedCaptureOrientation; - - public Camera( - final Activity activity, - final SurfaceTextureEntry flutterTexture, - final DartMessenger dartMessenger, - final CameraProperties cameraProperties, - final ResolutionPreset resolutionPreset, - final boolean enableAudio) { - - if (activity == null) { - throw new IllegalStateException("No activity available!"); + private static final String TAG = "Camera"; + + /** + * Conversion from screen rotation to JPEG orientation. + */ + private static final SparseIntArray ORIENTATIONS = new SparseIntArray(); + + private static final HashMap supportedImageFormats; + + static { + ORIENTATIONS.append(Surface.ROTATION_0, 90); + ORIENTATIONS.append(Surface.ROTATION_90, 0); + ORIENTATIONS.append(Surface.ROTATION_180, 270); + ORIENTATIONS.append(Surface.ROTATION_270, 180); + } + + // Current supported outputs + static { + supportedImageFormats = new HashMap<>(); + supportedImageFormats.put("yuv420", ImageFormat.YUV_420_888); + supportedImageFormats.put("jpeg", ImageFormat.JPEG); } - this.activity = activity; - this.enableAudio = enableAudio; - this.flutterTexture = flutterTexture; - this.dartMessenger = dartMessenger; - this.applicationContext = activity.getApplicationContext(); - this.cameraProperties = cameraProperties; - this.currentFlashMode = FlashMode.off; - this.currentExposureMode = ExposureMode.auto; - this.currentFocusMode = FocusMode.auto; - this.exposureOffset = 0; - - // Setup camera features - this.cameraFeatures = new HashMap() {{ - put(CameraFeatures.autoFocus, new AutoFocus()); - put(CameraFeatures.flash, new Flash()); - put(CameraFeatures.noiseReduction, new NoiseReduction()); - put(CameraFeatures.fpsRange, new FpsRange(cameraProperties)); - put(CameraFeatures.exposureOffset, new ExposureOffset()); - - // TODO: cameraRegions is going to be null here - put(CameraFeatures.exposureLock, new ExposureLock(cameraRegions)); - }}; - - mCaptureCallback = CameraCaptureCallback.create(this); - - // Setup orientation - sensorOrientation = cameraProperties.getSensorOrientation(); - boolean isFrontFacing = cameraProperties.getLensFacing() == CameraMetadata.LENS_FACING_FRONT; - - deviceOrientationListener = DeviceOrientationManager.create( + + /** + * Holds all of the camera features/settings and will be used to + * update the request builder when one changes. + */ + private final Map cameraFeatures; + + private final SurfaceTextureEntry flutterTexture; + private final DeviceOrientationManager deviceOrientationListener; + private final int sensorOrientation; + private final Size captureSize; + private final Size previewSize; + private final boolean enableAudio; + private final Context applicationContext; + private final CamcorderProfile recordingProfile; + private final DartMessenger dartMessenger; + private final CameraZoom cameraZoom; + private final CameraProperties cameraProperties; + private final Activity activity; + /** + * This manages the state of the camera and the current capture request. + */ + PictureCaptureRequest pictureCaptureRequest; + /** Whether the current camera device supports auto focus or not. */ + /** + * The state of the camera. By default we are in the preview state. + */ + private CameraState cameraState = CameraState.STATE_PREVIEW; + /** + * A {@link Handler} for running tasks in the background. + */ + private Handler mBackgroundHandler; + /** + * This a callback object for the {@link ImageReader}. "onImageAvailable" will be called when a + * still image is ready to be saved. + */ + private final ImageReader.OnImageAvailableListener mOnImageAvailableListener = + new ImageReader.OnImageAvailableListener() { + @Override + public void onImageAvailable(ImageReader reader) { + Log.i(TAG, "onImageAvailable"); + + // Use acquireNextImage since our image reader is only for 1 image. + mBackgroundHandler.post( + new ImageSaver( + reader.acquireNextImage(), pictureCaptureRequest.file, pictureCaptureRequest)); + cameraState = CameraState.STATE_PREVIEW; + } + }; + + /** + * An additional thread for running tasks that shouldn't block the UI. + */ + private HandlerThread mBackgroundThread; + + private CameraDevice cameraDevice; + private CameraCaptureSession captureSession; + private ImageReader pictureImageReader; + private ImageReader imageStreamReader; + /** + * {@link CaptureRequest.Builder} for the camera preview + */ + private CaptureRequest.Builder mPreviewRequestBuilder; + /** + * {@link CaptureRequest} generated by {@link #mPreviewRequestBuilder} + */ + private MediaRecorder mediaRecorder; + + private boolean recordingVideo; + private File videoRecordingFile; + private CameraRegions cameraRegions; + /** + * A {@link CameraCaptureSession.CaptureCallback} that handles events related to JPEG capture. + */ + private final CameraCaptureCallback mCaptureCallback; + private PlatformChannel.DeviceOrientation lockedCaptureOrientation; + + public Camera( + final Activity activity, + final SurfaceTextureEntry flutterTexture, + final DartMessenger dartMessenger, + final CameraProperties cameraProperties, + final ResolutionPreset resolutionPreset, + final boolean enableAudio) { + + if (activity == null) { + throw new IllegalStateException("No activity available!"); + } + this.activity = activity; + this.enableAudio = enableAudio; + this.flutterTexture = flutterTexture; + this.dartMessenger = dartMessenger; + this.applicationContext = activity.getApplicationContext(); + this.cameraProperties = cameraProperties; + + // Setup camera features + this.cameraFeatures = new HashMap() {{ + put(CameraFeatures.autoFocus, new AutoFocus()); + put(CameraFeatures.exposureLock, new ExposureLock(cameraRegions)); // TODO: cameraRegions is going to be null here + put(CameraFeatures.exposureOffset, new ExposureOffset(cameraProperties)); + put(CameraFeatures.exposurePoint, new ExposurePoint(() -> getCameraRegions())); + put(CameraFeatures.focusPoint, new FocusPoint(() -> getCameraRegions())); + put(CameraFeatures.flash, new Flash()); + put(CameraFeatures.fpsRange, new FpsRange(cameraProperties)); + put(CameraFeatures.noiseReduction, new NoiseReduction()); + }}; + + mCaptureCallback = CameraCaptureCallback.create(this); + + // Setup orientation + sensorOrientation = cameraProperties.getSensorOrientation(); + boolean isFrontFacing = cameraProperties.getLensFacing() == CameraMetadata.LENS_FACING_FRONT; + + deviceOrientationListener = DeviceOrientationManager.create( activity, dartMessenger, isFrontFacing, sensorOrientation); - deviceOrientationListener.start(); - - String cameraName = cameraProperties.getCameraName(); - - // Resolution configuration - recordingProfile = - CameraUtils.getBestAvailableCamcorderProfileForResolutionPreset( - cameraName, resolutionPreset); - captureSize = new Size(recordingProfile.videoFrameWidth, recordingProfile.videoFrameHeight); - Log.i(TAG, "captureSize: " + captureSize); - - previewSize = computeBestPreviewSize(cameraName, resolutionPreset); - - // Zoom setup - cameraZoom = - new CameraZoom( - cameraProperties.getSensorInfoActiveArraySize(), - cameraProperties.getScalerAvailableMaxDigitalZoom()); - - // Start background thread. - startBackgroundThread(); - } - - @Override - public void onConverged() { - takePictureAfterPrecapture(); - } - - @Override - public void onPrecapture() { - runPrecaptureSequence(); - } - - @Override - public void onPrecaptureTimeout() { - unlockAutoFocus(); - } - - /** Get the current camera state (use for testing). */ - public CameraState getState() { - return this.cameraState; - } - - /** - * Update the builder settings with all of our available features. - * @param requestBuilder - */ - private void updateBuilderSettings(CaptureRequest.Builder requestBuilder) { - for (Map.Entry feature : cameraFeatures.entrySet()) { - feature.getValue().updateBuilder(requestBuilder); + deviceOrientationListener.start(); + + String cameraName = cameraProperties.getCameraName(); + + // Resolution configuration + recordingProfile = + CameraUtils.getBestAvailableCamcorderProfileForResolutionPreset( + cameraName, resolutionPreset); + captureSize = new Size(recordingProfile.videoFrameWidth, recordingProfile.videoFrameHeight); + Log.i(TAG, "captureSize: " + captureSize); + + previewSize = computeBestPreviewSize(cameraName, resolutionPreset); + + // Zoom setup + cameraZoom = + new CameraZoom( + cameraProperties.getSensorInfoActiveArraySize(), + cameraProperties.getScalerAvailableMaxDigitalZoom()); + + // Start background thread. + startBackgroundThread(); } - } - private void prepareMediaRecorder(String outputFilePath) throws IOException { - Log.i(TAG, "prepareMediaRecorder"); + @Override + public void onConverged() { + takePictureAfterPrecapture(); + } - if (mediaRecorder != null) { - mediaRecorder.release(); + @Override + public void onPrecapture() { + runPrecaptureSequence(); } - mediaRecorder = - new MediaRecorderBuilder(recordingProfile, outputFilePath) - .setEnableAudio(enableAudio) - .setMediaOrientation( - lockedCaptureOrientation == null - ? deviceOrientationListener.getMediaOrientation() - : deviceOrientationListener.getMediaOrientation(lockedCaptureOrientation)) - .build(); - } - - @SuppressLint("MissingPermission") - public void open(String imageFormatGroup) throws CameraAccessException { - // We always capture using JPEG format. - pictureImageReader = - ImageReader.newInstance( - captureSize.getWidth(), captureSize.getHeight(), ImageFormat.JPEG, 1); - - // For image streaming, we use the provided image format or fall back to YUV420. - Integer imageFormat = supportedImageFormats.get(imageFormatGroup); - if (imageFormat == null) { - Log.w(TAG, "The selected imageFormatGroup is not supported by Android. Defaulting to yuv420"); - imageFormat = ImageFormat.YUV_420_888; + @Override + public void onPrecaptureTimeout() { + unlockAutoFocus(); } - imageStreamReader = - ImageReader.newInstance(previewSize.getWidth(), previewSize.getHeight(), imageFormat, 1); - - // Open the camera now - CameraManager cameraManager = CameraUtils.getCameraManager(activity); - cameraManager.openCamera( - cameraProperties.getCameraName(), - new CameraDevice.StateCallback() { - @Override - public void onOpened(@NonNull CameraDevice device) { - Log.i(TAG, "open | onOpened"); - - cameraDevice = device; - try { - startPreview(); - dartMessenger.sendCameraInitializedEvent( - previewSize.getWidth(), - previewSize.getHeight(), - currentExposureMode, - currentFocusMode, - isExposurePointSupported(), - isFocusPointSupported()); - } catch (CameraAccessException e) { - dartMessenger.sendCameraErrorEvent(e.getMessage()); - close(); - } - } - - @Override - public void onClosed(@NonNull CameraDevice camera) { - Log.i(TAG, "open | onClosed"); - - dartMessenger.sendCameraClosingEvent(); - super.onClosed(camera); - } - - @Override - public void onDisconnected(@NonNull CameraDevice cameraDevice) { - Log.i(TAG, "open | onDisconnected"); - - close(); - dartMessenger.sendCameraErrorEvent("The camera was disconnected."); - } - - @Override - public void onError(@NonNull CameraDevice cameraDevice, int errorCode) { - Log.i(TAG, "open | onError"); - - close(); - String errorDescription; - switch (errorCode) { - case ERROR_CAMERA_IN_USE: - errorDescription = "The camera device is in use already."; - break; - case ERROR_MAX_CAMERAS_IN_USE: - errorDescription = "Max cameras in use"; - break; - case ERROR_CAMERA_DISABLED: - errorDescription = "The camera device could not be opened due to a device policy."; - break; - case ERROR_CAMERA_DEVICE: - errorDescription = "The camera device has encountered a fatal error"; - break; - case ERROR_CAMERA_SERVICE: - errorDescription = "The camera service has encountered a fatal error."; - break; - default: - errorDescription = "Unknown camera error"; - } - dartMessenger.sendCameraErrorEvent(errorDescription); - } - }, - mBackgroundHandler); - } - - private void createCaptureSession(int templateType, Surface... surfaces) - throws CameraAccessException { - createCaptureSession(templateType, null, surfaces); - } - - private void createCaptureSession( - int templateType, Runnable onSuccessCallback, Surface... surfaces) - throws CameraAccessException { - Log.i(TAG, "createCaptureSession"); - - // Close any existing capture session. - closeCaptureSession(); - - // Create a new capture builder. - mPreviewRequestBuilder = cameraDevice.createCaptureRequest(templateType); - - // Build Flutter surface to render to - SurfaceTexture surfaceTexture = flutterTexture.surfaceTexture(); - surfaceTexture.setDefaultBufferSize(previewSize.getWidth(), previewSize.getHeight()); - Surface flutterSurface = new Surface(surfaceTexture); - mPreviewRequestBuilder.addTarget(flutterSurface); - - List remainingSurfaces = Arrays.asList(surfaces); - if (templateType != CameraDevice.TEMPLATE_PREVIEW) { - // If it is not preview mode, add all surfaces as targets. - for (Surface surface : remainingSurfaces) { - mPreviewRequestBuilder.addTarget(surface); - } + + /** + * Get the current camera state (use for testing). + */ + public CameraState getState() { + return this.cameraState; } - cameraRegions = new CameraRegions(getRegionBoundaries()); - - // Prepare the callback - CameraCaptureSession.StateCallback callback = - new CameraCaptureSession.StateCallback() { - @Override - public void onConfigured(@NonNull CameraCaptureSession session) { - // Camera was already closed. - if (cameraDevice == null) { - dartMessenger.sendCameraErrorEvent("The camera was closed during configuration."); - return; - } - captureSession = session; + /** + * Update the builder settings with all of our available features. + * + * @param requestBuilder + */ + private void updateBuilderSettings(CaptureRequest.Builder requestBuilder) { + for (Map.Entry feature : cameraFeatures.entrySet()) { + feature.getValue().updateBuilder(requestBuilder); + } + } - updateBuilderSettings(mPreviewRequestBuilder); + private void prepareMediaRecorder(String outputFilePath) throws IOException { + Log.i(TAG, "prepareMediaRecorder"); - refreshPreviewCaptureSession( - onSuccessCallback, (code, message) -> dartMessenger.sendCameraErrorEvent(message)); - } - - @Override - public void onConfigureFailed(@NonNull CameraCaptureSession cameraCaptureSession) { - dartMessenger.sendCameraErrorEvent("Failed to configure camera session."); - } - }; - - // Start the session - if (VERSION.SDK_INT >= VERSION_CODES.P) { - // Collect all surfaces we want to render to. - List configs = new ArrayList<>(); - configs.add(new OutputConfiguration(flutterSurface)); - for (Surface surface : remainingSurfaces) { - configs.add(new OutputConfiguration(surface)); - } - createCaptureSessionWithSessionConfig(configs, callback); - } else { - // Collect all surfaces we want to render to. - List surfaceList = new ArrayList<>(); - surfaceList.add(flutterSurface); - surfaceList.addAll(remainingSurfaces); - createCaptureSession(surfaceList, callback); + if (mediaRecorder != null) { + mediaRecorder.release(); + } + + mediaRecorder = + new MediaRecorderBuilder(recordingProfile, outputFilePath) + .setEnableAudio(enableAudio) + .setMediaOrientation( + lockedCaptureOrientation == null + ? deviceOrientationListener.getMediaOrientation() + : deviceOrientationListener.getMediaOrientation(lockedCaptureOrientation)) + .build(); } - } - - @TargetApi(VERSION_CODES.P) - private void createCaptureSessionWithSessionConfig( - List outputConfigs, CameraCaptureSession.StateCallback callback) - throws CameraAccessException { - cameraDevice.createCaptureSession( - new SessionConfiguration( - SessionConfiguration.SESSION_REGULAR, - outputConfigs, - Executors.newSingleThreadExecutor(), - callback)); - } - - @TargetApi(VERSION_CODES.LOLLIPOP) - @SuppressWarnings("deprecation") - private void createCaptureSession( - List surfaces, CameraCaptureSession.StateCallback callback) - throws CameraAccessException { - cameraDevice.createCaptureSession(surfaces, callback, mBackgroundHandler); - } - - // Send a repeating request to refresh our capture session. - private void refreshPreviewCaptureSession( - @Nullable Runnable onSuccessCallback, @NonNull ErrorCallback onErrorCallback) { - Log.i(TAG, "refreshPreviewCaptureSession"); - if (captureSession == null) { - Log.i(TAG, "[refreshPreviewCaptureSession] mPreviewSession null, returning"); - return; + + @SuppressLint("MissingPermission") + public void open(String imageFormatGroup) throws CameraAccessException { + // We always capture using JPEG format. + pictureImageReader = + ImageReader.newInstance( + captureSize.getWidth(), captureSize.getHeight(), ImageFormat.JPEG, 1); + + // For image streaming, we use the provided image format or fall back to YUV420. + Integer imageFormat = supportedImageFormats.get(imageFormatGroup); + if (imageFormat == null) { + Log.w(TAG, "The selected imageFormatGroup is not supported by Android. Defaulting to yuv420"); + imageFormat = ImageFormat.YUV_420_888; + } + imageStreamReader = + ImageReader.newInstance(previewSize.getWidth(), previewSize.getHeight(), imageFormat, 1); + + // Open the camera now + CameraManager cameraManager = CameraUtils.getCameraManager(activity); + cameraManager.openCamera( + cameraProperties.getCameraName(), + new CameraDevice.StateCallback() { + @Override + public void onOpened(@NonNull CameraDevice device) { + Log.i(TAG, "open | onOpened"); + + cameraDevice = device; + try { + startPreview(); + dartMessenger.sendCameraInitializedEvent( + previewSize.getWidth(), + previewSize.getHeight(), + (ExposureMode) cameraFeatures.get(CameraFeatures.exposureLock).getValue(), + (FocusMode) cameraFeatures.get(CameraFeatures.autoFocus).getValue(), + cameraFeatures.get(CameraFeatures.exposurePoint).isSupported(cameraProperties), + isFocusPointSupported()); + } catch (CameraAccessException e) { + dartMessenger.sendCameraErrorEvent(e.getMessage()); + close(); + } + } + + @Override + public void onClosed(@NonNull CameraDevice camera) { + Log.i(TAG, "open | onClosed"); + + dartMessenger.sendCameraClosingEvent(); + super.onClosed(camera); + } + + @Override + public void onDisconnected(@NonNull CameraDevice cameraDevice) { + Log.i(TAG, "open | onDisconnected"); + + close(); + dartMessenger.sendCameraErrorEvent("The camera was disconnected."); + } + + @Override + public void onError(@NonNull CameraDevice cameraDevice, int errorCode) { + Log.i(TAG, "open | onError"); + + close(); + String errorDescription; + switch (errorCode) { + case ERROR_CAMERA_IN_USE: + errorDescription = "The camera device is in use already."; + break; + case ERROR_MAX_CAMERAS_IN_USE: + errorDescription = "Max cameras in use"; + break; + case ERROR_CAMERA_DISABLED: + errorDescription = "The camera device could not be opened due to a device policy."; + break; + case ERROR_CAMERA_DEVICE: + errorDescription = "The camera device has encountered a fatal error"; + break; + case ERROR_CAMERA_SERVICE: + errorDescription = "The camera service has encountered a fatal error."; + break; + default: + errorDescription = "Unknown camera error"; + } + dartMessenger.sendCameraErrorEvent(errorDescription); + } + }, + mBackgroundHandler); + } + + private void createCaptureSession(int templateType, Surface... surfaces) + throws CameraAccessException { + createCaptureSession(templateType, null, surfaces); } - try { - captureSession.setRepeatingRequest( - mPreviewRequestBuilder.build(), mCaptureCallback, mBackgroundHandler); + private void createCaptureSession( + int templateType, Runnable onSuccessCallback, Surface... surfaces) + throws CameraAccessException { + Log.i(TAG, "createCaptureSession"); - if (onSuccessCallback != null) { - onSuccessCallback.run(); - } + // Close any existing capture session. + closeCaptureSession(); - } catch (CameraAccessException | IllegalStateException | IllegalArgumentException e) { - onErrorCallback.onError("cameraAccess", e.getMessage()); + // Create a new capture builder. + mPreviewRequestBuilder = cameraDevice.createCaptureRequest(templateType); + + // Build Flutter surface to render to + SurfaceTexture surfaceTexture = flutterTexture.surfaceTexture(); + surfaceTexture.setDefaultBufferSize(previewSize.getWidth(), previewSize.getHeight()); + Surface flutterSurface = new Surface(surfaceTexture); + mPreviewRequestBuilder.addTarget(flutterSurface); + + List remainingSurfaces = Arrays.asList(surfaces); + if (templateType != CameraDevice.TEMPLATE_PREVIEW) { + // If it is not preview mode, add all surfaces as targets. + for (Surface surface : remainingSurfaces) { + mPreviewRequestBuilder.addTarget(surface); + } + } + + // Update camera regions + cameraRegions = new CameraRegions(getRegionBoundaries()); + + // Prepare the callback + CameraCaptureSession.StateCallback callback = + new CameraCaptureSession.StateCallback() { + @Override + public void onConfigured(@NonNull CameraCaptureSession session) { + // Camera was already closed. + if (cameraDevice == null) { + dartMessenger.sendCameraErrorEvent("The camera was closed during configuration."); + return; + } + captureSession = session; + + updateBuilderSettings(mPreviewRequestBuilder); + + refreshPreviewCaptureSession( + onSuccessCallback, (code, message) -> dartMessenger.sendCameraErrorEvent(message)); + } + + @Override + public void onConfigureFailed(@NonNull CameraCaptureSession cameraCaptureSession) { + dartMessenger.sendCameraErrorEvent("Failed to configure camera session."); + } + }; + + // Start the session + if (VERSION.SDK_INT >= VERSION_CODES.P) { + // Collect all surfaces we want to render to. + List configs = new ArrayList<>(); + configs.add(new OutputConfiguration(flutterSurface)); + for (Surface surface : remainingSurfaces) { + configs.add(new OutputConfiguration(surface)); + } + createCaptureSessionWithSessionConfig(configs, callback); + } else { + // Collect all surfaces we want to render to. + List surfaceList = new ArrayList<>(); + surfaceList.add(flutterSurface); + surfaceList.addAll(remainingSurfaces); + createCaptureSession(surfaceList, callback); + } } - } - public void takePicture(@NonNull final Result result) { - Log.i(TAG, "takePicture | useAutoFocus: " + useAutoFocus); + @TargetApi(VERSION_CODES.P) + private void createCaptureSessionWithSessionConfig( + List outputConfigs, CameraCaptureSession.StateCallback callback) + throws CameraAccessException { + cameraDevice.createCaptureSession( + new SessionConfiguration( + SessionConfiguration.SESSION_REGULAR, + outputConfigs, + Executors.newSingleThreadExecutor(), + callback)); + } - // Only take one 1 picture at a time. - if (pictureCaptureRequest != null && !pictureCaptureRequest.isFinished()) { - result.error("captureAlreadyActive", "Picture is currently already being captured", null); - return; + @TargetApi(VERSION_CODES.LOLLIPOP) + @SuppressWarnings("deprecation") + private void createCaptureSession( + List surfaces, CameraCaptureSession.StateCallback callback) + throws CameraAccessException { + cameraDevice.createCaptureSession(surfaces, callback, mBackgroundHandler); } - // Create temporary file - final File outputDir = applicationContext.getCacheDir(); - try { - final File file = File.createTempFile("CAP", ".jpg", outputDir); - - // Start a new capture - pictureCaptureRequest = PictureCaptureRequest.create(result, file, dartMessenger); - mCaptureCallback.setPictureCaptureRequest(pictureCaptureRequest); - } catch (IOException | SecurityException e) { - pictureCaptureRequest.error("cannotCreateFile", e.getMessage(), null); - return; + // Send a repeating request to refresh our capture session. + private void refreshPreviewCaptureSession( + @Nullable Runnable onSuccessCallback, @NonNull ErrorCallback onErrorCallback) { + Log.i(TAG, "refreshPreviewCaptureSession"); + if (captureSession == null) { + Log.i(TAG, "[refreshPreviewCaptureSession] mPreviewSession null, returning"); + return; + } + + try { + captureSession.setRepeatingRequest( + mPreviewRequestBuilder.build(), mCaptureCallback, mBackgroundHandler); + + if (onSuccessCallback != null) { + onSuccessCallback.run(); + } + + } catch (CameraAccessException | IllegalStateException | IllegalArgumentException e) { + onErrorCallback.onError("cameraAccess", e.getMessage()); + } } - // Listen for picture being taken - pictureImageReader.setOnImageAvailableListener(mOnImageAvailableListener, mBackgroundHandler); + public void takePicture(@NonNull final Result result) { + Log.i(TAG, "takePicture | useAutoFocus: " + cameraFeatures.get(CameraFeatures.autoFocus).getValue()); + + // Only take one 1 picture at a time. + if (pictureCaptureRequest != null && !pictureCaptureRequest.isFinished()) { + result.error("captureAlreadyActive", "Picture is currently already being captured", null); + return; + } + + // Create temporary file + final File outputDir = applicationContext.getCacheDir(); + try { + final File file = File.createTempFile("CAP", ".jpg", outputDir); + + // Start a new capture + pictureCaptureRequest = PictureCaptureRequest.create(result, file, dartMessenger); + mCaptureCallback.setPictureCaptureRequest(pictureCaptureRequest); + } catch (IOException | SecurityException e) { + pictureCaptureRequest.error("cannotCreateFile", e.getMessage(), null); + return; + } + + // Listen for picture being taken + pictureImageReader.setOnImageAvailableListener(mOnImageAvailableListener, mBackgroundHandler); - if (useAutoFocus) { - runPictureAutoFocus(); - } else { - runPrecaptureSequence(); + if (cameraFeatures.get(CameraFeatures.autoFocus).getValue() == FocusMode.auto) { + runPictureAutoFocus(); + } else { + runPrecaptureSequence(); + } } - } - - /** - * Run the precapture sequence for capturing a still image. This method should be called when we - * get a response in {@link #mCaptureCallback} from lockFocus(). - */ - private void runPrecaptureSequence() { - Log.i(TAG, "runPrecaptureSequence"); - try { - // First set precapture state to idle or else it can hang in STATE_WAITING_PRECAPTURE - mPreviewRequestBuilder.set( - CaptureRequest.CONTROL_AE_PRECAPTURE_TRIGGER, - CaptureRequest.CONTROL_AE_PRECAPTURE_TRIGGER_IDLE); - captureSession.capture(mPreviewRequestBuilder.build(), mCaptureCallback, mBackgroundHandler); - - // Repeating request to refresh preview session - refreshPreviewCaptureSession( - null, (code, message) -> pictureCaptureRequest.error("cameraAccess", message, null)); - - // Start precapture now - cameraState = CameraState.STATE_WAITING_PRECAPTURE_START; - - mPreviewRequestBuilder.set( - CaptureRequest.CONTROL_AE_PRECAPTURE_TRIGGER, - CaptureRequest.CONTROL_AE_PRECAPTURE_TRIGGER_START); - - // Trigger one capture to start AE sequence - captureSession.capture(mPreviewRequestBuilder.build(), mCaptureCallback, mBackgroundHandler); - - } catch (CameraAccessException e) { - e.printStackTrace(); + + /** + * Run the precapture sequence for capturing a still image. This method should be called when we + * get a response in {@link #mCaptureCallback} from lockFocus(). + */ + private void runPrecaptureSequence() { + Log.i(TAG, "runPrecaptureSequence"); + try { + // First set precapture state to idle or else it can hang in STATE_WAITING_PRECAPTURE + mPreviewRequestBuilder.set( + CaptureRequest.CONTROL_AE_PRECAPTURE_TRIGGER, + CaptureRequest.CONTROL_AE_PRECAPTURE_TRIGGER_IDLE); + captureSession.capture(mPreviewRequestBuilder.build(), mCaptureCallback, mBackgroundHandler); + + // Repeating request to refresh preview session + refreshPreviewCaptureSession( + null, (code, message) -> pictureCaptureRequest.error("cameraAccess", message, null)); + + // Start precapture now + cameraState = CameraState.STATE_WAITING_PRECAPTURE_START; + + mPreviewRequestBuilder.set( + CaptureRequest.CONTROL_AE_PRECAPTURE_TRIGGER, + CaptureRequest.CONTROL_AE_PRECAPTURE_TRIGGER_START); + + // Trigger one capture to start AE sequence + captureSession.capture(mPreviewRequestBuilder.build(), mCaptureCallback, mBackgroundHandler); + + } catch (CameraAccessException e) { + e.printStackTrace(); + } } - } - - /** - * Capture a still picture. This method should be called when we get a response in {@link - * #mCaptureCallback} from both lockFocus(). - */ - private void takePictureAfterPrecapture() { - Log.i(TAG, "captureStillPicture"); - cameraState = CameraState.STATE_CAPTURING; - pictureCaptureRequest.setState(PictureCaptureRequestState.STATE_CAPTURING); - - try { - if (null == cameraDevice) { - return; - } - // This is the CaptureRequest.Builder that we use to take a picture. - final CaptureRequest.Builder stillBuilder = - cameraDevice.createCaptureRequest(CameraDevice.TEMPLATE_STILL_CAPTURE); - stillBuilder.addTarget(pictureImageReader.getSurface()); - - // Zoom - stillBuilder.set( - CaptureRequest.SCALER_CROP_REGION, - mPreviewRequestBuilder.get(CaptureRequest.SCALER_CROP_REGION)); - - // Update builder settings - updateBuilderSettings(stillBuilder); - - // Orientation - int rotation = activity.getWindowManager().getDefaultDisplay().getRotation(); - stillBuilder.set(CaptureRequest.JPEG_ORIENTATION, getOrientation(rotation)); - - CameraCaptureSession.CaptureCallback captureCallback = - new CameraCaptureSession.CaptureCallback() { - - @Override - public void onCaptureStarted( - @NonNull CameraCaptureSession session, - @NonNull CaptureRequest request, - long timestamp, - long frameNumber) { - Log.i(TAG, "onCaptureStarted"); - } - @Override - public void onCaptureProgressed( - @NonNull CameraCaptureSession session, - @NonNull CaptureRequest request, - @NonNull CaptureResult partialResult) { - Log.i(TAG, "onCaptureProgressed"); - } + /** + * Capture a still picture. This method should be called when we get a response in {@link + * #mCaptureCallback} from both lockFocus(). + */ + private void takePictureAfterPrecapture() { + Log.i(TAG, "captureStillPicture"); + cameraState = CameraState.STATE_CAPTURING; + pictureCaptureRequest.setState(PictureCaptureRequestState.STATE_CAPTURING); - @Override - public void onCaptureCompleted( - @NonNull CameraCaptureSession session, - @NonNull CaptureRequest request, - @NonNull TotalCaptureResult result) { - Log.i(TAG, "onCaptureCompleted"); - unlockAutoFocus(); + try { + if (null == cameraDevice) { + return; } - }; - - captureSession.stopRepeating(); - captureSession.abortCaptures(); - Log.i(TAG, "sending capture request"); - captureSession.capture(stillBuilder.build(), captureCallback, mBackgroundHandler); - } catch (CameraAccessException e) { - pictureCaptureRequest.error("cameraAccess", e.getMessage(), null); + // This is the CaptureRequest.Builder that we use to take a picture. + final CaptureRequest.Builder stillBuilder = + cameraDevice.createCaptureRequest(CameraDevice.TEMPLATE_STILL_CAPTURE); + stillBuilder.addTarget(pictureImageReader.getSurface()); + + // Zoom + stillBuilder.set( + CaptureRequest.SCALER_CROP_REGION, + mPreviewRequestBuilder.get(CaptureRequest.SCALER_CROP_REGION)); + + // Update builder settings + updateBuilderSettings(stillBuilder); + + // Orientation + int rotation = activity.getWindowManager().getDefaultDisplay().getRotation(); + stillBuilder.set(CaptureRequest.JPEG_ORIENTATION, getOrientation(rotation)); + + CameraCaptureSession.CaptureCallback captureCallback = + new CameraCaptureSession.CaptureCallback() { + + @Override + public void onCaptureStarted( + @NonNull CameraCaptureSession session, + @NonNull CaptureRequest request, + long timestamp, + long frameNumber) { + Log.i(TAG, "onCaptureStarted"); + } + + @Override + public void onCaptureProgressed( + @NonNull CameraCaptureSession session, + @NonNull CaptureRequest request, + @NonNull CaptureResult partialResult) { + Log.i(TAG, "onCaptureProgressed"); + } + + @Override + public void onCaptureCompleted( + @NonNull CameraCaptureSession session, + @NonNull CaptureRequest request, + @NonNull TotalCaptureResult result) { + Log.i(TAG, "onCaptureCompleted"); + unlockAutoFocus(); + } + }; + + captureSession.stopRepeating(); + captureSession.abortCaptures(); + Log.i(TAG, "sending capture request"); + captureSession.capture(stillBuilder.build(), captureCallback, mBackgroundHandler); + } catch (CameraAccessException e) { + pictureCaptureRequest.error("cameraAccess", e.getMessage(), null); + } } - } - - /** Starts a background thread and its {@link Handler}. TODO: call when activity resumed */ - private void startBackgroundThread() { - mBackgroundThread = new HandlerThread("CameraBackground"); - mBackgroundThread.start(); - mBackgroundHandler = new Handler(mBackgroundThread.getLooper()); - } - - /** Stops the background thread and its {@link Handler}. TODO: call when activity paused */ - private void stopBackgroundThread() { - try { - if (mBackgroundThread != null) { - mBackgroundThread.quitSafely(); - mBackgroundThread.join(); - mBackgroundThread = null; - } - - mBackgroundHandler = null; - } catch (InterruptedException e) { - pictureCaptureRequest.error("cameraAccess", e.getMessage(), null); + + /** + * Starts a background thread and its {@link Handler}. TODO: call when activity resumed + */ + private void startBackgroundThread() { + mBackgroundThread = new HandlerThread("CameraBackground"); + mBackgroundThread.start(); + mBackgroundHandler = new Handler(mBackgroundThread.getLooper()); } - } - - /** - * Retrieves the JPEG orientation from the specified screen rotation. - * - * @param rotation The screen rotation. - * @return The JPEG orientation (one of 0, 90, 270, and 360) - */ - private int getOrientation(int rotation) { - // Sensor orientation is 90 for most devices, or 270 for some devices (eg. Nexus 5X) - // We have to take that into account and rotate JPEG properly. - // For devices with orientation of 90, we simply return our mapping from ORIENTATIONS. - // For devices with orientation of 270, we need to rotate the JPEG 180 degrees. - return (ORIENTATIONS.get(rotation) + sensorOrientation + 270) % 360; - } - - /** Start capturing a picture, doing autofocus first. */ - private void runPictureAutoFocus() { - Log.i(TAG, "runPictureAutoFocus"); - assert (pictureCaptureRequest != null); - - cameraState = CameraState.STATE_WAITING_FOCUS; - pictureCaptureRequest.setState(PictureCaptureRequestState.STATE_WAITING_FOCUS); - lockAutoFocus(); - } - - /** Start the autofocus routine on the current capture request. */ - private void lockAutoFocus() { - Log.i(TAG, "lockAutoFocus"); - pictureCaptureRequest.setState(PictureCaptureRequestState.STATE_WAITING_PRECAPTURE_START); - - mPreviewRequestBuilder.set( - CaptureRequest.CONTROL_AF_TRIGGER, CaptureRequest.CONTROL_AF_TRIGGER_START); - - refreshPreviewCaptureSession( - null, (code, message) -> pictureCaptureRequest.error(code, message, null)); - } - - /** Cancel and reset auto focus state and refresh the preview session. */ - private void unlockAutoFocus() { - Log.i(TAG, "unlockAutoFocus"); - try { - // Cancel existing AF state - mPreviewRequestBuilder.set( - CaptureRequest.CONTROL_AF_TRIGGER, CameraMetadata.CONTROL_AF_TRIGGER_CANCEL); - captureSession.capture(mPreviewRequestBuilder.build(), null, mBackgroundHandler); - - // Set AE state to idle again - mPreviewRequestBuilder.set( - CaptureRequest.CONTROL_AE_PRECAPTURE_TRIGGER, - CaptureRequest.CONTROL_AE_PRECAPTURE_TRIGGER_IDLE); - - // Set AF state to idle again - mPreviewRequestBuilder.set( - CaptureRequest.CONTROL_AF_TRIGGER, CameraMetadata.CONTROL_AF_TRIGGER_IDLE); - - captureSession.capture(mPreviewRequestBuilder.build(), null, mBackgroundHandler); - } catch (CameraAccessException e) { - Log.i(TAG, "Error unlocking focus: " + e.getMessage()); - dartMessenger.sendCameraErrorEvent(e.getMessage()); - return; + + /** + * Stops the background thread and its {@link Handler}. TODO: call when activity paused + */ + private void stopBackgroundThread() { + try { + if (mBackgroundThread != null) { + mBackgroundThread.quitSafely(); + mBackgroundThread.join(); + mBackgroundThread = null; + } + + mBackgroundHandler = null; + } catch (InterruptedException e) { + pictureCaptureRequest.error("cameraAccess", e.getMessage(), null); + } } - refreshPreviewCaptureSession( - null, - (errorCode, errorMessage) -> pictureCaptureRequest.error(errorCode, errorMessage, null)); - } - - public void startVideoRecording(Result result) { - final File outputDir = applicationContext.getCacheDir(); - try { - videoRecordingFile = File.createTempFile("REC", ".mp4", outputDir); - } catch (IOException | SecurityException e) { - result.error("cannotCreateFile", e.getMessage(), null); - return; + /** + * Retrieves the JPEG orientation from the specified screen rotation. + * + * @param rotation The screen rotation. + * @return The JPEG orientation (one of 0, 90, 270, and 360) + */ + private int getOrientation(int rotation) { + // Sensor orientation is 90 for most devices, or 270 for some devices (eg. Nexus 5X) + // We have to take that into account and rotate JPEG properly. + // For devices with orientation of 90, we simply return our mapping from ORIENTATIONS. + // For devices with orientation of 270, we need to rotate the JPEG 180 degrees. + return (ORIENTATIONS.get(rotation) + sensorOrientation + 270) % 360; } - try { - prepareMediaRecorder(videoRecordingFile.getAbsolutePath()); - recordingVideo = true; - createCaptureSession( - CameraDevice.TEMPLATE_RECORD, () -> mediaRecorder.start(), mediaRecorder.getSurface()); - result.success(null); - } catch (CameraAccessException | IOException e) { - recordingVideo = false; - videoRecordingFile = null; - result.error("videoRecordingFailed", e.getMessage(), null); + /** + * Start capturing a picture, doing autofocus first. + */ + private void runPictureAutoFocus() { + Log.i(TAG, "runPictureAutoFocus"); + assert (pictureCaptureRequest != null); + + cameraState = CameraState.STATE_WAITING_FOCUS; + pictureCaptureRequest.setState(PictureCaptureRequestState.STATE_WAITING_FOCUS); + lockAutoFocus(); } - } - public void stopVideoRecording(@NonNull final Result result) { - if (!recordingVideo) { - result.success(null); - return; + /** + * Start the autofocus routine on the current capture request. + */ + private void lockAutoFocus() { + Log.i(TAG, "lockAutoFocus"); + pictureCaptureRequest.setState(PictureCaptureRequestState.STATE_WAITING_PRECAPTURE_START); + + mPreviewRequestBuilder.set( + CaptureRequest.CONTROL_AF_TRIGGER, CaptureRequest.CONTROL_AF_TRIGGER_START); + + refreshPreviewCaptureSession( + null, (code, message) -> pictureCaptureRequest.error(code, message, null)); } - try { - recordingVideo = false; - - try { - captureSession.abortCaptures(); - mediaRecorder.stop(); - } catch (CameraAccessException | IllegalStateException e) { - // Ignore exceptions and try to continue (changes are camera session already aborted capture) - } - - mediaRecorder.reset(); - startPreview(); - result.success(videoRecordingFile.getAbsolutePath()); - videoRecordingFile = null; - } catch (CameraAccessException | IllegalStateException e) { - result.error("videoRecordingFailed", e.getMessage(), null); + /** + * Cancel and reset auto focus state and refresh the preview session. + */ + private void unlockAutoFocus() { + Log.i(TAG, "unlockAutoFocus"); + try { + // Cancel existing AF state + mPreviewRequestBuilder.set( + CaptureRequest.CONTROL_AF_TRIGGER, CameraMetadata.CONTROL_AF_TRIGGER_CANCEL); + captureSession.capture(mPreviewRequestBuilder.build(), null, mBackgroundHandler); + + // Set AE state to idle again + mPreviewRequestBuilder.set( + CaptureRequest.CONTROL_AE_PRECAPTURE_TRIGGER, + CaptureRequest.CONTROL_AE_PRECAPTURE_TRIGGER_IDLE); + + // Set AF state to idle again + mPreviewRequestBuilder.set( + CaptureRequest.CONTROL_AF_TRIGGER, CameraMetadata.CONTROL_AF_TRIGGER_IDLE); + + captureSession.capture(mPreviewRequestBuilder.build(), null, mBackgroundHandler); + } catch (CameraAccessException e) { + Log.i(TAG, "Error unlocking focus: " + e.getMessage()); + dartMessenger.sendCameraErrorEvent(e.getMessage()); + return; + } + + refreshPreviewCaptureSession( + null, + (errorCode, errorMessage) -> pictureCaptureRequest.error(errorCode, errorMessage, null)); } - } - public void pauseVideoRecording(@NonNull final Result result) { - if (!recordingVideo) { - result.success(null); - return; + public void startVideoRecording(Result result) { + final File outputDir = applicationContext.getCacheDir(); + try { + videoRecordingFile = File.createTempFile("REC", ".mp4", outputDir); + } catch (IOException | SecurityException e) { + result.error("cannotCreateFile", e.getMessage(), null); + return; + } + + try { + prepareMediaRecorder(videoRecordingFile.getAbsolutePath()); + recordingVideo = true; + createCaptureSession( + CameraDevice.TEMPLATE_RECORD, () -> mediaRecorder.start(), mediaRecorder.getSurface()); + result.success(null); + } catch (CameraAccessException | IOException e) { + recordingVideo = false; + videoRecordingFile = null; + result.error("videoRecordingFailed", e.getMessage(), null); + } } - try { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { - mediaRecorder.pause(); - } else { - result.error("videoRecordingFailed", "pauseVideoRecording requires Android API +24.", null); - return; - } - } catch (IllegalStateException e) { - result.error("videoRecordingFailed", e.getMessage(), null); - return; + public void stopVideoRecording(@NonNull final Result result) { + if (!recordingVideo) { + result.success(null); + return; + } + + try { + recordingVideo = false; + + try { + captureSession.abortCaptures(); + mediaRecorder.stop(); + } catch (CameraAccessException | IllegalStateException e) { + // Ignore exceptions and try to continue (changes are camera session already aborted capture) + } + + mediaRecorder.reset(); + startPreview(); + result.success(videoRecordingFile.getAbsolutePath()); + videoRecordingFile = null; + } catch (CameraAccessException | IllegalStateException e) { + result.error("videoRecordingFailed", e.getMessage(), null); + } } - result.success(null); - } + public void pauseVideoRecording(@NonNull final Result result) { + if (!recordingVideo) { + result.success(null); + return; + } + + try { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { + mediaRecorder.pause(); + } else { + result.error("videoRecordingFailed", "pauseVideoRecording requires Android API +24.", null); + return; + } + } catch (IllegalStateException e) { + result.error("videoRecordingFailed", e.getMessage(), null); + return; + } - public void resumeVideoRecording(@NonNull final Result result) { - if (!recordingVideo) { - result.success(null); - return; + result.success(null); } - try { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { - mediaRecorder.resume(); - } else { - result.error( - "videoRecordingFailed", "resumeVideoRecording requires Android API +24.", null); - return; - } - } catch (IllegalStateException e) { - result.error("videoRecordingFailed", e.getMessage(), null); - return; + public void resumeVideoRecording(@NonNull final Result result) { + if (!recordingVideo) { + result.success(null); + return; + } + + try { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { + mediaRecorder.resume(); + } else { + result.error( + "videoRecordingFailed", "resumeVideoRecording requires Android API +24.", null); + return; + } + } catch (IllegalStateException e) { + result.error("videoRecordingFailed", e.getMessage(), null); + return; + } + + result.success(null); } - result.success(null); - } - - /** - * Dart handler when it's time to set a new flash mode. This will try to set a new flash mode to - * the current camera. - * - * @param result - * @param newMode - * @throws CameraAccessException - */ - public void setFlashMode(@NonNull final Result result, FlashMode newMode) { - // Save the new flash mode setting - cameraFeatures.get(CameraFeatures.flash).setValue(newMode); - cameraFeatures.get(CameraFeatures.flash).updateBuilder(mPreviewRequestBuilder); - - refreshPreviewCaptureSession( - () -> result.success(null), - (code, message) -> result.error("setFlashModeFailed", "Could not set flash mode.", null)); - } - - /** - * Dart handler for setting new exposure mode setting. - * - * @param result - * @param newMode - * @throws CameraAccessException - */ - public void setExposureMode(@NonNull final Result result, ExposureMode newMode) - throws CameraAccessException { - currentExposureMode = newMode; - updateExposureMode(mPreviewRequestBuilder); - - refreshPreviewCaptureSession( - null, - (code, message) -> - result.error("setExposureModeFailed", "Could not set exposure mode.", null)); - - result.success(null); - } - - public void setExposurePoint(@NonNull final Result result, Double x, Double y) - throws CameraAccessException { - // Check if exposure point functionality is available. - if (!isExposurePointSupported()) { - result.error( - "setExposurePointFailed", "Device does not have exposure point capabilities", null); - return; + /** + * Dart handler when it's time to set a new flash mode. This will try to set a new flash mode to + * the current camera. + * + * @param result + * @param newMode + * @throws CameraAccessException + */ + public void setFlashMode(@NonNull final Result result, FlashMode newMode) { + // Save the new flash mode setting + cameraFeatures.get(CameraFeatures.flash).setValue(newMode); + cameraFeatures.get(CameraFeatures.flash).updateBuilder(mPreviewRequestBuilder); + + refreshPreviewCaptureSession( + () -> result.success(null), + (code, message) -> result.error("setFlashModeFailed", "Could not set flash mode.", null)); } - // Check if the current region boundaries are known - if (cameraRegions.getMaxBoundaries() == null) { - result.error("setExposurePointFailed", "Could not determine max region boundaries", null); - return; + + /** + * Dart handler for setting new exposure mode setting. + * + * @param result + * @param newMode + * @throws CameraAccessException + */ + public void setExposureMode(@NonNull final Result result, ExposureMode newMode) { + cameraFeatures.get(CameraFeatures.exposureLock).setValue(newMode); + cameraFeatures.get(CameraFeatures.exposureLock).updateBuilder(mPreviewRequestBuilder); + + refreshPreviewCaptureSession( + () -> result.success(null), + (code, message) -> + result.error("setExposureModeFailed", "Could not set exposure mode.", null)); } - // Set the metering rectangle - if (x == null || y == null) cameraRegions.resetAutoExposureMeteringRectangle(); - else cameraRegions.setAutoExposureMeteringRectangleFromPoint(y, 1 - x); - // Apply it - updateExposureMode(mPreviewRequestBuilder); - refreshPreviewCaptureSession( - () -> result.success(null), (code, message) -> result.error("CameraAccess", message, null)); - } - - /** - * Set new focus mode from dart. - * - * @param result - * @param newMode - * @throws CameraAccessException - */ - public void setFocusMode(@NonNull final Result result, FocusMode newMode) - throws CameraAccessException { - Log.i(TAG, "setFocusMode: " + newMode); - - // Set new focus mode - currentFocusMode = newMode; - - // Sync new focus mode to the current capture request builder - updateFocusMode(mPreviewRequestBuilder); - - // Now depending on the new mode we either want to restart the AF routine (if setting to auto) - // or we want to trigger a one-time focus and then set AF to idle (locked mode). - switch (newMode) { - case auto: - Log.i(TAG, "Triggering AF start with mode " + currentFocusMode); - // Reset state of autofocus so it goes back to passive scanning. - mPreviewRequestBuilder.set( - CaptureRequest.CONTROL_AF_TRIGGER, CaptureRequest.CONTROL_AF_TRIGGER_CANCEL); - // Refresh preview session using repeating request as it will be in CONTROL_AF_MODE_CONTINUOUS_PICTURE + /** + * Get the current camera regions. Used in ExposurePoint feature so it can + * always get a reference to the latest camera regions instance here. + *

+ * The CameraRegions will be replaced every time a new capture session is started. + * + * @return + */ + public CameraRegions getCameraRegions() { + return this.cameraRegions; + } + + /** + * Set new exposure point from dart. + * + * @param result + * @param x + * @param y + */ + public void setExposurePoint(@NonNull final Result result, Double x, Double y) { + cameraFeatures.get(CameraFeatures.exposurePoint).setValue(new Point(x, y)); + cameraFeatures.get(CameraFeatures.exposurePoint).updateBuilder(mPreviewRequestBuilder); + refreshPreviewCaptureSession( - () -> result.success(null), - (code, message) -> result.error("setFocusMode", message, null)); - break; + () -> result.success(null), + (code, message) -> + result.error("setExposurePointFailed", "Could not set exposure point.", null)); - case locked: - // AF mode will be in Auto so we just want to perform one AF routine - mPreviewRequestBuilder.set( - CaptureRequest.CONTROL_AF_TRIGGER, CaptureRequest.CONTROL_AF_TRIGGER_START); + } - // Refresh the AF once. When the AF start is completed triggering then we will set it to idle mode. - // If we don't wait for the callback like this, then setting it to idle just resets the focus to infinity - // on some devices like Sony XZ. - try { - captureSession.capture( - mPreviewRequestBuilder.build(), - new CameraCaptureSession.CaptureCallback() { - @Override - public void onCaptureCompleted( - @NonNull CameraCaptureSession session, - @NonNull CaptureRequest request, - @NonNull TotalCaptureResult _result) { - Log.i(TAG, "Success after triggering AF start for locked focus"); - - mPreviewRequestBuilder.set( - CaptureRequest.CONTROL_AF_TRIGGER, CaptureRequest.CONTROL_AF_TRIGGER_IDLE); - refreshPreviewCaptureSession( - null, (code, message) -> result.error("setFocusMode", message, null)); - } - }, - mBackgroundHandler); + /** + * Return the max exposure offset value supported by the camera to dart. + */ + public double getMaxExposureOffset() { + final ExposureOffsetValue val = (ExposureOffsetValue) cameraFeatures.get(CameraFeatures.exposureOffset).getValue(); + return val.max; + } - result.success(null); - } catch (CameraAccessException e) { - result.error("setFocusMode", e.getMessage(), null); - } - break; + /** + * Return the min exposure offset value supported by the camera to dart. + */ + public double getMinExposureOffset() { + final ExposureOffsetValue val = (ExposureOffsetValue) cameraFeatures.get(CameraFeatures.exposureOffset).getValue(); + return val.min; } - } - - public void setFocusPoint(@NonNull final Result result, Double x, Double y) - throws CameraAccessException { - // Check if focus point functionality is available. - if (!isFocusPointSupported()) { - result.error("setFocusPointFailed", "Device does not have focus point capabilities", null); - return; + + /** + * Set new focus mode from dart. + * + * @param result + * @param newMode + * @throws CameraAccessException + */ + public void setFocusMode(@NonNull final Result result, FocusMode newMode) { + cameraFeatures.get(CameraFeatures.autoFocus).setValue(newMode); + cameraFeatures.get(CameraFeatures.autoFocus).updateBuilder(mPreviewRequestBuilder); + + refreshPreviewCaptureSession( + () -> result.success(null), + (code, message) -> + result.error("setFocusModeFailed", "Could not set focus mode.", null)); } - // Check if the current region boundaries are known - if (cameraRegions.getMaxBoundaries() == null) { - result.error("setFocusPointFailed", "Could not determine max region boundaries", null); - return; + /** + * Sets new focus point from dart. + * + * @param result + * @param x + * @param y + */ + public void setFocusPoint(@NonNull final Result result, Double x, Double y) { + cameraFeatures.get(CameraFeatures.focusPoint).setValue(new Point(x, y)); + cameraFeatures.get(CameraFeatures.focusPoint).updateBuilder(mPreviewRequestBuilder); + + refreshPreviewCaptureSession( + () -> result.success(null), + (code, message) -> + result.error("setFocusPointFailed", "Could not set focus point.", null)); + } - // Set the metering rectangle - if (x == null || y == null) { - cameraRegions.resetAutoFocusMeteringRectangle(); - } else { - cameraRegions.setAutoFocusMeteringRectangleFromPoint(y, 1 - x); + @TargetApi(VERSION_CODES.P) + private boolean supportsDistortionCorrection() { + int[] availableDistortionCorrectionModes = + cameraProperties.getDistortionCorrectionAvailableModes(); + if (availableDistortionCorrectionModes == null) + availableDistortionCorrectionModes = new int[0]; + long nonOffModesSupported = + Arrays.stream(availableDistortionCorrectionModes) + .filter((value) -> value != CaptureRequest.DISTORTION_CORRECTION_MODE_OFF) + .count(); + return nonOffModesSupported > 0; } - // Apply the new metering rectangle - setFocusMode(result, currentFocusMode); - } - - @TargetApi(VERSION_CODES.P) - private boolean supportsDistortionCorrection() { - int[] availableDistortionCorrectionModes = - cameraProperties.getDistortionCorrectionAvailableModes(); - if (availableDistortionCorrectionModes == null) availableDistortionCorrectionModes = new int[0]; - long nonOffModesSupported = - Arrays.stream(availableDistortionCorrectionModes) - .filter((value) -> value != CaptureRequest.DISTORTION_CORRECTION_MODE_OFF) - .count(); - return nonOffModesSupported > 0; - } - - private Size getRegionBoundaries() { - // No distortion correction support - if (android.os.Build.VERSION.SDK_INT < VERSION_CODES.P || !supportsDistortionCorrection()) { - return cameraProperties.getSensorInfoPixelArraySize(); + private Size getRegionBoundaries() { + // No distortion correction support + if (android.os.Build.VERSION.SDK_INT < VERSION_CODES.P || !supportsDistortionCorrection()) { + return cameraProperties.getSensorInfoPixelArraySize(); + } + // Get the current distortion correction mode + Integer distortionCorrectionMode = + mPreviewRequestBuilder.get(CaptureRequest.DISTORTION_CORRECTION_MODE); + // Return the correct boundaries depending on the mode + android.graphics.Rect rect; + if (distortionCorrectionMode == null + || distortionCorrectionMode == CaptureRequest.DISTORTION_CORRECTION_MODE_OFF) { + rect = cameraProperties.getSensorInfoPreCorrectionActiveArraySize(); + } else { + rect = cameraProperties.getSensorInfoActiveArraySize(); + } + return rect == null ? null : new Size(rect.width(), rect.height()); } - // Get the current distortion correction mode - Integer distortionCorrectionMode = - mPreviewRequestBuilder.get(CaptureRequest.DISTORTION_CORRECTION_MODE); - // Return the correct boundaries depending on the mode - android.graphics.Rect rect; - if (distortionCorrectionMode == null - || distortionCorrectionMode == CaptureRequest.DISTORTION_CORRECTION_MODE_OFF) { - rect = cameraProperties.getSensorInfoPreCorrectionActiveArraySize(); - } else { - rect = cameraProperties.getSensorInfoActiveArraySize(); + + private boolean isFocusPointSupported() { + Integer supportedRegions = cameraProperties.getControlMaxRegionsAutoFocus(); + return supportedRegions != null && supportedRegions > 0; } - return rect == null ? null : new Size(rect.width(), rect.height()); - } - - private boolean isExposurePointSupported() { - Integer supportedRegions = cameraProperties.getControlMaxRegionsAutoExposure(); - return supportedRegions != null && supportedRegions > 0; - } - - private boolean isFocusPointSupported() { - Integer supportedRegions = cameraProperties.getControlMaxRegionsAutoFocus(); - return supportedRegions != null && supportedRegions > 0; - } - - public double getMinExposureOffset() { - Range range = cameraProperties.getControlAutoExposureCompensationRange(); - double minStepped = range == null ? 0 : range.getLower(); - double stepSize = getExposureOffsetStepSize(); - return minStepped * stepSize; - } - - public double getMaxExposureOffset() { - Range range = cameraProperties.getControlAutoExposureCompensationRange(); - double maxStepped = range == null ? 0 : range.getUpper(); - double stepSize = getExposureOffsetStepSize(); - return maxStepped * stepSize; - } - - public double getExposureOffsetStepSize() { - Rational stepSize = cameraProperties.getControlAutoExposureCompensationStep(); - return stepSize == null ? 0.0 : stepSize.doubleValue(); - } - - public void setExposureOffset(@NonNull final Result result, double offset) { - // Set the exposure offset - double stepSize = getExposureOffsetStepSize(); - exposureOffset = (int) (offset / stepSize); - // Apply it - updateExposureMode(mPreviewRequestBuilder); - - // Refresh capture session - refreshPreviewCaptureSession( - () -> result.success(offset), - (code, message) -> - result.error("setExposureModeFailed", "Could not set flash mode.", null)); - } - - public float getMaxZoomLevel() { - return cameraZoom.maxZoom; - } - - public float getMinZoomLevel() { - return CameraZoom.DEFAULT_ZOOM_FACTOR; - } - - public void setZoomLevel(@NonNull final Result result, float zoom) throws CameraAccessException { - float maxZoom = cameraZoom.maxZoom; - float minZoom = CameraZoom.DEFAULT_ZOOM_FACTOR; - - if (zoom > maxZoom || zoom < minZoom) { - String errorMessage = - String.format( - Locale.ENGLISH, - "Zoom level out of bounds (zoom level should be between %f and %f).", - minZoom, - maxZoom); - result.error("ZOOM_ERROR", errorMessage, null); - return; + + /** + * Set a new exposure offset from dart. From dart the offset comes as a double, like +1.3 or -1.3. + * + * @param result + * @param offset + */ + public void setExposureOffset(@NonNull final Result result, double offset) { + cameraFeatures.get(CameraFeatures.exposureOffset).setValue(offset); + cameraFeatures.get(CameraFeatures.exposureOffset).updateBuilder(mPreviewRequestBuilder); + + refreshPreviewCaptureSession( + () -> result.success(null), + (code, message) -> + result.error("setFocusModeFailed", "Could not set focus mode.", null)); } - //Zoom area is calculated relative to sensor area (activeRect) - if (mPreviewRequestBuilder != null) { - final Rect computedZoom = cameraZoom.computeZoom(zoom); - mPreviewRequestBuilder.set(CaptureRequest.SCALER_CROP_REGION, computedZoom); - captureSession.setRepeatingRequest(mPreviewRequestBuilder.build(), null, mBackgroundHandler); + public float getMaxZoomLevel() { + return cameraZoom.maxZoom; } - result.success(null); - } + public float getMinZoomLevel() { + return CameraZoom.DEFAULT_ZOOM_FACTOR; + } - public void lockCaptureOrientation(PlatformChannel.DeviceOrientation orientation) { - this.lockedCaptureOrientation = orientation; - } + public void setZoomLevel(@NonNull final Result result, float zoom) throws CameraAccessException { + float maxZoom = cameraZoom.maxZoom; + float minZoom = CameraZoom.DEFAULT_ZOOM_FACTOR; + + if (zoom > maxZoom || zoom < minZoom) { + String errorMessage = + String.format( + Locale.ENGLISH, + "Zoom level out of bounds (zoom level should be between %f and %f).", + minZoom, + maxZoom); + result.error("ZOOM_ERROR", errorMessage, null); + return; + } - public void unlockCaptureOrientation() { - this.lockedCaptureOrientation = null; - } + //Zoom area is calculated relative to sensor area (activeRect) + if (mPreviewRequestBuilder != null) { + final Rect computedZoom = cameraZoom.computeZoom(zoom); + mPreviewRequestBuilder.set(CaptureRequest.SCALER_CROP_REGION, computedZoom); + captureSession.setRepeatingRequest(mPreviewRequestBuilder.build(), null, mBackgroundHandler); + } - /** Set current fps range setting to the current preview request builder */ - private void updateFpsRange() { - if (fpsRange == null) { - return; + result.success(null); } - mPreviewRequestBuilder.set(CaptureRequest.CONTROL_AE_TARGET_FPS_RANGE, fpsRange); - } - - /** - * Sync the focus mode setting to the provided capture request builder. - *updateFps - * @param requestBuilder - */ - private void updateFocusMode(CaptureRequest.Builder requestBuilder) { - Log.i(TAG, "updateFocusMode currentFocusMode: " + currentFocusMode); - - if (!mAutoFocusSupported) { - useAutoFocus = false; - requestBuilder.set(CaptureRequest.CONTROL_AF_MODE, CaptureRequest.CONTROL_AF_MODE_OFF); - } else { - switch (currentFocusMode) { - case locked: - useAutoFocus = false; - requestBuilder.set(CaptureRequest.CONTROL_AF_MODE, CaptureRequest.CONTROL_AF_MODE_AUTO); - break; - - case auto: - useAutoFocus = true; - requestBuilder.set( - CaptureRequest.CONTROL_AF_MODE, - recordingVideo - ? CaptureRequest.CONTROL_AF_MODE_CONTINUOUS_VIDEO - : CaptureRequest.CONTROL_AF_MODE_CONTINUOUS_PICTURE); - default: - break; - } + public void lockCaptureOrientation(PlatformChannel.DeviceOrientation orientation) { + this.lockedCaptureOrientation = orientation; } - // Some devices use an extremely high noise reduction setting by default (pixel 4 selfie mode), which - // causes the preview/capture to look blurry and out of focus. To fix this we set NR to off. - // TODO: we should add a noise reduction setting in dart/ios in the future. - requestBuilder.set( - CaptureRequest.NOISE_REDUCTION_MODE, CaptureRequest.NOISE_REDUCTION_MODE_OFF); - - // Update metering - MeteringRectangle afRect = cameraRegions.getAFMeteringRectangle(); - requestBuilder.set( - CaptureRequest.CONTROL_AF_REGIONS, - afRect == null ? null : new MeteringRectangle[] {afRect}); - } - - public void startPreview() throws CameraAccessException { - if (pictureImageReader == null || pictureImageReader.getSurface() == null) return; - Log.i(TAG, "startPreview"); - - createCaptureSession(CameraDevice.TEMPLATE_PREVIEW, pictureImageReader.getSurface()); - } - - public void startPreviewWithImageStream(EventChannel imageStreamChannel) - throws CameraAccessException { - createCaptureSession(CameraDevice.TEMPLATE_RECORD, imageStreamReader.getSurface()); - Log.i(TAG, "startPreviewWithImageStream"); - - imageStreamChannel.setStreamHandler( - new EventChannel.StreamHandler() { - @Override - public void onListen(Object o, EventChannel.EventSink imageStreamSink) { - setImageStreamImageAvailableListener(imageStreamSink); - } - - @Override - public void onCancel(Object o) { - imageStreamReader.setOnImageAvailableListener(null, mBackgroundHandler); - } - }); - } - - private void setImageStreamImageAvailableListener(final EventChannel.EventSink imageStreamSink) { - imageStreamReader.setOnImageAvailableListener( - reader -> { - // Use acquireNextImage since our image reader is only for 1 image. - Image img = reader.acquireNextImage(); - if (img == null) return; - - List> planes = new ArrayList<>(); - for (Image.Plane plane : img.getPlanes()) { - ByteBuffer buffer = plane.getBuffer(); - - byte[] bytes = new byte[buffer.remaining()]; - buffer.get(bytes, 0, bytes.length); - - Map planeBuffer = new HashMap<>(); - planeBuffer.put("bytesPerRow", plane.getRowStride()); - planeBuffer.put("bytesPerPixel", plane.getPixelStride()); - planeBuffer.put("bytes", bytes); - - planes.add(planeBuffer); - } - - Map imageBuffer = new HashMap<>(); - imageBuffer.put("width", img.getWidth()); - imageBuffer.put("height", img.getHeight()); - imageBuffer.put("format", img.getFormat()); - imageBuffer.put("planes", planes); - - final Handler handler = new Handler(Looper.getMainLooper()); - handler.post(() -> imageStreamSink.success(imageBuffer)); - img.close(); - }, - mBackgroundHandler); - } - - private void closeCaptureSession() { - if (captureSession != null) { - Log.i(TAG, "closeCaptureSession"); - - captureSession.close(); - captureSession = null; + public void unlockCaptureOrientation() { + this.lockedCaptureOrientation = null; } - } - public void close() { - Log.i(TAG, "close"); - closeCaptureSession(); + public void startPreview() throws CameraAccessException { + if (pictureImageReader == null || pictureImageReader.getSurface() == null) return; + Log.i(TAG, "startPreview"); - if (cameraDevice != null) { - cameraDevice.close(); - cameraDevice = null; + createCaptureSession(CameraDevice.TEMPLATE_PREVIEW, pictureImageReader.getSurface()); } - if (pictureImageReader != null) { - pictureImageReader.close(); - pictureImageReader = null; + + public void startPreviewWithImageStream(EventChannel imageStreamChannel) + throws CameraAccessException { + createCaptureSession(CameraDevice.TEMPLATE_RECORD, imageStreamReader.getSurface()); + Log.i(TAG, "startPreviewWithImageStream"); + + imageStreamChannel.setStreamHandler( + new EventChannel.StreamHandler() { + @Override + public void onListen(Object o, EventChannel.EventSink imageStreamSink) { + setImageStreamImageAvailableListener(imageStreamSink); + } + + @Override + public void onCancel(Object o) { + imageStreamReader.setOnImageAvailableListener(null, mBackgroundHandler); + } + }); } - if (imageStreamReader != null) { - imageStreamReader.close(); - imageStreamReader = null; + + private void setImageStreamImageAvailableListener(final EventChannel.EventSink imageStreamSink) { + imageStreamReader.setOnImageAvailableListener( + reader -> { + // Use acquireNextImage since our image reader is only for 1 image. + Image img = reader.acquireNextImage(); + if (img == null) return; + + List> planes = new ArrayList<>(); + for (Image.Plane plane : img.getPlanes()) { + ByteBuffer buffer = plane.getBuffer(); + + byte[] bytes = new byte[buffer.remaining()]; + buffer.get(bytes, 0, bytes.length); + + Map planeBuffer = new HashMap<>(); + planeBuffer.put("bytesPerRow", plane.getRowStride()); + planeBuffer.put("bytesPerPixel", plane.getPixelStride()); + planeBuffer.put("bytes", bytes); + + planes.add(planeBuffer); + } + + Map imageBuffer = new HashMap<>(); + imageBuffer.put("width", img.getWidth()); + imageBuffer.put("height", img.getHeight()); + imageBuffer.put("format", img.getFormat()); + imageBuffer.put("planes", planes); + + final Handler handler = new Handler(Looper.getMainLooper()); + handler.post(() -> imageStreamSink.success(imageBuffer)); + img.close(); + }, + mBackgroundHandler); } - if (mediaRecorder != null) { - mediaRecorder.reset(); - mediaRecorder.release(); - mediaRecorder = null; + + private void closeCaptureSession() { + if (captureSession != null) { + Log.i(TAG, "closeCaptureSession"); + + captureSession.close(); + captureSession = null; + } } - stopBackgroundThread(); - } + public void close() { + Log.i(TAG, "close"); + closeCaptureSession(); - public void dispose() { - Log.i(TAG, "dispose"); + if (cameraDevice != null) { + cameraDevice.close(); + cameraDevice = null; + } + if (pictureImageReader != null) { + pictureImageReader.close(); + pictureImageReader = null; + } + if (imageStreamReader != null) { + imageStreamReader.close(); + imageStreamReader = null; + } + if (mediaRecorder != null) { + mediaRecorder.reset(); + mediaRecorder.release(); + mediaRecorder = null; + } - close(); - flutterTexture.release(); - deviceOrientationListener.stop(); - } + stopBackgroundThread(); + } + + public void dispose() { + Log.i(TAG, "dispose"); + + close(); + flutterTexture.release(); + deviceOrientationListener.stop(); + } } diff --git a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/CameraProperties.java b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/CameraProperties.java index a3734effb504..1483268a255f 100644 --- a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/CameraProperties.java +++ b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/CameraProperties.java @@ -44,6 +44,8 @@ public interface CameraProperties { int getSensorOrientation(); int getHardwareLevel(); + + int[] getAvailableNoiseReductionModes(); } class CameraPropertiesImpl implements CameraProperties { @@ -147,4 +149,9 @@ public int getSensorOrientation() { public int getHardwareLevel() { return cameraCharacteristics.get(CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL); } + + @Override + public int[] getAvailableNoiseReductionModes() { + return cameraCharacteristics.get(CameraCharacteristics.NOISE_REDUCTION_AVAILABLE_NOISE_REDUCTION_MODES); + } } diff --git a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/AutoFocus.java b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/AutoFocus.java index 65b57fe261b0..f6dfde08740f 100644 --- a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/AutoFocus.java +++ b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/AutoFocus.java @@ -10,7 +10,7 @@ public class AutoFocus implements CameraFeature { // private final boolean recordingVideo; private boolean isSupported; - private FocusMode currentSetting; + private FocusMode currentSetting = FocusMode.auto; // public AutoFocus(boolean recordingVideo) { // this.recordingVideo = recordingVideo; @@ -27,7 +27,7 @@ public void setValue(FocusMode value) { } @Override - public boolean isSupported(CameraProperties cameraProperties, CameraCharacteristics cameraCharacteristics) { + public boolean isSupported(CameraProperties cameraProperties) { int[] modes = cameraProperties.getControlAutoFocusAvailableModes(); Log.i("Camera", "checkAutoFocusSupported | modes:"); for (int mode : modes) { diff --git a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/CameraFeature.java b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/CameraFeature.java index 0a135ff99e9f..fc9ed1505217 100644 --- a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/CameraFeature.java +++ b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/CameraFeature.java @@ -23,7 +23,7 @@ public interface CameraFeature { * given camera properties. * @return */ - public boolean isSupported(CameraProperties cameraProperties, CameraCharacteristics cameraCharacteristics); + public boolean isSupported(CameraProperties cameraProperties); /** * Update the setting in a provided request builder. diff --git a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/CameraFeatures.java b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/CameraFeatures.java index 9fcd4cde3159..7e990bbf0398 100644 --- a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/CameraFeatures.java +++ b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/CameraFeatures.java @@ -10,7 +10,9 @@ public enum CameraFeatures { exposureLock("exposureLock"), exposureOffset("exposureOffset"), flash("flash"), + focusPoint("focusPoint"), fpsRange("fpsRange"), + exposurePoint("exposurePoint"), noiseReduction("noiseReduction"); private final String strValue; diff --git a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/ExposureLock.java b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/ExposureLock.java index a32d41fb5d0f..2957e91fea6c 100644 --- a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/ExposureLock.java +++ b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/ExposureLock.java @@ -16,7 +16,7 @@ */ public class ExposureLock implements CameraFeature { private boolean isSupported; - private ExposureMode currentSetting; + private ExposureMode currentSetting = ExposureMode.auto; private CameraRegions cameraRegions; public ExposureLock(CameraRegions cameraRegions) { @@ -35,7 +35,7 @@ public void setValue(ExposureMode value) { // Available on all devices. @Override - public boolean isSupported(CameraProperties cameraProperties, CameraCharacteristics cameraCharacteristics) { + public boolean isSupported(CameraProperties cameraProperties) { return true; } diff --git a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/ExposureOffset.java b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/ExposureOffset.java index da01f9f45286..91ff020d4b96 100644 --- a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/ExposureOffset.java +++ b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/ExposureOffset.java @@ -4,6 +4,8 @@ import android.hardware.camera2.CaptureRequest; import android.hardware.camera2.params.MeteringRectangle; import android.util.Log; +import android.util.Range; +import android.util.Rational; import io.flutter.plugins.camera.CameraProperties; import io.flutter.plugins.camera.CameraRegions; @@ -12,23 +14,36 @@ /** * Exposure offset makes the image brighter or darker. */ -public class ExposureOffset implements CameraFeature { +public class ExposureOffset implements CameraFeature { private boolean isSupported; - private Integer currentSetting; + private ExposureOffsetValue currentSetting; + private CameraProperties cameraProperties; + private final double min; + private final double max; + + public ExposureOffset(CameraProperties cameraProperties) { + this.cameraProperties = cameraProperties; + this.min = getMinExposureOffset(cameraProperties); + this.max = getMaxExposureOffset(cameraProperties); + + // Initial offset of 0 + this.currentSetting = new ExposureOffsetValue(this.min, this.max, 0); + } @Override - public Integer getValue() { + public ExposureOffsetValue getValue() { return currentSetting; } @Override - public void setValue(Integer value) { - this.currentSetting = value; + public void setValue(ExposureOffsetValue value) { + double stepSize = getExposureOffsetStepSize(cameraProperties); + this.currentSetting = new ExposureOffsetValue(min, max, (value.value / stepSize)); } // Available on all devices. @Override - public boolean isSupported(CameraProperties cameraProperties, CameraCharacteristics cameraCharacteristics) { + public boolean isSupported(CameraProperties cameraProperties) { return true; } @@ -41,6 +56,45 @@ public void updateBuilder(CaptureRequest.Builder requestBuilder) { return; } - requestBuilder.set(CaptureRequest.CONTROL_AE_EXPOSURE_COMPENSATION, currentSetting); + requestBuilder.set(CaptureRequest.CONTROL_AE_EXPOSURE_COMPENSATION, (int) currentSetting.value); + } + + /** + * Return the minimum exposure offset double value. + * @param cameraProperties + * @return + */ + private double getMinExposureOffset(CameraProperties cameraProperties) { + Range range = cameraProperties.getControlAutoExposureCompensationRange(); + double minStepped = range == null ? 0 : range.getLower(); + double stepSize = getExposureOffsetStepSize(cameraProperties); + return minStepped * stepSize; + } + + /** + * Return the max exposure offset double value. + * @param cameraProperties + * @return + */ + private double getMaxExposureOffset(CameraProperties cameraProperties) { + Range range = cameraProperties.getControlAutoExposureCompensationRange(); + double maxStepped = range == null ? 0 : range.getUpper(); + double stepSize = getExposureOffsetStepSize(cameraProperties); + return maxStepped * stepSize; + } + + /** + * Returns the exposure offset step size. This is the smallest amount which the + * exposure offset can be changed. + * + * Example: if this has a value of 0.5, then an aeExposureCompensation setting of + * -2 means that the actual AE offset is -1. + * @param cameraProperties + * @return + */ + private double getExposureOffsetStepSize(CameraProperties cameraProperties) { + Rational stepSize = cameraProperties.getControlAutoExposureCompensationStep(); + return stepSize == null ? 0.0 : stepSize.doubleValue(); } } + diff --git a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/ExposureOffsetValue.java b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/ExposureOffsetValue.java new file mode 100644 index 000000000000..ff03c6ae87be --- /dev/null +++ b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/ExposureOffsetValue.java @@ -0,0 +1,17 @@ +package io.flutter.plugins.camera.features; + +/** + * This represents the exposure offset value. It holds the minimum and + * maximum values, as well as the current setting value. + */ +public class ExposureOffsetValue { + final public double min; + final public double max; + final public double value; + + public ExposureOffsetValue(double min, double max, double value) { + this.min = min; + this.max = max; + this.value = value; + } +} diff --git a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/ExposurePoint.java b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/ExposurePoint.java new file mode 100644 index 000000000000..976d1849990c --- /dev/null +++ b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/ExposurePoint.java @@ -0,0 +1,76 @@ +package io.flutter.plugins.camera.features; + +import android.hardware.camera2.CameraCharacteristics; +import android.hardware.camera2.CaptureRequest; +import android.hardware.camera2.params.MeteringRectangle; +import android.util.Log; + +import java.util.concurrent.Callable; + +import io.flutter.plugins.camera.CameraProperties; +import io.flutter.plugins.camera.CameraRegions; + +/** + * Exposure point controls where in the frame exposure metering will come from. + */ +public class ExposurePoint implements CameraFeature { + // Used later to always get the correct camera regions instance. + private final Callable getCameraRegions; + private boolean isSupported; + private Point currentSetting = new Point(0.0, 0.0); + + public ExposurePoint(Callable getCameraRegions) { + this.getCameraRegions = getCameraRegions; + } + + @Override + public Point getValue() { + return currentSetting; + } + + @Override + public void setValue(Point value) { + this.currentSetting = value; + + try { + if (value.x == null || value.y == null) { + getCameraRegions.call().resetAutoExposureMeteringRectangle(); + } else { + getCameraRegions.call().setAutoExposureMeteringRectangleFromPoint(value.x, value.y); + } + } catch (Exception e) { + e.printStackTrace(); + } + } + + // Whether or not this camera can set the exposure point. + @Override + public boolean isSupported(CameraProperties cameraProperties) { + Integer supportedRegions = cameraProperties.getControlMaxRegionsAutoExposure(); + final boolean supported = supportedRegions != null && supportedRegions > 0; + isSupported = supported; + return supported; + } + + @Override + public void updateBuilder(CaptureRequest.Builder requestBuilder) { + Log.i("Camera", "updateExposureMode"); + + // Don't try to set if the current camera doesn't support it. + if (!isSupported) { + return; + } + + MeteringRectangle aeRect = null; + try { + aeRect = getCameraRegions.call().getAEMeteringRectangle(); + requestBuilder.set( + CaptureRequest.CONTROL_AE_REGIONS, + aeRect == null ? null : new MeteringRectangle[]{getCameraRegions.call().getAEMeteringRectangle()}); + } catch (Exception e) { + e.printStackTrace(); + } + + } +} + diff --git a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/Flash.java b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/Flash.java index e594f27a6c40..4744735ebacb 100644 --- a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/Flash.java +++ b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/Flash.java @@ -9,7 +9,7 @@ public class Flash implements CameraFeature { private boolean isSupported; - private FlashMode currentSetting; + private FlashMode currentSetting = FlashMode.auto; @Override public FlashMode getValue() { @@ -22,7 +22,7 @@ public void setValue(FlashMode value) { } @Override - public boolean isSupported(CameraProperties cameraProperties, CameraCharacteristics cameraCharacteristics) { + public boolean isSupported(CameraProperties cameraProperties) { Boolean available = cameraProperties.getFlashInfoAvailable(); final boolean supported = available != null && available; isSupported = supported; diff --git a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/FocusPoint.java b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/FocusPoint.java new file mode 100644 index 000000000000..9dc4fb689b14 --- /dev/null +++ b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/FocusPoint.java @@ -0,0 +1,60 @@ +package io.flutter.plugins.camera.features; + +import android.hardware.camera2.CameraCharacteristics; +import android.hardware.camera2.CaptureRequest; +import android.hardware.camera2.params.MeteringRectangle; +import android.util.Log; + +import java.util.concurrent.Callable; + +import io.flutter.plugins.camera.CameraProperties; +import io.flutter.plugins.camera.CameraRegions; + +/** + * Focus point controls where in the frame focus will come from. + */ +public class FocusPoint implements CameraFeature { + // Used later to always get the correct camera regions instance. + private final Callable getCameraRegions; + private boolean isSupported; + private Point currentSetting = new Point(0.0, 0.0); + + public FocusPoint(Callable getCameraRegions) { + this.getCameraRegions = getCameraRegions; + } + + @Override + public Point getValue() { + return currentSetting; + } + + @Override + public void setValue(Point value) { + this.currentSetting = value; + + try { + if (value.x == null || value.y == null) { + getCameraRegions.call().resetAutoFocusMeteringRectangle(); + } else { + getCameraRegions.call().setAutoFocusMeteringRectangleFromPoint(value.x, value.y); + } + } catch (Exception e) { + e.printStackTrace(); + } + } + + // Whether or not this camera can set the exposure point. + @Override + public boolean isSupported(CameraProperties cameraProperties) { + Integer supportedRegions = cameraProperties.getControlMaxRegionsAutoFocus(); + final boolean supported = supportedRegions != null && supportedRegions > 0; + isSupported = supported; + return supported; + } + + @Override + public void updateBuilder(CaptureRequest.Builder requestBuilder) { +// Not used + } +} + diff --git a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/FpsRange.java b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/FpsRange.java index 2ba5a721500d..dcc5441f3c56 100644 --- a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/FpsRange.java +++ b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/FpsRange.java @@ -48,7 +48,7 @@ public void setValue(Range value) { // Always supported @Override - public boolean isSupported(CameraProperties cameraProperties, CameraCharacteristics cameraCharacteristics) { + public boolean isSupported(CameraProperties cameraProperties) { return true; } diff --git a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/NoiseReduction.java b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/NoiseReduction.java index 50b83c4cc695..f32f41f6098b 100644 --- a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/NoiseReduction.java +++ b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/NoiseReduction.java @@ -27,7 +27,7 @@ public void setValue(NoiseReductionMode value) { } @Override - public boolean isSupported(CameraProperties cameraProperties, CameraCharacteristics cameraCharacteristics) { + public boolean isSupported(CameraProperties cameraProperties) { /** * Available settings: * public static final int NOISE_REDUCTION_MODE_FAST = 1; @@ -42,7 +42,7 @@ public boolean isSupported(CameraProperties cameraProperties, CameraCharacterist */ // Can be null on some devices. - int[] modes = cameraCharacteristics.get(CameraCharacteristics.NOISE_REDUCTION_AVAILABLE_NOISE_REDUCTION_MODES); + int[] modes = cameraProperties.getAvailableNoiseReductionModes(); /// If there's at least one mode available then we are supported. return modes != null && modes.length > 0; diff --git a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/Point.java b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/Point.java new file mode 100644 index 000000000000..657708b6844c --- /dev/null +++ b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/Point.java @@ -0,0 +1,14 @@ +package io.flutter.plugins.camera.features; + +/** + * Represents a point on an x/y axis. + */ +public class Point { + final public Double x; + final public Double y; + + public Point(Double x, Double y) { + this.x = x; + this.y = y; + } +} From 1b4432bb923743d0e946fcd82a416134e22fb447 Mon Sep 17 00:00:00 2001 From: Andrew Coutts Date: Sun, 7 Mar 2021 10:01:59 -0500 Subject: [PATCH 068/114] Refactoring --- .../io/flutter/plugins/camera/Camera.java | 1982 ++++++++--------- .../plugins/camera/CameraCaptureCallback.java | 81 +- .../plugins/camera/CameraProperties.java | 4 +- .../flutter/plugins/camera/CameraUtils.java | 51 - .../flutter/plugins/camera/DartMessenger.java | 8 +- .../plugins/camera/PictureCaptureRequest.java | 8 +- .../plugins/camera/features/AutoFocus.java | 85 - .../camera/features/CameraFeature.java | 55 +- .../camera/features/CameraFeatures.java | 52 +- .../plugins/camera/features/ExposureLock.java | 67 - .../camera/features/ExposureOffset.java | 100 - .../camera/features/ExposureOffsetValue.java | 17 - .../camera/features/ExposurePoint.java | 76 - .../plugins/camera/features/Flash.java | 74 - .../plugins/camera/features/FocusPoint.java | 60 - .../plugins/camera/features/FpsRange.java | 63 - .../camera/features/NoiseReduction.java | 64 - .../plugins/camera/features/Point.java | 16 +- .../camera/features/autoFocus/AutoFocus.java | 85 + .../autoFocus}/FocusMode.java | 2 +- .../features/exposurelock/ExposureLock.java | 51 + .../exposurelock}/ExposureMode.java | 2 +- .../exposureoffset/ExposureOffset.java | 96 + .../exposureoffset/ExposureOffsetValue.java | 17 + .../features/exposurepoint/ExposurePoint.java | 73 + .../plugins/camera/features/flash/Flash.java | 73 + .../{types => features/flash}/FlashMode.java | 2 +- .../features/focuspoint/FocusPoint.java | 54 + .../camera/features/fpsrange/FpsRange.java | 61 + .../noisereduction/NoiseReduction.java | 61 + .../noisereduction/NoiseReductionMode.java | 24 + .../regionboundaries}/CameraRegions.java | 2 +- .../regionboundaries/RegionBoundaries.java | 88 + .../features/resolution/Resolution.java | 111 + .../resolution}/ResolutionPreset.java | 2 +- .../DeviceOrientationManager.java | 15 +- .../sensororientation/SensorOrientation.java | 49 + .../{ => features/zoomlevel}/CameraZoom.java | 2 +- .../camera/features/zoomlevel/ZoomLevel.java | 56 + .../camera/types/NoiseReductionMode.java | 26 - .../io/flutter/plugins/camera/CameraTest.java | 26 +- .../camera/PictureCaptureRequestTest.java | 17 +- .../media/MediaRecorderBuilderTest.java | 36 +- 43 files changed, 2033 insertions(+), 1861 deletions(-) delete mode 100644 packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/AutoFocus.java delete mode 100644 packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/ExposureLock.java delete mode 100644 packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/ExposureOffset.java delete mode 100644 packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/ExposureOffsetValue.java delete mode 100644 packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/ExposurePoint.java delete mode 100644 packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/Flash.java delete mode 100644 packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/FocusPoint.java delete mode 100644 packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/FpsRange.java delete mode 100644 packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/NoiseReduction.java create mode 100644 packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/autoFocus/AutoFocus.java rename packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/{types => features/autoFocus}/FocusMode.java (91%) create mode 100644 packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/exposurelock/ExposureLock.java rename packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/{types => features/exposurelock}/ExposureMode.java (91%) create mode 100644 packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/exposureoffset/ExposureOffset.java create mode 100644 packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/exposureoffset/ExposureOffsetValue.java create mode 100644 packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/exposurepoint/ExposurePoint.java create mode 100644 packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/flash/Flash.java rename packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/{types => features/flash}/FlashMode.java (92%) create mode 100644 packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/focuspoint/FocusPoint.java create mode 100644 packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/fpsrange/FpsRange.java create mode 100644 packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/noisereduction/NoiseReduction.java create mode 100644 packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/noisereduction/NoiseReductionMode.java rename packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/{ => features/regionboundaries}/CameraRegions.java (97%) create mode 100644 packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/regionboundaries/RegionBoundaries.java create mode 100644 packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/resolution/Resolution.java rename packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/{types => features/resolution}/ResolutionPreset.java (83%) rename packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/{ => features/sensororientation}/DeviceOrientationManager.java (95%) create mode 100644 packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/sensororientation/SensorOrientation.java rename packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/{ => features/zoomlevel}/CameraZoom.java (96%) create mode 100644 packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/zoomlevel/ZoomLevel.java delete mode 100644 packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/types/NoiseReductionMode.java 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 ba3d7e194f6d..dd14db71259f 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 @@ -9,7 +9,6 @@ import android.app.Activity; import android.content.Context; import android.graphics.ImageFormat; -import android.graphics.Rect; import android.graphics.SurfaceTexture; import android.hardware.camera2.CameraAccessException; import android.hardware.camera2.CameraCaptureSession; @@ -35,10 +34,36 @@ import android.util.Size; import android.util.SparseIntArray; import android.view.Surface; - import androidx.annotation.NonNull; import androidx.annotation.Nullable; - +import io.flutter.embedding.engine.systemchannels.PlatformChannel; +import io.flutter.plugin.common.EventChannel; +import io.flutter.plugin.common.MethodChannel.Result; +import io.flutter.plugins.camera.features.CameraFeature; +import io.flutter.plugins.camera.features.CameraFeatures; +import io.flutter.plugins.camera.features.Point; +import io.flutter.plugins.camera.features.autofocus.AutoFocus; +import io.flutter.plugins.camera.features.autofocus.FocusMode; +import io.flutter.plugins.camera.features.exposurelock.ExposureLock; +import io.flutter.plugins.camera.features.exposurelock.ExposureMode; +import io.flutter.plugins.camera.features.exposureoffset.ExposureOffset; +import io.flutter.plugins.camera.features.exposureoffset.ExposureOffsetValue; +import io.flutter.plugins.camera.features.exposurepoint.ExposurePoint; +import io.flutter.plugins.camera.features.flash.Flash; +import io.flutter.plugins.camera.features.flash.FlashMode; +import io.flutter.plugins.camera.features.focuspoint.FocusPoint; +import io.flutter.plugins.camera.features.fpsrange.FpsRange; +import io.flutter.plugins.camera.features.noisereduction.NoiseReduction; +import io.flutter.plugins.camera.features.regionboundaries.CameraRegions; +import io.flutter.plugins.camera.features.regionboundaries.RegionBoundaries; +import io.flutter.plugins.camera.features.resolution.Resolution; +import io.flutter.plugins.camera.features.resolution.ResolutionPreset; +import io.flutter.plugins.camera.features.sensororientation.DeviceOrientationManager; +import io.flutter.plugins.camera.features.sensororientation.SensorOrientation; +import io.flutter.plugins.camera.features.zoomlevel.CameraZoom; +import io.flutter.plugins.camera.features.zoomlevel.ZoomLevel; +import io.flutter.plugins.camera.media.MediaRecorderBuilder; +import io.flutter.view.TextureRegistry.SurfaceTextureEntry; import java.io.File; import java.io.IOException; import java.nio.ByteBuffer; @@ -50,1097 +75,1028 @@ import java.util.Map; import java.util.concurrent.Executors; -import io.flutter.embedding.engine.systemchannels.PlatformChannel; -import io.flutter.plugin.common.EventChannel; -import io.flutter.plugin.common.MethodChannel.Result; -import io.flutter.plugins.camera.features.AutoFocus; -import io.flutter.plugins.camera.features.CameraFeature; -import io.flutter.plugins.camera.features.CameraFeatures; -import io.flutter.plugins.camera.features.ExposureLock; -import io.flutter.plugins.camera.features.ExposureOffset; -import io.flutter.plugins.camera.features.ExposureOffsetValue; -import io.flutter.plugins.camera.features.ExposurePoint; -import io.flutter.plugins.camera.features.Flash; -import io.flutter.plugins.camera.features.FocusPoint; -import io.flutter.plugins.camera.features.FpsRange; -import io.flutter.plugins.camera.features.NoiseReduction; -import io.flutter.plugins.camera.features.Point; -import io.flutter.plugins.camera.media.MediaRecorderBuilder; -import io.flutter.plugins.camera.types.ExposureMode; -import io.flutter.plugins.camera.types.FlashMode; -import io.flutter.plugins.camera.types.FocusMode; -import io.flutter.plugins.camera.types.ResolutionPreset; -import io.flutter.view.TextureRegistry.SurfaceTextureEntry; - -import static io.flutter.plugins.camera.CameraUtils.computeBestPreviewSize; - @FunctionalInterface interface ErrorCallback { - void onError(String errorCode, String errorMessage); + void onError(String errorCode, String errorMessage); } /** - * Note: at this time we do not implement zero shutter lag (ZSL) capture. This is a potentail - * improvement we can use in the future. The idea is in a TEMPLATE_ZERO_SHUTTER_LAG capture - * session, the system maintains a ring buffer of images from the preview. It must be in full - * auto moved (flash, ae, focus, etc). When you capture an image, it simply picks one out of - * the ring buffer, thus capturing an image with zero shutter lag. - *

- * This is a potential improvement for the future. A good example is the AOSP camera here: + * Note: at this time we do not implement zero shutter lag (ZSL) capture. This is a potential + * improvement we can use in the future. The idea is in a TEMPLATE_ZERO_SHUTTER_LAG capture session, + * the system maintains a ring buffer of images from the preview. It must be in full auto moved + * (flash, ae, focus, etc). When you capture an image, it simply picks one out of the ring buffer, + * thus capturing an image with zero shutter lag. + * + *

This is a potential improvement for the future. A good example is the AOSP camera here: * https://android.googlesource.com/platform/packages/apps/Camera2/+/9c94ab3/src/com/android/camera/one/v2/OneCameraZslImpl.java - *

- * But one note- they mention sometimes ZSL captures can be very low quality so it might not - * be preferred on some devices. If we do add support for this in the future, we should allow - * it to be enabled from dart. + * + *

But one note- they mention sometimes ZSL captures can be very low quality so it might not be + * preferred on some devices. If we do add support for this in the future, we should allow it to be + * enabled from dart. */ - class Camera implements CameraCaptureCallback.CameraCaptureStateListener { - private static final String TAG = "Camera"; - - /** - * Conversion from screen rotation to JPEG orientation. - */ - private static final SparseIntArray ORIENTATIONS = new SparseIntArray(); - - private static final HashMap supportedImageFormats; - - static { - ORIENTATIONS.append(Surface.ROTATION_0, 90); - ORIENTATIONS.append(Surface.ROTATION_90, 0); - ORIENTATIONS.append(Surface.ROTATION_180, 270); - ORIENTATIONS.append(Surface.ROTATION_270, 180); - } - - // Current supported outputs - static { - supportedImageFormats = new HashMap<>(); - supportedImageFormats.put("yuv420", ImageFormat.YUV_420_888); - supportedImageFormats.put("jpeg", ImageFormat.JPEG); - } - - /** - * Holds all of the camera features/settings and will be used to - * update the request builder when one changes. - */ - private final Map cameraFeatures; - - private final SurfaceTextureEntry flutterTexture; - private final DeviceOrientationManager deviceOrientationListener; - private final int sensorOrientation; - private final Size captureSize; - private final Size previewSize; - private final boolean enableAudio; - private final Context applicationContext; - private final CamcorderProfile recordingProfile; - private final DartMessenger dartMessenger; - private final CameraZoom cameraZoom; - private final CameraProperties cameraProperties; - private final Activity activity; - /** - * This manages the state of the camera and the current capture request. - */ - PictureCaptureRequest pictureCaptureRequest; - /** Whether the current camera device supports auto focus or not. */ - /** - * The state of the camera. By default we are in the preview state. - */ - private CameraState cameraState = CameraState.STATE_PREVIEW; - /** - * A {@link Handler} for running tasks in the background. - */ - private Handler mBackgroundHandler; - /** - * This a callback object for the {@link ImageReader}. "onImageAvailable" will be called when a - * still image is ready to be saved. - */ - private final ImageReader.OnImageAvailableListener mOnImageAvailableListener = - new ImageReader.OnImageAvailableListener() { - @Override - public void onImageAvailable(ImageReader reader) { - Log.i(TAG, "onImageAvailable"); - - // Use acquireNextImage since our image reader is only for 1 image. - mBackgroundHandler.post( - new ImageSaver( - reader.acquireNextImage(), pictureCaptureRequest.file, pictureCaptureRequest)); - cameraState = CameraState.STATE_PREVIEW; - } - }; - - /** - * An additional thread for running tasks that shouldn't block the UI. - */ - private HandlerThread mBackgroundThread; - - private CameraDevice cameraDevice; - private CameraCaptureSession captureSession; - private ImageReader pictureImageReader; - private ImageReader imageStreamReader; - /** - * {@link CaptureRequest.Builder} for the camera preview - */ - private CaptureRequest.Builder mPreviewRequestBuilder; - /** - * {@link CaptureRequest} generated by {@link #mPreviewRequestBuilder} - */ - private MediaRecorder mediaRecorder; - - private boolean recordingVideo; - private File videoRecordingFile; - private CameraRegions cameraRegions; - /** - * A {@link CameraCaptureSession.CaptureCallback} that handles events related to JPEG capture. - */ - private final CameraCaptureCallback mCaptureCallback; - private PlatformChannel.DeviceOrientation lockedCaptureOrientation; - - public Camera( - final Activity activity, - final SurfaceTextureEntry flutterTexture, - final DartMessenger dartMessenger, - final CameraProperties cameraProperties, - final ResolutionPreset resolutionPreset, - final boolean enableAudio) { - - if (activity == null) { - throw new IllegalStateException("No activity available!"); + private static final String TAG = "Camera"; + + /** Conversion from screen rotation to JPEG orientation. */ + private static final SparseIntArray ORIENTATIONS = new SparseIntArray(); + + private static final HashMap supportedImageFormats; + + static { + ORIENTATIONS.append(Surface.ROTATION_0, 90); + ORIENTATIONS.append(Surface.ROTATION_90, 0); + ORIENTATIONS.append(Surface.ROTATION_180, 270); + ORIENTATIONS.append(Surface.ROTATION_270, 180); + } + + // Current supported outputs + static { + supportedImageFormats = new HashMap<>(); + supportedImageFormats.put("yuv420", ImageFormat.YUV_420_888); + supportedImageFormats.put("jpeg", ImageFormat.JPEG); + } + + /** + * Holds all of the camera features/settings and will be used to update the request builder when + * one changes. + */ + private final Map cameraFeatures; + + private final SurfaceTextureEntry flutterTexture; + + private final boolean enableAudio; + private final Context applicationContext; + + private final DartMessenger dartMessenger; + private final CameraProperties cameraProperties; + private final Activity activity; + /** This manages the state of the camera and the current capture request. */ + PictureCaptureRequest pictureCaptureRequest; + /** Whether the current camera device supports auto focus or not. */ + /** The state of the camera. By default we are in the preview state. */ + private CameraState cameraState = CameraState.STATE_PREVIEW; + /** A {@link Handler} for running tasks in the background. */ + private Handler mBackgroundHandler; + /** + * This a callback object for the {@link ImageReader}. "onImageAvailable" will be called when a + * still image is ready to be saved. + */ + private final ImageReader.OnImageAvailableListener mOnImageAvailableListener = + new ImageReader.OnImageAvailableListener() { + @Override + public void onImageAvailable(ImageReader reader) { + Log.i(TAG, "onImageAvailable"); + + // Use acquireNextImage since our image reader is only for 1 image. + mBackgroundHandler.post( + new ImageSaver( + reader.acquireNextImage(), pictureCaptureRequest.file, pictureCaptureRequest)); + cameraState = CameraState.STATE_PREVIEW; } - this.activity = activity; - this.enableAudio = enableAudio; - this.flutterTexture = flutterTexture; - this.dartMessenger = dartMessenger; - this.applicationContext = activity.getApplicationContext(); - this.cameraProperties = cameraProperties; - - // Setup camera features - this.cameraFeatures = new HashMap() {{ + }; + + /** An additional thread for running tasks that shouldn't block the UI. */ + private HandlerThread mBackgroundThread; + + private CameraDevice cameraDevice; + private CameraCaptureSession captureSession; + private ImageReader pictureImageReader; + private ImageReader imageStreamReader; + /** {@link CaptureRequest.Builder} for the camera preview */ + private CaptureRequest.Builder mPreviewRequestBuilder; + /** {@link CaptureRequest} generated by {@link #mPreviewRequestBuilder} */ + private MediaRecorder mediaRecorder; + + private boolean recordingVideo; + private File videoRecordingFile; + /** A {@link CameraCaptureSession.CaptureCallback} that handles events related to JPEG capture. */ + private final CameraCaptureCallback mCaptureCallback; + + private PlatformChannel.DeviceOrientation lockedCaptureOrientation; + + public Camera( + final Activity activity, + final SurfaceTextureEntry flutterTexture, + final DartMessenger dartMessenger, + final CameraProperties cameraProperties, + final ResolutionPreset resolutionPreset, + final boolean enableAudio) { + + if (activity == null) { + throw new IllegalStateException("No activity available!"); + } + this.activity = activity; + this.enableAudio = enableAudio; + this.flutterTexture = flutterTexture; + this.dartMessenger = dartMessenger; + this.applicationContext = activity.getApplicationContext(); + this.cameraProperties = cameraProperties; + + // Setup camera features + this.cameraFeatures = + new HashMap() { + { + put( + CameraFeatures.resolution, + new Resolution(resolutionPreset, cameraProperties.getCameraName())); put(CameraFeatures.autoFocus, new AutoFocus()); - put(CameraFeatures.exposureLock, new ExposureLock(cameraRegions)); // TODO: cameraRegions is going to be null here + put( + CameraFeatures.sensorOrientation, + new SensorOrientation(cameraProperties, activity, dartMessenger)); + put(CameraFeatures.exposureLock, new ExposureLock()); put(CameraFeatures.exposureOffset, new ExposureOffset(cameraProperties)); put(CameraFeatures.exposurePoint, new ExposurePoint(() -> getCameraRegions())); put(CameraFeatures.focusPoint, new FocusPoint(() -> getCameraRegions())); put(CameraFeatures.flash, new Flash()); put(CameraFeatures.fpsRange, new FpsRange(cameraProperties)); put(CameraFeatures.noiseReduction, new NoiseReduction()); - }}; - - mCaptureCallback = CameraCaptureCallback.create(this); - - // Setup orientation - sensorOrientation = cameraProperties.getSensorOrientation(); - boolean isFrontFacing = cameraProperties.getLensFacing() == CameraMetadata.LENS_FACING_FRONT; - - deviceOrientationListener = DeviceOrientationManager.create( - activity, dartMessenger, isFrontFacing, sensorOrientation); - deviceOrientationListener.start(); - - String cameraName = cameraProperties.getCameraName(); - - // Resolution configuration - recordingProfile = - CameraUtils.getBestAvailableCamcorderProfileForResolutionPreset( - cameraName, resolutionPreset); - captureSize = new Size(recordingProfile.videoFrameWidth, recordingProfile.videoFrameHeight); - Log.i(TAG, "captureSize: " + captureSize); - - previewSize = computeBestPreviewSize(cameraName, resolutionPreset); - - // Zoom setup - cameraZoom = - new CameraZoom( - cameraProperties.getSensorInfoActiveArraySize(), - cameraProperties.getScalerAvailableMaxDigitalZoom()); - - // Start background thread. - startBackgroundThread(); + put(CameraFeatures.zoomLevel, new ZoomLevel(cameraProperties)); + put( + CameraFeatures.regionBoundaries, + new RegionBoundaries(cameraProperties, mPreviewRequestBuilder)); + } + }; + + // Create capture callback + mCaptureCallback = CameraCaptureCallback.create(this); + + // Start background thread. + startBackgroundThread(); + } + + @Override + public void onConverged() { + takePictureAfterPrecapture(); + } + + @Override + public void onPrecapture() { + runPrecaptureSequence(); + } + + @Override + public void onPrecaptureTimeout() { + unlockAutoFocus(); + } + + /** Get the current camera state (use for testing). */ + public CameraState getState() { + return this.cameraState; + } + + /** + * Update the builder settings with all of our available features. + * + * @param requestBuilder + */ + private void updateBuilderSettings(CaptureRequest.Builder requestBuilder) { + for (Map.Entry feature : cameraFeatures.entrySet()) { + feature.getValue().updateBuilder(requestBuilder); } + } - @Override - public void onConverged() { - takePictureAfterPrecapture(); - } + private void prepareMediaRecorder(String outputFilePath) throws IOException { + Log.i(TAG, "prepareMediaRecorder"); - @Override - public void onPrecapture() { - runPrecaptureSequence(); + if (mediaRecorder != null) { + mediaRecorder.release(); } - @Override - public void onPrecaptureTimeout() { - unlockAutoFocus(); + mediaRecorder = + new MediaRecorderBuilder(getRecordingProfile(), outputFilePath) + .setEnableAudio(enableAudio) + .setMediaOrientation( + lockedCaptureOrientation == null + ? getDeviceOrientationManager().getMediaOrientation() + : getDeviceOrientationManager().getMediaOrientation(lockedCaptureOrientation)) + .build(); + } + + @SuppressLint("MissingPermission") + public void open(String imageFormatGroup) throws CameraAccessException { + // We always capture using JPEG format. + pictureImageReader = + ImageReader.newInstance( + getCaptureSize().getWidth(), getCaptureSize().getHeight(), ImageFormat.JPEG, 1); + + // For image streaming, we use the provided image format or fall back to YUV420. + Integer imageFormat = supportedImageFormats.get(imageFormatGroup); + if (imageFormat == null) { + Log.w(TAG, "The selected imageFormatGroup is not supported by Android. Defaulting to yuv420"); + imageFormat = ImageFormat.YUV_420_888; } - - /** - * Get the current camera state (use for testing). - */ - public CameraState getState() { - return this.cameraState; - } - - /** - * Update the builder settings with all of our available features. - * - * @param requestBuilder - */ - private void updateBuilderSettings(CaptureRequest.Builder requestBuilder) { - for (Map.Entry feature : cameraFeatures.entrySet()) { - feature.getValue().updateBuilder(requestBuilder); - } - } - - private void prepareMediaRecorder(String outputFilePath) throws IOException { - Log.i(TAG, "prepareMediaRecorder"); - - if (mediaRecorder != null) { - mediaRecorder.release(); - } - - mediaRecorder = - new MediaRecorderBuilder(recordingProfile, outputFilePath) - .setEnableAudio(enableAudio) - .setMediaOrientation( - lockedCaptureOrientation == null - ? deviceOrientationListener.getMediaOrientation() - : deviceOrientationListener.getMediaOrientation(lockedCaptureOrientation)) - .build(); - } - - @SuppressLint("MissingPermission") - public void open(String imageFormatGroup) throws CameraAccessException { - // We always capture using JPEG format. - pictureImageReader = - ImageReader.newInstance( - captureSize.getWidth(), captureSize.getHeight(), ImageFormat.JPEG, 1); - - // For image streaming, we use the provided image format or fall back to YUV420. - Integer imageFormat = supportedImageFormats.get(imageFormatGroup); - if (imageFormat == null) { - Log.w(TAG, "The selected imageFormatGroup is not supported by Android. Defaulting to yuv420"); - imageFormat = ImageFormat.YUV_420_888; - } - imageStreamReader = - ImageReader.newInstance(previewSize.getWidth(), previewSize.getHeight(), imageFormat, 1); - - // Open the camera now - CameraManager cameraManager = CameraUtils.getCameraManager(activity); - cameraManager.openCamera( - cameraProperties.getCameraName(), - new CameraDevice.StateCallback() { - @Override - public void onOpened(@NonNull CameraDevice device) { - Log.i(TAG, "open | onOpened"); - - cameraDevice = device; - try { - startPreview(); - dartMessenger.sendCameraInitializedEvent( - previewSize.getWidth(), - previewSize.getHeight(), - (ExposureMode) cameraFeatures.get(CameraFeatures.exposureLock).getValue(), - (FocusMode) cameraFeatures.get(CameraFeatures.autoFocus).getValue(), - cameraFeatures.get(CameraFeatures.exposurePoint).isSupported(cameraProperties), - isFocusPointSupported()); - } catch (CameraAccessException e) { - dartMessenger.sendCameraErrorEvent(e.getMessage()); - close(); - } - } - - @Override - public void onClosed(@NonNull CameraDevice camera) { - Log.i(TAG, "open | onClosed"); - - dartMessenger.sendCameraClosingEvent(); - super.onClosed(camera); - } - - @Override - public void onDisconnected(@NonNull CameraDevice cameraDevice) { - Log.i(TAG, "open | onDisconnected"); - - close(); - dartMessenger.sendCameraErrorEvent("The camera was disconnected."); - } - - @Override - public void onError(@NonNull CameraDevice cameraDevice, int errorCode) { - Log.i(TAG, "open | onError"); - - close(); - String errorDescription; - switch (errorCode) { - case ERROR_CAMERA_IN_USE: - errorDescription = "The camera device is in use already."; - break; - case ERROR_MAX_CAMERAS_IN_USE: - errorDescription = "Max cameras in use"; - break; - case ERROR_CAMERA_DISABLED: - errorDescription = "The camera device could not be opened due to a device policy."; - break; - case ERROR_CAMERA_DEVICE: - errorDescription = "The camera device has encountered a fatal error"; - break; - case ERROR_CAMERA_SERVICE: - errorDescription = "The camera service has encountered a fatal error."; - break; - default: - errorDescription = "Unknown camera error"; - } - dartMessenger.sendCameraErrorEvent(errorDescription); - } - }, - mBackgroundHandler); - } - - private void createCaptureSession(int templateType, Surface... surfaces) - throws CameraAccessException { - createCaptureSession(templateType, null, surfaces); - } - - private void createCaptureSession( - int templateType, Runnable onSuccessCallback, Surface... surfaces) - throws CameraAccessException { - Log.i(TAG, "createCaptureSession"); - - // Close any existing capture session. - closeCaptureSession(); - - // Create a new capture builder. - mPreviewRequestBuilder = cameraDevice.createCaptureRequest(templateType); - - // Build Flutter surface to render to - SurfaceTexture surfaceTexture = flutterTexture.surfaceTexture(); - surfaceTexture.setDefaultBufferSize(previewSize.getWidth(), previewSize.getHeight()); - Surface flutterSurface = new Surface(surfaceTexture); - mPreviewRequestBuilder.addTarget(flutterSurface); - - List remainingSurfaces = Arrays.asList(surfaces); - if (templateType != CameraDevice.TEMPLATE_PREVIEW) { - // If it is not preview mode, add all surfaces as targets. - for (Surface surface : remainingSurfaces) { - mPreviewRequestBuilder.addTarget(surface); + imageStreamReader = + ImageReader.newInstance( + getPreviewSize().getWidth(), getPreviewSize().getHeight(), imageFormat, 1); + + // Open the camera now + CameraManager cameraManager = CameraUtils.getCameraManager(activity); + cameraManager.openCamera( + cameraProperties.getCameraName(), + new CameraDevice.StateCallback() { + @Override + public void onOpened(@NonNull CameraDevice device) { + Log.i(TAG, "open | onOpened"); + + cameraDevice = device; + try { + startPreview(); + dartMessenger.sendCameraInitializedEvent( + getPreviewSize().getWidth(), + getPreviewSize().getHeight(), + (ExposureMode) cameraFeatures.get(CameraFeatures.exposureLock).getValue(), + (FocusMode) cameraFeatures.get(CameraFeatures.autoFocus).getValue(), + cameraFeatures.get(CameraFeatures.exposurePoint).isSupported(cameraProperties), + cameraFeatures.get(CameraFeatures.focusPoint).isSupported(cameraProperties)); + } catch (CameraAccessException e) { + dartMessenger.sendCameraErrorEvent(e.getMessage()); + close(); } - } - - // Update camera regions - cameraRegions = new CameraRegions(getRegionBoundaries()); - - // Prepare the callback - CameraCaptureSession.StateCallback callback = - new CameraCaptureSession.StateCallback() { - @Override - public void onConfigured(@NonNull CameraCaptureSession session) { - // Camera was already closed. - if (cameraDevice == null) { - dartMessenger.sendCameraErrorEvent("The camera was closed during configuration."); - return; - } - captureSession = session; - - updateBuilderSettings(mPreviewRequestBuilder); - - refreshPreviewCaptureSession( - onSuccessCallback, (code, message) -> dartMessenger.sendCameraErrorEvent(message)); - } - - @Override - public void onConfigureFailed(@NonNull CameraCaptureSession cameraCaptureSession) { - dartMessenger.sendCameraErrorEvent("Failed to configure camera session."); - } - }; - - // Start the session - if (VERSION.SDK_INT >= VERSION_CODES.P) { - // Collect all surfaces we want to render to. - List configs = new ArrayList<>(); - configs.add(new OutputConfiguration(flutterSurface)); - for (Surface surface : remainingSurfaces) { - configs.add(new OutputConfiguration(surface)); + } + + @Override + public void onClosed(@NonNull CameraDevice camera) { + Log.i(TAG, "open | onClosed"); + + dartMessenger.sendCameraClosingEvent(); + super.onClosed(camera); + } + + @Override + public void onDisconnected(@NonNull CameraDevice cameraDevice) { + Log.i(TAG, "open | onDisconnected"); + + close(); + dartMessenger.sendCameraErrorEvent("The camera was disconnected."); + } + + @Override + public void onError(@NonNull CameraDevice cameraDevice, int errorCode) { + Log.i(TAG, "open | onError"); + + close(); + String errorDescription; + switch (errorCode) { + case ERROR_CAMERA_IN_USE: + errorDescription = "The camera device is in use already."; + break; + case ERROR_MAX_CAMERAS_IN_USE: + errorDescription = "Max cameras in use"; + break; + case ERROR_CAMERA_DISABLED: + errorDescription = "The camera device could not be opened due to a device policy."; + break; + case ERROR_CAMERA_DEVICE: + errorDescription = "The camera device has encountered a fatal error"; + break; + case ERROR_CAMERA_SERVICE: + errorDescription = "The camera service has encountered a fatal error."; + break; + default: + errorDescription = "Unknown camera error"; } - createCaptureSessionWithSessionConfig(configs, callback); - } else { - // Collect all surfaces we want to render to. - List surfaceList = new ArrayList<>(); - surfaceList.add(flutterSurface); - surfaceList.addAll(remainingSurfaces); - createCaptureSession(surfaceList, callback); - } - } - - @TargetApi(VERSION_CODES.P) - private void createCaptureSessionWithSessionConfig( - List outputConfigs, CameraCaptureSession.StateCallback callback) - throws CameraAccessException { - cameraDevice.createCaptureSession( - new SessionConfiguration( - SessionConfiguration.SESSION_REGULAR, - outputConfigs, - Executors.newSingleThreadExecutor(), - callback)); - } - - @TargetApi(VERSION_CODES.LOLLIPOP) - @SuppressWarnings("deprecation") - private void createCaptureSession( - List surfaces, CameraCaptureSession.StateCallback callback) - throws CameraAccessException { - cameraDevice.createCaptureSession(surfaces, callback, mBackgroundHandler); + dartMessenger.sendCameraErrorEvent(errorDescription); + } + }, + mBackgroundHandler); + } + + private void createCaptureSession(int templateType, Surface... surfaces) + throws CameraAccessException { + createCaptureSession(templateType, null, surfaces); + } + + private void createCaptureSession( + int templateType, Runnable onSuccessCallback, Surface... surfaces) + throws CameraAccessException { + Log.i(TAG, "createCaptureSession"); + + // Close any existing capture session. + closeCaptureSession(); + + // Create a new capture builder. + mPreviewRequestBuilder = cameraDevice.createCaptureRequest(templateType); + + // Build Flutter surface to render to + SurfaceTexture surfaceTexture = flutterTexture.surfaceTexture(); + surfaceTexture.setDefaultBufferSize(getPreviewSize().getWidth(), getPreviewSize().getHeight()); + Surface flutterSurface = new Surface(surfaceTexture); + mPreviewRequestBuilder.addTarget(flutterSurface); + + List remainingSurfaces = Arrays.asList(surfaces); + if (templateType != CameraDevice.TEMPLATE_PREVIEW) { + // If it is not preview mode, add all surfaces as targets. + for (Surface surface : remainingSurfaces) { + mPreviewRequestBuilder.addTarget(surface); + } } - // Send a repeating request to refresh our capture session. - private void refreshPreviewCaptureSession( - @Nullable Runnable onSuccessCallback, @NonNull ErrorCallback onErrorCallback) { - Log.i(TAG, "refreshPreviewCaptureSession"); - if (captureSession == null) { - Log.i(TAG, "[refreshPreviewCaptureSession] mPreviewSession null, returning"); - return; - } - - try { - captureSession.setRepeatingRequest( - mPreviewRequestBuilder.build(), mCaptureCallback, mBackgroundHandler); - - if (onSuccessCallback != null) { - onSuccessCallback.run(); + // Update camera regions + cameraFeatures.put( + CameraFeatures.regionBoundaries, + new RegionBoundaries(cameraProperties, mPreviewRequestBuilder)); + + // Prepare the callback + CameraCaptureSession.StateCallback callback = + new CameraCaptureSession.StateCallback() { + @Override + public void onConfigured(@NonNull CameraCaptureSession session) { + // Camera was already closed. + if (cameraDevice == null) { + dartMessenger.sendCameraErrorEvent("The camera was closed during configuration."); + return; } + captureSession = session; - } catch (CameraAccessException | IllegalStateException | IllegalArgumentException e) { - onErrorCallback.onError("cameraAccess", e.getMessage()); - } - } + updateBuilderSettings(mPreviewRequestBuilder); - public void takePicture(@NonNull final Result result) { - Log.i(TAG, "takePicture | useAutoFocus: " + cameraFeatures.get(CameraFeatures.autoFocus).getValue()); - - // Only take one 1 picture at a time. - if (pictureCaptureRequest != null && !pictureCaptureRequest.isFinished()) { - result.error("captureAlreadyActive", "Picture is currently already being captured", null); - return; - } - - // Create temporary file - final File outputDir = applicationContext.getCacheDir(); - try { - final File file = File.createTempFile("CAP", ".jpg", outputDir); - - // Start a new capture - pictureCaptureRequest = PictureCaptureRequest.create(result, file, dartMessenger); - mCaptureCallback.setPictureCaptureRequest(pictureCaptureRequest); - } catch (IOException | SecurityException e) { - pictureCaptureRequest.error("cannotCreateFile", e.getMessage(), null); - return; - } - - // Listen for picture being taken - pictureImageReader.setOnImageAvailableListener(mOnImageAvailableListener, mBackgroundHandler); - - if (cameraFeatures.get(CameraFeatures.autoFocus).getValue() == FocusMode.auto) { - runPictureAutoFocus(); - } else { - runPrecaptureSequence(); - } - } - - /** - * Run the precapture sequence for capturing a still image. This method should be called when we - * get a response in {@link #mCaptureCallback} from lockFocus(). - */ - private void runPrecaptureSequence() { - Log.i(TAG, "runPrecaptureSequence"); - try { - // First set precapture state to idle or else it can hang in STATE_WAITING_PRECAPTURE - mPreviewRequestBuilder.set( - CaptureRequest.CONTROL_AE_PRECAPTURE_TRIGGER, - CaptureRequest.CONTROL_AE_PRECAPTURE_TRIGGER_IDLE); - captureSession.capture(mPreviewRequestBuilder.build(), mCaptureCallback, mBackgroundHandler); - - // Repeating request to refresh preview session refreshPreviewCaptureSession( - null, (code, message) -> pictureCaptureRequest.error("cameraAccess", message, null)); - - // Start precapture now - cameraState = CameraState.STATE_WAITING_PRECAPTURE_START; - - mPreviewRequestBuilder.set( - CaptureRequest.CONTROL_AE_PRECAPTURE_TRIGGER, - CaptureRequest.CONTROL_AE_PRECAPTURE_TRIGGER_START); - - // Trigger one capture to start AE sequence - captureSession.capture(mPreviewRequestBuilder.build(), mCaptureCallback, mBackgroundHandler); - - } catch (CameraAccessException e) { - e.printStackTrace(); - } + onSuccessCallback, (code, message) -> dartMessenger.sendCameraErrorEvent(message)); + } + + @Override + public void onConfigureFailed(@NonNull CameraCaptureSession cameraCaptureSession) { + dartMessenger.sendCameraErrorEvent("Failed to configure camera session."); + } + }; + + // Start the session + if (VERSION.SDK_INT >= VERSION_CODES.P) { + // Collect all surfaces we want to render to. + List configs = new ArrayList<>(); + configs.add(new OutputConfiguration(flutterSurface)); + for (Surface surface : remainingSurfaces) { + configs.add(new OutputConfiguration(surface)); + } + createCaptureSessionWithSessionConfig(configs, callback); + } else { + // Collect all surfaces we want to render to. + List surfaceList = new ArrayList<>(); + surfaceList.add(flutterSurface); + surfaceList.addAll(remainingSurfaces); + createCaptureSession(surfaceList, callback); } - - /** - * Capture a still picture. This method should be called when we get a response in {@link - * #mCaptureCallback} from both lockFocus(). - */ - private void takePictureAfterPrecapture() { - Log.i(TAG, "captureStillPicture"); - cameraState = CameraState.STATE_CAPTURING; - pictureCaptureRequest.setState(PictureCaptureRequestState.STATE_CAPTURING); - - try { - if (null == cameraDevice) { - return; - } - // This is the CaptureRequest.Builder that we use to take a picture. - final CaptureRequest.Builder stillBuilder = - cameraDevice.createCaptureRequest(CameraDevice.TEMPLATE_STILL_CAPTURE); - stillBuilder.addTarget(pictureImageReader.getSurface()); - - // Zoom - stillBuilder.set( - CaptureRequest.SCALER_CROP_REGION, - mPreviewRequestBuilder.get(CaptureRequest.SCALER_CROP_REGION)); - - // Update builder settings - updateBuilderSettings(stillBuilder); - - // Orientation - int rotation = activity.getWindowManager().getDefaultDisplay().getRotation(); - stillBuilder.set(CaptureRequest.JPEG_ORIENTATION, getOrientation(rotation)); - - CameraCaptureSession.CaptureCallback captureCallback = - new CameraCaptureSession.CaptureCallback() { - - @Override - public void onCaptureStarted( - @NonNull CameraCaptureSession session, - @NonNull CaptureRequest request, - long timestamp, - long frameNumber) { - Log.i(TAG, "onCaptureStarted"); - } - - @Override - public void onCaptureProgressed( - @NonNull CameraCaptureSession session, - @NonNull CaptureRequest request, - @NonNull CaptureResult partialResult) { - Log.i(TAG, "onCaptureProgressed"); - } - - @Override - public void onCaptureCompleted( - @NonNull CameraCaptureSession session, - @NonNull CaptureRequest request, - @NonNull TotalCaptureResult result) { - Log.i(TAG, "onCaptureCompleted"); - unlockAutoFocus(); - } - }; - - captureSession.stopRepeating(); - captureSession.abortCaptures(); - Log.i(TAG, "sending capture request"); - captureSession.capture(stillBuilder.build(), captureCallback, mBackgroundHandler); - } catch (CameraAccessException e) { - pictureCaptureRequest.error("cameraAccess", e.getMessage(), null); - } + } + + @TargetApi(VERSION_CODES.P) + private void createCaptureSessionWithSessionConfig( + List outputConfigs, CameraCaptureSession.StateCallback callback) + throws CameraAccessException { + cameraDevice.createCaptureSession( + new SessionConfiguration( + SessionConfiguration.SESSION_REGULAR, + outputConfigs, + Executors.newSingleThreadExecutor(), + callback)); + } + + @TargetApi(VERSION_CODES.LOLLIPOP) + @SuppressWarnings("deprecation") + private void createCaptureSession( + List surfaces, CameraCaptureSession.StateCallback callback) + throws CameraAccessException { + cameraDevice.createCaptureSession(surfaces, callback, mBackgroundHandler); + } + + // Send a repeating request to refresh our capture session. + private void refreshPreviewCaptureSession( + @Nullable Runnable onSuccessCallback, @NonNull ErrorCallback onErrorCallback) { + Log.i(TAG, "refreshPreviewCaptureSession"); + if (captureSession == null) { + Log.i(TAG, "[refreshPreviewCaptureSession] mPreviewSession null, returning"); + return; } - /** - * Starts a background thread and its {@link Handler}. TODO: call when activity resumed - */ - private void startBackgroundThread() { - mBackgroundThread = new HandlerThread("CameraBackground"); - mBackgroundThread.start(); - mBackgroundHandler = new Handler(mBackgroundThread.getLooper()); - } + try { + captureSession.setRepeatingRequest( + mPreviewRequestBuilder.build(), mCaptureCallback, mBackgroundHandler); - /** - * Stops the background thread and its {@link Handler}. TODO: call when activity paused - */ - private void stopBackgroundThread() { - try { - if (mBackgroundThread != null) { - mBackgroundThread.quitSafely(); - mBackgroundThread.join(); - mBackgroundThread = null; - } + if (onSuccessCallback != null) { + onSuccessCallback.run(); + } - mBackgroundHandler = null; - } catch (InterruptedException e) { - pictureCaptureRequest.error("cameraAccess", e.getMessage(), null); - } - } - - /** - * Retrieves the JPEG orientation from the specified screen rotation. - * - * @param rotation The screen rotation. - * @return The JPEG orientation (one of 0, 90, 270, and 360) - */ - private int getOrientation(int rotation) { - // Sensor orientation is 90 for most devices, or 270 for some devices (eg. Nexus 5X) - // We have to take that into account and rotate JPEG properly. - // For devices with orientation of 90, we simply return our mapping from ORIENTATIONS. - // For devices with orientation of 270, we need to rotate the JPEG 180 degrees. - return (ORIENTATIONS.get(rotation) + sensorOrientation + 270) % 360; + } catch (CameraAccessException | IllegalStateException | IllegalArgumentException e) { + onErrorCallback.onError("cameraAccess", e.getMessage()); } + } - /** - * Start capturing a picture, doing autofocus first. - */ - private void runPictureAutoFocus() { - Log.i(TAG, "runPictureAutoFocus"); - assert (pictureCaptureRequest != null); + public void takePicture(@NonNull final Result result) { + Log.i( + TAG, + "takePicture | useAutoFocus: " + cameraFeatures.get(CameraFeatures.autoFocus).getValue()); - cameraState = CameraState.STATE_WAITING_FOCUS; - pictureCaptureRequest.setState(PictureCaptureRequestState.STATE_WAITING_FOCUS); - lockAutoFocus(); + // Only take one 1 picture at a time. + if (pictureCaptureRequest != null && !pictureCaptureRequest.isFinished()) { + result.error("captureAlreadyActive", "Picture is currently already being captured", null); + return; } - /** - * Start the autofocus routine on the current capture request. - */ - private void lockAutoFocus() { - Log.i(TAG, "lockAutoFocus"); - pictureCaptureRequest.setState(PictureCaptureRequestState.STATE_WAITING_PRECAPTURE_START); - - mPreviewRequestBuilder.set( - CaptureRequest.CONTROL_AF_TRIGGER, CaptureRequest.CONTROL_AF_TRIGGER_START); - - refreshPreviewCaptureSession( - null, (code, message) -> pictureCaptureRequest.error(code, message, null)); + // Create temporary file + final File outputDir = applicationContext.getCacheDir(); + try { + final File file = File.createTempFile("CAP", ".jpg", outputDir); + + // Start a new capture + pictureCaptureRequest = PictureCaptureRequest.create(result, file, dartMessenger); + mCaptureCallback.setPictureCaptureRequest(pictureCaptureRequest); + } catch (IOException | SecurityException e) { + pictureCaptureRequest.error("cannotCreateFile", e.getMessage(), null); + return; } - /** - * Cancel and reset auto focus state and refresh the preview session. - */ - private void unlockAutoFocus() { - Log.i(TAG, "unlockAutoFocus"); - try { - // Cancel existing AF state - mPreviewRequestBuilder.set( - CaptureRequest.CONTROL_AF_TRIGGER, CameraMetadata.CONTROL_AF_TRIGGER_CANCEL); - captureSession.capture(mPreviewRequestBuilder.build(), null, mBackgroundHandler); - - // Set AE state to idle again - mPreviewRequestBuilder.set( - CaptureRequest.CONTROL_AE_PRECAPTURE_TRIGGER, - CaptureRequest.CONTROL_AE_PRECAPTURE_TRIGGER_IDLE); - - // Set AF state to idle again - mPreviewRequestBuilder.set( - CaptureRequest.CONTROL_AF_TRIGGER, CameraMetadata.CONTROL_AF_TRIGGER_IDLE); - - captureSession.capture(mPreviewRequestBuilder.build(), null, mBackgroundHandler); - } catch (CameraAccessException e) { - Log.i(TAG, "Error unlocking focus: " + e.getMessage()); - dartMessenger.sendCameraErrorEvent(e.getMessage()); - return; - } + // Listen for picture being taken + pictureImageReader.setOnImageAvailableListener(mOnImageAvailableListener, mBackgroundHandler); - refreshPreviewCaptureSession( - null, - (errorCode, errorMessage) -> pictureCaptureRequest.error(errorCode, errorMessage, null)); + if (cameraFeatures.get(CameraFeatures.autoFocus).getValue() == FocusMode.auto) { + runPictureAutoFocus(); + } else { + runPrecaptureSequence(); } - - public void startVideoRecording(Result result) { - final File outputDir = applicationContext.getCacheDir(); - try { - videoRecordingFile = File.createTempFile("REC", ".mp4", outputDir); - } catch (IOException | SecurityException e) { - result.error("cannotCreateFile", e.getMessage(), null); - return; - } - - try { - prepareMediaRecorder(videoRecordingFile.getAbsolutePath()); - recordingVideo = true; - createCaptureSession( - CameraDevice.TEMPLATE_RECORD, () -> mediaRecorder.start(), mediaRecorder.getSurface()); - result.success(null); - } catch (CameraAccessException | IOException e) { - recordingVideo = false; - videoRecordingFile = null; - result.error("videoRecordingFailed", e.getMessage(), null); - } + } + + /** + * Run the precapture sequence for capturing a still image. This method should be called when we + * get a response in {@link #mCaptureCallback} from lockFocus(). + */ + private void runPrecaptureSequence() { + Log.i(TAG, "runPrecaptureSequence"); + try { + // First set precapture state to idle or else it can hang in STATE_WAITING_PRECAPTURE + mPreviewRequestBuilder.set( + CaptureRequest.CONTROL_AE_PRECAPTURE_TRIGGER, + CaptureRequest.CONTROL_AE_PRECAPTURE_TRIGGER_IDLE); + captureSession.capture(mPreviewRequestBuilder.build(), mCaptureCallback, mBackgroundHandler); + + // Repeating request to refresh preview session + refreshPreviewCaptureSession( + null, (code, message) -> pictureCaptureRequest.error("cameraAccess", message, null)); + + // Start precapture now + cameraState = CameraState.STATE_WAITING_PRECAPTURE_START; + + mPreviewRequestBuilder.set( + CaptureRequest.CONTROL_AE_PRECAPTURE_TRIGGER, + CaptureRequest.CONTROL_AE_PRECAPTURE_TRIGGER_START); + + // Trigger one capture to start AE sequence + captureSession.capture(mPreviewRequestBuilder.build(), mCaptureCallback, mBackgroundHandler); + + } catch (CameraAccessException e) { + e.printStackTrace(); } - - public void stopVideoRecording(@NonNull final Result result) { - if (!recordingVideo) { - result.success(null); - return; - } - - try { - recordingVideo = false; - - try { - captureSession.abortCaptures(); - mediaRecorder.stop(); - } catch (CameraAccessException | IllegalStateException e) { - // Ignore exceptions and try to continue (changes are camera session already aborted capture) + } + + /** + * Capture a still picture. This method should be called when we get a response in {@link + * #mCaptureCallback} from both lockFocus(). + */ + private void takePictureAfterPrecapture() { + Log.i(TAG, "captureStillPicture"); + cameraState = CameraState.STATE_CAPTURING; + pictureCaptureRequest.setState(PictureCaptureRequestState.STATE_CAPTURING); + + try { + if (null == cameraDevice) { + return; + } + // This is the CaptureRequest.Builder that we use to take a picture. + final CaptureRequest.Builder stillBuilder = + cameraDevice.createCaptureRequest(CameraDevice.TEMPLATE_STILL_CAPTURE); + stillBuilder.addTarget(pictureImageReader.getSurface()); + + // Zoom + stillBuilder.set( + CaptureRequest.SCALER_CROP_REGION, + mPreviewRequestBuilder.get(CaptureRequest.SCALER_CROP_REGION)); + + // Update builder settings + updateBuilderSettings(stillBuilder); + + // Orientation + int rotation = activity.getWindowManager().getDefaultDisplay().getRotation(); + stillBuilder.set(CaptureRequest.JPEG_ORIENTATION, getOrientation(rotation)); + + CameraCaptureSession.CaptureCallback captureCallback = + new CameraCaptureSession.CaptureCallback() { + + @Override + public void onCaptureStarted( + @NonNull CameraCaptureSession session, + @NonNull CaptureRequest request, + long timestamp, + long frameNumber) { + Log.i(TAG, "onCaptureStarted"); } - mediaRecorder.reset(); - startPreview(); - result.success(videoRecordingFile.getAbsolutePath()); - videoRecordingFile = null; - } catch (CameraAccessException | IllegalStateException e) { - result.error("videoRecordingFailed", e.getMessage(), null); - } - } - - public void pauseVideoRecording(@NonNull final Result result) { - if (!recordingVideo) { - result.success(null); - return; - } - - try { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { - mediaRecorder.pause(); - } else { - result.error("videoRecordingFailed", "pauseVideoRecording requires Android API +24.", null); - return; + @Override + public void onCaptureProgressed( + @NonNull CameraCaptureSession session, + @NonNull CaptureRequest request, + @NonNull CaptureResult partialResult) { + Log.i(TAG, "onCaptureProgressed"); } - } catch (IllegalStateException e) { - result.error("videoRecordingFailed", e.getMessage(), null); - return; - } - - result.success(null); - } - - public void resumeVideoRecording(@NonNull final Result result) { - if (!recordingVideo) { - result.success(null); - return; - } - try { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { - mediaRecorder.resume(); - } else { - result.error( - "videoRecordingFailed", "resumeVideoRecording requires Android API +24.", null); - return; + @Override + public void onCaptureCompleted( + @NonNull CameraCaptureSession session, + @NonNull CaptureRequest request, + @NonNull TotalCaptureResult result) { + Log.i(TAG, "onCaptureCompleted"); + unlockAutoFocus(); } - } catch (IllegalStateException e) { - result.error("videoRecordingFailed", e.getMessage(), null); - return; - } - - result.success(null); + }; + + captureSession.stopRepeating(); + captureSession.abortCaptures(); + Log.i(TAG, "sending capture request"); + captureSession.capture(stillBuilder.build(), captureCallback, mBackgroundHandler); + } catch (CameraAccessException e) { + pictureCaptureRequest.error("cameraAccess", e.getMessage(), null); } - - /** - * Dart handler when it's time to set a new flash mode. This will try to set a new flash mode to - * the current camera. - * - * @param result - * @param newMode - * @throws CameraAccessException - */ - public void setFlashMode(@NonNull final Result result, FlashMode newMode) { - // Save the new flash mode setting - cameraFeatures.get(CameraFeatures.flash).setValue(newMode); - cameraFeatures.get(CameraFeatures.flash).updateBuilder(mPreviewRequestBuilder); - - refreshPreviewCaptureSession( - () -> result.success(null), - (code, message) -> result.error("setFlashModeFailed", "Could not set flash mode.", null)); + } + + /** Starts a background thread and its {@link Handler}. TODO: call when activity resumed */ + private void startBackgroundThread() { + mBackgroundThread = new HandlerThread("CameraBackground"); + mBackgroundThread.start(); + mBackgroundHandler = new Handler(mBackgroundThread.getLooper()); + } + + /** Stops the background thread and its {@link Handler}. TODO: call when activity paused */ + private void stopBackgroundThread() { + try { + if (mBackgroundThread != null) { + mBackgroundThread.quitSafely(); + mBackgroundThread.join(); + mBackgroundThread = null; + } + + mBackgroundHandler = null; + } catch (InterruptedException e) { + pictureCaptureRequest.error("cameraAccess", e.getMessage(), null); } - - /** - * Dart handler for setting new exposure mode setting. - * - * @param result - * @param newMode - * @throws CameraAccessException - */ - public void setExposureMode(@NonNull final Result result, ExposureMode newMode) { - cameraFeatures.get(CameraFeatures.exposureLock).setValue(newMode); - cameraFeatures.get(CameraFeatures.exposureLock).updateBuilder(mPreviewRequestBuilder); - - refreshPreviewCaptureSession( - () -> result.success(null), - (code, message) -> - result.error("setExposureModeFailed", "Could not set exposure mode.", null)); + } + + /** + * Retrieves the JPEG orientation from the specified screen rotation. + * + * @param rotation The screen rotation. + * @return The JPEG orientation (one of 0, 90, 270, and 360) + */ + private int getOrientation(int rotation) { + // Sensor orientation is 90 for most devices, or 270 for some devices (eg. Nexus 5X) + // We have to take that into account and rotate JPEG properly. + // For devices with orientation of 90, we simply return our mapping from ORIENTATIONS. + // For devices with orientation of 270, we need to rotate the JPEG 180 degrees. + final Integer sensorOrientation = + (Integer) cameraFeatures.get(CameraFeatures.sensorOrientation).getValue(); + return (ORIENTATIONS.get(rotation) + sensorOrientation + 270) % 360; + } + + /** Start capturing a picture, doing autofocus first. */ + private void runPictureAutoFocus() { + Log.i(TAG, "runPictureAutoFocus"); + assert (pictureCaptureRequest != null); + + cameraState = CameraState.STATE_WAITING_FOCUS; + pictureCaptureRequest.setState(PictureCaptureRequestState.STATE_WAITING_FOCUS); + lockAutoFocus(); + } + + /** Start the autofocus routine on the current capture request. */ + private void lockAutoFocus() { + Log.i(TAG, "lockAutoFocus"); + pictureCaptureRequest.setState(PictureCaptureRequestState.STATE_WAITING_PRECAPTURE_START); + + mPreviewRequestBuilder.set( + CaptureRequest.CONTROL_AF_TRIGGER, CaptureRequest.CONTROL_AF_TRIGGER_START); + + refreshPreviewCaptureSession( + null, (code, message) -> pictureCaptureRequest.error(code, message, null)); + } + + /** Cancel and reset auto focus state and refresh the preview session. */ + private void unlockAutoFocus() { + Log.i(TAG, "unlockAutoFocus"); + try { + // Cancel existing AF state + mPreviewRequestBuilder.set( + CaptureRequest.CONTROL_AF_TRIGGER, CameraMetadata.CONTROL_AF_TRIGGER_CANCEL); + captureSession.capture(mPreviewRequestBuilder.build(), null, mBackgroundHandler); + + // Set AE state to idle again + mPreviewRequestBuilder.set( + CaptureRequest.CONTROL_AE_PRECAPTURE_TRIGGER, + CaptureRequest.CONTROL_AE_PRECAPTURE_TRIGGER_IDLE); + + // Set AF state to idle again + mPreviewRequestBuilder.set( + CaptureRequest.CONTROL_AF_TRIGGER, CameraMetadata.CONTROL_AF_TRIGGER_IDLE); + + captureSession.capture(mPreviewRequestBuilder.build(), null, mBackgroundHandler); + } catch (CameraAccessException e) { + Log.i(TAG, "Error unlocking focus: " + e.getMessage()); + dartMessenger.sendCameraErrorEvent(e.getMessage()); + return; } - /** - * Get the current camera regions. Used in ExposurePoint feature so it can - * always get a reference to the latest camera regions instance here. - *

- * The CameraRegions will be replaced every time a new capture session is started. - * - * @return - */ - public CameraRegions getCameraRegions() { - return this.cameraRegions; + refreshPreviewCaptureSession( + null, + (errorCode, errorMessage) -> pictureCaptureRequest.error(errorCode, errorMessage, null)); + } + + public void startVideoRecording(Result result) { + final File outputDir = applicationContext.getCacheDir(); + try { + videoRecordingFile = File.createTempFile("REC", ".mp4", outputDir); + } catch (IOException | SecurityException e) { + result.error("cannotCreateFile", e.getMessage(), null); + return; } - /** - * Set new exposure point from dart. - * - * @param result - * @param x - * @param y - */ - public void setExposurePoint(@NonNull final Result result, Double x, Double y) { - cameraFeatures.get(CameraFeatures.exposurePoint).setValue(new Point(x, y)); - cameraFeatures.get(CameraFeatures.exposurePoint).updateBuilder(mPreviewRequestBuilder); - - refreshPreviewCaptureSession( - () -> result.success(null), - (code, message) -> - result.error("setExposurePointFailed", "Could not set exposure point.", null)); - + try { + prepareMediaRecorder(videoRecordingFile.getAbsolutePath()); + recordingVideo = true; + createCaptureSession( + CameraDevice.TEMPLATE_RECORD, () -> mediaRecorder.start(), mediaRecorder.getSurface()); + result.success(null); + } catch (CameraAccessException | IOException e) { + recordingVideo = false; + videoRecordingFile = null; + result.error("videoRecordingFailed", e.getMessage(), null); } + } - /** - * Return the max exposure offset value supported by the camera to dart. - */ - public double getMaxExposureOffset() { - final ExposureOffsetValue val = (ExposureOffsetValue) cameraFeatures.get(CameraFeatures.exposureOffset).getValue(); - return val.max; + public void stopVideoRecording(@NonNull final Result result) { + if (!recordingVideo) { + result.success(null); + return; } - /** - * Return the min exposure offset value supported by the camera to dart. - */ - public double getMinExposureOffset() { - final ExposureOffsetValue val = (ExposureOffsetValue) cameraFeatures.get(CameraFeatures.exposureOffset).getValue(); - return val.min; + try { + recordingVideo = false; + + try { + captureSession.abortCaptures(); + mediaRecorder.stop(); + } catch (CameraAccessException | IllegalStateException e) { + // Ignore exceptions and try to continue (changes are camera session already aborted capture) + } + + mediaRecorder.reset(); + startPreview(); + result.success(videoRecordingFile.getAbsolutePath()); + videoRecordingFile = null; + } catch (CameraAccessException | IllegalStateException e) { + result.error("videoRecordingFailed", e.getMessage(), null); } + } - /** - * Set new focus mode from dart. - * - * @param result - * @param newMode - * @throws CameraAccessException - */ - public void setFocusMode(@NonNull final Result result, FocusMode newMode) { - cameraFeatures.get(CameraFeatures.autoFocus).setValue(newMode); - cameraFeatures.get(CameraFeatures.autoFocus).updateBuilder(mPreviewRequestBuilder); - - refreshPreviewCaptureSession( - () -> result.success(null), - (code, message) -> - result.error("setFocusModeFailed", "Could not set focus mode.", null)); + public void pauseVideoRecording(@NonNull final Result result) { + if (!recordingVideo) { + result.success(null); + return; } - /** - * Sets new focus point from dart. - * - * @param result - * @param x - * @param y - */ - public void setFocusPoint(@NonNull final Result result, Double x, Double y) { - cameraFeatures.get(CameraFeatures.focusPoint).setValue(new Point(x, y)); - cameraFeatures.get(CameraFeatures.focusPoint).updateBuilder(mPreviewRequestBuilder); - - refreshPreviewCaptureSession( - () -> result.success(null), - (code, message) -> - result.error("setFocusPointFailed", "Could not set focus point.", null)); - + try { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { + mediaRecorder.pause(); + } else { + result.error("videoRecordingFailed", "pauseVideoRecording requires Android API +24.", null); + return; + } + } catch (IllegalStateException e) { + result.error("videoRecordingFailed", e.getMessage(), null); + return; } - @TargetApi(VERSION_CODES.P) - private boolean supportsDistortionCorrection() { - int[] availableDistortionCorrectionModes = - cameraProperties.getDistortionCorrectionAvailableModes(); - if (availableDistortionCorrectionModes == null) - availableDistortionCorrectionModes = new int[0]; - long nonOffModesSupported = - Arrays.stream(availableDistortionCorrectionModes) - .filter((value) -> value != CaptureRequest.DISTORTION_CORRECTION_MODE_OFF) - .count(); - return nonOffModesSupported > 0; - } + result.success(null); + } - private Size getRegionBoundaries() { - // No distortion correction support - if (android.os.Build.VERSION.SDK_INT < VERSION_CODES.P || !supportsDistortionCorrection()) { - return cameraProperties.getSensorInfoPixelArraySize(); - } - // Get the current distortion correction mode - Integer distortionCorrectionMode = - mPreviewRequestBuilder.get(CaptureRequest.DISTORTION_CORRECTION_MODE); - // Return the correct boundaries depending on the mode - android.graphics.Rect rect; - if (distortionCorrectionMode == null - || distortionCorrectionMode == CaptureRequest.DISTORTION_CORRECTION_MODE_OFF) { - rect = cameraProperties.getSensorInfoPreCorrectionActiveArraySize(); - } else { - rect = cameraProperties.getSensorInfoActiveArraySize(); - } - return rect == null ? null : new Size(rect.width(), rect.height()); + public void resumeVideoRecording(@NonNull final Result result) { + if (!recordingVideo) { + result.success(null); + return; } - private boolean isFocusPointSupported() { - Integer supportedRegions = cameraProperties.getControlMaxRegionsAutoFocus(); - return supportedRegions != null && supportedRegions > 0; + try { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { + mediaRecorder.resume(); + } else { + result.error( + "videoRecordingFailed", "resumeVideoRecording requires Android API +24.", null); + return; + } + } catch (IllegalStateException e) { + result.error("videoRecordingFailed", e.getMessage(), null); + return; } - /** - * Set a new exposure offset from dart. From dart the offset comes as a double, like +1.3 or -1.3. - * - * @param result - * @param offset - */ - public void setExposureOffset(@NonNull final Result result, double offset) { - cameraFeatures.get(CameraFeatures.exposureOffset).setValue(offset); - cameraFeatures.get(CameraFeatures.exposureOffset).updateBuilder(mPreviewRequestBuilder); - - refreshPreviewCaptureSession( - () -> result.success(null), - (code, message) -> - result.error("setFocusModeFailed", "Could not set focus mode.", null)); + result.success(null); + } + + /** + * Dart handler when it's time to set a new flash mode. This will try to set a new flash mode to + * the current camera. + * + * @param result + * @param newMode + * @throws CameraAccessException + */ + public void setFlashMode(@NonNull final Result result, FlashMode newMode) { + // Save the new flash mode setting + cameraFeatures.get(CameraFeatures.flash).setValue(newMode); + cameraFeatures.get(CameraFeatures.flash).updateBuilder(mPreviewRequestBuilder); + + refreshPreviewCaptureSession( + () -> result.success(null), + (code, message) -> result.error("setFlashModeFailed", "Could not set flash mode.", null)); + } + + /** + * Dart handler for setting new exposure mode setting. + * + * @param result + * @param newMode + * @throws CameraAccessException + */ + public void setExposureMode(@NonNull final Result result, ExposureMode newMode) { + cameraFeatures.get(CameraFeatures.exposureLock).setValue(newMode); + cameraFeatures.get(CameraFeatures.exposureLock).updateBuilder(mPreviewRequestBuilder); + + refreshPreviewCaptureSession( + () -> result.success(null), + (code, message) -> + result.error("setExposureModeFailed", "Could not set exposure mode.", null)); + } + + /** + * Get the current camera regions. Used in ExposurePoint feature so it can always get a reference + * to the latest camera regions instance here. + * + *

The CameraRegions will be replaced every time a new capture session is started. + * + * @return + */ + public CameraRegions getCameraRegions() { + final RegionBoundaries regionBoundaries = + (RegionBoundaries) cameraFeatures.get(CameraFeatures.regionBoundaries); + return regionBoundaries.getCameraRegions(); + } + + /** + * Set new exposure point from dart. + * + * @param result + * @param x + * @param y + */ + public void setExposurePoint(@NonNull final Result result, Double x, Double y) { + cameraFeatures.get(CameraFeatures.exposurePoint).setValue(new Point(x, y)); + cameraFeatures.get(CameraFeatures.exposurePoint).updateBuilder(mPreviewRequestBuilder); + + refreshPreviewCaptureSession( + () -> result.success(null), + (code, message) -> + result.error("setExposurePointFailed", "Could not set exposure point.", null)); + } + + /** Return the max exposure offset value supported by the camera to dart. */ + public double getMaxExposureOffset() { + final ExposureOffsetValue val = + (ExposureOffsetValue) cameraFeatures.get(CameraFeatures.exposureOffset).getValue(); + return val.max; + } + + /** Return the min exposure offset value supported by the camera to dart. */ + public double getMinExposureOffset() { + final ExposureOffsetValue val = + (ExposureOffsetValue) cameraFeatures.get(CameraFeatures.exposureOffset).getValue(); + return val.min; + } + + /** Return the exposure offset step size to dart. */ + public double getExposureOffsetStepSize() { + final ExposureOffset val = (ExposureOffset) cameraFeatures.get(CameraFeatures.exposureOffset); + return val.getExposureOffsetStepSize(cameraProperties); + } + + /** + * Set new focus mode from dart. + * + * @param result + * @param newMode + * @throws CameraAccessException + */ + public void setFocusMode(@NonNull final Result result, FocusMode newMode) { + cameraFeatures.get(CameraFeatures.autoFocus).setValue(newMode); + cameraFeatures.get(CameraFeatures.autoFocus).updateBuilder(mPreviewRequestBuilder); + + refreshPreviewCaptureSession( + () -> result.success(null), + (code, message) -> result.error("setFocusModeFailed", "Could not set focus mode.", null)); + } + + /** + * Sets new focus point from dart. + * + * @param result + * @param x + * @param y + */ + public void setFocusPoint(@NonNull final Result result, Double x, Double y) { + cameraFeatures.get(CameraFeatures.focusPoint).setValue(new Point(x, y)); + cameraFeatures.get(CameraFeatures.focusPoint).updateBuilder(mPreviewRequestBuilder); + + refreshPreviewCaptureSession( + () -> result.success(null), + (code, message) -> result.error("setFocusPointFailed", "Could not set focus point.", null)); + } + + /** + * Set a new exposure offset from dart. From dart the offset comes as a double, like +1.3 or -1.3. + * + * @param result + * @param offset + */ + public void setExposureOffset(@NonNull final Result result, double offset) { + cameraFeatures.get(CameraFeatures.exposureOffset).setValue(offset); + cameraFeatures.get(CameraFeatures.exposureOffset).updateBuilder(mPreviewRequestBuilder); + + refreshPreviewCaptureSession( + () -> result.success(null), + (code, message) -> result.error("setFocusModeFailed", "Could not set focus mode.", null)); + } + + public float getMaxZoomLevel() { + final ZoomLevel zoomLevel = (ZoomLevel) cameraFeatures.get(CameraFeatures.zoomLevel); + return zoomLevel.getCameraZoom().maxZoom; + } + + public float getMinZoomLevel() { + return CameraZoom.DEFAULT_ZOOM_FACTOR; + } + + /** Shortcut to get current preview size. */ + Size getPreviewSize() { + return ((Resolution) cameraFeatures.get(CameraFeatures.resolution)).getPreviewSize(); + } + + /** Shortcut to get current capture size. */ + Size getCaptureSize() { + return ((Resolution) cameraFeatures.get(CameraFeatures.resolution)).getCaptureSize(); + } + + /** Shortcut to get current recording profile. */ + CamcorderProfile getRecordingProfile() { + return ((Resolution) cameraFeatures.get(CameraFeatures.resolution)).getRecordingProfile(); + } + + /** Shortut to get deviceOrientationListener. */ + DeviceOrientationManager getDeviceOrientationManager() { + return ((SensorOrientation) cameraFeatures.get(CameraFeatures.sensorOrientation)) + .getDeviceOrientationManager(); + } + + /** + * Set zoom level from dart. + * + * @param result + * @param zoom + * @throws CameraAccessException + */ + public void setZoomLevel(@NonNull final Result result, float zoom) throws CameraAccessException { + final ZoomLevel zoomLevel = (ZoomLevel) cameraFeatures.get(CameraFeatures.zoomLevel); + float maxZoom = zoomLevel.getCameraZoom().maxZoom; + float minZoom = CameraZoom.DEFAULT_ZOOM_FACTOR; + + if (zoom > maxZoom || zoom < minZoom) { + String errorMessage = + String.format( + Locale.ENGLISH, + "Zoom level out of bounds (zoom level should be between %f and %f).", + minZoom, + maxZoom); + result.error("ZOOM_ERROR", errorMessage, null); + return; } - public float getMaxZoomLevel() { - return cameraZoom.maxZoom; + zoomLevel.setValue(zoom); + zoomLevel.updateBuilder(mPreviewRequestBuilder); + + refreshPreviewCaptureSession( + () -> result.success(null), + (code, message) -> result.error("setZoomLevelFailed", "Could not set zoom level.", null)); + } + + public void lockCaptureOrientation(PlatformChannel.DeviceOrientation orientation) { + this.lockedCaptureOrientation = orientation; + } + + public void unlockCaptureOrientation() { + this.lockedCaptureOrientation = null; + } + + public void startPreview() throws CameraAccessException { + if (pictureImageReader == null || pictureImageReader.getSurface() == null) return; + Log.i(TAG, "startPreview"); + + createCaptureSession(CameraDevice.TEMPLATE_PREVIEW, pictureImageReader.getSurface()); + } + + public void startPreviewWithImageStream(EventChannel imageStreamChannel) + throws CameraAccessException { + createCaptureSession(CameraDevice.TEMPLATE_RECORD, imageStreamReader.getSurface()); + Log.i(TAG, "startPreviewWithImageStream"); + + imageStreamChannel.setStreamHandler( + new EventChannel.StreamHandler() { + @Override + public void onListen(Object o, EventChannel.EventSink imageStreamSink) { + setImageStreamImageAvailableListener(imageStreamSink); + } + + @Override + public void onCancel(Object o) { + imageStreamReader.setOnImageAvailableListener(null, mBackgroundHandler); + } + }); + } + + private void setImageStreamImageAvailableListener(final EventChannel.EventSink imageStreamSink) { + imageStreamReader.setOnImageAvailableListener( + reader -> { + // Use acquireNextImage since our image reader is only for 1 image. + Image img = reader.acquireNextImage(); + if (img == null) return; + + List> planes = new ArrayList<>(); + for (Image.Plane plane : img.getPlanes()) { + ByteBuffer buffer = plane.getBuffer(); + + byte[] bytes = new byte[buffer.remaining()]; + buffer.get(bytes, 0, bytes.length); + + Map planeBuffer = new HashMap<>(); + planeBuffer.put("bytesPerRow", plane.getRowStride()); + planeBuffer.put("bytesPerPixel", plane.getPixelStride()); + planeBuffer.put("bytes", bytes); + + planes.add(planeBuffer); + } + + Map imageBuffer = new HashMap<>(); + imageBuffer.put("width", img.getWidth()); + imageBuffer.put("height", img.getHeight()); + imageBuffer.put("format", img.getFormat()); + imageBuffer.put("planes", planes); + + final Handler handler = new Handler(Looper.getMainLooper()); + handler.post(() -> imageStreamSink.success(imageBuffer)); + img.close(); + }, + mBackgroundHandler); + } + + private void closeCaptureSession() { + if (captureSession != null) { + Log.i(TAG, "closeCaptureSession"); + + captureSession.close(); + captureSession = null; } + } - public float getMinZoomLevel() { - return CameraZoom.DEFAULT_ZOOM_FACTOR; - } - - public void setZoomLevel(@NonNull final Result result, float zoom) throws CameraAccessException { - float maxZoom = cameraZoom.maxZoom; - float minZoom = CameraZoom.DEFAULT_ZOOM_FACTOR; - - if (zoom > maxZoom || zoom < minZoom) { - String errorMessage = - String.format( - Locale.ENGLISH, - "Zoom level out of bounds (zoom level should be between %f and %f).", - minZoom, - maxZoom); - result.error("ZOOM_ERROR", errorMessage, null); - return; - } + public void close() { + Log.i(TAG, "close"); + closeCaptureSession(); - //Zoom area is calculated relative to sensor area (activeRect) - if (mPreviewRequestBuilder != null) { - final Rect computedZoom = cameraZoom.computeZoom(zoom); - mPreviewRequestBuilder.set(CaptureRequest.SCALER_CROP_REGION, computedZoom); - captureSession.setRepeatingRequest(mPreviewRequestBuilder.build(), null, mBackgroundHandler); - } - - result.success(null); + if (cameraDevice != null) { + cameraDevice.close(); + cameraDevice = null; } - - public void lockCaptureOrientation(PlatformChannel.DeviceOrientation orientation) { - this.lockedCaptureOrientation = orientation; + if (pictureImageReader != null) { + pictureImageReader.close(); + pictureImageReader = null; } - - public void unlockCaptureOrientation() { - this.lockedCaptureOrientation = null; + if (imageStreamReader != null) { + imageStreamReader.close(); + imageStreamReader = null; } - - public void startPreview() throws CameraAccessException { - if (pictureImageReader == null || pictureImageReader.getSurface() == null) return; - Log.i(TAG, "startPreview"); - - createCaptureSession(CameraDevice.TEMPLATE_PREVIEW, pictureImageReader.getSurface()); - } - - public void startPreviewWithImageStream(EventChannel imageStreamChannel) - throws CameraAccessException { - createCaptureSession(CameraDevice.TEMPLATE_RECORD, imageStreamReader.getSurface()); - Log.i(TAG, "startPreviewWithImageStream"); - - imageStreamChannel.setStreamHandler( - new EventChannel.StreamHandler() { - @Override - public void onListen(Object o, EventChannel.EventSink imageStreamSink) { - setImageStreamImageAvailableListener(imageStreamSink); - } - - @Override - public void onCancel(Object o) { - imageStreamReader.setOnImageAvailableListener(null, mBackgroundHandler); - } - }); - } - - private void setImageStreamImageAvailableListener(final EventChannel.EventSink imageStreamSink) { - imageStreamReader.setOnImageAvailableListener( - reader -> { - // Use acquireNextImage since our image reader is only for 1 image. - Image img = reader.acquireNextImage(); - if (img == null) return; - - List> planes = new ArrayList<>(); - for (Image.Plane plane : img.getPlanes()) { - ByteBuffer buffer = plane.getBuffer(); - - byte[] bytes = new byte[buffer.remaining()]; - buffer.get(bytes, 0, bytes.length); - - Map planeBuffer = new HashMap<>(); - planeBuffer.put("bytesPerRow", plane.getRowStride()); - planeBuffer.put("bytesPerPixel", plane.getPixelStride()); - planeBuffer.put("bytes", bytes); - - planes.add(planeBuffer); - } - - Map imageBuffer = new HashMap<>(); - imageBuffer.put("width", img.getWidth()); - imageBuffer.put("height", img.getHeight()); - imageBuffer.put("format", img.getFormat()); - imageBuffer.put("planes", planes); - - final Handler handler = new Handler(Looper.getMainLooper()); - handler.post(() -> imageStreamSink.success(imageBuffer)); - img.close(); - }, - mBackgroundHandler); - } - - private void closeCaptureSession() { - if (captureSession != null) { - Log.i(TAG, "closeCaptureSession"); - - captureSession.close(); - captureSession = null; - } + if (mediaRecorder != null) { + mediaRecorder.reset(); + mediaRecorder.release(); + mediaRecorder = null; } - public void close() { - Log.i(TAG, "close"); - closeCaptureSession(); + stopBackgroundThread(); + } - if (cameraDevice != null) { - cameraDevice.close(); - cameraDevice = null; - } - if (pictureImageReader != null) { - pictureImageReader.close(); - pictureImageReader = null; - } - if (imageStreamReader != null) { - imageStreamReader.close(); - imageStreamReader = null; - } - if (mediaRecorder != null) { - mediaRecorder.reset(); - mediaRecorder.release(); - mediaRecorder = null; - } + public void dispose() { + Log.i(TAG, "dispose"); - stopBackgroundThread(); - } - - public void dispose() { - Log.i(TAG, "dispose"); - - close(); - flutterTexture.release(); - deviceOrientationListener.stop(); - } + close(); + flutterTexture.release(); + getDeviceOrientationManager().stop(); + } } diff --git a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/CameraCaptureCallback.java b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/CameraCaptureCallback.java index 67b872c01179..91c5c8701cc1 100644 --- a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/CameraCaptureCallback.java +++ b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/CameraCaptureCallback.java @@ -10,7 +10,9 @@ class CameraCaptureCallback extends CaptureCallback { interface CameraCaptureStateListener { void onConverged(); + void onPrecapture(); + void onPrecaptureTimeout(); } @@ -24,8 +26,7 @@ public static CameraCaptureCallback create( return new CameraCaptureCallback(cameraStateListener); } - private CameraCaptureCallback( - @NonNull CameraCaptureStateListener cameraStateListener) { + private CameraCaptureCallback(@NonNull CameraCaptureStateListener cameraStateListener) { cameraState = CameraState.STATE_PREVIEW; this.cameraStateListener = cameraStateListener; this.pictureCaptureRequest = pictureCaptureRequest; @@ -39,8 +40,7 @@ public void setCameraState(@NonNull CameraState state) { cameraState = state; if (pictureCaptureRequest != null && state == CameraState.STATE_WAITING_PRECAPTURE_DONE) { - pictureCaptureRequest.setState( - PictureCaptureRequestState.STATE_WAITING_PRECAPTURE_DONE); + pictureCaptureRequest.setState(PictureCaptureRequestState.STATE_WAITING_PRECAPTURE_DONE); } } @@ -54,51 +54,52 @@ private void process(CaptureResult result) { switch (cameraState) { case STATE_PREVIEW: - { - // We have nothing to do when the camera preview is working normally. - break; - } + { + // We have nothing to do when the camera preview is working normally. + break; + } case STATE_WAITING_FOCUS: - { - if (afState == null) { - return; - } else if (afState == CaptureRequest.CONTROL_AF_STATE_PASSIVE_SCAN - || afState == CaptureRequest.CONTROL_AF_STATE_FOCUSED_LOCKED - || afState == CaptureRequest.CONTROL_AF_STATE_NOT_FOCUSED_LOCKED) { - // CONTROL_AE_STATE can be null on some devices - - if (aeState == null || aeState == CaptureRequest.CONTROL_AE_STATE_CONVERGED) { - cameraStateListener.onConverged(); - } else { - cameraStateListener.onPrecapture(); + { + if (afState == null) { + return; + } else if (afState == CaptureRequest.CONTROL_AF_STATE_PASSIVE_SCAN + || afState == CaptureRequest.CONTROL_AF_STATE_FOCUSED_LOCKED + || afState == CaptureRequest.CONTROL_AF_STATE_NOT_FOCUSED_LOCKED) { + // CONTROL_AE_STATE can be null on some devices + + if (aeState == null || aeState == CaptureRequest.CONTROL_AE_STATE_CONVERGED) { + cameraStateListener.onConverged(); + } else { + cameraStateListener.onPrecapture(); + } } + break; } - break; - } case STATE_WAITING_PRECAPTURE_START: - { - // CONTROL_AE_STATE can be null on some devices - if (aeState == null - || aeState == CaptureResult.CONTROL_AE_STATE_CONVERGED - || aeState == CaptureResult.CONTROL_AE_STATE_PRECAPTURE - || aeState == CaptureResult.CONTROL_AE_STATE_FLASH_REQUIRED) { - setCameraState(CameraState.STATE_WAITING_PRECAPTURE_DONE); + { + // CONTROL_AE_STATE can be null on some devices + if (aeState == null + || aeState == CaptureResult.CONTROL_AE_STATE_CONVERGED + || aeState == CaptureResult.CONTROL_AE_STATE_PRECAPTURE + || aeState == CaptureResult.CONTROL_AE_STATE_FLASH_REQUIRED) { + setCameraState(CameraState.STATE_WAITING_PRECAPTURE_DONE); + } + break; } - break; - } case STATE_WAITING_PRECAPTURE_DONE: - { - // CONTROL_AE_STATE can be null on some devices - if (aeState == null || aeState != CaptureResult.CONTROL_AE_STATE_PRECAPTURE) { - cameraStateListener.onConverged(); - } else if (pictureCaptureRequest != null && pictureCaptureRequest.hitPreCaptureTimeout()) { - // Log.i(TAG, "===> Hit precapture timeout"); - cameraStateListener.onPrecaptureTimeout(); + { + // CONTROL_AE_STATE can be null on some devices + if (aeState == null || aeState != CaptureResult.CONTROL_AE_STATE_PRECAPTURE) { + cameraStateListener.onConverged(); + } else if (pictureCaptureRequest != null + && pictureCaptureRequest.hitPreCaptureTimeout()) { + // Log.i(TAG, "===> Hit precapture timeout"); + cameraStateListener.onPrecaptureTimeout(); + } + break; } - break; - } } } diff --git a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/CameraProperties.java b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/CameraProperties.java index 1483268a255f..bc669709df99 100644 --- a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/CameraProperties.java +++ b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/CameraProperties.java @@ -143,6 +143,7 @@ public int getSensorOrientation() { /** * Returns the hardware level of the camera. + * * @return */ @Override @@ -152,6 +153,7 @@ public int getHardwareLevel() { @Override public int[] getAvailableNoiseReductionModes() { - return cameraCharacteristics.get(CameraCharacteristics.NOISE_REDUCTION_AVAILABLE_NOISE_REDUCTION_MODES); + return cameraCharacteristics.get( + CameraCharacteristics.NOISE_REDUCTION_AVAILABLE_NOISE_REDUCTION_MODES); } } diff --git a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/CameraUtils.java b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/CameraUtils.java index 3cd8313bf070..847095aa5f23 100644 --- a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/CameraUtils.java +++ b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/CameraUtils.java @@ -12,10 +12,8 @@ import android.hardware.camera2.CameraManager; import android.hardware.camera2.CameraMetadata; import android.hardware.camera2.params.StreamConfigurationMap; -import android.media.CamcorderProfile; import android.util.Size; import io.flutter.embedding.engine.systemchannels.PlatformChannel; -import io.flutter.plugins.camera.types.ResolutionPreset; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; @@ -86,16 +84,6 @@ static PlatformChannel.DeviceOrientation deserializeDeviceOrientation(String ori } } - static Size computeBestPreviewSize(String cameraName, ResolutionPreset preset) { - if (preset.ordinal() > ResolutionPreset.high.ordinal()) { - preset = ResolutionPreset.high; - } - - CamcorderProfile profile = - getBestAvailableCamcorderProfileForResolutionPreset(cameraName, preset); - return new Size(profile.videoFrameWidth, profile.videoFrameHeight); - } - static Size computeBestCaptureSize(StreamConfigurationMap streamConfigurationMap) { // For still image captures, we use the largest available size. return Collections.max( @@ -132,45 +120,6 @@ public static List> getAvailableCameras(Activity activity) return cameras; } - static CamcorderProfile getBestAvailableCamcorderProfileForResolutionPreset( - String cameraName, ResolutionPreset preset) { - int cameraId = Integer.parseInt(cameraName); - switch (preset) { - // All of these cases deliberately fall through to get the best available profile. - case max: - if (CamcorderProfile.hasProfile(cameraId, CamcorderProfile.QUALITY_HIGH)) { - return CamcorderProfile.get(cameraId, CamcorderProfile.QUALITY_HIGH); - } - case ultraHigh: - if (CamcorderProfile.hasProfile(cameraId, CamcorderProfile.QUALITY_2160P)) { - return CamcorderProfile.get(cameraId, CamcorderProfile.QUALITY_2160P); - } - case veryHigh: - if (CamcorderProfile.hasProfile(cameraId, CamcorderProfile.QUALITY_1080P)) { - return CamcorderProfile.get(cameraId, CamcorderProfile.QUALITY_1080P); - } - case high: - if (CamcorderProfile.hasProfile(cameraId, CamcorderProfile.QUALITY_720P)) { - return CamcorderProfile.get(cameraId, CamcorderProfile.QUALITY_720P); - } - case medium: - if (CamcorderProfile.hasProfile(cameraId, CamcorderProfile.QUALITY_480P)) { - return CamcorderProfile.get(cameraId, CamcorderProfile.QUALITY_480P); - } - case low: - if (CamcorderProfile.hasProfile(cameraId, CamcorderProfile.QUALITY_QVGA)) { - return CamcorderProfile.get(cameraId, CamcorderProfile.QUALITY_QVGA); - } - default: - if (CamcorderProfile.hasProfile(cameraId, CamcorderProfile.QUALITY_LOW)) { - return CamcorderProfile.get(cameraId, CamcorderProfile.QUALITY_LOW); - } else { - throw new IllegalArgumentException( - "No capture session available for current capture session."); - } - } - } - private static class CompareSizesByArea implements Comparator { @Override public int compare(Size lhs, Size rhs) { diff --git a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/DartMessenger.java b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/DartMessenger.java index 59dfec9c43db..6030ed4bd993 100644 --- a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/DartMessenger.java +++ b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/DartMessenger.java @@ -11,12 +11,12 @@ import io.flutter.embedding.engine.systemchannels.PlatformChannel; import io.flutter.plugin.common.BinaryMessenger; import io.flutter.plugin.common.MethodChannel; -import io.flutter.plugins.camera.types.ExposureMode; -import io.flutter.plugins.camera.types.FocusMode; +import io.flutter.plugins.camera.features.autofocus.FocusMode; +import io.flutter.plugins.camera.features.exposurelock.ExposureMode; import java.util.HashMap; import java.util.Map; -class DartMessenger { +public class DartMessenger { @Nullable private MethodChannel cameraChannel; @Nullable private MethodChannel deviceChannel; @@ -46,7 +46,7 @@ public DartMessenger(BinaryMessenger messenger, long cameraId) { deviceChannel = new MethodChannel(messenger, "flutter.io/cameraPlugin/device"); } - void sendDeviceOrientationChangeEvent(PlatformChannel.DeviceOrientation orientation) { + public void sendDeviceOrientationChangeEvent(PlatformChannel.DeviceOrientation orientation) { assert (orientation != null); this.send( DeviceEventType.ORIENTATION_CHANGED, diff --git a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/PictureCaptureRequest.java b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/PictureCaptureRequest.java index 7613083bfd72..4b68cca0218c 100644 --- a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/PictureCaptureRequest.java +++ b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/PictureCaptureRequest.java @@ -57,9 +57,7 @@ class PictureCaptureRequest { * @param file */ static PictureCaptureRequest create( - MethodChannel.Result result, - File file, - DartMessenger dartMessenger) { + MethodChannel.Result result, File file, DartMessenger dartMessenger) { return new PictureCaptureRequest(result, file, dartMessenger); } @@ -70,9 +68,7 @@ static PictureCaptureRequest create( * @param file */ private PictureCaptureRequest( - MethodChannel.Result result, - File file, - DartMessenger dartMessenger) { + MethodChannel.Result result, File file, DartMessenger dartMessenger) { this.result = result; this.file = file; diff --git a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/AutoFocus.java b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/AutoFocus.java deleted file mode 100644 index f6dfde08740f..000000000000 --- a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/AutoFocus.java +++ /dev/null @@ -1,85 +0,0 @@ -package io.flutter.plugins.camera.features; - -import android.hardware.camera2.CameraCharacteristics; -import android.hardware.camera2.CaptureRequest; -import android.util.Log; - -import io.flutter.plugins.camera.CameraProperties; -import io.flutter.plugins.camera.types.FocusMode; - -public class AutoFocus implements CameraFeature { -// private final boolean recordingVideo; - private boolean isSupported; - private FocusMode currentSetting = FocusMode.auto; - -// public AutoFocus(boolean recordingVideo) { -// this.recordingVideo = recordingVideo; -// } - - @Override - public FocusMode getValue() { - return currentSetting; - } - - @Override - public void setValue(FocusMode value) { - this.currentSetting = value; - } - - @Override - public boolean isSupported(CameraProperties cameraProperties) { - int[] modes = cameraProperties.getControlAutoFocusAvailableModes(); - Log.i("Camera", "checkAutoFocusSupported | modes:"); - for (int mode : modes) { - Log.i("Camera", "checkAutoFocusSupported | ==> " + mode); - } - - // Check if fixed focal length lens. If LENS_INFO_MINIMUM_FOCUS_DISTANCE=0, then this is fixed. - // Can be null on some devices. - final Float minFocus = cameraProperties.getLensInfoMinimumFocusDistance(); - // final Float maxFocus = cameraCharacteristics.get(CameraCharacteristics.LENS_INFO_HYPERFOCAL_DISTANCE); - - // Value can be null on some devices: - // https://developer.android.com/reference/android/hardware/camera2/CameraCharacteristics#LENS_INFO_MINIMUM_FOCUS_DISTANCE - boolean isFixedLength; - if (minFocus == null) { - isFixedLength = true; - } else { - isFixedLength = minFocus == 0; - } - Log.i("Camera", "checkAutoFocusSupported | minFocus " + minFocus); - - final boolean supported = !isFixedLength - && !(modes == null - || modes.length == 0 - || (modes.length == 1 && modes[0] == CameraCharacteristics.CONTROL_AF_MODE_OFF)); - isSupported = supported; - return supported; - } - - @Override - public void updateBuilder(CaptureRequest.Builder requestBuilder) { - Log.i("Camera", "updateFocusMode currentFocusMode: " + currentSetting); - - if (!isSupported) { - return; - } - - switch (currentSetting) { - case locked: - requestBuilder.set(CaptureRequest.CONTROL_AF_MODE, CaptureRequest.CONTROL_AF_MODE_AUTO); - break; - - case auto: - requestBuilder.set( - CaptureRequest.CONTROL_AF_MODE, -// recordingVideo -// ? CaptureRequest.CONTROL_AF_MODE_CONTINUOUS_VIDEO -// : - CaptureRequest.CONTROL_AF_MODE_CONTINUOUS_PICTURE); - default: - break; - } - - } -} diff --git a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/CameraFeature.java b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/CameraFeature.java index fc9ed1505217..c90caca6bb6d 100644 --- a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/CameraFeature.java +++ b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/CameraFeature.java @@ -1,33 +1,42 @@ package io.flutter.plugins.camera.features; -import android.hardware.camera2.CameraCharacteristics; import android.hardware.camera2.CaptureRequest; - import io.flutter.plugins.camera.CameraProperties; +/** + * An interface describing a feature in the camera. This holds a setting value of type T and must + * implement a means to check if this setting is supported by the current camera properties. It also + * must implement a builder update method which will update a given capture request builder for this + * feature's current setting value. + * + * @param + */ public interface CameraFeature { - /** - * Get the current value of this feature's setting. - * @return - */ - public T getValue(); + /** + * Get the current value of this feature's setting. + * + * @return + */ + public T getValue(); - /** - * Set a new value for this feature's setting. - * @param value - */ - public void setValue(T value); + /** + * Set a new value for this feature's setting. + * + * @param value + */ + public void setValue(T value); - /** - * Returns whether or not this feature is supported on the - * given camera properties. - * @return - */ - public boolean isSupported(CameraProperties cameraProperties); + /** + * Returns whether or not this feature is supported on the given camera properties. + * + * @return + */ + public boolean isSupported(CameraProperties cameraProperties); - /** - * Update the setting in a provided request builder. - * @param requestBuilder - */ - public void updateBuilder(CaptureRequest.Builder requestBuilder); + /** + * Update the setting in a provided request builder. + * + * @param requestBuilder + */ + public void updateBuilder(CaptureRequest.Builder requestBuilder); } diff --git a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/CameraFeatures.java b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/CameraFeatures.java index 7e990bbf0398..653007376a95 100644 --- a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/CameraFeatures.java +++ b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/CameraFeatures.java @@ -1,36 +1,38 @@ package io.flutter.plugins.camera.features; /** - * This is all of our available features in the camera. Used in the features map - * of the camera to safely access feature class instances when we need to change - * their setting values. + * This is all of our available features in the camera. Used in the features map of the camera to + * safely access feature class instances when we need to change their setting values. */ public enum CameraFeatures { - autoFocus("autoFocus"), - exposureLock("exposureLock"), - exposureOffset("exposureOffset"), - flash("flash"), - focusPoint("focusPoint"), - fpsRange("fpsRange"), - exposurePoint("exposurePoint"), - noiseReduction("noiseReduction"); + autoFocus("autoFocus"), + exposureLock("exposureLock"), + exposureOffset("exposureOffset"), + flash("flash"), + resolution("resolution"), + focusPoint("focusPoint"), + fpsRange("fpsRange"), + sensorOrientation("sensorOrientation"), + zoomLevel("zoomLevel"), + regionBoundaries("regionBoundaries"), + exposurePoint("exposurePoint"), + noiseReduction("noiseReduction"); - private final String strValue; + private final String strValue; - CameraFeatures(String strValue) { - this.strValue = strValue; - } + CameraFeatures(String strValue) { + this.strValue = strValue; + } - public static CameraFeatures getValueForString(String modeStr) { - for (CameraFeatures value : values()) { - if (value.strValue.equals(modeStr)) return value; - } - return null; + public static CameraFeatures getValueForString(String modeStr) { + for (CameraFeatures value : values()) { + if (value.strValue.equals(modeStr)) return value; } + return null; + } - @Override - public String toString() { - return strValue; - } + @Override + public String toString() { + return strValue; + } } - diff --git a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/ExposureLock.java b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/ExposureLock.java deleted file mode 100644 index 2957e91fea6c..000000000000 --- a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/ExposureLock.java +++ /dev/null @@ -1,67 +0,0 @@ -package io.flutter.plugins.camera.features; - -import android.hardware.camera2.CameraCharacteristics; -import android.hardware.camera2.CaptureRequest; -import android.hardware.camera2.params.MeteringRectangle; -import android.util.Log; - -import io.flutter.plugins.camera.CameraProperties; -import io.flutter.plugins.camera.CameraRegions; -import io.flutter.plugins.camera.types.ExposureMode; -import io.flutter.plugins.camera.types.FlashMode; - -/** - * Exposure lock controls whether or not exposure mode is currenty locked or - * automatically metering. - */ -public class ExposureLock implements CameraFeature { - private boolean isSupported; - private ExposureMode currentSetting = ExposureMode.auto; - private CameraRegions cameraRegions; - - public ExposureLock(CameraRegions cameraRegions) { - this.cameraRegions = cameraRegions; - } - - @Override - public ExposureMode getValue() { - return currentSetting; - } - - @Override - public void setValue(ExposureMode value) { - this.currentSetting = value; - } - - // Available on all devices. - @Override - public boolean isSupported(CameraProperties cameraProperties) { - return true; - } - - @Override - public void updateBuilder(CaptureRequest.Builder requestBuilder) { - Log.i("Camera", "updateExposureMode"); - - // Don't try to set if the current camera doesn't support it. - if (!isSupported) { - return; - } - - // Applying auto exposure - MeteringRectangle aeRect = cameraRegions.getAEMeteringRectangle(); - requestBuilder.set( - CaptureRequest.CONTROL_AE_REGIONS, - aeRect == null ? null : new MeteringRectangle[] {cameraRegions.getAEMeteringRectangle()}); - - switch (currentSetting) { - case locked: - requestBuilder.set(CaptureRequest.CONTROL_AE_LOCK, true); - break; - case auto: - default: - requestBuilder.set(CaptureRequest.CONTROL_AE_LOCK, false); - break; - } - } -} diff --git a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/ExposureOffset.java b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/ExposureOffset.java deleted file mode 100644 index 91ff020d4b96..000000000000 --- a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/ExposureOffset.java +++ /dev/null @@ -1,100 +0,0 @@ -package io.flutter.plugins.camera.features; - -import android.hardware.camera2.CameraCharacteristics; -import android.hardware.camera2.CaptureRequest; -import android.hardware.camera2.params.MeteringRectangle; -import android.util.Log; -import android.util.Range; -import android.util.Rational; - -import io.flutter.plugins.camera.CameraProperties; -import io.flutter.plugins.camera.CameraRegions; -import io.flutter.plugins.camera.types.ExposureMode; - -/** - * Exposure offset makes the image brighter or darker. - */ -public class ExposureOffset implements CameraFeature { - private boolean isSupported; - private ExposureOffsetValue currentSetting; - private CameraProperties cameraProperties; - private final double min; - private final double max; - - public ExposureOffset(CameraProperties cameraProperties) { - this.cameraProperties = cameraProperties; - this.min = getMinExposureOffset(cameraProperties); - this.max = getMaxExposureOffset(cameraProperties); - - // Initial offset of 0 - this.currentSetting = new ExposureOffsetValue(this.min, this.max, 0); - } - - @Override - public ExposureOffsetValue getValue() { - return currentSetting; - } - - @Override - public void setValue(ExposureOffsetValue value) { - double stepSize = getExposureOffsetStepSize(cameraProperties); - this.currentSetting = new ExposureOffsetValue(min, max, (value.value / stepSize)); - } - - // Available on all devices. - @Override - public boolean isSupported(CameraProperties cameraProperties) { - return true; - } - - @Override - public void updateBuilder(CaptureRequest.Builder requestBuilder) { - Log.i("Camera", "updateExposureOffset"); - - // Don't try to set if the current camera doesn't support it. - if (!isSupported) { - return; - } - - requestBuilder.set(CaptureRequest.CONTROL_AE_EXPOSURE_COMPENSATION, (int) currentSetting.value); - } - - /** - * Return the minimum exposure offset double value. - * @param cameraProperties - * @return - */ - private double getMinExposureOffset(CameraProperties cameraProperties) { - Range range = cameraProperties.getControlAutoExposureCompensationRange(); - double minStepped = range == null ? 0 : range.getLower(); - double stepSize = getExposureOffsetStepSize(cameraProperties); - return minStepped * stepSize; - } - - /** - * Return the max exposure offset double value. - * @param cameraProperties - * @return - */ - private double getMaxExposureOffset(CameraProperties cameraProperties) { - Range range = cameraProperties.getControlAutoExposureCompensationRange(); - double maxStepped = range == null ? 0 : range.getUpper(); - double stepSize = getExposureOffsetStepSize(cameraProperties); - return maxStepped * stepSize; - } - - /** - * Returns the exposure offset step size. This is the smallest amount which the - * exposure offset can be changed. - * - * Example: if this has a value of 0.5, then an aeExposureCompensation setting of - * -2 means that the actual AE offset is -1. - * @param cameraProperties - * @return - */ - private double getExposureOffsetStepSize(CameraProperties cameraProperties) { - Rational stepSize = cameraProperties.getControlAutoExposureCompensationStep(); - return stepSize == null ? 0.0 : stepSize.doubleValue(); - } -} - diff --git a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/ExposureOffsetValue.java b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/ExposureOffsetValue.java deleted file mode 100644 index ff03c6ae87be..000000000000 --- a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/ExposureOffsetValue.java +++ /dev/null @@ -1,17 +0,0 @@ -package io.flutter.plugins.camera.features; - -/** - * This represents the exposure offset value. It holds the minimum and - * maximum values, as well as the current setting value. - */ -public class ExposureOffsetValue { - final public double min; - final public double max; - final public double value; - - public ExposureOffsetValue(double min, double max, double value) { - this.min = min; - this.max = max; - this.value = value; - } -} diff --git a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/ExposurePoint.java b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/ExposurePoint.java deleted file mode 100644 index 976d1849990c..000000000000 --- a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/ExposurePoint.java +++ /dev/null @@ -1,76 +0,0 @@ -package io.flutter.plugins.camera.features; - -import android.hardware.camera2.CameraCharacteristics; -import android.hardware.camera2.CaptureRequest; -import android.hardware.camera2.params.MeteringRectangle; -import android.util.Log; - -import java.util.concurrent.Callable; - -import io.flutter.plugins.camera.CameraProperties; -import io.flutter.plugins.camera.CameraRegions; - -/** - * Exposure point controls where in the frame exposure metering will come from. - */ -public class ExposurePoint implements CameraFeature { - // Used later to always get the correct camera regions instance. - private final Callable getCameraRegions; - private boolean isSupported; - private Point currentSetting = new Point(0.0, 0.0); - - public ExposurePoint(Callable getCameraRegions) { - this.getCameraRegions = getCameraRegions; - } - - @Override - public Point getValue() { - return currentSetting; - } - - @Override - public void setValue(Point value) { - this.currentSetting = value; - - try { - if (value.x == null || value.y == null) { - getCameraRegions.call().resetAutoExposureMeteringRectangle(); - } else { - getCameraRegions.call().setAutoExposureMeteringRectangleFromPoint(value.x, value.y); - } - } catch (Exception e) { - e.printStackTrace(); - } - } - - // Whether or not this camera can set the exposure point. - @Override - public boolean isSupported(CameraProperties cameraProperties) { - Integer supportedRegions = cameraProperties.getControlMaxRegionsAutoExposure(); - final boolean supported = supportedRegions != null && supportedRegions > 0; - isSupported = supported; - return supported; - } - - @Override - public void updateBuilder(CaptureRequest.Builder requestBuilder) { - Log.i("Camera", "updateExposureMode"); - - // Don't try to set if the current camera doesn't support it. - if (!isSupported) { - return; - } - - MeteringRectangle aeRect = null; - try { - aeRect = getCameraRegions.call().getAEMeteringRectangle(); - requestBuilder.set( - CaptureRequest.CONTROL_AE_REGIONS, - aeRect == null ? null : new MeteringRectangle[]{getCameraRegions.call().getAEMeteringRectangle()}); - } catch (Exception e) { - e.printStackTrace(); - } - - } -} - diff --git a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/Flash.java b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/Flash.java deleted file mode 100644 index 4744735ebacb..000000000000 --- a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/Flash.java +++ /dev/null @@ -1,74 +0,0 @@ -package io.flutter.plugins.camera.features; - -import android.hardware.camera2.CameraCharacteristics; -import android.hardware.camera2.CaptureRequest; -import android.util.Log; - -import io.flutter.plugins.camera.CameraProperties; -import io.flutter.plugins.camera.types.FlashMode; - -public class Flash implements CameraFeature { - private boolean isSupported; - private FlashMode currentSetting = FlashMode.auto; - - @Override - public FlashMode getValue() { - return currentSetting; - } - - @Override - public void setValue(FlashMode value) { - this.currentSetting = value; - } - - @Override - public boolean isSupported(CameraProperties cameraProperties) { - Boolean available = cameraProperties.getFlashInfoAvailable(); - final boolean supported = available != null && available; - isSupported = supported; - return supported; - } - - @Override - public void updateBuilder(CaptureRequest.Builder requestBuilder) { - Log.i("Camera", "updateFlash"); - - // Don't try to set flash enabled if the current camera doesn't - // support it. - if (!isSupported) { - return; - } - - switch (currentSetting) { - case off: - requestBuilder.set(CaptureRequest.CONTROL_AE_MODE, CaptureRequest.CONTROL_AE_MODE_ON); - requestBuilder.set(CaptureRequest.FLASH_MODE, CaptureRequest.FLASH_MODE_OFF); - break; - - case always: - requestBuilder.set( - CaptureRequest.CONTROL_AE_MODE, CaptureRequest.CONTROL_AE_MODE_ON_ALWAYS_FLASH); - requestBuilder.set(CaptureRequest.FLASH_MODE, CaptureRequest.FLASH_MODE_OFF); - break; - - case torch: - requestBuilder.set(CaptureRequest.CONTROL_AE_MODE, CaptureRequest.CONTROL_AE_MODE_ON); - requestBuilder.set(CaptureRequest.FLASH_MODE, CaptureRequest.FLASH_MODE_TORCH); - break; - - case auto: - requestBuilder.set( - CaptureRequest.CONTROL_AE_MODE, CaptureRequest.CONTROL_AE_MODE_ON_AUTO_FLASH); - requestBuilder.set(CaptureRequest.FLASH_MODE, CaptureRequest.FLASH_MODE_OFF); - break; - - // TODO: to be implemented someday. Need to add it to dart/iOS as another flash mode setting. - // case autoRedEye: - // requestBuilder.set(CaptureRequest.CONTROL_AE_MODE, - // CaptureRequest.CONTROL_AE_MODE_ON_AUTO_FLASH_REDEYE); - // requestBuilder.set(CaptureRequest.FLASH_MODE, - // CaptureRequest.FLASH_MODE_OFF); - // break; - } - } -} diff --git a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/FocusPoint.java b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/FocusPoint.java deleted file mode 100644 index 9dc4fb689b14..000000000000 --- a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/FocusPoint.java +++ /dev/null @@ -1,60 +0,0 @@ -package io.flutter.plugins.camera.features; - -import android.hardware.camera2.CameraCharacteristics; -import android.hardware.camera2.CaptureRequest; -import android.hardware.camera2.params.MeteringRectangle; -import android.util.Log; - -import java.util.concurrent.Callable; - -import io.flutter.plugins.camera.CameraProperties; -import io.flutter.plugins.camera.CameraRegions; - -/** - * Focus point controls where in the frame focus will come from. - */ -public class FocusPoint implements CameraFeature { - // Used later to always get the correct camera regions instance. - private final Callable getCameraRegions; - private boolean isSupported; - private Point currentSetting = new Point(0.0, 0.0); - - public FocusPoint(Callable getCameraRegions) { - this.getCameraRegions = getCameraRegions; - } - - @Override - public Point getValue() { - return currentSetting; - } - - @Override - public void setValue(Point value) { - this.currentSetting = value; - - try { - if (value.x == null || value.y == null) { - getCameraRegions.call().resetAutoFocusMeteringRectangle(); - } else { - getCameraRegions.call().setAutoFocusMeteringRectangleFromPoint(value.x, value.y); - } - } catch (Exception e) { - e.printStackTrace(); - } - } - - // Whether or not this camera can set the exposure point. - @Override - public boolean isSupported(CameraProperties cameraProperties) { - Integer supportedRegions = cameraProperties.getControlMaxRegionsAutoFocus(); - final boolean supported = supportedRegions != null && supportedRegions > 0; - isSupported = supported; - return supported; - } - - @Override - public void updateBuilder(CaptureRequest.Builder requestBuilder) { -// Not used - } -} - diff --git a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/FpsRange.java b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/FpsRange.java deleted file mode 100644 index dcc5441f3c56..000000000000 --- a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/FpsRange.java +++ /dev/null @@ -1,63 +0,0 @@ -package io.flutter.plugins.camera.features; - -import android.hardware.camera2.CameraCharacteristics; -import android.hardware.camera2.CaptureRequest; -import android.util.Log; -import android.util.Range; - -import io.flutter.plugins.camera.CameraProperties; -import io.flutter.plugins.camera.types.FocusMode; - -public class FpsRange implements CameraFeature> { - private boolean isSupported; - private Range currentSetting; - - public FpsRange(CameraProperties cameraProperties) { - Log.i("Camera", "getAvailableFpsRange"); - - try { - Range[] ranges = cameraProperties.getControlAutoExposureAvailableTargetFpsRanges(); - - if (ranges != null) { - for (Range range : ranges) { - int upper = range.getUpper(); - Log.i("Camera", "[FPS Range Available] is:" + range); - if (upper >= 10) { - if (currentSetting == null || upper > currentSetting.getUpper()) { - currentSetting = range; - } - } - } - } - } catch (Exception e) { - // TODO: maybe just send a dart error back -// pictureCaptureRequest.error("cameraAccess", e.getMessage(), null); - } - Log.i("Camera", "[FPS Range] is:" + currentSetting); - } - - @Override - public Range getValue() { - return currentSetting; - } - - @Override - public void setValue(Range value) { - this.currentSetting = value; - } - - // Always supported - @Override - public boolean isSupported(CameraProperties cameraProperties) { - return true; - } - - @Override - public void updateBuilder(CaptureRequest.Builder requestBuilder) { - if (currentSetting == null) { - return; - } - - requestBuilder.set(CaptureRequest.CONTROL_AE_TARGET_FPS_RANGE, currentSetting); - } -} diff --git a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/NoiseReduction.java b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/NoiseReduction.java deleted file mode 100644 index f32f41f6098b..000000000000 --- a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/NoiseReduction.java +++ /dev/null @@ -1,64 +0,0 @@ -package io.flutter.plugins.camera.features; - -import android.hardware.camera2.CameraCharacteristics; -import android.hardware.camera2.CaptureRequest; -import android.util.Log; - -import io.flutter.plugins.camera.CameraProperties; -import io.flutter.plugins.camera.types.NoiseReductionMode; - -/** - * This can either be enabled or disabled. Only full capability devices - * can set this to off. Legacy and full support the fast mode. - * https://developer.android.com/reference/android/hardware/camera2/CameraCharacteristics#NOISE_REDUCTION_AVAILABLE_NOISE_REDUCTION_MODES - */ -public class NoiseReduction implements CameraFeature { - private boolean isSupported; - private NoiseReductionMode currentSetting; - - @Override - public NoiseReductionMode getValue() { - return currentSetting; - } - - @Override - public void setValue(NoiseReductionMode value) { - this.currentSetting = value; - } - - @Override - public boolean isSupported(CameraProperties cameraProperties) { - /** - * Available settings: - * public static final int NOISE_REDUCTION_MODE_FAST = 1; - * public static final int NOISE_REDUCTION_MODE_HIGH_QUALITY = 2; - * public static final int NOISE_REDUCTION_MODE_MINIMAL = 3; - * public static final int NOISE_REDUCTION_MODE_OFF = 0; - * public static final int NOISE_REDUCTION_MODE_ZERO_SHUTTER_LAG = 4; - * - * Full-capability camera devices will always support OFF and FAST. - * Camera devices that support YUV_REPROCESSING or PRIVATE_REPROCESSING will support ZERO_SHUTTER_LAG. - * Legacy-capability camera devices will only support FAST mode. - */ - - // Can be null on some devices. - int[] modes = cameraProperties.getAvailableNoiseReductionModes(); - - /// If there's at least one mode available then we are supported. - return modes != null && modes.length > 0; - } - - @Override - public void updateBuilder(CaptureRequest.Builder requestBuilder) { - Log.i("Camera", "updateFlash"); - - // Don't try to set if the current camera doesn't support it. - if (!isSupported) { - return; - } - - // Always use fast mode. - requestBuilder.set(CaptureRequest.NOISE_REDUCTION_MODE, CaptureRequest.NOISE_REDUCTION_MODE_FAST); - } -} - diff --git a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/Point.java b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/Point.java index 657708b6844c..3da843176ae6 100644 --- a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/Point.java +++ b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/Point.java @@ -1,14 +1,12 @@ package io.flutter.plugins.camera.features; -/** - * Represents a point on an x/y axis. - */ +/** Represents a point on an x/y axis. */ public class Point { - final public Double x; - final public Double y; + public final Double x; + public final Double y; - public Point(Double x, Double y) { - this.x = x; - this.y = y; - } + public Point(Double x, Double y) { + this.x = x; + this.y = y; + } } diff --git a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/autoFocus/AutoFocus.java b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/autoFocus/AutoFocus.java new file mode 100644 index 000000000000..a753b52fbe49 --- /dev/null +++ b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/autoFocus/AutoFocus.java @@ -0,0 +1,85 @@ +package io.flutter.plugins.camera.features.autofocus; + +import android.hardware.camera2.CameraCharacteristics; +import android.hardware.camera2.CaptureRequest; +import android.util.Log; +import io.flutter.plugins.camera.CameraProperties; +import io.flutter.plugins.camera.features.CameraFeature; +import io.flutter.plugins.camera.types.FocusMode; + +public class AutoFocus implements CameraFeature { + // private final boolean recordingVideo; + private boolean isSupported; + private FocusMode currentSetting = FocusMode.auto; + + // public AutoFocus(boolean recordingVideo) { + // this.recordingVideo = recordingVideo; + // } + + @Override + public FocusMode getValue() { + return currentSetting; + } + + @Override + public void setValue(FocusMode value) { + this.currentSetting = value; + } + + @Override + public boolean isSupported(CameraProperties cameraProperties) { + int[] modes = cameraProperties.getControlAutoFocusAvailableModes(); + Log.i("Camera", "checkAutoFocusSupported | modes:"); + for (int mode : modes) { + Log.i("Camera", "checkAutoFocusSupported | ==> " + mode); + } + + // Check if fixed focal length lens. If LENS_INFO_MINIMUM_FOCUS_DISTANCE=0, then this is fixed. + // Can be null on some devices. + final Float minFocus = cameraProperties.getLensInfoMinimumFocusDistance(); + // final Float maxFocus = cameraCharacteristics.get(CameraCharacteristics.LENS_INFO_HYPERFOCAL_DISTANCE); + + // Value can be null on some devices: + // https://developer.android.com/reference/android/hardware/camera2/CameraCharacteristics#LENS_INFO_MINIMUM_FOCUS_DISTANCE + boolean isFixedLength; + if (minFocus == null) { + isFixedLength = true; + } else { + isFixedLength = minFocus == 0; + } + Log.i("Camera", "checkAutoFocusSupported | minFocus " + minFocus); + + final boolean supported = + !isFixedLength + && !(modes == null + || modes.length == 0 + || (modes.length == 1 && modes[0] == CameraCharacteristics.CONTROL_AF_MODE_OFF)); + isSupported = supported; + return supported; + } + + @Override + public void updateBuilder(CaptureRequest.Builder requestBuilder) { + Log.i("Camera", "updateFocusMode currentFocusMode: " + currentSetting); + + if (!isSupported) { + return; + } + + switch (currentSetting) { + case locked: + requestBuilder.set(CaptureRequest.CONTROL_AF_MODE, CaptureRequest.CONTROL_AF_MODE_AUTO); + break; + + case auto: + requestBuilder.set( + CaptureRequest.CONTROL_AF_MODE, + // recordingVideo + // ? CaptureRequest.CONTROL_AF_MODE_CONTINUOUS_VIDEO + // : + CaptureRequest.CONTROL_AF_MODE_CONTINUOUS_PICTURE); + default: + break; + } + } +} diff --git a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/types/FocusMode.java b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/autoFocus/FocusMode.java similarity index 91% rename from packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/types/FocusMode.java rename to packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/autoFocus/FocusMode.java index b0dba047f7eb..c77ac4b71a08 100644 --- a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/types/FocusMode.java +++ b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/autoFocus/FocusMode.java @@ -2,7 +2,7 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -package io.flutter.plugins.camera.types; +package io.flutter.plugins.camera.features.autofocus; // Mirrors focus_mode.dart public enum FocusMode { diff --git a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/exposurelock/ExposureLock.java b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/exposurelock/ExposureLock.java new file mode 100644 index 000000000000..ab5ce22bc65e --- /dev/null +++ b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/exposurelock/ExposureLock.java @@ -0,0 +1,51 @@ +package io.flutter.plugins.camera.features.exposurelock; + +import android.hardware.camera2.CaptureRequest; +import android.util.Log; +import io.flutter.plugins.camera.CameraProperties; +import io.flutter.plugins.camera.features.CameraFeature; +import io.flutter.plugins.camera.types.ExposureMode; + +/** + * Exposure lock controls whether or not exposure mode is currenty locked or automatically metering. + */ +public class ExposureLock implements CameraFeature { + private boolean isSupported; + private ExposureMode currentSetting = ExposureMode.auto; + + @Override + public ExposureMode getValue() { + return currentSetting; + } + + @Override + public void setValue(ExposureMode value) { + this.currentSetting = value; + } + + // Available on all devices. + @Override + public boolean isSupported(CameraProperties cameraProperties) { + return true; + } + + @Override + public void updateBuilder(CaptureRequest.Builder requestBuilder) { + Log.i("Camera", "updateExposureMode"); + + // Don't try to set if the current camera doesn't support it. + if (!isSupported) { + return; + } + + switch (currentSetting) { + case locked: + requestBuilder.set(CaptureRequest.CONTROL_AE_LOCK, true); + break; + case auto: + default: + requestBuilder.set(CaptureRequest.CONTROL_AE_LOCK, false); + break; + } + } +} diff --git a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/types/ExposureMode.java b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/exposurelock/ExposureMode.java similarity index 91% rename from packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/types/ExposureMode.java rename to packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/exposurelock/ExposureMode.java index 595206fa2216..ae73016270e9 100644 --- a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/types/ExposureMode.java +++ b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/exposurelock/ExposureMode.java @@ -2,7 +2,7 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -package io.flutter.plugins.camera.types; +package io.flutter.plugins.camera.features.exposurelock; // Mirrors exposure_mode.dart public enum ExposureMode { diff --git a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/exposureoffset/ExposureOffset.java b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/exposureoffset/ExposureOffset.java new file mode 100644 index 000000000000..ace6877cb745 --- /dev/null +++ b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/exposureoffset/ExposureOffset.java @@ -0,0 +1,96 @@ +package io.flutter.plugins.camera.features.exposureoffset; + +import android.hardware.camera2.CaptureRequest; +import android.util.Log; +import android.util.Range; +import android.util.Rational; +import io.flutter.plugins.camera.CameraProperties; +import io.flutter.plugins.camera.features.CameraFeature; + +/** Exposure offset makes the image brighter or darker. */ +public class ExposureOffset implements CameraFeature { + private boolean isSupported; + private ExposureOffsetValue currentSetting; + private CameraProperties cameraProperties; + private final double min; + private final double max; + + public ExposureOffset(CameraProperties cameraProperties) { + this.cameraProperties = cameraProperties; + this.min = getMinExposureOffset(cameraProperties); + this.max = getMaxExposureOffset(cameraProperties); + + // Initial offset of 0 + this.currentSetting = new ExposureOffsetValue(this.min, this.max, 0); + } + + @Override + public ExposureOffsetValue getValue() { + return currentSetting; + } + + @Override + public void setValue(ExposureOffsetValue value) { + double stepSize = getExposureOffsetStepSize(cameraProperties); + this.currentSetting = new ExposureOffsetValue(min, max, (value.value / stepSize)); + } + + // Available on all devices. + @Override + public boolean isSupported(CameraProperties cameraProperties) { + return true; + } + + @Override + public void updateBuilder(CaptureRequest.Builder requestBuilder) { + Log.i("Camera", "updateExposureOffset"); + + // Don't try to set if the current camera doesn't support it. + if (!isSupported) { + return; + } + + requestBuilder.set(CaptureRequest.CONTROL_AE_EXPOSURE_COMPENSATION, (int) currentSetting.value); + } + + /** + * Return the minimum exposure offset double value. + * + * @param cameraProperties + * @return + */ + private double getMinExposureOffset(CameraProperties cameraProperties) { + Range range = cameraProperties.getControlAutoExposureCompensationRange(); + double minStepped = range == null ? 0 : range.getLower(); + double stepSize = getExposureOffsetStepSize(cameraProperties); + return minStepped * stepSize; + } + + /** + * Return the max exposure offset double value. + * + * @param cameraProperties + * @return + */ + private double getMaxExposureOffset(CameraProperties cameraProperties) { + Range range = cameraProperties.getControlAutoExposureCompensationRange(); + double maxStepped = range == null ? 0 : range.getUpper(); + double stepSize = getExposureOffsetStepSize(cameraProperties); + return maxStepped * stepSize; + } + + /** + * Returns the exposure offset step size. This is the smallest amount which the exposure offset + * can be changed. + * + *

Example: if this has a value of 0.5, then an aeExposureCompensation setting of -2 means that + * the actual AE offset is -1. + * + * @param cameraProperties + * @return + */ + public double getExposureOffsetStepSize(CameraProperties cameraProperties) { + Rational stepSize = cameraProperties.getControlAutoExposureCompensationStep(); + return stepSize == null ? 0.0 : stepSize.doubleValue(); + } +} diff --git a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/exposureoffset/ExposureOffsetValue.java b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/exposureoffset/ExposureOffsetValue.java new file mode 100644 index 000000000000..a8c1d97219d9 --- /dev/null +++ b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/exposureoffset/ExposureOffsetValue.java @@ -0,0 +1,17 @@ +package io.flutter.plugins.camera.features.exposureoffset; + +/** + * This represents the exposure offset value. It holds the minimum and maximum values, as well as + * the current setting value. + */ +public class ExposureOffsetValue { + public final double min; + public final double max; + public final double value; + + public ExposureOffsetValue(double min, double max, double value) { + this.min = min; + this.max = max; + this.value = value; + } +} diff --git a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/exposurepoint/ExposurePoint.java b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/exposurepoint/ExposurePoint.java new file mode 100644 index 000000000000..af0684c03478 --- /dev/null +++ b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/exposurepoint/ExposurePoint.java @@ -0,0 +1,73 @@ +package io.flutter.plugins.camera.features.exposurepoint; + +import android.hardware.camera2.CaptureRequest; +import android.hardware.camera2.params.MeteringRectangle; +import android.util.Log; +import io.flutter.plugins.camera.CameraProperties; +import io.flutter.plugins.camera.features.CameraFeature; +import io.flutter.plugins.camera.features.Point; +import io.flutter.plugins.camera.features.regionboundaries.CameraRegions; +import java.util.concurrent.Callable; + +/** Exposure point controls where in the frame exposure metering will come from. */ +public class ExposurePoint implements CameraFeature { + // Used later to always get the correct camera regions instance. + private final Callable getCameraRegions; + private boolean isSupported; + private Point currentSetting = new Point(0.0, 0.0); + + public ExposurePoint(Callable getCameraRegions) { + this.getCameraRegions = getCameraRegions; + } + + @Override + public Point getValue() { + return currentSetting; + } + + @Override + public void setValue(Point value) { + this.currentSetting = value; + + try { + if (value.x == null || value.y == null) { + getCameraRegions.call().resetAutoExposureMeteringRectangle(); + } else { + getCameraRegions.call().setAutoExposureMeteringRectangleFromPoint(value.x, value.y); + } + } catch (Exception e) { + e.printStackTrace(); + } + } + + // Whether or not this camera can set the exposure point. + @Override + public boolean isSupported(CameraProperties cameraProperties) { + Integer supportedRegions = cameraProperties.getControlMaxRegionsAutoExposure(); + final boolean supported = supportedRegions != null && supportedRegions > 0; + isSupported = supported; + return supported; + } + + @Override + public void updateBuilder(CaptureRequest.Builder requestBuilder) { + Log.i("Camera", "updateExposureMode"); + + // Don't try to set if the current camera doesn't support it. + if (!isSupported) { + return; + } + + MeteringRectangle aeRect = null; + try { + aeRect = getCameraRegions.call().getAEMeteringRectangle(); + requestBuilder.set( + CaptureRequest.CONTROL_AE_REGIONS, + aeRect == null + ? null + : new MeteringRectangle[] {getCameraRegions.call().getAEMeteringRectangle()}); + } catch (Exception e) { + e.printStackTrace(); + } + } +} diff --git a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/flash/Flash.java b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/flash/Flash.java new file mode 100644 index 000000000000..1a71c205b676 --- /dev/null +++ b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/flash/Flash.java @@ -0,0 +1,73 @@ +package io.flutter.plugins.camera.features.flash; + +import android.hardware.camera2.CaptureRequest; +import android.util.Log; +import io.flutter.plugins.camera.CameraProperties; +import io.flutter.plugins.camera.features.CameraFeature; +import io.flutter.plugins.camera.types.FlashMode; + +public class Flash implements CameraFeature { + private boolean isSupported; + private FlashMode currentSetting = FlashMode.auto; + + @Override + public FlashMode getValue() { + return currentSetting; + } + + @Override + public void setValue(FlashMode value) { + this.currentSetting = value; + } + + @Override + public boolean isSupported(CameraProperties cameraProperties) { + Boolean available = cameraProperties.getFlashInfoAvailable(); + final boolean supported = available != null && available; + isSupported = supported; + return supported; + } + + @Override + public void updateBuilder(CaptureRequest.Builder requestBuilder) { + Log.i("Camera", "updateFlash"); + + // Don't try to set flash enabled if the current camera doesn't + // support it. + if (!isSupported) { + return; + } + + switch (currentSetting) { + case off: + requestBuilder.set(CaptureRequest.CONTROL_AE_MODE, CaptureRequest.CONTROL_AE_MODE_ON); + requestBuilder.set(CaptureRequest.FLASH_MODE, CaptureRequest.FLASH_MODE_OFF); + break; + + case always: + requestBuilder.set( + CaptureRequest.CONTROL_AE_MODE, CaptureRequest.CONTROL_AE_MODE_ON_ALWAYS_FLASH); + requestBuilder.set(CaptureRequest.FLASH_MODE, CaptureRequest.FLASH_MODE_OFF); + break; + + case torch: + requestBuilder.set(CaptureRequest.CONTROL_AE_MODE, CaptureRequest.CONTROL_AE_MODE_ON); + requestBuilder.set(CaptureRequest.FLASH_MODE, CaptureRequest.FLASH_MODE_TORCH); + break; + + case auto: + requestBuilder.set( + CaptureRequest.CONTROL_AE_MODE, CaptureRequest.CONTROL_AE_MODE_ON_AUTO_FLASH); + requestBuilder.set(CaptureRequest.FLASH_MODE, CaptureRequest.FLASH_MODE_OFF); + break; + + // TODO: to be implemented someday. Need to add it to dart/iOS as another flash mode setting. + // case autoRedEye: + // requestBuilder.set(CaptureRequest.CONTROL_AE_MODE, + // CaptureRequest.CONTROL_AE_MODE_ON_AUTO_FLASH_REDEYE); + // requestBuilder.set(CaptureRequest.FLASH_MODE, + // CaptureRequest.FLASH_MODE_OFF); + // break; + } + } +} diff --git a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/types/FlashMode.java b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/flash/FlashMode.java similarity index 92% rename from packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/types/FlashMode.java rename to packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/flash/FlashMode.java index c4f0998c418a..5390a7781989 100644 --- a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/types/FlashMode.java +++ b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/flash/FlashMode.java @@ -2,7 +2,7 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -package io.flutter.plugins.camera.types; +package io.flutter.plugins.camera.features.flash; // Mirrors flash_mode.dart public enum FlashMode { diff --git a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/focuspoint/FocusPoint.java b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/focuspoint/FocusPoint.java new file mode 100644 index 000000000000..380dfc85a6b0 --- /dev/null +++ b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/focuspoint/FocusPoint.java @@ -0,0 +1,54 @@ +package io.flutter.plugins.camera.features.focuspoint; + +import android.hardware.camera2.CaptureRequest; +import io.flutter.plugins.camera.CameraProperties; +import io.flutter.plugins.camera.features.CameraFeature; +import io.flutter.plugins.camera.features.Point; +import io.flutter.plugins.camera.features.regionboundaries.CameraRegions; +import java.util.concurrent.Callable; + +/** Focus point controls where in the frame focus will come from. */ +public class FocusPoint implements CameraFeature { + // Used later to always get the correct camera regions instance. + private final Callable getCameraRegions; + private boolean isSupported; + private Point currentSetting = new Point(0.0, 0.0); + + public FocusPoint(Callable getCameraRegions) { + this.getCameraRegions = getCameraRegions; + } + + @Override + public Point getValue() { + return currentSetting; + } + + @Override + public void setValue(Point value) { + this.currentSetting = value; + + try { + if (value.x == null || value.y == null) { + getCameraRegions.call().resetAutoFocusMeteringRectangle(); + } else { + getCameraRegions.call().setAutoFocusMeteringRectangleFromPoint(value.x, value.y); + } + } catch (Exception e) { + e.printStackTrace(); + } + } + + // Whether or not this camera can set the exposure point. + @Override + public boolean isSupported(CameraProperties cameraProperties) { + Integer supportedRegions = cameraProperties.getControlMaxRegionsAutoFocus(); + final boolean supported = supportedRegions != null && supportedRegions > 0; + isSupported = supported; + return supported; + } + + @Override + public void updateBuilder(CaptureRequest.Builder requestBuilder) { + // Not used + } +} diff --git a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/fpsrange/FpsRange.java b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/fpsrange/FpsRange.java new file mode 100644 index 000000000000..8073a5e6ee7a --- /dev/null +++ b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/fpsrange/FpsRange.java @@ -0,0 +1,61 @@ +package io.flutter.plugins.camera.features.fpsrange; + +import android.hardware.camera2.CaptureRequest; +import android.util.Log; +import android.util.Range; +import io.flutter.plugins.camera.CameraProperties; +import io.flutter.plugins.camera.features.CameraFeature; + +public class FpsRange implements CameraFeature> { + private boolean isSupported; + private Range currentSetting; + + public FpsRange(CameraProperties cameraProperties) { + Log.i("Camera", "getAvailableFpsRange"); + + try { + Range[] ranges = cameraProperties.getControlAutoExposureAvailableTargetFpsRanges(); + + if (ranges != null) { + for (Range range : ranges) { + int upper = range.getUpper(); + Log.i("Camera", "[FPS Range Available] is:" + range); + if (upper >= 10) { + if (currentSetting == null || upper > currentSetting.getUpper()) { + currentSetting = range; + } + } + } + } + } catch (Exception e) { + // TODO: maybe just send a dart error back + // pictureCaptureRequest.error("cameraAccess", e.getMessage(), null); + } + Log.i("Camera", "[FPS Range] is:" + currentSetting); + } + + @Override + public Range getValue() { + return currentSetting; + } + + @Override + public void setValue(Range value) { + this.currentSetting = value; + } + + // Always supported + @Override + public boolean isSupported(CameraProperties cameraProperties) { + return true; + } + + @Override + public void updateBuilder(CaptureRequest.Builder requestBuilder) { + if (currentSetting == null) { + return; + } + + requestBuilder.set(CaptureRequest.CONTROL_AE_TARGET_FPS_RANGE, currentSetting); + } +} diff --git a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/noisereduction/NoiseReduction.java b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/noisereduction/NoiseReduction.java new file mode 100644 index 000000000000..68130805a7bd --- /dev/null +++ b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/noisereduction/NoiseReduction.java @@ -0,0 +1,61 @@ +package io.flutter.plugins.camera.features.noisereduction; + +import android.hardware.camera2.CaptureRequest; +import android.util.Log; +import io.flutter.plugins.camera.CameraProperties; +import io.flutter.plugins.camera.features.CameraFeature; +import io.flutter.plugins.camera.types.NoiseReductionMode; + +/** + * This can either be enabled or disabled. Only full capability devices can set this to off. Legacy + * and full support the fast mode. + * https://developer.android.com/reference/android/hardware/camera2/CameraCharacteristics#NOISE_REDUCTION_AVAILABLE_NOISE_REDUCTION_MODES + */ +public class NoiseReduction implements CameraFeature { + private boolean isSupported; + private NoiseReductionMode currentSetting; + + @Override + public NoiseReductionMode getValue() { + return currentSetting; + } + + @Override + public void setValue(NoiseReductionMode value) { + this.currentSetting = value; + } + + @Override + public boolean isSupported(CameraProperties cameraProperties) { + /** + * Available settings: public static final int NOISE_REDUCTION_MODE_FAST = 1; public static + * final int NOISE_REDUCTION_MODE_HIGH_QUALITY = 2; public static final int + * NOISE_REDUCTION_MODE_MINIMAL = 3; public static final int NOISE_REDUCTION_MODE_OFF = 0; + * public static final int NOISE_REDUCTION_MODE_ZERO_SHUTTER_LAG = 4; + * + *

Full-capability camera devices will always support OFF and FAST. Camera devices that + * support YUV_REPROCESSING or PRIVATE_REPROCESSING will support ZERO_SHUTTER_LAG. + * Legacy-capability camera devices will only support FAST mode. + */ + + // Can be null on some devices. + int[] modes = cameraProperties.getAvailableNoiseReductionModes(); + + /// If there's at least one mode available then we are supported. + return modes != null && modes.length > 0; + } + + @Override + public void updateBuilder(CaptureRequest.Builder requestBuilder) { + Log.i("Camera", "updateFlash"); + + // Don't try to set if the current camera doesn't support it. + if (!isSupported) { + return; + } + + // Always use fast mode. + requestBuilder.set( + CaptureRequest.NOISE_REDUCTION_MODE, CaptureRequest.NOISE_REDUCTION_MODE_FAST); + } +} diff --git a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/noisereduction/NoiseReductionMode.java b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/noisereduction/NoiseReductionMode.java new file mode 100644 index 000000000000..944928920518 --- /dev/null +++ b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/noisereduction/NoiseReductionMode.java @@ -0,0 +1,24 @@ +package io.flutter.plugins.camera.features.noisereduction; + +/** Only supports fast mode for now. */ +public enum NoiseReductionMode { + fast("fast"); + + private final String strValue; + + NoiseReductionMode(String strValue) { + this.strValue = strValue; + } + + public static NoiseReductionMode getValueForString(String modeStr) { + for (NoiseReductionMode value : values()) { + if (value.strValue.equals(modeStr)) return value; + } + return null; + } + + @Override + public String toString() { + return strValue; + } +} diff --git a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/CameraRegions.java b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/regionboundaries/CameraRegions.java similarity index 97% rename from packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/CameraRegions.java rename to packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/regionboundaries/CameraRegions.java index 04412a56631f..48ef00158018 100644 --- a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/CameraRegions.java +++ b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/regionboundaries/CameraRegions.java @@ -2,7 +2,7 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -package io.flutter.plugins.camera; +package io.flutter.plugins.camera.features.regionboundaries; import android.hardware.camera2.params.MeteringRectangle; import android.util.Size; diff --git a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/regionboundaries/RegionBoundaries.java b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/regionboundaries/RegionBoundaries.java new file mode 100644 index 000000000000..7a98215a65fb --- /dev/null +++ b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/regionboundaries/RegionBoundaries.java @@ -0,0 +1,88 @@ +package io.flutter.plugins.camera.features.regionboundaries; + +import android.annotation.TargetApi; +import android.hardware.camera2.CaptureRequest; +import android.os.Build; +import android.util.Size; +import io.flutter.plugins.camera.CameraProperties; +import io.flutter.plugins.camera.features.CameraFeature; +import java.util.Arrays; + +/** + * Holds the current region boundaries. When this is created, you must provide a + * CaptureRequestBuilder for which we can read the distortion correction settings from. + */ +public class RegionBoundaries implements CameraFeature { + private boolean isSupported; + private Size currentSetting; + private CameraProperties cameraProperties; + private CameraRegions cameraRegions; + + public RegionBoundaries( + CameraProperties cameraProperties, CaptureRequest.Builder requestBuilder) { + this.cameraProperties = cameraProperties; + // No distortion correction support + if (android.os.Build.VERSION.SDK_INT < Build.VERSION_CODES.P + || !supportsDistortionCorrection()) { + setValue(cameraProperties.getSensorInfoPixelArraySize()); + } + + // Get the current distortion correction mode + Integer distortionCorrectionMode = null; + if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.P) { + distortionCorrectionMode = requestBuilder.get(CaptureRequest.DISTORTION_CORRECTION_MODE); + } + + // Return the correct boundaries depending on the mode + android.graphics.Rect rect; + if (distortionCorrectionMode == null + || distortionCorrectionMode == CaptureRequest.DISTORTION_CORRECTION_MODE_OFF) { + rect = cameraProperties.getSensorInfoPreCorrectionActiveArraySize(); + } else { + rect = cameraProperties.getSensorInfoActiveArraySize(); + } + + // Set new region size + setValue(rect == null ? null : new Size(rect.width(), rect.height())); + + // Create new camera regions using new size + cameraRegions = new CameraRegions(currentSetting); + } + + @Override + public Size getValue() { + return currentSetting; + } + + @Override + public void setValue(Size value) { + this.currentSetting = value; + } + + // Available on all devices. + @Override + public boolean isSupported(CameraProperties cameraProperties) { + return true; + } + + @Override + public void updateBuilder(CaptureRequest.Builder requestBuilder) { + // Not used + } + + @TargetApi(Build.VERSION_CODES.P) + private boolean supportsDistortionCorrection() { + int[] availableDistortionCorrectionModes = + cameraProperties.getDistortionCorrectionAvailableModes(); + if (availableDistortionCorrectionModes == null) availableDistortionCorrectionModes = new int[0]; + long nonOffModesSupported = + Arrays.stream(availableDistortionCorrectionModes) + .filter((value) -> value != CaptureRequest.DISTORTION_CORRECTION_MODE_OFF) + .count(); + return nonOffModesSupported > 0; + } + + public CameraRegions getCameraRegions() { + return this.cameraRegions; + } +} diff --git a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/resolution/Resolution.java b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/resolution/Resolution.java new file mode 100644 index 000000000000..ca9a8f60d8d3 --- /dev/null +++ b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/resolution/Resolution.java @@ -0,0 +1,111 @@ +package io.flutter.plugins.camera.features.resolution; + +import android.hardware.camera2.CaptureRequest; +import android.media.CamcorderProfile; +import android.util.Log; +import android.util.Size; +import io.flutter.plugins.camera.CameraProperties; +import io.flutter.plugins.camera.features.CameraFeature; + +public class Resolution implements CameraFeature { + private final Size captureSize; + private final Size previewSize; + private final CamcorderProfile recordingProfile; + // private final boolean recordingVideo; + private boolean isSupported; + private ResolutionPreset currentSetting; + + public Resolution(ResolutionPreset initialSetting, String cameraName) { + setValue(initialSetting); + + // Resolution configuration + recordingProfile = + getBestAvailableCamcorderProfileForResolutionPreset(cameraName, initialSetting); + captureSize = new Size(recordingProfile.videoFrameWidth, recordingProfile.videoFrameHeight); + Log.i("Camera", "captureSize: " + captureSize); + + previewSize = computeBestPreviewSize(cameraName, initialSetting); + } + + static CamcorderProfile getBestAvailableCamcorderProfileForResolutionPreset( + String cameraName, ResolutionPreset preset) { + int cameraId = Integer.parseInt(cameraName); + switch (preset) { + // All of these cases deliberately fall through to get the best available profile. + case max: + if (CamcorderProfile.hasProfile(cameraId, CamcorderProfile.QUALITY_HIGH)) { + return CamcorderProfile.get(cameraId, CamcorderProfile.QUALITY_HIGH); + } + case ultraHigh: + if (CamcorderProfile.hasProfile(cameraId, CamcorderProfile.QUALITY_2160P)) { + return CamcorderProfile.get(cameraId, CamcorderProfile.QUALITY_2160P); + } + case veryHigh: + if (CamcorderProfile.hasProfile(cameraId, CamcorderProfile.QUALITY_1080P)) { + return CamcorderProfile.get(cameraId, CamcorderProfile.QUALITY_1080P); + } + case high: + if (CamcorderProfile.hasProfile(cameraId, CamcorderProfile.QUALITY_720P)) { + return CamcorderProfile.get(cameraId, CamcorderProfile.QUALITY_720P); + } + case medium: + if (CamcorderProfile.hasProfile(cameraId, CamcorderProfile.QUALITY_480P)) { + return CamcorderProfile.get(cameraId, CamcorderProfile.QUALITY_480P); + } + case low: + if (CamcorderProfile.hasProfile(cameraId, CamcorderProfile.QUALITY_QVGA)) { + return CamcorderProfile.get(cameraId, CamcorderProfile.QUALITY_QVGA); + } + default: + if (CamcorderProfile.hasProfile(cameraId, CamcorderProfile.QUALITY_LOW)) { + return CamcorderProfile.get(cameraId, CamcorderProfile.QUALITY_LOW); + } else { + throw new IllegalArgumentException( + "No capture session available for current capture session."); + } + } + } + + static Size computeBestPreviewSize(String cameraName, ResolutionPreset preset) { + if (preset.ordinal() > ResolutionPreset.high.ordinal()) { + preset = ResolutionPreset.high; + } + + CamcorderProfile profile = + getBestAvailableCamcorderProfileForResolutionPreset(cameraName, preset); + return new Size(profile.videoFrameWidth, profile.videoFrameHeight); + } + + @Override + public ResolutionPreset getValue() { + return currentSetting; + } + + @Override + public void setValue(ResolutionPreset value) { + this.currentSetting = value; + } + + // Always supported + @Override + public boolean isSupported(CameraProperties cameraProperties) { + return true; + } + + @Override + public void updateBuilder(CaptureRequest.Builder requestBuilder) { + // Not used + } + + public CamcorderProfile getRecordingProfile() { + return this.recordingProfile; + } + + public Size getPreviewSize() { + return this.previewSize; + } + + public Size getCaptureSize() { + return this.captureSize; + } +} diff --git a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/types/ResolutionPreset.java b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/resolution/ResolutionPreset.java similarity index 83% rename from packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/types/ResolutionPreset.java rename to packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/resolution/ResolutionPreset.java index 1508dcefb293..5216fe2b2366 100644 --- a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/types/ResolutionPreset.java +++ b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/resolution/ResolutionPreset.java @@ -2,7 +2,7 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -package io.flutter.plugins.camera.types; +package io.flutter.plugins.camera.features.resolution; // Mirrors camera.dart public enum ResolutionPreset { diff --git a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/DeviceOrientationManager.java b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/sensororientation/DeviceOrientationManager.java similarity index 95% rename from packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/DeviceOrientationManager.java rename to packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/sensororientation/DeviceOrientationManager.java index a1852ce545de..5891f2ae29c7 100644 --- a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/DeviceOrientationManager.java +++ b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/sensororientation/DeviceOrientationManager.java @@ -2,7 +2,7 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -package io.flutter.plugins.camera; +package io.flutter.plugins.camera.features.sensororientation; import android.app.Activity; import android.content.BroadcastReceiver; @@ -17,8 +17,9 @@ import android.view.Surface; import android.view.WindowManager; import io.flutter.embedding.engine.systemchannels.PlatformChannel; +import io.flutter.plugins.camera.DartMessenger; -class DeviceOrientationManager { +public class DeviceOrientationManager { private static final IntentFilter orientationIntentFilter = new IntentFilter(Intent.ACTION_CONFIGURATION_CHANGED); @@ -31,15 +32,9 @@ class DeviceOrientationManager { private OrientationEventListener orientationEventListener; private BroadcastReceiver broadcastReceiver; - /** - * Factory method to create a device orientation manager. - */ + /** Factory method to create a device orientation manager. */ public static DeviceOrientationManager create( - Activity activity, - DartMessenger messenger, - boolean isFrontFacing, - int sensorOrientation - ) { + Activity activity, DartMessenger messenger, boolean isFrontFacing, int sensorOrientation) { return new DeviceOrientationManager(activity, messenger, isFrontFacing, sensorOrientation); } diff --git a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/sensororientation/SensorOrientation.java b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/sensororientation/SensorOrientation.java new file mode 100644 index 000000000000..07c292d8b428 --- /dev/null +++ b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/sensororientation/SensorOrientation.java @@ -0,0 +1,49 @@ +package io.flutter.plugins.camera.features.sensororientation; + +import android.app.Activity; +import android.hardware.camera2.CameraMetadata; +import android.hardware.camera2.CaptureRequest; +import io.flutter.plugins.camera.CameraProperties; +import io.flutter.plugins.camera.DartMessenger; +import io.flutter.plugins.camera.features.CameraFeature; + +public class SensorOrientation implements CameraFeature { + private boolean isSupported; + private Integer currentSetting = 0; + private final DeviceOrientationManager deviceOrientationListener; + + public SensorOrientation( + CameraProperties cameraProperties, Activity activity, DartMessenger dartMessenger) { + setValue(cameraProperties.getSensorOrientation()); + + boolean isFrontFacing = cameraProperties.getLensFacing() == CameraMetadata.LENS_FACING_FRONT; + deviceOrientationListener = + DeviceOrientationManager.create(activity, dartMessenger, isFrontFacing, currentSetting); + deviceOrientationListener.start(); + } + + @Override + public Integer getValue() { + return currentSetting; + } + + @Override + public void setValue(Integer value) { + this.currentSetting = value; + } + + @Override + public boolean isSupported(CameraProperties cameraProperties) { + // Always supported + return true; + } + + @Override + public void updateBuilder(CaptureRequest.Builder requestBuilder) { + // Not used + } + + public DeviceOrientationManager getDeviceOrientationManager() { + return this.deviceOrientationListener; + } +} diff --git a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/CameraZoom.java b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/zoomlevel/CameraZoom.java similarity index 96% rename from packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/CameraZoom.java rename to packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/zoomlevel/CameraZoom.java index 5eed9f4734b7..8a7909a29b72 100644 --- a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/CameraZoom.java +++ b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/zoomlevel/CameraZoom.java @@ -2,7 +2,7 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -package io.flutter.plugins.camera; +package io.flutter.plugins.camera.features.zoomlevel; import android.graphics.Rect; import androidx.annotation.NonNull; diff --git a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/zoomlevel/ZoomLevel.java b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/zoomlevel/ZoomLevel.java new file mode 100644 index 000000000000..24cde43bb8f3 --- /dev/null +++ b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/zoomlevel/ZoomLevel.java @@ -0,0 +1,56 @@ +package io.flutter.plugins.camera.features.zoomlevel; + +import android.graphics.Rect; +import android.hardware.camera2.CaptureRequest; +import android.util.Log; +import io.flutter.plugins.camera.CameraProperties; +import io.flutter.plugins.camera.features.CameraFeature; + +/** Exposure offset makes the image brighter or darker. */ +public class ZoomLevel implements CameraFeature { + private boolean isSupported; + private Float currentSetting; + private CameraProperties cameraProperties; + private CameraZoom cameraZoom; + + public ZoomLevel(CameraProperties cameraProperties) { + this.cameraProperties = cameraProperties; + this.cameraZoom = + new CameraZoom( + cameraProperties.getSensorInfoActiveArraySize(), + cameraProperties.getScalerAvailableMaxDigitalZoom()); + } + + @Override + public Float getValue() { + return currentSetting; + } + + @Override + public void setValue(Float value) { + this.currentSetting = value; + } + + // Available on all devices. + @Override + public boolean isSupported(CameraProperties cameraProperties) { + return true; + } + + @Override + public void updateBuilder(CaptureRequest.Builder requestBuilder) { + Log.i("Camera", "updateExposureOffset"); + + // Don't try to set if the current camera doesn't support it. + if (!isSupported) { + return; + } + + final Rect computedZoom = cameraZoom.computeZoom(currentSetting); + requestBuilder.set(CaptureRequest.SCALER_CROP_REGION, computedZoom); + } + + public CameraZoom getCameraZoom() { + return this.cameraZoom; + } +} diff --git a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/types/NoiseReductionMode.java b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/types/NoiseReductionMode.java deleted file mode 100644 index b02bd49defed..000000000000 --- a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/types/NoiseReductionMode.java +++ /dev/null @@ -1,26 +0,0 @@ -package io.flutter.plugins.camera.types; - -/** - * Only supports fast mode for now. - */ -public enum NoiseReductionMode { - fast("fast"); - - private final String strValue; - - NoiseReductionMode(String strValue) { - this.strValue = strValue; - } - - public static NoiseReductionMode getValueForString(String modeStr) { - for (NoiseReductionMode value : values()) { - if (value.strValue.equals(modeStr)) return value; - } - return null; - } - - @Override - public String toString() { - return strValue; - } -} 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 54bb190bd43b..d0c930796486 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 @@ -55,20 +55,20 @@ public void should_create_camera_plugin() throws CameraAccessException { when(mockCameraProperties.getControlAutoExposureAvailableTargetFpsRanges()).thenReturn(null); Camera camera = null; - try (MockedStatic mockOrientationManagerFactory = mockStatic(DeviceOrientationManager.class)) { - mockOrientationManagerFactory.when(() -> DeviceOrientationManager.create( - mockActivity, - dartMessengerMock, - true, - 0)).thenReturn(mockDeviceOrientationManager); + try (MockedStatic mockOrientationManagerFactory = + mockStatic(DeviceOrientationManager.class)) { + mockOrientationManagerFactory + .when(() -> DeviceOrientationManager.create(mockActivity, dartMessengerMock, true, 0)) + .thenReturn(mockDeviceOrientationManager); - camera = new Camera( - mockActivity, - flutterTextureMock, - dartMessengerMock, - mockCameraProperties, - resolutionPreset, - enableAudio); + camera = + new Camera( + mockActivity, + flutterTextureMock, + dartMessengerMock, + mockCameraProperties, + resolutionPreset, + enableAudio); } assertNotNull("should create a camera", camera); diff --git a/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/PictureCaptureRequestTest.java b/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/PictureCaptureRequestTest.java index 5f3a18a30f6f..cef547f889b8 100644 --- a/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/PictureCaptureRequestTest.java +++ b/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/PictureCaptureRequestTest.java @@ -68,14 +68,13 @@ public void setState_sends_camera_error_event_When_already_finished() { @Test public void setState_resets_timeout() { - try(MockedStatic mockTimeoutFactory = mockStatic(TimeoutHandler.class)) { + try (MockedStatic mockTimeoutFactory = mockStatic(TimeoutHandler.class)) { PictureCaptureRequest.TimeoutHandler mockTimeoutHandler = mock(PictureCaptureRequest.TimeoutHandler.class); mockTimeoutFactory.when(TimeoutHandler::create).thenReturn(mockTimeoutHandler); - PictureCaptureRequest req = PictureCaptureRequest - .create(null, null, null); + PictureCaptureRequest req = PictureCaptureRequest.create(null, null, null); req.setState(PictureCaptureRequestState.STATE_WAITING_FOCUS); req.setState(PictureCaptureRequestState.STATE_WAITING_PRECAPTURE_START); req.setState(PictureCaptureRequestState.STATE_WAITING_PRECAPTURE_DONE); @@ -87,7 +86,7 @@ public void setState_resets_timeout() { @Test public void setState_clears_timeout() { - try(MockedStatic mockTimeoutFactory = mockStatic(TimeoutHandler.class)) { + try (MockedStatic mockTimeoutFactory = mockStatic(TimeoutHandler.class)) { PictureCaptureRequest.TimeoutHandler mockTimeoutHandler = mock(PictureCaptureRequest.TimeoutHandler.class); @@ -117,15 +116,14 @@ public void finish_sets_result_and_state() { @Test public void finish_clears_timeout() { - try(MockedStatic mockTimeoutFactory = mockStatic(TimeoutHandler.class)) { + try (MockedStatic mockTimeoutFactory = mockStatic(TimeoutHandler.class)) { PictureCaptureRequest.TimeoutHandler mockTimeoutHandler = mock(PictureCaptureRequest.TimeoutHandler.class); mockTimeoutFactory.when(TimeoutHandler::create).thenReturn(mockTimeoutHandler); MethodChannel.Result mockResult = mock(MethodChannel.Result.class); - PictureCaptureRequest req = - PictureCaptureRequest.create(mockResult, null, null); + PictureCaptureRequest req = PictureCaptureRequest.create(mockResult, null, null); req.finish("/test/path"); verify(mockTimeoutHandler, never()).resetTimeout(any()); verify(mockTimeoutHandler).clearTimeout(any()); @@ -188,15 +186,14 @@ public void error_sets_result_and_state() { @Test public void error_clears_timeout() { - try(MockedStatic mockTimeoutFactory = mockStatic(TimeoutHandler.class)) { + try (MockedStatic mockTimeoutFactory = mockStatic(TimeoutHandler.class)) { PictureCaptureRequest.TimeoutHandler mockTimeoutHandler = mock(PictureCaptureRequest.TimeoutHandler.class); mockTimeoutFactory.when(TimeoutHandler::create).thenReturn(mockTimeoutHandler); MethodChannel.Result mockResult = mock(MethodChannel.Result.class); - PictureCaptureRequest req = PictureCaptureRequest - .create(mockResult, null, null); + PictureCaptureRequest req = PictureCaptureRequest.create(mockResult, null, null); req.error("ERROR_CODE", "Error Message", null); verify(mockTimeoutHandler, never()).resetTimeout(any()); verify(mockTimeoutHandler).clearTimeout(any()); diff --git a/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/media/MediaRecorderBuilderTest.java b/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/media/MediaRecorderBuilderTest.java index 2f018cbd4215..401a337fa43f 100644 --- a/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/media/MediaRecorderBuilderTest.java +++ b/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/media/MediaRecorderBuilderTest.java @@ -32,10 +32,10 @@ public void build_Should_set_values_in_correct_order_When_audio_is_disabled() th int mediaOrientation = 1; MediaRecorder recorder = null; - try (MockedStatic mockMediaRecorderFactory = mockStatic(MediaRecorderFactory.class)) { + try (MockedStatic mockMediaRecorderFactory = + mockStatic(MediaRecorderFactory.class)) { MediaRecorder mockMediaRecorder = mock(MediaRecorder.class); - mockMediaRecorderFactory.when(MediaRecorderFactory::create) - .thenReturn(mockMediaRecorder); + mockMediaRecorderFactory.when(MediaRecorderFactory::create).thenReturn(mockMediaRecorder); MediaRecorderBuilder builder = new MediaRecorderBuilder(recorderProfile, outputFilePath) @@ -44,18 +44,18 @@ public void build_Should_set_values_in_correct_order_When_audio_is_disabled() th recorder = builder.build(); } - InOrder inOrder = inOrder(recorder); - inOrder.verify(recorder).setVideoSource(MediaRecorder.VideoSource.SURFACE); - inOrder.verify(recorder).setOutputFormat(recorderProfile.fileFormat); - inOrder.verify(recorder).setVideoEncoder(recorderProfile.videoCodec); - inOrder.verify(recorder).setVideoEncodingBitRate(recorderProfile.videoBitRate); - inOrder.verify(recorder).setVideoFrameRate(recorderProfile.videoFrameRate); - inOrder - .verify(recorder) - .setVideoSize(recorderProfile.videoFrameWidth, recorderProfile.videoFrameHeight); - inOrder.verify(recorder).setOutputFile(outputFilePath); - inOrder.verify(recorder).setOrientationHint(mediaOrientation); - inOrder.verify(recorder).prepare(); + InOrder inOrder = inOrder(recorder); + inOrder.verify(recorder).setVideoSource(MediaRecorder.VideoSource.SURFACE); + inOrder.verify(recorder).setOutputFormat(recorderProfile.fileFormat); + inOrder.verify(recorder).setVideoEncoder(recorderProfile.videoCodec); + inOrder.verify(recorder).setVideoEncodingBitRate(recorderProfile.videoBitRate); + inOrder.verify(recorder).setVideoFrameRate(recorderProfile.videoFrameRate); + inOrder + .verify(recorder) + .setVideoSize(recorderProfile.videoFrameWidth, recorderProfile.videoFrameHeight); + inOrder.verify(recorder).setOutputFile(outputFilePath); + inOrder.verify(recorder).setOrientationHint(mediaOrientation); + inOrder.verify(recorder).prepare(); } @Test @@ -67,10 +67,10 @@ public void build_Should_set_values_in_correct_order_When_audio_is_enabled() thr String outputFilePath = "mock_video_file_path"; int mediaOrientation = 1; - try (MockedStatic mockMediaRecorderFactory = mockStatic(MediaRecorderFactory.class)) { + try (MockedStatic mockMediaRecorderFactory = + mockStatic(MediaRecorderFactory.class)) { MediaRecorder mockMediaRecorder = mock(MediaRecorder.class); - mockMediaRecorderFactory.when(MediaRecorderFactory::create) - .thenReturn(mockMediaRecorder); + mockMediaRecorderFactory.when(MediaRecorderFactory::create).thenReturn(mockMediaRecorder); MediaRecorderBuilder builder = new MediaRecorderBuilder(recorderProfile, outputFilePath) From c06762aed9e2d1d3356d47d55d596e07d3e12b17 Mon Sep 17 00:00:00 2001 From: Andrew Coutts Date: Sun, 7 Mar 2021 10:16:52 -0500 Subject: [PATCH 069/114] Linter errors --- .../java/io/flutter/plugins/camera/Camera.java | 8 +++++++- .../plugins/camera/MethodCallHandlerImpl.java | 8 ++++---- .../camera/features/autoFocus/AutoFocus.java | 18 ++++++++++-------- .../features/exposurelock/ExposureLock.java | 1 - .../plugins/camera/features/flash/Flash.java | 1 - .../noisereduction/NoiseReduction.java | 1 - 6 files changed, 21 insertions(+), 16 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 dd14db71259f..6eb7f61bb6b8 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 @@ -199,7 +199,7 @@ public Camera( put( CameraFeatures.resolution, new Resolution(resolutionPreset, cameraProperties.getCameraName())); - put(CameraFeatures.autoFocus, new AutoFocus()); + put(CameraFeatures.autoFocus, new AutoFocus(false)); put( CameraFeatures.sensorOrientation, new SensorOrientation(cameraProperties, activity, dartMessenger)); @@ -716,7 +716,11 @@ public void startVideoRecording(Result result) { try { prepareMediaRecorder(videoRecordingFile.getAbsolutePath()); + + // Re-create autofocus feature so it's using video focus mode now + cameraFeatures.put(CameraFeatures.autoFocus, new AutoFocus(true)); recordingVideo = true; + createCaptureSession( CameraDevice.TEMPLATE_RECORD, () -> mediaRecorder.start(), mediaRecorder.getSurface()); result.success(null); @@ -734,6 +738,8 @@ public void stopVideoRecording(@NonNull final Result result) { } try { + // Re-create autofocus feature so it's using continuous capture focus mode now + cameraFeatures.put(CameraFeatures.autoFocus, new AutoFocus(false)); recordingVideo = false; try { diff --git a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/MethodCallHandlerImpl.java b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/MethodCallHandlerImpl.java index 4561b9eb73bc..2891fbea8077 100644 --- a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/MethodCallHandlerImpl.java +++ b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/MethodCallHandlerImpl.java @@ -15,10 +15,10 @@ import io.flutter.plugin.common.MethodChannel; import io.flutter.plugin.common.MethodChannel.Result; import io.flutter.plugins.camera.CameraPermissions.PermissionsRegistry; -import io.flutter.plugins.camera.types.ExposureMode; -import io.flutter.plugins.camera.types.FlashMode; -import io.flutter.plugins.camera.types.FocusMode; -import io.flutter.plugins.camera.types.ResolutionPreset; +import io.flutter.plugins.camera.features.autofocus.FocusMode; +import io.flutter.plugins.camera.features.exposurelock.ExposureMode; +import io.flutter.plugins.camera.features.flash.FlashMode; +import io.flutter.plugins.camera.features.resolution.ResolutionPreset; import io.flutter.view.TextureRegistry; import java.util.HashMap; import java.util.Map; diff --git a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/autoFocus/AutoFocus.java b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/autoFocus/AutoFocus.java index a753b52fbe49..2ba518e685f2 100644 --- a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/autoFocus/AutoFocus.java +++ b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/autoFocus/AutoFocus.java @@ -5,16 +5,19 @@ import android.util.Log; import io.flutter.plugins.camera.CameraProperties; import io.flutter.plugins.camera.features.CameraFeature; -import io.flutter.plugins.camera.types.FocusMode; public class AutoFocus implements CameraFeature { // private final boolean recordingVideo; private boolean isSupported; private FocusMode currentSetting = FocusMode.auto; - // public AutoFocus(boolean recordingVideo) { - // this.recordingVideo = recordingVideo; - // } + // When we switch recording modes we re-create this feature with + // the appropriate setting here. + private final boolean recordingVideo; + + public AutoFocus(boolean recordingVideo) { + this.recordingVideo = recordingVideo; + } @Override public FocusMode getValue() { @@ -74,10 +77,9 @@ public void updateBuilder(CaptureRequest.Builder requestBuilder) { case auto: requestBuilder.set( CaptureRequest.CONTROL_AF_MODE, - // recordingVideo - // ? CaptureRequest.CONTROL_AF_MODE_CONTINUOUS_VIDEO - // : - CaptureRequest.CONTROL_AF_MODE_CONTINUOUS_PICTURE); + recordingVideo + ? CaptureRequest.CONTROL_AF_MODE_CONTINUOUS_VIDEO + : CaptureRequest.CONTROL_AF_MODE_CONTINUOUS_PICTURE); default: break; } diff --git a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/exposurelock/ExposureLock.java b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/exposurelock/ExposureLock.java index ab5ce22bc65e..37f53f3c2d01 100644 --- a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/exposurelock/ExposureLock.java +++ b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/exposurelock/ExposureLock.java @@ -4,7 +4,6 @@ import android.util.Log; import io.flutter.plugins.camera.CameraProperties; import io.flutter.plugins.camera.features.CameraFeature; -import io.flutter.plugins.camera.types.ExposureMode; /** * Exposure lock controls whether or not exposure mode is currenty locked or automatically metering. diff --git a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/flash/Flash.java b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/flash/Flash.java index 1a71c205b676..81d7aeb403ef 100644 --- a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/flash/Flash.java +++ b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/flash/Flash.java @@ -4,7 +4,6 @@ import android.util.Log; import io.flutter.plugins.camera.CameraProperties; import io.flutter.plugins.camera.features.CameraFeature; -import io.flutter.plugins.camera.types.FlashMode; public class Flash implements CameraFeature { private boolean isSupported; diff --git a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/noisereduction/NoiseReduction.java b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/noisereduction/NoiseReduction.java index 68130805a7bd..2b31c300765d 100644 --- a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/noisereduction/NoiseReduction.java +++ b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/noisereduction/NoiseReduction.java @@ -4,7 +4,6 @@ import android.util.Log; import io.flutter.plugins.camera.CameraProperties; import io.flutter.plugins.camera.features.CameraFeature; -import io.flutter.plugins.camera.types.NoiseReductionMode; /** * This can either be enabled or disabled. Only full capability devices can set this to off. Legacy From 445bb1c7728a8126f3ad0c7573f0a4577ffdea1b Mon Sep 17 00:00:00 2001 From: Andrew Coutts Date: Sun, 7 Mar 2021 10:32:26 -0500 Subject: [PATCH 070/114] Cleanup --- .../io/flutter/plugins/camera/Camera.java | 58 +++++++++---------- .../camera/features/autoFocus/AutoFocus.java | 4 +- .../features/exposurelock/ExposureLock.java | 5 +- .../exposureoffset/ExposureOffset.java | 5 +- .../exposureoffset/ExposureOffsetValue.java | 4 ++ .../features/exposurepoint/ExposurePoint.java | 5 +- .../plugins/camera/features/flash/Flash.java | 6 +- .../features/focuspoint/FocusPoint.java | 4 +- .../camera/features/fpsrange/FpsRange.java | 4 +- .../noisereduction/NoiseReduction.java | 5 +- .../regionboundaries/RegionBoundaries.java | 4 +- .../features/resolution/Resolution.java | 4 +- .../sensororientation/SensorOrientation.java | 4 +- .../camera/features/zoomlevel/ZoomLevel.java | 5 +- 14 files changed, 60 insertions(+), 57 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 6eb7f61bb6b8..0b0bdd83fe60 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 @@ -123,16 +123,13 @@ class Camera implements CameraCaptureCallback.CameraCaptureStateListener { private final Map cameraFeatures; private final SurfaceTextureEntry flutterTexture; - private final boolean enableAudio; private final Context applicationContext; - private final DartMessenger dartMessenger; private final CameraProperties cameraProperties; private final Activity activity; /** This manages the state of the camera and the current capture request. */ PictureCaptureRequest pictureCaptureRequest; - /** Whether the current camera device supports auto focus or not. */ /** The state of the camera. By default we are in the preview state. */ private CameraState cameraState = CameraState.STATE_PREVIEW; /** A {@link Handler} for running tasks in the background. */ @@ -167,7 +164,9 @@ public void onImageAvailable(ImageReader reader) { /** {@link CaptureRequest} generated by {@link #mPreviewRequestBuilder} */ private MediaRecorder mediaRecorder; + /** True when recording video. */ private boolean recordingVideo; + private File videoRecordingFile; /** A {@link CameraCaptureSession.CaptureCallback} that handles events related to JPEG capture. */ private final CameraCaptureCallback mCaptureCallback; @@ -247,7 +246,7 @@ public CameraState getState() { /** * Update the builder settings with all of our available features. * - * @param requestBuilder + * @param requestBuilder request builder to update. */ private void updateBuilderSettings(CaptureRequest.Builder requestBuilder) { for (Map.Entry feature : cameraFeatures.entrySet()) { @@ -805,13 +804,12 @@ public void resumeVideoRecording(@NonNull final Result result) { * Dart handler when it's time to set a new flash mode. This will try to set a new flash mode to * the current camera. * - * @param result - * @param newMode - * @throws CameraAccessException + * @param result Flutter result. + * @param newMode new mode. */ public void setFlashMode(@NonNull final Result result, FlashMode newMode) { // Save the new flash mode setting - cameraFeatures.get(CameraFeatures.flash).setValue(newMode); + ((Flash) cameraFeatures.get(CameraFeatures.flash)).setValue(newMode); cameraFeatures.get(CameraFeatures.flash).updateBuilder(mPreviewRequestBuilder); refreshPreviewCaptureSession( @@ -822,12 +820,11 @@ public void setFlashMode(@NonNull final Result result, FlashMode newMode) { /** * Dart handler for setting new exposure mode setting. * - * @param result - * @param newMode - * @throws CameraAccessException + * @param result Flutter result. + * @param newMode new mode. */ public void setExposureMode(@NonNull final Result result, ExposureMode newMode) { - cameraFeatures.get(CameraFeatures.exposureLock).setValue(newMode); + ((ExposureLock) cameraFeatures.get(CameraFeatures.exposureLock)).setValue(newMode); cameraFeatures.get(CameraFeatures.exposureLock).updateBuilder(mPreviewRequestBuilder); refreshPreviewCaptureSession( @@ -842,7 +839,7 @@ public void setExposureMode(@NonNull final Result result, ExposureMode newMode) * *

The CameraRegions will be replaced every time a new capture session is started. * - * @return + * @return camera region object. */ public CameraRegions getCameraRegions() { final RegionBoundaries regionBoundaries = @@ -853,12 +850,12 @@ public CameraRegions getCameraRegions() { /** * Set new exposure point from dart. * - * @param result - * @param x - * @param y + * @param result Flutter result. + * @param x new x. + * @param y new y. */ public void setExposurePoint(@NonNull final Result result, Double x, Double y) { - cameraFeatures.get(CameraFeatures.exposurePoint).setValue(new Point(x, y)); + ((ExposurePoint) cameraFeatures.get(CameraFeatures.exposurePoint)).setValue(new Point(x, y)); cameraFeatures.get(CameraFeatures.exposurePoint).updateBuilder(mPreviewRequestBuilder); refreshPreviewCaptureSession( @@ -890,12 +887,11 @@ public double getExposureOffsetStepSize() { /** * Set new focus mode from dart. * - * @param result - * @param newMode - * @throws CameraAccessException + * @param result Flutter result. + * @param newMode New mode. */ public void setFocusMode(@NonNull final Result result, FocusMode newMode) { - cameraFeatures.get(CameraFeatures.autoFocus).setValue(newMode); + ((AutoFocus) cameraFeatures.get(CameraFeatures.autoFocus)).setValue(newMode); cameraFeatures.get(CameraFeatures.autoFocus).updateBuilder(mPreviewRequestBuilder); refreshPreviewCaptureSession( @@ -906,12 +902,12 @@ public void setFocusMode(@NonNull final Result result, FocusMode newMode) { /** * Sets new focus point from dart. * - * @param result - * @param x - * @param y + * @param result Flutter result. + * @param x new x. + * @param y new y. */ public void setFocusPoint(@NonNull final Result result, Double x, Double y) { - cameraFeatures.get(CameraFeatures.focusPoint).setValue(new Point(x, y)); + ((FocusPoint) cameraFeatures.get(CameraFeatures.focusPoint)).setValue(new Point(x, y)); cameraFeatures.get(CameraFeatures.focusPoint).updateBuilder(mPreviewRequestBuilder); refreshPreviewCaptureSession( @@ -922,11 +918,12 @@ public void setFocusPoint(@NonNull final Result result, Double x, Double y) { /** * Set a new exposure offset from dart. From dart the offset comes as a double, like +1.3 or -1.3. * - * @param result - * @param offset + * @param result flutter result. + * @param offset new value. */ public void setExposureOffset(@NonNull final Result result, double offset) { - cameraFeatures.get(CameraFeatures.exposureOffset).setValue(offset); + ((ExposureOffset) cameraFeatures.get(CameraFeatures.exposureOffset)) + .setValue(new ExposureOffsetValue(offset)); cameraFeatures.get(CameraFeatures.exposureOffset).updateBuilder(mPreviewRequestBuilder); refreshPreviewCaptureSession( @@ -967,9 +964,8 @@ DeviceOrientationManager getDeviceOrientationManager() { /** * Set zoom level from dart. * - * @param result - * @param zoom - * @throws CameraAccessException + * @param result Flutter result. + * @param zoom new value. */ public void setZoomLevel(@NonNull final Result result, float zoom) throws CameraAccessException { final ZoomLevel zoomLevel = (ZoomLevel) cameraFeatures.get(CameraFeatures.zoomLevel); diff --git a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/autoFocus/AutoFocus.java b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/autoFocus/AutoFocus.java index 2ba518e685f2..7bf980aa65c4 100644 --- a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/autoFocus/AutoFocus.java +++ b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/autoFocus/AutoFocus.java @@ -63,12 +63,12 @@ public boolean isSupported(CameraProperties cameraProperties) { @Override public void updateBuilder(CaptureRequest.Builder requestBuilder) { - Log.i("Camera", "updateFocusMode currentFocusMode: " + currentSetting); - if (!isSupported) { return; } + Log.i("Camera", "updateFocusMode | currentSetting: " + currentSetting); + switch (currentSetting) { case locked: requestBuilder.set(CaptureRequest.CONTROL_AF_MODE, CaptureRequest.CONTROL_AF_MODE_AUTO); diff --git a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/exposurelock/ExposureLock.java b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/exposurelock/ExposureLock.java index 37f53f3c2d01..e26f4003dc08 100644 --- a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/exposurelock/ExposureLock.java +++ b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/exposurelock/ExposureLock.java @@ -30,13 +30,12 @@ public boolean isSupported(CameraProperties cameraProperties) { @Override public void updateBuilder(CaptureRequest.Builder requestBuilder) { - Log.i("Camera", "updateExposureMode"); - - // Don't try to set if the current camera doesn't support it. if (!isSupported) { return; } + Log.i("Camera", "updateExposureLock | currentSetting: " + currentSetting); + switch (currentSetting) { case locked: requestBuilder.set(CaptureRequest.CONTROL_AE_LOCK, true); diff --git a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/exposureoffset/ExposureOffset.java b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/exposureoffset/ExposureOffset.java index ace6877cb745..1013f44154ef 100644 --- a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/exposureoffset/ExposureOffset.java +++ b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/exposureoffset/ExposureOffset.java @@ -43,13 +43,12 @@ public boolean isSupported(CameraProperties cameraProperties) { @Override public void updateBuilder(CaptureRequest.Builder requestBuilder) { - Log.i("Camera", "updateExposureOffset"); - - // Don't try to set if the current camera doesn't support it. if (!isSupported) { return; } + Log.i("Camera", "updateExposureOffset | currentSetting: " + currentSetting); + requestBuilder.set(CaptureRequest.CONTROL_AE_EXPOSURE_COMPENSATION, (int) currentSetting.value); } diff --git a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/exposureoffset/ExposureOffsetValue.java b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/exposureoffset/ExposureOffsetValue.java index a8c1d97219d9..5e44e42c0cb1 100644 --- a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/exposureoffset/ExposureOffsetValue.java +++ b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/exposureoffset/ExposureOffsetValue.java @@ -14,4 +14,8 @@ public ExposureOffsetValue(double min, double max, double value) { this.max = max; this.value = value; } + + public ExposureOffsetValue(double value) { + this(0, 0, value); + } } diff --git a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/exposurepoint/ExposurePoint.java b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/exposurepoint/ExposurePoint.java index af0684c03478..382b36c8ec05 100644 --- a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/exposurepoint/ExposurePoint.java +++ b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/exposurepoint/ExposurePoint.java @@ -51,13 +51,12 @@ public boolean isSupported(CameraProperties cameraProperties) { @Override public void updateBuilder(CaptureRequest.Builder requestBuilder) { - Log.i("Camera", "updateExposureMode"); - - // Don't try to set if the current camera doesn't support it. if (!isSupported) { return; } + Log.i("Camera", "updateExposurePoint | currentSetting: " + currentSetting); + MeteringRectangle aeRect = null; try { aeRect = getCameraRegions.call().getAEMeteringRectangle(); diff --git a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/flash/Flash.java b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/flash/Flash.java index 81d7aeb403ef..5787f2cb1fe8 100644 --- a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/flash/Flash.java +++ b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/flash/Flash.java @@ -29,14 +29,12 @@ public boolean isSupported(CameraProperties cameraProperties) { @Override public void updateBuilder(CaptureRequest.Builder requestBuilder) { - Log.i("Camera", "updateFlash"); - - // Don't try to set flash enabled if the current camera doesn't - // support it. if (!isSupported) { return; } + Log.i("Camera", "updateFlash | currentSetting: " + currentSetting); + switch (currentSetting) { case off: requestBuilder.set(CaptureRequest.CONTROL_AE_MODE, CaptureRequest.CONTROL_AE_MODE_ON); diff --git a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/focuspoint/FocusPoint.java b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/focuspoint/FocusPoint.java index 380dfc85a6b0..4fbc13fc5ab3 100644 --- a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/focuspoint/FocusPoint.java +++ b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/focuspoint/FocusPoint.java @@ -49,6 +49,8 @@ public boolean isSupported(CameraProperties cameraProperties) { @Override public void updateBuilder(CaptureRequest.Builder requestBuilder) { - // Not used + if (!isSupported) { + return; + } } } diff --git a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/fpsrange/FpsRange.java b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/fpsrange/FpsRange.java index 8073a5e6ee7a..467a14118997 100644 --- a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/fpsrange/FpsRange.java +++ b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/fpsrange/FpsRange.java @@ -52,10 +52,12 @@ public boolean isSupported(CameraProperties cameraProperties) { @Override public void updateBuilder(CaptureRequest.Builder requestBuilder) { - if (currentSetting == null) { + if (!isSupported) { return; } + Log.i("Camera", "updateFpsRange | currentSetting: " + currentSetting); + requestBuilder.set(CaptureRequest.CONTROL_AE_TARGET_FPS_RANGE, currentSetting); } } diff --git a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/noisereduction/NoiseReduction.java b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/noisereduction/NoiseReduction.java index 2b31c300765d..d96a994e1cb5 100644 --- a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/noisereduction/NoiseReduction.java +++ b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/noisereduction/NoiseReduction.java @@ -46,13 +46,12 @@ public boolean isSupported(CameraProperties cameraProperties) { @Override public void updateBuilder(CaptureRequest.Builder requestBuilder) { - Log.i("Camera", "updateFlash"); - - // Don't try to set if the current camera doesn't support it. if (!isSupported) { return; } + Log.i("Camera", "updateNoiseReduction | currentSetting: " + currentSetting); + // Always use fast mode. requestBuilder.set( CaptureRequest.NOISE_REDUCTION_MODE, CaptureRequest.NOISE_REDUCTION_MODE_FAST); diff --git a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/regionboundaries/RegionBoundaries.java b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/regionboundaries/RegionBoundaries.java index 7a98215a65fb..b065c27b8888 100644 --- a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/regionboundaries/RegionBoundaries.java +++ b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/regionboundaries/RegionBoundaries.java @@ -67,7 +67,9 @@ public boolean isSupported(CameraProperties cameraProperties) { @Override public void updateBuilder(CaptureRequest.Builder requestBuilder) { - // Not used + if (!isSupported) { + return; + } } @TargetApi(Build.VERSION_CODES.P) diff --git a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/resolution/Resolution.java b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/resolution/Resolution.java index ca9a8f60d8d3..6dc470b05407 100644 --- a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/resolution/Resolution.java +++ b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/resolution/Resolution.java @@ -94,7 +94,9 @@ public boolean isSupported(CameraProperties cameraProperties) { @Override public void updateBuilder(CaptureRequest.Builder requestBuilder) { - // Not used + if (!isSupported) { + return; + } } public CamcorderProfile getRecordingProfile() { diff --git a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/sensororientation/SensorOrientation.java b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/sensororientation/SensorOrientation.java index 07c292d8b428..15ec4c99bd72 100644 --- a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/sensororientation/SensorOrientation.java +++ b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/sensororientation/SensorOrientation.java @@ -40,7 +40,9 @@ public boolean isSupported(CameraProperties cameraProperties) { @Override public void updateBuilder(CaptureRequest.Builder requestBuilder) { - // Not used + if (!isSupported) { + return; + } } public DeviceOrientationManager getDeviceOrientationManager() { diff --git a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/zoomlevel/ZoomLevel.java b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/zoomlevel/ZoomLevel.java index 24cde43bb8f3..9e72c03109e3 100644 --- a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/zoomlevel/ZoomLevel.java +++ b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/zoomlevel/ZoomLevel.java @@ -39,13 +39,12 @@ public boolean isSupported(CameraProperties cameraProperties) { @Override public void updateBuilder(CaptureRequest.Builder requestBuilder) { - Log.i("Camera", "updateExposureOffset"); - - // Don't try to set if the current camera doesn't support it. if (!isSupported) { return; } + Log.i("Camera", "updateZoomLevel | currentSetting: " + currentSetting); + final Rect computedZoom = cameraZoom.computeZoom(currentSetting); requestBuilder.set(CaptureRequest.SCALER_CROP_REGION, computedZoom); } From 4b0b0d8c4be4784ca301475a78906c38ffc21b02 Mon Sep 17 00:00:00 2001 From: Andrew Coutts Date: Sun, 7 Mar 2021 11:05:30 -0500 Subject: [PATCH 071/114] Cleanup --- .../io/flutter/plugins/camera/Camera.java | 35 +++++++++++-------- .../camera/features/CameraFeature.java | 5 ++- .../camera/features/autoFocus/AutoFocus.java | 16 ++++++--- .../features/exposurelock/ExposureLock.java | 15 +++++++- .../exposureoffset/ExposureOffset.java | 12 ++++++- .../features/exposurepoint/ExposurePoint.java | 16 +++++++-- .../plugins/camera/features/flash/Flash.java | 16 +++++++-- .../features/focuspoint/FocusPoint.java | 15 ++++++-- .../camera/features/fpsrange/FpsRange.java | 13 ++++++- .../noisereduction/NoiseReduction.java | 15 +++++++- .../regionboundaries/RegionBoundaries.java | 13 ++++++- .../features/resolution/Resolution.java | 15 ++++++-- .../sensororientation/SensorOrientation.java | 14 ++++++-- .../camera/features/zoomlevel/ZoomLevel.java | 14 ++++++-- 14 files changed, 176 insertions(+), 38 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 0b0bdd83fe60..7c43308acfd2 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 @@ -197,22 +197,26 @@ public Camera( { put( CameraFeatures.resolution, - new Resolution(resolutionPreset, cameraProperties.getCameraName())); - put(CameraFeatures.autoFocus, new AutoFocus(false)); + new Resolution( + cameraProperties, resolutionPreset, cameraProperties.getCameraName())); + put(CameraFeatures.autoFocus, new AutoFocus(cameraProperties, false)); put( CameraFeatures.sensorOrientation, new SensorOrientation(cameraProperties, activity, dartMessenger)); - put(CameraFeatures.exposureLock, new ExposureLock()); + put(CameraFeatures.exposureLock, new ExposureLock(cameraProperties)); put(CameraFeatures.exposureOffset, new ExposureOffset(cameraProperties)); - put(CameraFeatures.exposurePoint, new ExposurePoint(() -> getCameraRegions())); - put(CameraFeatures.focusPoint, new FocusPoint(() -> getCameraRegions())); - put(CameraFeatures.flash, new Flash()); + put( + CameraFeatures.exposurePoint, + new ExposurePoint(cameraProperties, () -> getCameraRegions())); + put( + CameraFeatures.focusPoint, + new FocusPoint(cameraProperties, () -> getCameraRegions())); + put(CameraFeatures.flash, new Flash(cameraProperties)); put(CameraFeatures.fpsRange, new FpsRange(cameraProperties)); - put(CameraFeatures.noiseReduction, new NoiseReduction()); + put(CameraFeatures.noiseReduction, new NoiseReduction(cameraProperties)); put(CameraFeatures.zoomLevel, new ZoomLevel(cameraProperties)); - put( - CameraFeatures.regionBoundaries, - new RegionBoundaries(cameraProperties, mPreviewRequestBuilder)); + + // Note: CameraFeatures.regionBoundaries will be created in CreateCaptureSession. } }; @@ -250,6 +254,7 @@ public CameraState getState() { */ private void updateBuilderSettings(CaptureRequest.Builder requestBuilder) { for (Map.Entry feature : cameraFeatures.entrySet()) { + Log.i(TAG, "Updating builder with feature: " + feature.getValue().getDebugName()); feature.getValue().updateBuilder(requestBuilder); } } @@ -305,8 +310,9 @@ public void onOpened(@NonNull CameraDevice device) { getPreviewSize().getHeight(), (ExposureMode) cameraFeatures.get(CameraFeatures.exposureLock).getValue(), (FocusMode) cameraFeatures.get(CameraFeatures.autoFocus).getValue(), - cameraFeatures.get(CameraFeatures.exposurePoint).isSupported(cameraProperties), - cameraFeatures.get(CameraFeatures.focusPoint).isSupported(cameraProperties)); + ((ExposurePoint) cameraFeatures.get(CameraFeatures.exposurePoint)) + .getIsSupported(), + ((FocusPoint) cameraFeatures.get(CameraFeatures.focusPoint)).getIsSupported()); } catch (CameraAccessException e) { dartMessenger.sendCameraErrorEvent(e.getMessage()); close(); @@ -407,6 +413,7 @@ public void onConfigured(@NonNull CameraCaptureSession session) { } captureSession = session; + Log.i(TAG, "Updating builder settings"); updateBuilderSettings(mPreviewRequestBuilder); refreshPreviewCaptureSession( @@ -717,7 +724,7 @@ public void startVideoRecording(Result result) { prepareMediaRecorder(videoRecordingFile.getAbsolutePath()); // Re-create autofocus feature so it's using video focus mode now - cameraFeatures.put(CameraFeatures.autoFocus, new AutoFocus(true)); + cameraFeatures.put(CameraFeatures.autoFocus, new AutoFocus(cameraProperties, true)); recordingVideo = true; createCaptureSession( @@ -738,7 +745,7 @@ public void stopVideoRecording(@NonNull final Result result) { try { // Re-create autofocus feature so it's using continuous capture focus mode now - cameraFeatures.put(CameraFeatures.autoFocus, new AutoFocus(false)); + cameraFeatures.put(CameraFeatures.autoFocus, new AutoFocus(cameraProperties, false)); recordingVideo = false; try { diff --git a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/CameraFeature.java b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/CameraFeature.java index c90caca6bb6d..947823fa1403 100644 --- a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/CameraFeature.java +++ b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/CameraFeature.java @@ -12,6 +12,9 @@ * @param */ public interface CameraFeature { + /** Debug name for this feature. */ + public String getDebugName(); + /** * Get the current value of this feature's setting. * @@ -31,7 +34,7 @@ public interface CameraFeature { * * @return */ - public boolean isSupported(CameraProperties cameraProperties); + public boolean checkIsSupported(CameraProperties cameraProperties); /** * Update the setting in a provided request builder. diff --git a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/autoFocus/AutoFocus.java b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/autoFocus/AutoFocus.java index 7bf980aa65c4..e7ea2b742e51 100644 --- a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/autoFocus/AutoFocus.java +++ b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/autoFocus/AutoFocus.java @@ -7,7 +7,6 @@ import io.flutter.plugins.camera.features.CameraFeature; public class AutoFocus implements CameraFeature { - // private final boolean recordingVideo; private boolean isSupported; private FocusMode currentSetting = FocusMode.auto; @@ -15,8 +14,14 @@ public class AutoFocus implements CameraFeature { // the appropriate setting here. private final boolean recordingVideo; - public AutoFocus(boolean recordingVideo) { + public AutoFocus(CameraProperties cameraProperties, boolean recordingVideo) { this.recordingVideo = recordingVideo; + this.isSupported = checkIsSupported(cameraProperties); + } + + @Override + public String getDebugName() { + return "AutoFocus"; } @Override @@ -30,7 +35,7 @@ public void setValue(FocusMode value) { } @Override - public boolean isSupported(CameraProperties cameraProperties) { + public boolean checkIsSupported(CameraProperties cameraProperties) { int[] modes = cameraProperties.getControlAutoFocusAvailableModes(); Log.i("Camera", "checkAutoFocusSupported | modes:"); for (int mode : modes) { @@ -57,7 +62,6 @@ public boolean isSupported(CameraProperties cameraProperties) { && !(modes == null || modes.length == 0 || (modes.length == 1 && modes[0] == CameraCharacteristics.CONTROL_AF_MODE_OFF)); - isSupported = supported; return supported; } @@ -84,4 +88,8 @@ public void updateBuilder(CaptureRequest.Builder requestBuilder) { break; } } + + public boolean getIsSupported() { + return this.isSupported; + } } diff --git a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/exposurelock/ExposureLock.java b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/exposurelock/ExposureLock.java index e26f4003dc08..fef875eae7a6 100644 --- a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/exposurelock/ExposureLock.java +++ b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/exposurelock/ExposureLock.java @@ -12,6 +12,15 @@ public class ExposureLock implements CameraFeature { private boolean isSupported; private ExposureMode currentSetting = ExposureMode.auto; + public ExposureLock(CameraProperties cameraProperties) { + this.isSupported = checkIsSupported(cameraProperties); + } + + @Override + public String getDebugName() { + return "ExposureLock"; + } + @Override public ExposureMode getValue() { return currentSetting; @@ -24,7 +33,7 @@ public void setValue(ExposureMode value) { // Available on all devices. @Override - public boolean isSupported(CameraProperties cameraProperties) { + public boolean checkIsSupported(CameraProperties cameraProperties) { return true; } @@ -46,4 +55,8 @@ public void updateBuilder(CaptureRequest.Builder requestBuilder) { break; } } + + public boolean getIsSupported() { + return this.isSupported; + } } diff --git a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/exposureoffset/ExposureOffset.java b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/exposureoffset/ExposureOffset.java index 1013f44154ef..c6e38512799e 100644 --- a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/exposureoffset/ExposureOffset.java +++ b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/exposureoffset/ExposureOffset.java @@ -22,6 +22,12 @@ public ExposureOffset(CameraProperties cameraProperties) { // Initial offset of 0 this.currentSetting = new ExposureOffsetValue(this.min, this.max, 0); + this.isSupported = checkIsSupported(cameraProperties); + } + + @Override + public String getDebugName() { + return "ExposureOffset"; } @Override @@ -37,7 +43,7 @@ public void setValue(ExposureOffsetValue value) { // Available on all devices. @Override - public boolean isSupported(CameraProperties cameraProperties) { + public boolean checkIsSupported(CameraProperties cameraProperties) { return true; } @@ -92,4 +98,8 @@ public double getExposureOffsetStepSize(CameraProperties cameraProperties) { Rational stepSize = cameraProperties.getControlAutoExposureCompensationStep(); return stepSize == null ? 0.0 : stepSize.doubleValue(); } + + public boolean getIsSupported() { + return this.isSupported; + } } diff --git a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/exposurepoint/ExposurePoint.java b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/exposurepoint/ExposurePoint.java index 382b36c8ec05..5d69f04cbd37 100644 --- a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/exposurepoint/ExposurePoint.java +++ b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/exposurepoint/ExposurePoint.java @@ -16,8 +16,15 @@ public class ExposurePoint implements CameraFeature { private boolean isSupported; private Point currentSetting = new Point(0.0, 0.0); - public ExposurePoint(Callable getCameraRegions) { + public ExposurePoint( + CameraProperties cameraProperties, Callable getCameraRegions) { this.getCameraRegions = getCameraRegions; + this.isSupported = checkIsSupported(cameraProperties); + } + + @Override + public String getDebugName() { + return "ExposurePoint"; } @Override @@ -42,10 +49,9 @@ public void setValue(Point value) { // Whether or not this camera can set the exposure point. @Override - public boolean isSupported(CameraProperties cameraProperties) { + public boolean checkIsSupported(CameraProperties cameraProperties) { Integer supportedRegions = cameraProperties.getControlMaxRegionsAutoExposure(); final boolean supported = supportedRegions != null && supportedRegions > 0; - isSupported = supported; return supported; } @@ -69,4 +75,8 @@ public void updateBuilder(CaptureRequest.Builder requestBuilder) { e.printStackTrace(); } } + + public boolean getIsSupported() { + return this.isSupported; + } } diff --git a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/flash/Flash.java b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/flash/Flash.java index 5787f2cb1fe8..da0bf9bcb818 100644 --- a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/flash/Flash.java +++ b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/flash/Flash.java @@ -9,6 +9,15 @@ public class Flash implements CameraFeature { private boolean isSupported; private FlashMode currentSetting = FlashMode.auto; + public Flash(CameraProperties cameraProperties) { + this.isSupported = checkIsSupported(cameraProperties); + } + + @Override + public String getDebugName() { + return "Flash"; + } + @Override public FlashMode getValue() { return currentSetting; @@ -20,10 +29,9 @@ public void setValue(FlashMode value) { } @Override - public boolean isSupported(CameraProperties cameraProperties) { + public boolean checkIsSupported(CameraProperties cameraProperties) { Boolean available = cameraProperties.getFlashInfoAvailable(); final boolean supported = available != null && available; - isSupported = supported; return supported; } @@ -67,4 +75,8 @@ public void updateBuilder(CaptureRequest.Builder requestBuilder) { // break; } } + + public boolean getIsSupported() { + return this.isSupported; + } } diff --git a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/focuspoint/FocusPoint.java b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/focuspoint/FocusPoint.java index 4fbc13fc5ab3..3b89b3ba22d4 100644 --- a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/focuspoint/FocusPoint.java +++ b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/focuspoint/FocusPoint.java @@ -14,8 +14,14 @@ public class FocusPoint implements CameraFeature { private boolean isSupported; private Point currentSetting = new Point(0.0, 0.0); - public FocusPoint(Callable getCameraRegions) { + public FocusPoint(CameraProperties cameraProperties, Callable getCameraRegions) { this.getCameraRegions = getCameraRegions; + this.isSupported = checkIsSupported(cameraProperties); + } + + @Override + public String getDebugName() { + return "FocusPoint"; } @Override @@ -40,10 +46,9 @@ public void setValue(Point value) { // Whether or not this camera can set the exposure point. @Override - public boolean isSupported(CameraProperties cameraProperties) { + public boolean checkIsSupported(CameraProperties cameraProperties) { Integer supportedRegions = cameraProperties.getControlMaxRegionsAutoFocus(); final boolean supported = supportedRegions != null && supportedRegions > 0; - isSupported = supported; return supported; } @@ -53,4 +58,8 @@ public void updateBuilder(CaptureRequest.Builder requestBuilder) { return; } } + + public boolean getIsSupported() { + return this.isSupported; + } } diff --git a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/fpsrange/FpsRange.java b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/fpsrange/FpsRange.java index 467a14118997..912eda61ec63 100644 --- a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/fpsrange/FpsRange.java +++ b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/fpsrange/FpsRange.java @@ -32,6 +32,13 @@ public FpsRange(CameraProperties cameraProperties) { // pictureCaptureRequest.error("cameraAccess", e.getMessage(), null); } Log.i("Camera", "[FPS Range] is:" + currentSetting); + + this.isSupported = checkIsSupported(cameraProperties); + } + + @Override + public String getDebugName() { + return "FpsRange"; } @Override @@ -46,7 +53,7 @@ public void setValue(Range value) { // Always supported @Override - public boolean isSupported(CameraProperties cameraProperties) { + public boolean checkIsSupported(CameraProperties cameraProperties) { return true; } @@ -60,4 +67,8 @@ public void updateBuilder(CaptureRequest.Builder requestBuilder) { requestBuilder.set(CaptureRequest.CONTROL_AE_TARGET_FPS_RANGE, currentSetting); } + + public boolean getIsSupported() { + return this.isSupported; + } } diff --git a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/noisereduction/NoiseReduction.java b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/noisereduction/NoiseReduction.java index d96a994e1cb5..24f7376bff5f 100644 --- a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/noisereduction/NoiseReduction.java +++ b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/noisereduction/NoiseReduction.java @@ -14,6 +14,15 @@ public class NoiseReduction implements CameraFeature { private boolean isSupported; private NoiseReductionMode currentSetting; + public NoiseReduction(CameraProperties cameraProperties) { + this.isSupported = checkIsSupported(cameraProperties); + } + + @Override + public String getDebugName() { + return "NoiseReduction"; + } + @Override public NoiseReductionMode getValue() { return currentSetting; @@ -25,7 +34,7 @@ public void setValue(NoiseReductionMode value) { } @Override - public boolean isSupported(CameraProperties cameraProperties) { + public boolean checkIsSupported(CameraProperties cameraProperties) { /** * Available settings: public static final int NOISE_REDUCTION_MODE_FAST = 1; public static * final int NOISE_REDUCTION_MODE_HIGH_QUALITY = 2; public static final int @@ -56,4 +65,8 @@ public void updateBuilder(CaptureRequest.Builder requestBuilder) { requestBuilder.set( CaptureRequest.NOISE_REDUCTION_MODE, CaptureRequest.NOISE_REDUCTION_MODE_FAST); } + + public boolean getIsSupported() { + return this.isSupported; + } } diff --git a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/regionboundaries/RegionBoundaries.java b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/regionboundaries/RegionBoundaries.java index b065c27b8888..b6c3da725ef3 100644 --- a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/regionboundaries/RegionBoundaries.java +++ b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/regionboundaries/RegionBoundaries.java @@ -47,6 +47,13 @@ public RegionBoundaries( // Create new camera regions using new size cameraRegions = new CameraRegions(currentSetting); + + this.isSupported = checkIsSupported(cameraProperties); + } + + @Override + public String getDebugName() { + return "RegionBoundaries"; } @Override @@ -61,7 +68,7 @@ public void setValue(Size value) { // Available on all devices. @Override - public boolean isSupported(CameraProperties cameraProperties) { + public boolean checkIsSupported(CameraProperties cameraProperties) { return true; } @@ -87,4 +94,8 @@ private boolean supportsDistortionCorrection() { public CameraRegions getCameraRegions() { return this.cameraRegions; } + + public boolean getIsSupported() { + return this.isSupported; + } } diff --git a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/resolution/Resolution.java b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/resolution/Resolution.java index 6dc470b05407..786a57a8dccd 100644 --- a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/resolution/Resolution.java +++ b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/resolution/Resolution.java @@ -15,7 +15,8 @@ public class Resolution implements CameraFeature { private boolean isSupported; private ResolutionPreset currentSetting; - public Resolution(ResolutionPreset initialSetting, String cameraName) { + public Resolution( + CameraProperties cameraProperties, ResolutionPreset initialSetting, String cameraName) { setValue(initialSetting); // Resolution configuration @@ -25,6 +26,7 @@ public Resolution(ResolutionPreset initialSetting, String cameraName) { Log.i("Camera", "captureSize: " + captureSize); previewSize = computeBestPreviewSize(cameraName, initialSetting); + this.isSupported = checkIsSupported(cameraProperties); } static CamcorderProfile getBestAvailableCamcorderProfileForResolutionPreset( @@ -76,6 +78,11 @@ static Size computeBestPreviewSize(String cameraName, ResolutionPreset preset) { return new Size(profile.videoFrameWidth, profile.videoFrameHeight); } + @Override + public String getDebugName() { + return "Resolution"; + } + @Override public ResolutionPreset getValue() { return currentSetting; @@ -88,7 +95,7 @@ public void setValue(ResolutionPreset value) { // Always supported @Override - public boolean isSupported(CameraProperties cameraProperties) { + public boolean checkIsSupported(CameraProperties cameraProperties) { return true; } @@ -110,4 +117,8 @@ public Size getPreviewSize() { public Size getCaptureSize() { return this.captureSize; } + + public boolean getIsSupported() { + return this.isSupported; + } } diff --git a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/sensororientation/SensorOrientation.java b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/sensororientation/SensorOrientation.java index 15ec4c99bd72..9eeceb86da67 100644 --- a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/sensororientation/SensorOrientation.java +++ b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/sensororientation/SensorOrientation.java @@ -20,6 +20,13 @@ public SensorOrientation( deviceOrientationListener = DeviceOrientationManager.create(activity, dartMessenger, isFrontFacing, currentSetting); deviceOrientationListener.start(); + + this.isSupported = checkIsSupported(cameraProperties); + } + + @Override + public String getDebugName() { + return "SensorOrientation"; } @Override @@ -33,8 +40,7 @@ public void setValue(Integer value) { } @Override - public boolean isSupported(CameraProperties cameraProperties) { - // Always supported + public boolean checkIsSupported(CameraProperties cameraProperties) { return true; } @@ -48,4 +54,8 @@ public void updateBuilder(CaptureRequest.Builder requestBuilder) { public DeviceOrientationManager getDeviceOrientationManager() { return this.deviceOrientationListener; } + + public boolean getIsSupported() { + return this.isSupported; + } } diff --git a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/zoomlevel/ZoomLevel.java b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/zoomlevel/ZoomLevel.java index 9e72c03109e3..fd52fafc5631 100644 --- a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/zoomlevel/ZoomLevel.java +++ b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/zoomlevel/ZoomLevel.java @@ -9,7 +9,7 @@ /** Exposure offset makes the image brighter or darker. */ public class ZoomLevel implements CameraFeature { private boolean isSupported; - private Float currentSetting; + private Float currentSetting = CameraZoom.DEFAULT_ZOOM_FACTOR; private CameraProperties cameraProperties; private CameraZoom cameraZoom; @@ -19,6 +19,12 @@ public ZoomLevel(CameraProperties cameraProperties) { new CameraZoom( cameraProperties.getSensorInfoActiveArraySize(), cameraProperties.getScalerAvailableMaxDigitalZoom()); + this.isSupported = checkIsSupported(cameraProperties); + } + + @Override + public String getDebugName() { + return "ZoomLevel"; } @Override @@ -33,7 +39,7 @@ public void setValue(Float value) { // Available on all devices. @Override - public boolean isSupported(CameraProperties cameraProperties) { + public boolean checkIsSupported(CameraProperties cameraProperties) { return true; } @@ -52,4 +58,8 @@ public void updateBuilder(CaptureRequest.Builder requestBuilder) { public CameraZoom getCameraZoom() { return this.cameraZoom; } + + public boolean getIsSupported() { + return this.isSupported; + } } From bae2e6176eaa9f01bad3cf7c24de1082800da9f7 Mon Sep 17 00:00:00 2001 From: Andrew Coutts Date: Sun, 7 Mar 2021 11:55:07 -0500 Subject: [PATCH 072/114] Refactor orientation lock into feature and fix camera capture states --- .../io/flutter/plugins/camera/Camera.java | 44 +++++------ .../plugins/camera/CameraCaptureCallback.java | 9 +-- .../plugins/camera/PictureCaptureRequest.java | 74 +------------------ .../camera/PictureCaptureRequestState.java | 32 -------- 4 files changed, 27 insertions(+), 132 deletions(-) delete mode 100644 packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/PictureCaptureRequestState.java 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 7c43308acfd2..7a8635a3d4a5 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 @@ -130,8 +130,6 @@ class Camera implements CameraCaptureCallback.CameraCaptureStateListener { private final Activity activity; /** This manages the state of the camera and the current capture request. */ PictureCaptureRequest pictureCaptureRequest; - /** The state of the camera. By default we are in the preview state. */ - private CameraState cameraState = CameraState.STATE_PREVIEW; /** A {@link Handler} for running tasks in the background. */ private Handler mBackgroundHandler; /** @@ -148,7 +146,7 @@ public void onImageAvailable(ImageReader reader) { mBackgroundHandler.post( new ImageSaver( reader.acquireNextImage(), pictureCaptureRequest.file, pictureCaptureRequest)); - cameraState = CameraState.STATE_PREVIEW; + mCaptureCallback.setCameraState(CameraState.STATE_PREVIEW); } }; @@ -171,8 +169,6 @@ public void onImageAvailable(ImageReader reader) { /** A {@link CameraCaptureSession.CaptureCallback} that handles events related to JPEG capture. */ private final CameraCaptureCallback mCaptureCallback; - private PlatformChannel.DeviceOrientation lockedCaptureOrientation; - public Camera( final Activity activity, final SurfaceTextureEntry flutterTexture, @@ -242,11 +238,6 @@ public void onPrecaptureTimeout() { unlockAutoFocus(); } - /** Get the current camera state (use for testing). */ - public CameraState getState() { - return this.cameraState; - } - /** * Update the builder settings with all of our available features. * @@ -266,13 +257,17 @@ private void prepareMediaRecorder(String outputFilePath) throws IOException { mediaRecorder.release(); } + final PlatformChannel.DeviceOrientation lockedOrientation = + ((SensorOrientation) cameraFeatures.get(CameraFeatures.sensorOrientation)) + .getLockedCaptureOrientation(); + mediaRecorder = new MediaRecorderBuilder(getRecordingProfile(), outputFilePath) .setEnableAudio(enableAudio) .setMediaOrientation( - lockedCaptureOrientation == null + lockedOrientation == null ? getDeviceOrientationManager().getMediaOrientation() - : getDeviceOrientationManager().getMediaOrientation(lockedCaptureOrientation)) + : getDeviceOrientationManager().getMediaOrientation(lockedOrientation)) .build(); } @@ -492,7 +487,7 @@ public void takePicture(@NonNull final Result result) { "takePicture | useAutoFocus: " + cameraFeatures.get(CameraFeatures.autoFocus).getValue()); // Only take one 1 picture at a time. - if (pictureCaptureRequest != null && !pictureCaptureRequest.isFinished()) { + if (mCaptureCallback.getCameraState() != CameraState.STATE_PREVIEW) { result.error("captureAlreadyActive", "Picture is currently already being captured", null); return; } @@ -527,7 +522,7 @@ public void takePicture(@NonNull final Result result) { private void runPrecaptureSequence() { Log.i(TAG, "runPrecaptureSequence"); try { - // First set precapture state to idle or else it can hang in STATE_WAITING_PRECAPTURE + // First set precapture state to idle or else it can hang in STATE_stPRECAPTURE mPreviewRequestBuilder.set( CaptureRequest.CONTROL_AE_PRECAPTURE_TRIGGER, CaptureRequest.CONTROL_AE_PRECAPTURE_TRIGGER_IDLE); @@ -538,7 +533,7 @@ private void runPrecaptureSequence() { null, (code, message) -> pictureCaptureRequest.error("cameraAccess", message, null)); // Start precapture now - cameraState = CameraState.STATE_WAITING_PRECAPTURE_START; + mCaptureCallback.setCameraState(CameraState.STATE_WAITING_PRECAPTURE_START); mPreviewRequestBuilder.set( CaptureRequest.CONTROL_AE_PRECAPTURE_TRIGGER, @@ -558,8 +553,7 @@ private void runPrecaptureSequence() { */ private void takePictureAfterPrecapture() { Log.i(TAG, "captureStillPicture"); - cameraState = CameraState.STATE_CAPTURING; - pictureCaptureRequest.setState(PictureCaptureRequestState.STATE_CAPTURING); + mCaptureCallback.setCameraState(CameraState.STATE_CAPTURING); try { if (null == cameraDevice) { @@ -664,15 +658,13 @@ private void runPictureAutoFocus() { Log.i(TAG, "runPictureAutoFocus"); assert (pictureCaptureRequest != null); - cameraState = CameraState.STATE_WAITING_FOCUS; - pictureCaptureRequest.setState(PictureCaptureRequestState.STATE_WAITING_FOCUS); + mCaptureCallback.setCameraState(CameraState.STATE_WAITING_FOCUS); lockAutoFocus(); } /** Start the autofocus routine on the current capture request. */ private void lockAutoFocus() { Log.i(TAG, "lockAutoFocus"); - pictureCaptureRequest.setState(PictureCaptureRequestState.STATE_WAITING_PRECAPTURE_START); mPreviewRequestBuilder.set( CaptureRequest.CONTROL_AF_TRIGGER, CaptureRequest.CONTROL_AF_TRIGGER_START); @@ -998,12 +990,20 @@ public void setZoomLevel(@NonNull final Result result, float zoom) throws Camera (code, message) -> result.error("setZoomLevelFailed", "Could not set zoom level.", null)); } + /** + * Lock capture orientation from dart. + * + * @param orientation new orientation. + */ public void lockCaptureOrientation(PlatformChannel.DeviceOrientation orientation) { - this.lockedCaptureOrientation = orientation; + ((SensorOrientation) cameraFeatures.get(CameraFeatures.sensorOrientation)) + .lockCaptureOrientation(orientation); } + /** Unlock capture orientation from dart. */ public void unlockCaptureOrientation() { - this.lockedCaptureOrientation = null; + ((SensorOrientation) cameraFeatures.get(CameraFeatures.sensorOrientation)) + .unlockCaptureOrientation(); } public void startPreview() throws CameraAccessException { diff --git a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/CameraCaptureCallback.java b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/CameraCaptureCallback.java index 91c5c8701cc1..77fe3eea540f 100644 --- a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/CameraCaptureCallback.java +++ b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/CameraCaptureCallback.java @@ -5,6 +5,7 @@ import android.hardware.camera2.CaptureRequest; import android.hardware.camera2.CaptureResult; import android.hardware.camera2.TotalCaptureResult; +import android.util.Log; import androidx.annotation.NonNull; class CameraCaptureCallback extends CaptureCallback { @@ -17,7 +18,6 @@ interface CameraCaptureStateListener { } private final CameraCaptureStateListener cameraStateListener; - private CameraState cameraState; private PictureCaptureRequest pictureCaptureRequest; @@ -29,7 +29,6 @@ public static CameraCaptureCallback create( private CameraCaptureCallback(@NonNull CameraCaptureStateListener cameraStateListener) { cameraState = CameraState.STATE_PREVIEW; this.cameraStateListener = cameraStateListener; - this.pictureCaptureRequest = pictureCaptureRequest; } public CameraState getCameraState() { @@ -38,10 +37,6 @@ public CameraState getCameraState() { public void setCameraState(@NonNull CameraState state) { cameraState = state; - - if (pictureCaptureRequest != null && state == CameraState.STATE_WAITING_PRECAPTURE_DONE) { - pictureCaptureRequest.setState(PictureCaptureRequestState.STATE_WAITING_PRECAPTURE_DONE); - } } public void setPictureCaptureRequest(@NonNull PictureCaptureRequest pictureCaptureRequest) { @@ -52,6 +47,8 @@ private void process(CaptureResult result) { Integer aeState = result.get(CaptureResult.CONTROL_AE_STATE); Integer afState = result.get(CaptureResult.CONTROL_AF_STATE); + Log.i("Camera", "CameraCaptureCallback | state: " + cameraState); + switch (cameraState) { case STATE_PREVIEW: { diff --git a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/PictureCaptureRequest.java b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/PictureCaptureRequest.java index 4b68cca0218c..eb52cebba24d 100644 --- a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/PictureCaptureRequest.java +++ b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/PictureCaptureRequest.java @@ -41,13 +41,10 @@ class PictureCaptureRequest { * has timed out. */ private long preCaptureStartTime; - /** The state of this picture capture request. */ - private PictureCaptureRequestState state = PictureCaptureRequestState.STATE_IDLE; private final Runnable timeoutCallback = () -> { - error("captureTimeout", "Picture capture request timed out", state.toString()); - setState(PictureCaptureRequestState.STATE_ERROR); + error("captureTimeout", "Picture capture request timed out", null); }; /** @@ -76,62 +73,17 @@ private PictureCaptureRequest( this.timeoutHandler = TimeoutHandler.create(); } - /** - * Return the current state of this picture capture request. - * - * @return - */ - public PictureCaptureRequestState getState() { - return state; - } - - /** - * Set the picture capture request to a new state. - * - * @param newState - */ - public void setState(PictureCaptureRequestState newState) { - // Log.i("Camera", "====> PictureCaptureRequest setState: " + newState); - - // Once a request is finished, that's it for its lifecycle. - if (state == PictureCaptureRequestState.STATE_FINISHED) { - dartMessenger.sendCameraErrorEvent("Request has already been finished"); - return; - } - - state = newState; - onStateChange(newState); - } - - public boolean isFinished() { - return state == PictureCaptureRequestState.STATE_FINISHED - || state == PictureCaptureRequestState.STATE_ERROR; - } - /** * Send the picture result back to Flutter. Returns the image path. * * @param absolutePath */ public void finish(String absolutePath) { - if (state == PictureCaptureRequestState.STATE_ERROR) { - return; - } - - if (isFinished()) throw new IllegalStateException("Request has already been finished"); - setState(PictureCaptureRequestState.STATE_FINISHED); - // Log.i("Camera", "PictureCaptureRequest finish"); result.success(absolutePath); } public void error( String errorCode, @Nullable String errorMessage, @Nullable Object errorDetails) { - if (state == PictureCaptureRequestState.STATE_ERROR) { - return; - } - - if (isFinished()) throw new IllegalStateException("Request has already been finished"); - setState(PictureCaptureRequestState.STATE_ERROR); result.error(errorCode, errorMessage, errorDetails); } @@ -150,28 +102,6 @@ public void setPreCaptureStartTime() { preCaptureStartTime = SystemClock.elapsedRealtime(); } - /** Handle new state changes. */ - private void onStateChange(PictureCaptureRequestState newState) { - switch (newState) { - case STATE_CAPTURING: - case STATE_WAITING_FOCUS: - case STATE_WAITING_PRECAPTURE_START: - timeoutHandler.resetTimeout(timeoutCallback); - break; - - case STATE_WAITING_PRECAPTURE_DONE: - setPreCaptureStartTime(); - timeoutHandler.resetTimeout(timeoutCallback); - break; - - case STATE_IDLE: - case STATE_FINISHED: - case STATE_ERROR: - timeoutHandler.clearTimeout(timeoutCallback); - break; - } - } - /** * This handles the timeout for capture requests so they return within a reasonable amount of * time. @@ -189,7 +119,7 @@ private TimeoutHandler() { } public void resetTimeout(Runnable runnable) { - // Log.i("Camear", "PictureCaptureRequest | resetting timeout"); + // Log.i("Camera", "PictureCaptureRequest | resetting timeout"); clearTimeout(runnable); handler.postDelayed(runnable, REQUEST_TIMEOUT); } diff --git a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/PictureCaptureRequestState.java b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/PictureCaptureRequestState.java deleted file mode 100644 index 979346c9bb09..000000000000 --- a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/PictureCaptureRequestState.java +++ /dev/null @@ -1,32 +0,0 @@ -package io.flutter.plugins.camera; - -/** - * This describes the state of the current picture capture request. This is different from the - * camera state because this simply says whether or not the current capture is finished and if there - * was an error. - * - *

We have to separate this state because a picture capture request only exists in the context of - * a dart call where we have a result to return. - */ -public enum PictureCaptureRequestState { - /** Not doing anything yet. */ - STATE_IDLE, - - /** Starting and waiting for autofocus to complete. */ - STATE_WAITING_FOCUS, - - /** Start performing autoexposure. */ - STATE_WAITING_PRECAPTURE_START, - - /** waiting for autoexposure to complete. */ - STATE_WAITING_PRECAPTURE_DONE, - - /** Picture is being captured. */ - STATE_CAPTURING, - - /** Picture capture is finished. */ - STATE_FINISHED, - - /** An error occurred. */ - STATE_ERROR, -} From b1d3e1fdeb5624bf461c49b820787fb47fe9002a Mon Sep 17 00:00:00 2001 From: Andrew Coutts Date: Sun, 7 Mar 2021 11:55:10 -0500 Subject: [PATCH 073/114] Update SensorOrientation.java --- .../sensororientation/SensorOrientation.java | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/sensororientation/SensorOrientation.java b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/sensororientation/SensorOrientation.java index 9eeceb86da67..f07465565582 100644 --- a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/sensororientation/SensorOrientation.java +++ b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/sensororientation/SensorOrientation.java @@ -3,6 +3,7 @@ import android.app.Activity; import android.hardware.camera2.CameraMetadata; import android.hardware.camera2.CaptureRequest; +import io.flutter.embedding.engine.systemchannels.PlatformChannel; import io.flutter.plugins.camera.CameraProperties; import io.flutter.plugins.camera.DartMessenger; import io.flutter.plugins.camera.features.CameraFeature; @@ -11,6 +12,7 @@ public class SensorOrientation implements CameraFeature { private boolean isSupported; private Integer currentSetting = 0; private final DeviceOrientationManager deviceOrientationListener; + private PlatformChannel.DeviceOrientation lockedCaptureOrientation; public SensorOrientation( CameraProperties cameraProperties, Activity activity, DartMessenger dartMessenger) { @@ -58,4 +60,16 @@ public DeviceOrientationManager getDeviceOrientationManager() { public boolean getIsSupported() { return this.isSupported; } + + public void lockCaptureOrientation(PlatformChannel.DeviceOrientation orientation) { + this.lockedCaptureOrientation = orientation; + } + + public void unlockCaptureOrientation() { + this.lockedCaptureOrientation = null; + } + + public PlatformChannel.DeviceOrientation getLockedCaptureOrientation() { + return this.lockedCaptureOrientation; + } } From 154b58c15a6ee09accb572f30dc46c637e1a0a3d Mon Sep 17 00:00:00 2001 From: Andrew Coutts Date: Sun, 7 Mar 2021 12:09:33 -0500 Subject: [PATCH 074/114] Capturing photos works again --- .../src/main/java/io/flutter/plugins/camera/Camera.java | 3 ++- .../io/flutter/plugins/camera/CameraCaptureCallback.java | 7 ++++--- 2 files changed, 6 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 7a8635a3d4a5..f04973df50a6 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 @@ -508,7 +508,8 @@ public void takePicture(@NonNull final Result result) { // Listen for picture being taken pictureImageReader.setOnImageAvailableListener(mOnImageAvailableListener, mBackgroundHandler); - if (cameraFeatures.get(CameraFeatures.autoFocus).getValue() == FocusMode.auto) { + final AutoFocus autoFocusFeature = (AutoFocus) cameraFeatures.get(CameraFeatures.autoFocus); + if (autoFocusFeature.getIsSupported() && autoFocusFeature.getValue() == FocusMode.auto) { runPictureAutoFocus(); } else { runPrecaptureSequence(); diff --git a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/CameraCaptureCallback.java b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/CameraCaptureCallback.java index 77fe3eea540f..734f8c47de58 100644 --- a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/CameraCaptureCallback.java +++ b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/CameraCaptureCallback.java @@ -47,7 +47,9 @@ private void process(CaptureResult result) { Integer aeState = result.get(CaptureResult.CONTROL_AE_STATE); Integer afState = result.get(CaptureResult.CONTROL_AF_STATE); - Log.i("Camera", "CameraCaptureCallback | state: " + cameraState); + if (cameraState != CameraState.STATE_PREVIEW) { + Log.i("Camera", "CameraCaptureCallback | state: " + cameraState + " | afState: " + afState + " | aeState: " + aeState); + } switch (cameraState) { case STATE_PREVIEW: @@ -59,8 +61,7 @@ private void process(CaptureResult result) { { if (afState == null) { return; - } else if (afState == CaptureRequest.CONTROL_AF_STATE_PASSIVE_SCAN - || afState == CaptureRequest.CONTROL_AF_STATE_FOCUSED_LOCKED + } else if (afState == CaptureRequest.CONTROL_AF_STATE_FOCUSED_LOCKED || afState == CaptureRequest.CONTROL_AF_STATE_NOT_FOCUSED_LOCKED) { // CONTROL_AE_STATE can be null on some devices From 44a781e9659ec693c2d050b73ddd12ed1628aa94 Mon Sep 17 00:00:00 2001 From: Andrew Coutts Date: Sun, 7 Mar 2021 14:25:06 -0500 Subject: [PATCH 075/114] Fixed focus lock on sony and pixel devices --- .../io/flutter/plugins/camera/Camera.java | 60 ++++++++++++++----- .../plugins/camera/CameraCaptureCallback.java | 9 ++- .../camera/features/autoFocus/AutoFocus.java | 20 +++++++ 3 files changed, 73 insertions(+), 16 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 f04973df50a6..a1db41ed086b 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 @@ -128,6 +128,8 @@ class Camera implements CameraCaptureCallback.CameraCaptureStateListener { private final DartMessenger dartMessenger; private final CameraProperties cameraProperties; private final Activity activity; + /** A {@link CameraCaptureSession.CaptureCallback} that handles events related to JPEG capture. */ + private final CameraCaptureCallback mCaptureCallback; /** This manages the state of the camera and the current capture request. */ PictureCaptureRequest pictureCaptureRequest; /** A {@link Handler} for running tasks in the background. */ @@ -149,7 +151,6 @@ public void onImageAvailable(ImageReader reader) { mCaptureCallback.setCameraState(CameraState.STATE_PREVIEW); } }; - /** An additional thread for running tasks that shouldn't block the UI. */ private HandlerThread mBackgroundThread; @@ -161,13 +162,10 @@ public void onImageAvailable(ImageReader reader) { private CaptureRequest.Builder mPreviewRequestBuilder; /** {@link CaptureRequest} generated by {@link #mPreviewRequestBuilder} */ private MediaRecorder mediaRecorder; - /** True when recording video. */ private boolean recordingVideo; private File videoRecordingFile; - /** A {@link CameraCaptureSession.CaptureCallback} that handles events related to JPEG capture. */ - private final CameraCaptureCallback mCaptureCallback; public Camera( final Activity activity, @@ -663,15 +661,26 @@ private void runPictureAutoFocus() { lockAutoFocus(); } - /** Start the autofocus routine on the current capture request. */ + /** + * Start the autofocus routine on the current capture request and calls the onCompleted callback. + */ private void lockAutoFocus() { Log.i(TAG, "lockAutoFocus"); + // Trigger AF to start mPreviewRequestBuilder.set( CaptureRequest.CONTROL_AF_TRIGGER, CaptureRequest.CONTROL_AF_TRIGGER_START); - refreshPreviewCaptureSession( - null, (code, message) -> pictureCaptureRequest.error(code, message, null)); + try { + captureSession.capture( + mPreviewRequestBuilder.build(), + null, + mBackgroundHandler); + } catch (CameraAccessException e) { + Log.i(TAG, "Error unlocking focus: " + e.getMessage()); + dartMessenger.sendCameraErrorEvent(e.getMessage()); + return; + } } /** Cancel and reset auto focus state and refresh the preview session. */ @@ -683,11 +692,6 @@ private void unlockAutoFocus() { CaptureRequest.CONTROL_AF_TRIGGER, CameraMetadata.CONTROL_AF_TRIGGER_CANCEL); captureSession.capture(mPreviewRequestBuilder.build(), null, mBackgroundHandler); - // Set AE state to idle again - mPreviewRequestBuilder.set( - CaptureRequest.CONTROL_AE_PRECAPTURE_TRIGGER, - CaptureRequest.CONTROL_AE_PRECAPTURE_TRIGGER_IDLE); - // Set AF state to idle again mPreviewRequestBuilder.set( CaptureRequest.CONTROL_AF_TRIGGER, CameraMetadata.CONTROL_AF_TRIGGER_IDLE); @@ -892,11 +896,37 @@ public double getExposureOffsetStepSize() { */ public void setFocusMode(@NonNull final Result result, FocusMode newMode) { ((AutoFocus) cameraFeatures.get(CameraFeatures.autoFocus)).setValue(newMode); + cameraFeatures.get(CameraFeatures.autoFocus).updateBuilder(mPreviewRequestBuilder); - refreshPreviewCaptureSession( - () -> result.success(null), - (code, message) -> result.error("setFocusModeFailed", "Could not set focus mode.", null)); + /** + * For focus mode we need to do an extra step of actually locking/unlocking the + * focus in order to ensure it goes into the correct state. + */ + switch (newMode) { + case locked: + // Perform a single focus trigger + lockAutoFocus(); + + // Set AF state to idle again + mPreviewRequestBuilder.set( + CaptureRequest.CONTROL_AF_TRIGGER, CameraMetadata.CONTROL_AF_TRIGGER_IDLE); + + try { + captureSession.setRepeatingRequest( + mPreviewRequestBuilder.build(), null, mBackgroundHandler); + } catch (CameraAccessException e) { + result.error("setFocusModeFailed", "Error setting focus mode: " + e.getMessage(), null); + } + break; + + case auto: + // Cancel current AF trigger and set AF to idle again + unlockAutoFocus(); + break; + } + + result.success(null); } /** diff --git a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/CameraCaptureCallback.java b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/CameraCaptureCallback.java index 734f8c47de58..20477c41c305 100644 --- a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/CameraCaptureCallback.java +++ b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/CameraCaptureCallback.java @@ -48,7 +48,14 @@ private void process(CaptureResult result) { Integer afState = result.get(CaptureResult.CONTROL_AF_STATE); if (cameraState != CameraState.STATE_PREVIEW) { - Log.i("Camera", "CameraCaptureCallback | state: " + cameraState + " | afState: " + afState + " | aeState: " + aeState); + Log.i( + "Camera", + "CameraCaptureCallback | state: " + + cameraState + + " | afState: " + + afState + + " | aeState: " + + aeState); } switch (cameraState) { diff --git a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/autoFocus/AutoFocus.java b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/autoFocus/AutoFocus.java index e7ea2b742e51..b4074a6f846f 100644 --- a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/autoFocus/AutoFocus.java +++ b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/autoFocus/AutoFocus.java @@ -32,6 +32,25 @@ public FocusMode getValue() { @Override public void setValue(FocusMode value) { this.currentSetting = value; + + // /** + // * If we are locking AF, we should perform a one-time + // */ + // switch (currentSetting) { + // case locked: + // requestBuilder.set(CaptureRequest.CONTROL_AF_MODE, CaptureRequest.CONTROL_AF_MODE_AUTO); + // unlockAutoFocus.run(); + // break; + // + // case auto: + // requestBuilder.set( + // CaptureRequest.CONTROL_AF_MODE, + // recordingVideo + // ? CaptureRequest.CONTROL_AF_MODE_CONTINUOUS_VIDEO + // : CaptureRequest.CONTROL_AF_MODE_CONTINUOUS_PICTURE); + // default: + // break; + // } } @Override @@ -75,6 +94,7 @@ public void updateBuilder(CaptureRequest.Builder requestBuilder) { switch (currentSetting) { case locked: + /** If we're locking AF we should do a one-time focus, then set the AF to idle */ requestBuilder.set(CaptureRequest.CONTROL_AF_MODE, CaptureRequest.CONTROL_AF_MODE_AUTO); break; From 367d496a76b9e2ab856ab449cd28f18819eab129 Mon Sep 17 00:00:00 2001 From: Maurits van Beusekom Date: Mon, 8 Mar 2021 13:47:18 +0100 Subject: [PATCH 076/114] Fix tests after feature refactor --- .../io/flutter/plugins/camera/Camera.java | 124 +++++---- .../plugins/camera/CameraCaptureCallback.java | 4 + .../plugins/camera/CameraProperties.java | 4 + .../flutter/plugins/camera/CameraState.java | 4 + .../io/flutter/plugins/camera/ImageSaver.java | 4 + .../plugins/camera/MethodCallHandlerImpl.java | 2 + .../plugins/camera/PictureCaptureRequest.java | 5 - .../camera/features/CameraFeature.java | 26 +- .../camera/features/CameraFeatureFactory.java | 148 ++++++++++ .../features/CameraFeatureFactoryImpl.java | 105 ++++++++ .../camera/features/CameraFeatures.java | 11 +- .../AutoFocusFeature.java} | 25 +- .../{autoFocus => autofocus}/FocusMode.java | 0 ...sureLock.java => ExposureLockFeature.java} | 19 +- ...Offset.java => ExposureOffsetFeature.java} | 41 ++- .../exposureoffset/ExposureOffsetValue.java | 4 + ...rePoint.java => ExposurePointFeature.java} | 20 +- .../flash/{Flash.java => FlashFeature.java} | 19 +- ...FocusPoint.java => FocusPointFeature.java} | 24 +- .../{FpsRange.java => FpsRangeFeature.java} | 21 +- ...uction.java => NoiseReductionFeature.java} | 21 +- .../noisereduction/NoiseReductionMode.java | 4 + .../regionboundaries/CameraRegions.java | 3 +- ...ries.java => RegionBoundariesFeature.java} | 28 +- ...Resolution.java => ResolutionFeature.java} | 24 +- .../DeviceOrientationManager.java | 11 +- ...ion.java => SensorOrientationFeature.java} | 27 +- .../{ZoomLevel.java => ZoomLevelFeature.java} | 21 +- .../plugins/camera/CameraRegionsTest.java | 252 +++++++++--------- .../io/flutter/plugins/camera/CameraTest.java | 92 +++---- .../plugins/camera/CameraZoomTest.java | 125 --------- .../plugins/camera/DartMessengerTest.java | 4 +- .../camera/PictureCaptureRequestTest.java | 225 ---------------- .../media/MediaRecorderBuilderTest.java | 6 +- .../camera/types/ExposureModeTest.java | 1 + .../plugins/camera/types/FlashModeTest.java | 1 + .../plugins/camera/types/FocusModeTest.java | 1 + 37 files changed, 692 insertions(+), 764 deletions(-) create mode 100644 packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/CameraFeatureFactory.java create mode 100644 packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/CameraFeatureFactoryImpl.java rename packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/{autoFocus/AutoFocus.java => autofocus/AutoFocusFeature.java} (87%) rename packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/{autoFocus => autofocus}/FocusMode.java (100%) rename packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/exposurelock/{ExposureLock.java => ExposureLockFeature.java} (74%) rename packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/exposureoffset/{ExposureOffset.java => ExposureOffsetFeature.java} (66%) rename packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/exposurepoint/{ExposurePoint.java => ExposurePointFeature.java} (83%) rename packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/flash/{Flash.java => FlashFeature.java} (85%) rename packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/focuspoint/{FocusPoint.java => FocusPointFeature.java} (72%) rename packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/fpsrange/{FpsRange.java => FpsRangeFeature.java} (80%) rename packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/noisereduction/{NoiseReduction.java => NoiseReductionFeature.java} (82%) rename packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/regionboundaries/{RegionBoundaries.java => RegionBoundariesFeature.java} (83%) rename packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/resolution/{Resolution.java => ResolutionFeature.java} (87%) rename packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/sensororientation/{SensorOrientation.java => SensorOrientationFeature.java} (74%) rename packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/zoomlevel/{ZoomLevel.java => ZoomLevelFeature.java} (73%) delete mode 100644 packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/CameraZoomTest.java delete mode 100644 packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/PictureCaptureRequestTest.java 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 a1db41ed086b..f51c8d1756a4 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 @@ -40,28 +40,27 @@ import io.flutter.plugin.common.EventChannel; import io.flutter.plugin.common.MethodChannel.Result; import io.flutter.plugins.camera.features.CameraFeature; +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.AutoFocus; +import io.flutter.plugins.camera.features.autofocus.AutoFocusFeature; import io.flutter.plugins.camera.features.autofocus.FocusMode; -import io.flutter.plugins.camera.features.exposurelock.ExposureLock; +import io.flutter.plugins.camera.features.exposurelock.ExposureLockFeature; import io.flutter.plugins.camera.features.exposurelock.ExposureMode; -import io.flutter.plugins.camera.features.exposureoffset.ExposureOffset; +import io.flutter.plugins.camera.features.exposureoffset.ExposureOffsetFeature; import io.flutter.plugins.camera.features.exposureoffset.ExposureOffsetValue; -import io.flutter.plugins.camera.features.exposurepoint.ExposurePoint; -import io.flutter.plugins.camera.features.flash.Flash; +import io.flutter.plugins.camera.features.exposurepoint.ExposurePointFeature; +import io.flutter.plugins.camera.features.flash.FlashFeature; import io.flutter.plugins.camera.features.flash.FlashMode; -import io.flutter.plugins.camera.features.focuspoint.FocusPoint; -import io.flutter.plugins.camera.features.fpsrange.FpsRange; -import io.flutter.plugins.camera.features.noisereduction.NoiseReduction; +import io.flutter.plugins.camera.features.focuspoint.FocusPointFeature; import io.flutter.plugins.camera.features.regionboundaries.CameraRegions; -import io.flutter.plugins.camera.features.regionboundaries.RegionBoundaries; -import io.flutter.plugins.camera.features.resolution.Resolution; +import io.flutter.plugins.camera.features.regionboundaries.RegionBoundariesFeature; +import io.flutter.plugins.camera.features.resolution.ResolutionFeature; import io.flutter.plugins.camera.features.resolution.ResolutionPreset; import io.flutter.plugins.camera.features.sensororientation.DeviceOrientationManager; -import io.flutter.plugins.camera.features.sensororientation.SensorOrientation; +import io.flutter.plugins.camera.features.sensororientation.SensorOrientationFeature; import io.flutter.plugins.camera.features.zoomlevel.CameraZoom; -import io.flutter.plugins.camera.features.zoomlevel.ZoomLevel; +import io.flutter.plugins.camera.features.zoomlevel.ZoomLevelFeature; import io.flutter.plugins.camera.media.MediaRecorderBuilder; import io.flutter.view.TextureRegistry.SurfaceTextureEntry; import java.io.File; @@ -170,6 +169,7 @@ public void onImageAvailable(ImageReader reader) { public Camera( final Activity activity, final SurfaceTextureEntry flutterTexture, + final CameraFeatureFactory cameraFeatureFactory, final DartMessenger dartMessenger, final CameraProperties cameraProperties, final ResolutionPreset resolutionPreset, @@ -191,24 +191,38 @@ public Camera( { put( CameraFeatures.resolution, - new Resolution( + cameraFeatureFactory.createResolutionFeature( cameraProperties, resolutionPreset, cameraProperties.getCameraName())); - put(CameraFeatures.autoFocus, new AutoFocus(cameraProperties, false)); + put( + CameraFeatures.autoFocus, + cameraFeatureFactory.createAutoFocusFeature(cameraProperties, false)); put( CameraFeatures.sensorOrientation, - new SensorOrientation(cameraProperties, activity, dartMessenger)); - put(CameraFeatures.exposureLock, new ExposureLock(cameraProperties)); - put(CameraFeatures.exposureOffset, new ExposureOffset(cameraProperties)); + cameraFeatureFactory.createSensorOrientationFeature(cameraProperties, activity, dartMessenger)); + put( + CameraFeatures.exposureLock, + cameraFeatureFactory.createExposureLockFeature(cameraProperties)); + put( + CameraFeatures.exposureOffset, + cameraFeatureFactory.createExposureOffsetFeature(cameraProperties)); put( CameraFeatures.exposurePoint, - new ExposurePoint(cameraProperties, () -> getCameraRegions())); + cameraFeatureFactory.createExposurePointFeature(cameraProperties, () -> getCameraRegions())); put( CameraFeatures.focusPoint, - new FocusPoint(cameraProperties, () -> getCameraRegions())); - put(CameraFeatures.flash, new Flash(cameraProperties)); - put(CameraFeatures.fpsRange, new FpsRange(cameraProperties)); - put(CameraFeatures.noiseReduction, new NoiseReduction(cameraProperties)); - put(CameraFeatures.zoomLevel, new ZoomLevel(cameraProperties)); + cameraFeatureFactory.createFocusPointFeature(cameraProperties, () -> getCameraRegions())); + put( + CameraFeatures.flash, + cameraFeatureFactory.createFlashFeature(cameraProperties)); + put( + CameraFeatures.fpsRange, + cameraFeatureFactory.createFpsRangeFeature(cameraProperties)); + put( + CameraFeatures.noiseReduction, + cameraFeatureFactory.createNoiseReductionFeature(cameraProperties)); + put( + CameraFeatures.zoomLevel, + cameraFeatureFactory.createZoomLevelFeature(cameraProperties)); // Note: CameraFeatures.regionBoundaries will be created in CreateCaptureSession. } @@ -256,7 +270,7 @@ private void prepareMediaRecorder(String outputFilePath) throws IOException { } final PlatformChannel.DeviceOrientation lockedOrientation = - ((SensorOrientation) cameraFeatures.get(CameraFeatures.sensorOrientation)) + ((SensorOrientationFeature) cameraFeatures.get(CameraFeatures.sensorOrientation)) .getLockedCaptureOrientation(); mediaRecorder = @@ -298,14 +312,21 @@ public void onOpened(@NonNull CameraDevice device) { cameraDevice = device; try { startPreview(); + + final boolean isExposurePointSupported = cameraFeatures + .get(CameraFeatures.exposurePoint) + .checkIsSupported(); + final boolean isFocusPointSupported = cameraFeatures + .get(CameraFeatures.focusPoint) + .checkIsSupported(); + dartMessenger.sendCameraInitializedEvent( getPreviewSize().getWidth(), getPreviewSize().getHeight(), (ExposureMode) cameraFeatures.get(CameraFeatures.exposureLock).getValue(), (FocusMode) cameraFeatures.get(CameraFeatures.autoFocus).getValue(), - ((ExposurePoint) cameraFeatures.get(CameraFeatures.exposurePoint)) - .getIsSupported(), - ((FocusPoint) cameraFeatures.get(CameraFeatures.focusPoint)).getIsSupported()); + isExposurePointSupported, + isFocusPointSupported); } catch (CameraAccessException e) { dartMessenger.sendCameraErrorEvent(e.getMessage()); close(); @@ -392,7 +413,7 @@ private void createCaptureSession( // Update camera regions cameraFeatures.put( CameraFeatures.regionBoundaries, - new RegionBoundaries(cameraProperties, mPreviewRequestBuilder)); + new RegionBoundariesFeature(cameraProperties, mPreviewRequestBuilder)); // Prepare the callback CameraCaptureSession.StateCallback callback = @@ -506,8 +527,9 @@ public void takePicture(@NonNull final Result result) { // Listen for picture being taken pictureImageReader.setOnImageAvailableListener(mOnImageAvailableListener, mBackgroundHandler); - final AutoFocus autoFocusFeature = (AutoFocus) cameraFeatures.get(CameraFeatures.autoFocus); - if (autoFocusFeature.getIsSupported() && autoFocusFeature.getValue() == FocusMode.auto) { + final AutoFocusFeature autoFocusFeature = (AutoFocusFeature) cameraFeatures.get(CameraFeatures.autoFocus); + final boolean isAutoFocusSupported = autoFocusFeature.checkIsSupported(); + if (isAutoFocusSupported && autoFocusFeature.getValue() == FocusMode.auto) { runPictureAutoFocus(); } else { runPrecaptureSequence(); @@ -721,7 +743,7 @@ public void startVideoRecording(Result result) { prepareMediaRecorder(videoRecordingFile.getAbsolutePath()); // Re-create autofocus feature so it's using video focus mode now - cameraFeatures.put(CameraFeatures.autoFocus, new AutoFocus(cameraProperties, true)); + cameraFeatures.put(CameraFeatures.autoFocus, new AutoFocusFeature(cameraProperties, true)); recordingVideo = true; createCaptureSession( @@ -742,7 +764,7 @@ public void stopVideoRecording(@NonNull final Result result) { try { // Re-create autofocus feature so it's using continuous capture focus mode now - cameraFeatures.put(CameraFeatures.autoFocus, new AutoFocus(cameraProperties, false)); + cameraFeatures.put(CameraFeatures.autoFocus, new AutoFocusFeature(cameraProperties, false)); recordingVideo = false; try { @@ -813,7 +835,7 @@ public void resumeVideoRecording(@NonNull final Result result) { */ public void setFlashMode(@NonNull final Result result, FlashMode newMode) { // Save the new flash mode setting - ((Flash) cameraFeatures.get(CameraFeatures.flash)).setValue(newMode); + ((FlashFeature) cameraFeatures.get(CameraFeatures.flash)).setValue(newMode); cameraFeatures.get(CameraFeatures.flash).updateBuilder(mPreviewRequestBuilder); refreshPreviewCaptureSession( @@ -828,7 +850,7 @@ public void setFlashMode(@NonNull final Result result, FlashMode newMode) { * @param newMode new mode. */ public void setExposureMode(@NonNull final Result result, ExposureMode newMode) { - ((ExposureLock) cameraFeatures.get(CameraFeatures.exposureLock)).setValue(newMode); + ((ExposureLockFeature) cameraFeatures.get(CameraFeatures.exposureLock)).setValue(newMode); cameraFeatures.get(CameraFeatures.exposureLock).updateBuilder(mPreviewRequestBuilder); refreshPreviewCaptureSession( @@ -846,8 +868,8 @@ public void setExposureMode(@NonNull final Result result, ExposureMode newMode) * @return camera region object. */ public CameraRegions getCameraRegions() { - final RegionBoundaries regionBoundaries = - (RegionBoundaries) cameraFeatures.get(CameraFeatures.regionBoundaries); + final RegionBoundariesFeature regionBoundaries = + (RegionBoundariesFeature) cameraFeatures.get(CameraFeatures.regionBoundaries); return regionBoundaries.getCameraRegions(); } @@ -859,7 +881,7 @@ public CameraRegions getCameraRegions() { * @param y new y. */ public void setExposurePoint(@NonNull final Result result, Double x, Double y) { - ((ExposurePoint) cameraFeatures.get(CameraFeatures.exposurePoint)).setValue(new Point(x, y)); + ((ExposurePointFeature) cameraFeatures.get(CameraFeatures.exposurePoint)).setValue(new Point(x, y)); cameraFeatures.get(CameraFeatures.exposurePoint).updateBuilder(mPreviewRequestBuilder); refreshPreviewCaptureSession( @@ -884,8 +906,8 @@ public double getMinExposureOffset() { /** Return the exposure offset step size to dart. */ public double getExposureOffsetStepSize() { - final ExposureOffset val = (ExposureOffset) cameraFeatures.get(CameraFeatures.exposureOffset); - return val.getExposureOffsetStepSize(cameraProperties); + final ExposureOffsetFeature val = (ExposureOffsetFeature) cameraFeatures.get(CameraFeatures.exposureOffset); + return val.getExposureOffsetStepSize(); } /** @@ -895,11 +917,11 @@ public double getExposureOffsetStepSize() { * @param newMode New mode. */ public void setFocusMode(@NonNull final Result result, FocusMode newMode) { - ((AutoFocus) cameraFeatures.get(CameraFeatures.autoFocus)).setValue(newMode); + ((AutoFocusFeature) cameraFeatures.get(CameraFeatures.autoFocus)).setValue(newMode); cameraFeatures.get(CameraFeatures.autoFocus).updateBuilder(mPreviewRequestBuilder); - /** + /* * For focus mode we need to do an extra step of actually locking/unlocking the * focus in order to ensure it goes into the correct state. */ @@ -937,7 +959,7 @@ public void setFocusMode(@NonNull final Result result, FocusMode newMode) { * @param y new y. */ public void setFocusPoint(@NonNull final Result result, Double x, Double y) { - ((FocusPoint) cameraFeatures.get(CameraFeatures.focusPoint)).setValue(new Point(x, y)); + ((FocusPointFeature) cameraFeatures.get(CameraFeatures.focusPoint)).setValue(new Point(x, y)); cameraFeatures.get(CameraFeatures.focusPoint).updateBuilder(mPreviewRequestBuilder); refreshPreviewCaptureSession( @@ -952,7 +974,7 @@ public void setFocusPoint(@NonNull final Result result, Double x, Double y) { * @param offset new value. */ public void setExposureOffset(@NonNull final Result result, double offset) { - ((ExposureOffset) cameraFeatures.get(CameraFeatures.exposureOffset)) + ((ExposureOffsetFeature) cameraFeatures.get(CameraFeatures.exposureOffset)) .setValue(new ExposureOffsetValue(offset)); cameraFeatures.get(CameraFeatures.exposureOffset).updateBuilder(mPreviewRequestBuilder); @@ -962,7 +984,7 @@ public void setExposureOffset(@NonNull final Result result, double offset) { } public float getMaxZoomLevel() { - final ZoomLevel zoomLevel = (ZoomLevel) cameraFeatures.get(CameraFeatures.zoomLevel); + final ZoomLevelFeature zoomLevel = (ZoomLevelFeature) cameraFeatures.get(CameraFeatures.zoomLevel); return zoomLevel.getCameraZoom().maxZoom; } @@ -972,22 +994,22 @@ public float getMinZoomLevel() { /** Shortcut to get current preview size. */ Size getPreviewSize() { - return ((Resolution) cameraFeatures.get(CameraFeatures.resolution)).getPreviewSize(); + return ((ResolutionFeature) cameraFeatures.get(CameraFeatures.resolution)).getPreviewSize(); } /** Shortcut to get current capture size. */ Size getCaptureSize() { - return ((Resolution) cameraFeatures.get(CameraFeatures.resolution)).getCaptureSize(); + return ((ResolutionFeature) cameraFeatures.get(CameraFeatures.resolution)).getCaptureSize(); } /** Shortcut to get current recording profile. */ CamcorderProfile getRecordingProfile() { - return ((Resolution) cameraFeatures.get(CameraFeatures.resolution)).getRecordingProfile(); + return ((ResolutionFeature) cameraFeatures.get(CameraFeatures.resolution)).getRecordingProfile(); } /** Shortut to get deviceOrientationListener. */ DeviceOrientationManager getDeviceOrientationManager() { - return ((SensorOrientation) cameraFeatures.get(CameraFeatures.sensorOrientation)) + return ((SensorOrientationFeature) cameraFeatures.get(CameraFeatures.sensorOrientation)) .getDeviceOrientationManager(); } @@ -998,7 +1020,7 @@ DeviceOrientationManager getDeviceOrientationManager() { * @param zoom new value. */ public void setZoomLevel(@NonNull final Result result, float zoom) throws CameraAccessException { - final ZoomLevel zoomLevel = (ZoomLevel) cameraFeatures.get(CameraFeatures.zoomLevel); + final ZoomLevelFeature zoomLevel = (ZoomLevelFeature) cameraFeatures.get(CameraFeatures.zoomLevel); float maxZoom = zoomLevel.getCameraZoom().maxZoom; float minZoom = CameraZoom.DEFAULT_ZOOM_FACTOR; @@ -1027,13 +1049,13 @@ public void setZoomLevel(@NonNull final Result result, float zoom) throws Camera * @param orientation new orientation. */ public void lockCaptureOrientation(PlatformChannel.DeviceOrientation orientation) { - ((SensorOrientation) cameraFeatures.get(CameraFeatures.sensorOrientation)) + ((SensorOrientationFeature) cameraFeatures.get(CameraFeatures.sensorOrientation)) .lockCaptureOrientation(orientation); } /** Unlock capture orientation from dart. */ public void unlockCaptureOrientation() { - ((SensorOrientation) cameraFeatures.get(CameraFeatures.sensorOrientation)) + ((SensorOrientationFeature) cameraFeatures.get(CameraFeatures.sensorOrientation)) .unlockCaptureOrientation(); } diff --git a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/CameraCaptureCallback.java b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/CameraCaptureCallback.java index 20477c41c305..a9e6e6a98c6f 100644 --- a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/CameraCaptureCallback.java +++ b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/CameraCaptureCallback.java @@ -1,3 +1,7 @@ +// Copyright 2019 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + package io.flutter.plugins.camera; import android.hardware.camera2.CameraCaptureSession; diff --git a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/CameraProperties.java b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/CameraProperties.java index bc669709df99..35b676e3235c 100644 --- a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/CameraProperties.java +++ b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/CameraProperties.java @@ -1,3 +1,7 @@ +// Copyright 2019 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + package io.flutter.plugins.camera; import android.graphics.Rect; diff --git a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/CameraState.java b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/CameraState.java index 99dd3f0c6825..2df2298a58ab 100644 --- a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/CameraState.java +++ b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/CameraState.java @@ -1,3 +1,7 @@ +// Copyright 2019 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + package io.flutter.plugins.camera; /** diff --git a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/ImageSaver.java b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/ImageSaver.java index b467185c9096..61a60c22e46c 100644 --- a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/ImageSaver.java +++ b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/ImageSaver.java @@ -1,3 +1,7 @@ +// Copyright 2019 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + package io.flutter.plugins.camera; import android.media.Image; diff --git a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/MethodCallHandlerImpl.java b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/MethodCallHandlerImpl.java index 2891fbea8077..e54c786149cc 100644 --- a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/MethodCallHandlerImpl.java +++ b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/MethodCallHandlerImpl.java @@ -15,6 +15,7 @@ import io.flutter.plugin.common.MethodChannel; import io.flutter.plugin.common.MethodChannel.Result; import io.flutter.plugins.camera.CameraPermissions.PermissionsRegistry; +import io.flutter.plugins.camera.features.CameraFeatureFactoryImpl; import io.flutter.plugins.camera.features.autofocus.FocusMode; import io.flutter.plugins.camera.features.exposurelock.ExposureMode; import io.flutter.plugins.camera.features.flash.FlashMode; @@ -364,6 +365,7 @@ private void instantiateCamera(MethodCall call, Result result) throws CameraAcce new Camera( activity, flutterSurfaceTexture, + new CameraFeatureFactoryImpl(), dartMessenger, cameraProperties, resolutionPreset, diff --git a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/PictureCaptureRequest.java b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/PictureCaptureRequest.java index eb52cebba24d..032e9289a4c5 100644 --- a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/PictureCaptureRequest.java +++ b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/PictureCaptureRequest.java @@ -42,11 +42,6 @@ class PictureCaptureRequest { */ private long preCaptureStartTime; - private final Runnable timeoutCallback = - () -> { - error("captureTimeout", "Picture capture request timed out", null); - }; - /** * Factory method to create a picture capture request. * diff --git a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/CameraFeature.java b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/CameraFeature.java index 947823fa1403..2f96dae2a858 100644 --- a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/CameraFeature.java +++ b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/CameraFeature.java @@ -1,6 +1,11 @@ +// Copyright 2019 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + package io.flutter.plugins.camera.features; import android.hardware.camera2.CaptureRequest; +import androidx.annotation.NonNull; import io.flutter.plugins.camera.CameraProperties; /** @@ -11,35 +16,42 @@ * * @param */ -public interface CameraFeature { +public abstract class CameraFeature { + protected final CameraProperties cameraProperties; + + + protected CameraFeature(@NonNull CameraProperties cameraProperties) { + this.cameraProperties = cameraProperties; + } + /** Debug name for this feature. */ - public String getDebugName(); + public abstract String getDebugName(); /** * Get the current value of this feature's setting. * * @return */ - public T getValue(); + public abstract T getValue(); /** * Set a new value for this feature's setting. * * @param value */ - public void setValue(T value); + public abstract void setValue(T value); /** - * Returns whether or not this feature is supported on the given camera properties. + * Returns whether or not this feature is supported. * * @return */ - public boolean checkIsSupported(CameraProperties cameraProperties); + public abstract boolean checkIsSupported(); /** * Update the setting in a provided request builder. * * @param requestBuilder */ - public void updateBuilder(CaptureRequest.Builder requestBuilder); + public abstract void updateBuilder(CaptureRequest.Builder requestBuilder); } diff --git a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/CameraFeatureFactory.java b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/CameraFeatureFactory.java new file mode 100644 index 000000000000..2655b6b1f89d --- /dev/null +++ b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/CameraFeatureFactory.java @@ -0,0 +1,148 @@ +// Copyright 2019 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +package io.flutter.plugins.camera.features; + +import android.app.Activity; +import android.hardware.camera2.CaptureRequest; +import androidx.annotation.NonNull; +import io.flutter.plugins.camera.CameraProperties; +import io.flutter.plugins.camera.DartMessenger; +import io.flutter.plugins.camera.features.autofocus.AutoFocusFeature; +import io.flutter.plugins.camera.features.exposurelock.ExposureLockFeature; +import io.flutter.plugins.camera.features.exposureoffset.ExposureOffsetFeature; +import io.flutter.plugins.camera.features.exposurepoint.ExposurePointFeature; +import io.flutter.plugins.camera.features.flash.FlashFeature; +import io.flutter.plugins.camera.features.focuspoint.FocusPointFeature; +import io.flutter.plugins.camera.features.fpsrange.FpsRangeFeature; +import io.flutter.plugins.camera.features.noisereduction.NoiseReductionFeature; +import io.flutter.plugins.camera.features.regionboundaries.CameraRegions; +import io.flutter.plugins.camera.features.regionboundaries.RegionBoundariesFeature; +import io.flutter.plugins.camera.features.resolution.ResolutionFeature; +import io.flutter.plugins.camera.features.resolution.ResolutionPreset; +import io.flutter.plugins.camera.features.sensororientation.SensorOrientationFeature; +import io.flutter.plugins.camera.features.zoomlevel.ZoomLevelFeature; +import java.util.concurrent.Callable; + +public interface CameraFeatureFactory { + + /** + * Creates a new instance of the auto focus feature. + * + * @param cameraProperties instance of the CameraProperties class containing information about the cameras features. + * @param recordingVideo indicates if the camera is currently recording. + * @return newly created instance of the AutoFocusFeature class. + */ + AutoFocusFeature createAutoFocusFeature( + @NonNull CameraProperties cameraProperties, + boolean recordingVideo); + + /** + * Creates a new instance of the exposure lock feature. + * + * @param cameraProperties instance of the CameraProperties class containing information about the cameras features. + * @return newly created instance of the ExposureLockFeature class. + */ + ExposureLockFeature createExposureLockFeature(@NonNull CameraProperties cameraProperties); + + /** + * Creates a new instance of the exposure offset feature. + * + * @param cameraProperties instance of the CameraProperties class containing information about the cameras features. + * @return newly created instance of the ExposureOffsetFeature class. + */ + ExposureOffsetFeature createExposureOffsetFeature(@NonNull CameraProperties cameraProperties); + + /** + * Creates a new instance of the flash feature. + * + * @param cameraProperties instance of the CameraProperties class containing information about the cameras features. + * @return newly created instance of the FlashFeature class. + */ + FlashFeature createFlashFeature(@NonNull CameraProperties cameraProperties); + + /** + * Creates a new instance of the resolution feature. + * + * @param cameraProperties instance of the CameraProperties class containing information about the cameras features. + * @param initialSetting initial resolution preset. + * @param cameraName the name of the camera which can be used to identify the camera device. + * @return newly created instance of the ResolutionFeature class. + */ + ResolutionFeature createResolutionFeature( + @NonNull CameraProperties cameraProperties, + ResolutionPreset initialSetting, + String cameraName); + + /** + * Creates a new instance of the focus point feature. + * + * @param cameraProperties instance of the CameraProperties class containing information about the cameras features. + * @param getCameraRegions function which is used to retrieve the current camera regions. + * @return newly created instance of the FocusPointFeature class. + */ + FocusPointFeature createFocusPointFeature( + @NonNull CameraProperties cameraProperties, + Callable getCameraRegions); + + /** + * Creates a new instance of the FPS range feature. + * + * @param cameraProperties instance of the CameraProperties class containing information about the cameras features. + * @return newly created instance of the FpsRangeFeature class. + */ + FpsRangeFeature createFpsRangeFeature(@NonNull CameraProperties cameraProperties); + + /** + * Creates a new instance of the sensor orientation feature. + * + * @param cameraProperties instance of the CameraProperties class containing information about the cameras features. + * @param activity current activity associated with the camera plugin. + * @param dartMessenger instance of the DartMessenger class, used to send state updates back to Dart. + * @return newly created instance of the SensorOrientationFeature class. + */ + SensorOrientationFeature createSensorOrientationFeature( + @NonNull CameraProperties cameraProperties, + @NonNull Activity activity, + @NonNull DartMessenger dartMessenger); + + /** + * Creates a new instance of the zoom level feature. + * + * @param cameraProperties instance of the CameraProperties class containing information about the cameras features. + * @return newly created instance of the ZoomLevelFeature class. + */ + ZoomLevelFeature createZoomLevelFeature(@NonNull CameraProperties cameraProperties); + + /** + * Creates a new instance of the region boundaries feature. + * + * @param cameraProperties instance of the CameraProperties class containing information about the cameras features. + * @param requestBuilder instance of the CaptureRequest.Builder class, used to inform the Camera2 API that the settings are updated. + * @return newly created instance of the RegionBoundariesFeature class. + */ + RegionBoundariesFeature createRegionBoundariesFeature( + @NonNull CameraProperties cameraProperties, + @NonNull CaptureRequest.Builder requestBuilder); + + /** + * Creates a new instance of the exposure point feature. + * + * @param cameraProperties instance of the CameraProperties class containing information about the cameras features. + * @param getCameraRegions function which is used to retrieve the current camera regions. + * @return newly created instance of the ExposurePointFeature class. + */ + ExposurePointFeature createExposurePointFeature( + @NonNull CameraProperties cameraProperties, + @NonNull Callable getCameraRegions); + + /** + * Creates a new instance of the noise reduction feature. + * + * @param cameraProperties instance of the CameraProperties class containing information about the cameras features. + * @return newly created instance of the NoiseReductionFeature class. + */ + NoiseReductionFeature createNoiseReductionFeature( + @NonNull CameraProperties cameraProperties); +} diff --git a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/CameraFeatureFactoryImpl.java b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/CameraFeatureFactoryImpl.java new file mode 100644 index 000000000000..b49a907f96ca --- /dev/null +++ b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/CameraFeatureFactoryImpl.java @@ -0,0 +1,105 @@ +// Copyright 2019 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +package io.flutter.plugins.camera.features; + +import android.app.Activity; +import android.hardware.camera2.CaptureRequest; +import androidx.annotation.NonNull; +import io.flutter.plugins.camera.CameraProperties; +import io.flutter.plugins.camera.DartMessenger; +import io.flutter.plugins.camera.features.autofocus.AutoFocusFeature; +import io.flutter.plugins.camera.features.exposurelock.ExposureLockFeature; +import io.flutter.plugins.camera.features.exposureoffset.ExposureOffsetFeature; +import io.flutter.plugins.camera.features.exposurepoint.ExposurePointFeature; +import io.flutter.plugins.camera.features.flash.FlashFeature; +import io.flutter.plugins.camera.features.focuspoint.FocusPointFeature; +import io.flutter.plugins.camera.features.fpsrange.FpsRangeFeature; +import io.flutter.plugins.camera.features.noisereduction.NoiseReductionFeature; +import io.flutter.plugins.camera.features.regionboundaries.CameraRegions; +import io.flutter.plugins.camera.features.regionboundaries.RegionBoundariesFeature; +import io.flutter.plugins.camera.features.resolution.ResolutionFeature; +import io.flutter.plugins.camera.features.resolution.ResolutionPreset; +import io.flutter.plugins.camera.features.sensororientation.SensorOrientationFeature; +import io.flutter.plugins.camera.features.zoomlevel.ZoomLevelFeature; +import java.util.concurrent.Callable; + +public class CameraFeatureFactoryImpl implements CameraFeatureFactory { + + @Override + public AutoFocusFeature createAutoFocusFeature( + @NonNull CameraProperties cameraProperties, + boolean recordingVideo) { + return new AutoFocusFeature(cameraProperties, recordingVideo); + } + + @Override + public ExposureLockFeature createExposureLockFeature(@NonNull CameraProperties cameraProperties) { + return new ExposureLockFeature(cameraProperties); + } + + @Override + public ExposureOffsetFeature createExposureOffsetFeature( + @NonNull CameraProperties cameraProperties) { + return new ExposureOffsetFeature(cameraProperties); + } + + @Override + public FlashFeature createFlashFeature(@NonNull CameraProperties cameraProperties) { + return new FlashFeature(cameraProperties); + } + + @Override + public ResolutionFeature createResolutionFeature( + @NonNull CameraProperties cameraProperties, + ResolutionPreset initialSetting, + String cameraName) { + return new ResolutionFeature(cameraProperties, initialSetting, cameraName); + } + + @Override + public FocusPointFeature createFocusPointFeature( + @NonNull CameraProperties cameraProperties, + Callable getCameraRegions) { + return new FocusPointFeature(cameraProperties, getCameraRegions); + } + + @Override + public FpsRangeFeature createFpsRangeFeature(@NonNull CameraProperties cameraProperties) { + return new FpsRangeFeature(cameraProperties); + } + + @Override + public SensorOrientationFeature createSensorOrientationFeature( + @NonNull CameraProperties cameraProperties, + @NonNull Activity activity, + @NonNull DartMessenger dartMessenger) { + return new SensorOrientationFeature(cameraProperties, activity, dartMessenger); + } + + @Override + public ZoomLevelFeature createZoomLevelFeature(@NonNull CameraProperties cameraProperties) { + return new ZoomLevelFeature(cameraProperties); + } + + @Override + public RegionBoundariesFeature createRegionBoundariesFeature( + @NonNull CameraProperties cameraProperties, + @NonNull CaptureRequest.Builder requestBuilder) { + return new RegionBoundariesFeature(cameraProperties, requestBuilder); + } + + @Override + public ExposurePointFeature createExposurePointFeature( + @NonNull CameraProperties cameraProperties, + @NonNull Callable getCameraRegions) { + return new ExposurePointFeature(cameraProperties, getCameraRegions); + } + + @Override + public NoiseReductionFeature createNoiseReductionFeature( + @NonNull CameraProperties cameraProperties) { + return new NoiseReductionFeature(cameraProperties); + } +} diff --git a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/CameraFeatures.java b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/CameraFeatures.java index 653007376a95..d4db06f27b7b 100644 --- a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/CameraFeatures.java +++ b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/CameraFeatures.java @@ -1,3 +1,7 @@ +// Copyright 2019 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + package io.flutter.plugins.camera.features; /** @@ -24,13 +28,6 @@ public enum CameraFeatures { this.strValue = strValue; } - public static CameraFeatures getValueForString(String modeStr) { - for (CameraFeatures value : values()) { - if (value.strValue.equals(modeStr)) return value; - } - return null; - } - @Override public String toString() { return strValue; diff --git a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/autoFocus/AutoFocus.java b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/autofocus/AutoFocusFeature.java similarity index 87% rename from packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/autoFocus/AutoFocus.java rename to packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/autofocus/AutoFocusFeature.java index b4074a6f846f..f3cbaca8f5d6 100644 --- a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/autoFocus/AutoFocus.java +++ b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/autofocus/AutoFocusFeature.java @@ -1,3 +1,7 @@ +// Copyright 2019 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + package io.flutter.plugins.camera.features.autofocus; import android.hardware.camera2.CameraCharacteristics; @@ -6,17 +10,16 @@ import io.flutter.plugins.camera.CameraProperties; import io.flutter.plugins.camera.features.CameraFeature; -public class AutoFocus implements CameraFeature { - private boolean isSupported; +public class AutoFocusFeature extends CameraFeature { private FocusMode currentSetting = FocusMode.auto; // When we switch recording modes we re-create this feature with // the appropriate setting here. private final boolean recordingVideo; - public AutoFocus(CameraProperties cameraProperties, boolean recordingVideo) { + public AutoFocusFeature(CameraProperties cameraProperties, boolean recordingVideo) { + super(cameraProperties); this.recordingVideo = recordingVideo; - this.isSupported = checkIsSupported(cameraProperties); } @Override @@ -33,7 +36,7 @@ public FocusMode getValue() { public void setValue(FocusMode value) { this.currentSetting = value; - // /** + // /* // * If we are locking AF, we should perform a one-time // */ // switch (currentSetting) { @@ -54,7 +57,7 @@ public void setValue(FocusMode value) { } @Override - public boolean checkIsSupported(CameraProperties cameraProperties) { + public boolean checkIsSupported() { int[] modes = cameraProperties.getControlAutoFocusAvailableModes(); Log.i("Camera", "checkAutoFocusSupported | modes:"); for (int mode : modes) { @@ -78,15 +81,15 @@ public boolean checkIsSupported(CameraProperties cameraProperties) { final boolean supported = !isFixedLength - && !(modes == null - || modes.length == 0 + && !(modes.length == 0 || (modes.length == 1 && modes[0] == CameraCharacteristics.CONTROL_AF_MODE_OFF)); + return supported; } @Override public void updateBuilder(CaptureRequest.Builder requestBuilder) { - if (!isSupported) { + if (!checkIsSupported()) { return; } @@ -108,8 +111,4 @@ public void updateBuilder(CaptureRequest.Builder requestBuilder) { break; } } - - public boolean getIsSupported() { - return this.isSupported; - } } diff --git a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/autoFocus/FocusMode.java b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/autofocus/FocusMode.java similarity index 100% rename from packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/autoFocus/FocusMode.java rename to packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/autofocus/FocusMode.java diff --git a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/exposurelock/ExposureLock.java b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/exposurelock/ExposureLockFeature.java similarity index 74% rename from packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/exposurelock/ExposureLock.java rename to packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/exposurelock/ExposureLockFeature.java index fef875eae7a6..00bfabda8a03 100644 --- a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/exposurelock/ExposureLock.java +++ b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/exposurelock/ExposureLockFeature.java @@ -1,3 +1,7 @@ +// Copyright 2019 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + package io.flutter.plugins.camera.features.exposurelock; import android.hardware.camera2.CaptureRequest; @@ -8,12 +12,11 @@ /** * Exposure lock controls whether or not exposure mode is currenty locked or automatically metering. */ -public class ExposureLock implements CameraFeature { - private boolean isSupported; +public class ExposureLockFeature extends CameraFeature { private ExposureMode currentSetting = ExposureMode.auto; - public ExposureLock(CameraProperties cameraProperties) { - this.isSupported = checkIsSupported(cameraProperties); + public ExposureLockFeature(CameraProperties cameraProperties) { + super(cameraProperties); } @Override @@ -33,13 +36,13 @@ public void setValue(ExposureMode value) { // Available on all devices. @Override - public boolean checkIsSupported(CameraProperties cameraProperties) { + public boolean checkIsSupported() { return true; } @Override public void updateBuilder(CaptureRequest.Builder requestBuilder) { - if (!isSupported) { + if (!checkIsSupported()) { return; } @@ -55,8 +58,4 @@ public void updateBuilder(CaptureRequest.Builder requestBuilder) { break; } } - - public boolean getIsSupported() { - return this.isSupported; - } } diff --git a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/exposureoffset/ExposureOffset.java b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/exposureoffset/ExposureOffsetFeature.java similarity index 66% rename from packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/exposureoffset/ExposureOffset.java rename to packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/exposureoffset/ExposureOffsetFeature.java index c6e38512799e..aba9dfb18ac0 100644 --- a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/exposureoffset/ExposureOffset.java +++ b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/exposureoffset/ExposureOffsetFeature.java @@ -1,3 +1,7 @@ +// Copyright 2019 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + package io.flutter.plugins.camera.features.exposureoffset; import android.hardware.camera2.CaptureRequest; @@ -8,21 +12,19 @@ import io.flutter.plugins.camera.features.CameraFeature; /** Exposure offset makes the image brighter or darker. */ -public class ExposureOffset implements CameraFeature { - private boolean isSupported; +public class ExposureOffsetFeature extends CameraFeature { private ExposureOffsetValue currentSetting; - private CameraProperties cameraProperties; private final double min; private final double max; - public ExposureOffset(CameraProperties cameraProperties) { - this.cameraProperties = cameraProperties; - this.min = getMinExposureOffset(cameraProperties); - this.max = getMaxExposureOffset(cameraProperties); + public ExposureOffsetFeature(CameraProperties cameraProperties) { + super(cameraProperties); + + this.min = getMinExposureOffset(); + this.max = getMaxExposureOffset(); // Initial offset of 0 this.currentSetting = new ExposureOffsetValue(this.min, this.max, 0); - this.isSupported = checkIsSupported(cameraProperties); } @Override @@ -37,19 +39,19 @@ public ExposureOffsetValue getValue() { @Override public void setValue(ExposureOffsetValue value) { - double stepSize = getExposureOffsetStepSize(cameraProperties); + double stepSize = getExposureOffsetStepSize(); this.currentSetting = new ExposureOffsetValue(min, max, (value.value / stepSize)); } // Available on all devices. @Override - public boolean checkIsSupported(CameraProperties cameraProperties) { + public boolean checkIsSupported() { return true; } @Override public void updateBuilder(CaptureRequest.Builder requestBuilder) { - if (!isSupported) { + if (!checkIsSupported()) { return; } @@ -61,26 +63,24 @@ public void updateBuilder(CaptureRequest.Builder requestBuilder) { /** * Return the minimum exposure offset double value. * - * @param cameraProperties * @return */ - private double getMinExposureOffset(CameraProperties cameraProperties) { + private double getMinExposureOffset() { Range range = cameraProperties.getControlAutoExposureCompensationRange(); double minStepped = range == null ? 0 : range.getLower(); - double stepSize = getExposureOffsetStepSize(cameraProperties); + double stepSize = getExposureOffsetStepSize(); return minStepped * stepSize; } /** * Return the max exposure offset double value. * - * @param cameraProperties * @return */ - private double getMaxExposureOffset(CameraProperties cameraProperties) { + private double getMaxExposureOffset() { Range range = cameraProperties.getControlAutoExposureCompensationRange(); double maxStepped = range == null ? 0 : range.getUpper(); - double stepSize = getExposureOffsetStepSize(cameraProperties); + double stepSize = getExposureOffsetStepSize(); return maxStepped * stepSize; } @@ -91,15 +91,10 @@ private double getMaxExposureOffset(CameraProperties cameraProperties) { *

Example: if this has a value of 0.5, then an aeExposureCompensation setting of -2 means that * the actual AE offset is -1. * - * @param cameraProperties * @return */ - public double getExposureOffsetStepSize(CameraProperties cameraProperties) { + public double getExposureOffsetStepSize() { Rational stepSize = cameraProperties.getControlAutoExposureCompensationStep(); return stepSize == null ? 0.0 : stepSize.doubleValue(); } - - public boolean getIsSupported() { - return this.isSupported; - } } diff --git a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/exposureoffset/ExposureOffsetValue.java b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/exposureoffset/ExposureOffsetValue.java index 5e44e42c0cb1..686fc8721284 100644 --- a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/exposureoffset/ExposureOffsetValue.java +++ b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/exposureoffset/ExposureOffsetValue.java @@ -1,3 +1,7 @@ +// Copyright 2019 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + package io.flutter.plugins.camera.features.exposureoffset; /** diff --git a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/exposurepoint/ExposurePoint.java b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/exposurepoint/ExposurePointFeature.java similarity index 83% rename from packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/exposurepoint/ExposurePoint.java rename to packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/exposurepoint/ExposurePointFeature.java index 5d69f04cbd37..d62b6d3b4690 100644 --- a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/exposurepoint/ExposurePoint.java +++ b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/exposurepoint/ExposurePointFeature.java @@ -1,3 +1,7 @@ +// Copyright 2019 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + package io.flutter.plugins.camera.features.exposurepoint; import android.hardware.camera2.CaptureRequest; @@ -10,16 +14,14 @@ import java.util.concurrent.Callable; /** Exposure point controls where in the frame exposure metering will come from. */ -public class ExposurePoint implements CameraFeature { +public class ExposurePointFeature extends CameraFeature { // Used later to always get the correct camera regions instance. private final Callable getCameraRegions; - private boolean isSupported; private Point currentSetting = new Point(0.0, 0.0); - public ExposurePoint( - CameraProperties cameraProperties, Callable getCameraRegions) { + public ExposurePointFeature(CameraProperties cameraProperties, Callable getCameraRegions) { + super(cameraProperties); this.getCameraRegions = getCameraRegions; - this.isSupported = checkIsSupported(cameraProperties); } @Override @@ -49,7 +51,7 @@ public void setValue(Point value) { // Whether or not this camera can set the exposure point. @Override - public boolean checkIsSupported(CameraProperties cameraProperties) { + public boolean checkIsSupported() { Integer supportedRegions = cameraProperties.getControlMaxRegionsAutoExposure(); final boolean supported = supportedRegions != null && supportedRegions > 0; return supported; @@ -57,7 +59,7 @@ public boolean checkIsSupported(CameraProperties cameraProperties) { @Override public void updateBuilder(CaptureRequest.Builder requestBuilder) { - if (!isSupported) { + if (!checkIsSupported()) { return; } @@ -75,8 +77,4 @@ public void updateBuilder(CaptureRequest.Builder requestBuilder) { e.printStackTrace(); } } - - public boolean getIsSupported() { - return this.isSupported; - } } diff --git a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/flash/Flash.java b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/flash/FlashFeature.java similarity index 85% rename from packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/flash/Flash.java rename to packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/flash/FlashFeature.java index da0bf9bcb818..b0ad2dd48037 100644 --- a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/flash/Flash.java +++ b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/flash/FlashFeature.java @@ -1,3 +1,7 @@ +// Copyright 2019 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + package io.flutter.plugins.camera.features.flash; import android.hardware.camera2.CaptureRequest; @@ -5,12 +9,11 @@ import io.flutter.plugins.camera.CameraProperties; import io.flutter.plugins.camera.features.CameraFeature; -public class Flash implements CameraFeature { - private boolean isSupported; +public class FlashFeature extends CameraFeature { private FlashMode currentSetting = FlashMode.auto; - public Flash(CameraProperties cameraProperties) { - this.isSupported = checkIsSupported(cameraProperties); + public FlashFeature(CameraProperties cameraProperties) { + super(cameraProperties); } @Override @@ -29,7 +32,7 @@ public void setValue(FlashMode value) { } @Override - public boolean checkIsSupported(CameraProperties cameraProperties) { + public boolean checkIsSupported() { Boolean available = cameraProperties.getFlashInfoAvailable(); final boolean supported = available != null && available; return supported; @@ -37,7 +40,7 @@ public boolean checkIsSupported(CameraProperties cameraProperties) { @Override public void updateBuilder(CaptureRequest.Builder requestBuilder) { - if (!isSupported) { + if (!checkIsSupported()) { return; } @@ -75,8 +78,4 @@ public void updateBuilder(CaptureRequest.Builder requestBuilder) { // break; } } - - public boolean getIsSupported() { - return this.isSupported; - } } diff --git a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/focuspoint/FocusPoint.java b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/focuspoint/FocusPointFeature.java similarity index 72% rename from packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/focuspoint/FocusPoint.java rename to packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/focuspoint/FocusPointFeature.java index 3b89b3ba22d4..7c12c9686823 100644 --- a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/focuspoint/FocusPoint.java +++ b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/focuspoint/FocusPointFeature.java @@ -1,3 +1,7 @@ +// Copyright 2019 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + package io.flutter.plugins.camera.features.focuspoint; import android.hardware.camera2.CaptureRequest; @@ -8,15 +12,14 @@ import java.util.concurrent.Callable; /** Focus point controls where in the frame focus will come from. */ -public class FocusPoint implements CameraFeature { +public class FocusPointFeature extends CameraFeature { // Used later to always get the correct camera regions instance. private final Callable getCameraRegions; - private boolean isSupported; private Point currentSetting = new Point(0.0, 0.0); - public FocusPoint(CameraProperties cameraProperties, Callable getCameraRegions) { + public FocusPointFeature(CameraProperties cameraProperties, Callable getCameraRegions) { + super(cameraProperties); this.getCameraRegions = getCameraRegions; - this.isSupported = checkIsSupported(cameraProperties); } @Override @@ -46,20 +49,13 @@ public void setValue(Point value) { // Whether or not this camera can set the exposure point. @Override - public boolean checkIsSupported(CameraProperties cameraProperties) { + public boolean checkIsSupported() { Integer supportedRegions = cameraProperties.getControlMaxRegionsAutoFocus(); - final boolean supported = supportedRegions != null && supportedRegions > 0; - return supported; + return supportedRegions != null && supportedRegions > 0; } @Override public void updateBuilder(CaptureRequest.Builder requestBuilder) { - if (!isSupported) { - return; - } - } - - public boolean getIsSupported() { - return this.isSupported; + // Noop: when setting a focus point there is no need to update the request builder. } } diff --git a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/fpsrange/FpsRange.java b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/fpsrange/FpsRangeFeature.java similarity index 80% rename from packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/fpsrange/FpsRange.java rename to packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/fpsrange/FpsRangeFeature.java index 912eda61ec63..5055007b8f7f 100644 --- a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/fpsrange/FpsRange.java +++ b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/fpsrange/FpsRangeFeature.java @@ -1,3 +1,7 @@ +// Copyright 2019 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + package io.flutter.plugins.camera.features.fpsrange; import android.hardware.camera2.CaptureRequest; @@ -6,11 +10,12 @@ import io.flutter.plugins.camera.CameraProperties; import io.flutter.plugins.camera.features.CameraFeature; -public class FpsRange implements CameraFeature> { - private boolean isSupported; +public class FpsRangeFeature extends CameraFeature> { private Range currentSetting; - public FpsRange(CameraProperties cameraProperties) { + public FpsRangeFeature(CameraProperties cameraProperties) { + super(cameraProperties); + Log.i("Camera", "getAvailableFpsRange"); try { @@ -32,8 +37,6 @@ public FpsRange(CameraProperties cameraProperties) { // pictureCaptureRequest.error("cameraAccess", e.getMessage(), null); } Log.i("Camera", "[FPS Range] is:" + currentSetting); - - this.isSupported = checkIsSupported(cameraProperties); } @Override @@ -53,13 +56,13 @@ public void setValue(Range value) { // Always supported @Override - public boolean checkIsSupported(CameraProperties cameraProperties) { + public boolean checkIsSupported() { return true; } @Override public void updateBuilder(CaptureRequest.Builder requestBuilder) { - if (!isSupported) { + if (!checkIsSupported()) { return; } @@ -67,8 +70,4 @@ public void updateBuilder(CaptureRequest.Builder requestBuilder) { requestBuilder.set(CaptureRequest.CONTROL_AE_TARGET_FPS_RANGE, currentSetting); } - - public boolean getIsSupported() { - return this.isSupported; - } } diff --git a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/noisereduction/NoiseReduction.java b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/noisereduction/NoiseReductionFeature.java similarity index 82% rename from packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/noisereduction/NoiseReduction.java rename to packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/noisereduction/NoiseReductionFeature.java index 24f7376bff5f..d5206e457c3c 100644 --- a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/noisereduction/NoiseReduction.java +++ b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/noisereduction/NoiseReductionFeature.java @@ -1,3 +1,7 @@ +// Copyright 2019 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + package io.flutter.plugins.camera.features.noisereduction; import android.hardware.camera2.CaptureRequest; @@ -10,12 +14,11 @@ * and full support the fast mode. * https://developer.android.com/reference/android/hardware/camera2/CameraCharacteristics#NOISE_REDUCTION_AVAILABLE_NOISE_REDUCTION_MODES */ -public class NoiseReduction implements CameraFeature { - private boolean isSupported; +public class NoiseReductionFeature extends CameraFeature { private NoiseReductionMode currentSetting; - public NoiseReduction(CameraProperties cameraProperties) { - this.isSupported = checkIsSupported(cameraProperties); + public NoiseReductionFeature(CameraProperties cameraProperties) { + super(cameraProperties); } @Override @@ -34,8 +37,8 @@ public void setValue(NoiseReductionMode value) { } @Override - public boolean checkIsSupported(CameraProperties cameraProperties) { - /** + public boolean checkIsSupported() { + /* * Available settings: public static final int NOISE_REDUCTION_MODE_FAST = 1; public static * final int NOISE_REDUCTION_MODE_HIGH_QUALITY = 2; public static final int * NOISE_REDUCTION_MODE_MINIMAL = 3; public static final int NOISE_REDUCTION_MODE_OFF = 0; @@ -55,7 +58,7 @@ public boolean checkIsSupported(CameraProperties cameraProperties) { @Override public void updateBuilder(CaptureRequest.Builder requestBuilder) { - if (!isSupported) { + if (!checkIsSupported()) { return; } @@ -65,8 +68,4 @@ public void updateBuilder(CaptureRequest.Builder requestBuilder) { requestBuilder.set( CaptureRequest.NOISE_REDUCTION_MODE, CaptureRequest.NOISE_REDUCTION_MODE_FAST); } - - public boolean getIsSupported() { - return this.isSupported; - } } diff --git a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/noisereduction/NoiseReductionMode.java b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/noisereduction/NoiseReductionMode.java index 944928920518..51fd3bb05741 100644 --- a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/noisereduction/NoiseReductionMode.java +++ b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/noisereduction/NoiseReductionMode.java @@ -1,3 +1,7 @@ +// Copyright 2019 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + package io.flutter.plugins.camera.features.noisereduction; /** Only supports fast mode for now. */ diff --git a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/regionboundaries/CameraRegions.java b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/regionboundaries/CameraRegions.java index 48ef00158018..41f5d4810177 100644 --- a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/regionboundaries/CameraRegions.java +++ b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/regionboundaries/CameraRegions.java @@ -8,9 +8,10 @@ import android.util.Size; public final class CameraRegions { + private final Size maxBoundaries; + private MeteringRectangle aeMeteringRectangle; private MeteringRectangle afMeteringRectangle; - private Size maxBoundaries; public CameraRegions(Size maxBoundaries) { assert (maxBoundaries == null || maxBoundaries.getWidth() > 0); diff --git a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/regionboundaries/RegionBoundaries.java b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/regionboundaries/RegionBoundariesFeature.java similarity index 83% rename from packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/regionboundaries/RegionBoundaries.java rename to packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/regionboundaries/RegionBoundariesFeature.java index b6c3da725ef3..5b5264784bd5 100644 --- a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/regionboundaries/RegionBoundaries.java +++ b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/regionboundaries/RegionBoundariesFeature.java @@ -1,3 +1,7 @@ +// Copyright 2019 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + package io.flutter.plugins.camera.features.regionboundaries; import android.annotation.TargetApi; @@ -12,15 +16,15 @@ * Holds the current region boundaries. When this is created, you must provide a * CaptureRequestBuilder for which we can read the distortion correction settings from. */ -public class RegionBoundaries implements CameraFeature { - private boolean isSupported; +public class RegionBoundariesFeature extends CameraFeature { private Size currentSetting; - private CameraProperties cameraProperties; private CameraRegions cameraRegions; - public RegionBoundaries( - CameraProperties cameraProperties, CaptureRequest.Builder requestBuilder) { - this.cameraProperties = cameraProperties; + public RegionBoundariesFeature( + CameraProperties cameraProperties, + CaptureRequest.Builder requestBuilder) { + super(cameraProperties); + // No distortion correction support if (android.os.Build.VERSION.SDK_INT < Build.VERSION_CODES.P || !supportsDistortionCorrection()) { @@ -47,8 +51,6 @@ public RegionBoundaries( // Create new camera regions using new size cameraRegions = new CameraRegions(currentSetting); - - this.isSupported = checkIsSupported(cameraProperties); } @Override @@ -68,15 +70,13 @@ public void setValue(Size value) { // Available on all devices. @Override - public boolean checkIsSupported(CameraProperties cameraProperties) { + public boolean checkIsSupported() { return true; } @Override public void updateBuilder(CaptureRequest.Builder requestBuilder) { - if (!isSupported) { - return; - } + // Noop: when setting a region boundaries there is no need to update the request builder. } @TargetApi(Build.VERSION_CODES.P) @@ -94,8 +94,4 @@ private boolean supportsDistortionCorrection() { public CameraRegions getCameraRegions() { return this.cameraRegions; } - - public boolean getIsSupported() { - return this.isSupported; - } } diff --git a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/resolution/Resolution.java b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/resolution/ResolutionFeature.java similarity index 87% rename from packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/resolution/Resolution.java rename to packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/resolution/ResolutionFeature.java index 786a57a8dccd..99de9e67127f 100644 --- a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/resolution/Resolution.java +++ b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/resolution/ResolutionFeature.java @@ -1,3 +1,7 @@ +// Copyright 2019 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + package io.flutter.plugins.camera.features.resolution; import android.hardware.camera2.CaptureRequest; @@ -7,16 +11,15 @@ import io.flutter.plugins.camera.CameraProperties; import io.flutter.plugins.camera.features.CameraFeature; -public class Resolution implements CameraFeature { +public class ResolutionFeature extends CameraFeature { private final Size captureSize; private final Size previewSize; private final CamcorderProfile recordingProfile; - // private final boolean recordingVideo; - private boolean isSupported; private ResolutionPreset currentSetting; - public Resolution( + public ResolutionFeature( CameraProperties cameraProperties, ResolutionPreset initialSetting, String cameraName) { + super(cameraProperties); setValue(initialSetting); // Resolution configuration @@ -26,10 +29,9 @@ public Resolution( Log.i("Camera", "captureSize: " + captureSize); previewSize = computeBestPreviewSize(cameraName, initialSetting); - this.isSupported = checkIsSupported(cameraProperties); } - static CamcorderProfile getBestAvailableCamcorderProfileForResolutionPreset( + public static CamcorderProfile getBestAvailableCamcorderProfileForResolutionPreset( String cameraName, ResolutionPreset preset) { int cameraId = Integer.parseInt(cameraName); switch (preset) { @@ -95,15 +97,13 @@ public void setValue(ResolutionPreset value) { // Always supported @Override - public boolean checkIsSupported(CameraProperties cameraProperties) { + public boolean checkIsSupported() { return true; } @Override public void updateBuilder(CaptureRequest.Builder requestBuilder) { - if (!isSupported) { - return; - } + // Noop: when setting a resolution there is no need to update the request builder. } public CamcorderProfile getRecordingProfile() { @@ -117,8 +117,4 @@ public Size getPreviewSize() { public Size getCaptureSize() { return this.captureSize; } - - public boolean getIsSupported() { - return this.isSupported; - } } diff --git a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/sensororientation/DeviceOrientationManager.java b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/sensororientation/DeviceOrientationManager.java index 5891f2ae29c7..fbf447947988 100644 --- a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/sensororientation/DeviceOrientationManager.java +++ b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/sensororientation/DeviceOrientationManager.java @@ -16,6 +16,7 @@ import android.view.OrientationEventListener; import android.view.Surface; import android.view.WindowManager; +import androidx.annotation.NonNull; import io.flutter.embedding.engine.systemchannels.PlatformChannel; import io.flutter.plugins.camera.DartMessenger; @@ -34,12 +35,18 @@ public class DeviceOrientationManager { /** Factory method to create a device orientation manager. */ public static DeviceOrientationManager create( - Activity activity, DartMessenger messenger, boolean isFrontFacing, int sensorOrientation) { + @NonNull Activity activity, + @NonNull DartMessenger messenger, + boolean isFrontFacing, + int sensorOrientation) { return new DeviceOrientationManager(activity, messenger, isFrontFacing, sensorOrientation); } private DeviceOrientationManager( - Activity activity, DartMessenger messenger, boolean isFrontFacing, int sensorOrientation) { + @NonNull Activity activity, + @NonNull DartMessenger messenger, + boolean isFrontFacing, + int sensorOrientation) { this.activity = activity; this.messenger = messenger; this.isFrontFacing = isFrontFacing; diff --git a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/sensororientation/SensorOrientation.java b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/sensororientation/SensorOrientationFeature.java similarity index 74% rename from packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/sensororientation/SensorOrientation.java rename to packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/sensororientation/SensorOrientationFeature.java index f07465565582..876d35d40196 100644 --- a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/sensororientation/SensorOrientation.java +++ b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/sensororientation/SensorOrientationFeature.java @@ -1,29 +1,34 @@ +// Copyright 2019 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + package io.flutter.plugins.camera.features.sensororientation; import android.app.Activity; import android.hardware.camera2.CameraMetadata; import android.hardware.camera2.CaptureRequest; +import androidx.annotation.NonNull; import io.flutter.embedding.engine.systemchannels.PlatformChannel; import io.flutter.plugins.camera.CameraProperties; import io.flutter.plugins.camera.DartMessenger; import io.flutter.plugins.camera.features.CameraFeature; -public class SensorOrientation implements CameraFeature { - private boolean isSupported; +public class SensorOrientationFeature extends CameraFeature { private Integer currentSetting = 0; private final DeviceOrientationManager deviceOrientationListener; private PlatformChannel.DeviceOrientation lockedCaptureOrientation; - public SensorOrientation( - CameraProperties cameraProperties, Activity activity, DartMessenger dartMessenger) { + public SensorOrientationFeature( + @NonNull CameraProperties cameraProperties, + @NonNull Activity activity, + @NonNull DartMessenger dartMessenger) { + super(cameraProperties); setValue(cameraProperties.getSensorOrientation()); boolean isFrontFacing = cameraProperties.getLensFacing() == CameraMetadata.LENS_FACING_FRONT; deviceOrientationListener = DeviceOrientationManager.create(activity, dartMessenger, isFrontFacing, currentSetting); deviceOrientationListener.start(); - - this.isSupported = checkIsSupported(cameraProperties); } @Override @@ -42,25 +47,19 @@ public void setValue(Integer value) { } @Override - public boolean checkIsSupported(CameraProperties cameraProperties) { + public boolean checkIsSupported() { return true; } @Override public void updateBuilder(CaptureRequest.Builder requestBuilder) { - if (!isSupported) { - return; - } + // Noop: when setting the sensor orientation there is no need to update the request builder. } public DeviceOrientationManager getDeviceOrientationManager() { return this.deviceOrientationListener; } - public boolean getIsSupported() { - return this.isSupported; - } - public void lockCaptureOrientation(PlatformChannel.DeviceOrientation orientation) { this.lockedCaptureOrientation = orientation; } diff --git a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/zoomlevel/ZoomLevel.java b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/zoomlevel/ZoomLevelFeature.java similarity index 73% rename from packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/zoomlevel/ZoomLevel.java rename to packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/zoomlevel/ZoomLevelFeature.java index fd52fafc5631..85499a47a053 100644 --- a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/zoomlevel/ZoomLevel.java +++ b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/zoomlevel/ZoomLevelFeature.java @@ -1,3 +1,7 @@ +// Copyright 2019 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + package io.flutter.plugins.camera.features.zoomlevel; import android.graphics.Rect; @@ -7,19 +11,16 @@ import io.flutter.plugins.camera.features.CameraFeature; /** Exposure offset makes the image brighter or darker. */ -public class ZoomLevel implements CameraFeature { - private boolean isSupported; +public class ZoomLevelFeature extends CameraFeature { private Float currentSetting = CameraZoom.DEFAULT_ZOOM_FACTOR; - private CameraProperties cameraProperties; private CameraZoom cameraZoom; - public ZoomLevel(CameraProperties cameraProperties) { - this.cameraProperties = cameraProperties; + public ZoomLevelFeature(CameraProperties cameraProperties) { + super(cameraProperties); this.cameraZoom = new CameraZoom( cameraProperties.getSensorInfoActiveArraySize(), cameraProperties.getScalerAvailableMaxDigitalZoom()); - this.isSupported = checkIsSupported(cameraProperties); } @Override @@ -39,13 +40,13 @@ public void setValue(Float value) { // Available on all devices. @Override - public boolean checkIsSupported(CameraProperties cameraProperties) { + public boolean checkIsSupported() { return true; } @Override public void updateBuilder(CaptureRequest.Builder requestBuilder) { - if (!isSupported) { + if (!checkIsSupported()) { return; } @@ -58,8 +59,4 @@ public void updateBuilder(CaptureRequest.Builder requestBuilder) { public CameraZoom getCameraZoom() { return this.cameraZoom; } - - public boolean getIsSupported() { - return this.isSupported; - } } diff --git a/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/CameraRegionsTest.java b/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/CameraRegionsTest.java index 99745e56a857..1bb81314604b 100644 --- a/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/CameraRegionsTest.java +++ b/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/CameraRegionsTest.java @@ -1,126 +1,126 @@ -// Copyright 2019 The Chromium Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -package io.flutter.plugins.camera; - -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertNotNull; -import static org.junit.Assert.assertNull; - -import android.hardware.camera2.params.MeteringRectangle; -import android.util.Size; -import org.junit.Before; -import org.junit.Test; -import org.junit.runner.RunWith; -import org.robolectric.RobolectricTestRunner; - -@RunWith(RobolectricTestRunner.class) -public class CameraRegionsTest { - - CameraRegions cameraRegions; - - @Before - public void setUp() { - this.cameraRegions = new CameraRegions(new Size(100, 50)); - } - - @Test(expected = AssertionError.class) - public void getMeteringRectangleForPoint_should_throw_for_x_upper_bound() { - cameraRegions.getMeteringRectangleForPoint(new Size(10, 10), 1.5, 0); - } - - @Test(expected = AssertionError.class) - public void getMeteringRectangleForPoint_should_throw_for_x_lower_bound() { - cameraRegions.getMeteringRectangleForPoint(new Size(10, 10), -0.5, 0); - } - - @Test(expected = AssertionError.class) - public void getMeteringRectangleForPoint_should_throw_for_y_upper_bound() { - cameraRegions.getMeteringRectangleForPoint(new Size(10, 10), 0, 1.5); - } - - @Test(expected = AssertionError.class) - public void getMeteringRectangleForPoint_should_throw_for_y_lower_bound() { - cameraRegions.getMeteringRectangleForPoint(new Size(10, 10), 0, -0.5); - } - - @Test(expected = IllegalStateException.class) - public void getMeteringRectangleForPoint_should_throw_for_null_boundaries() { - cameraRegions.getMeteringRectangleForPoint(null, 0, -0); - } - - @Test - public void getMeteringRectangleForPoint_should_return_valid_MeteringRectangle() { - MeteringRectangle r; - // Center - r = cameraRegions.getMeteringRectangleForPoint(cameraRegions.getMaxBoundaries(), 0.5, 0.5); - assertEquals(new MeteringRectangle(45, 23, 10, 5, 1), r); - - // Top left - r = cameraRegions.getMeteringRectangleForPoint(cameraRegions.getMaxBoundaries(), 0.0, 0.0); - assertEquals(new MeteringRectangle(0, 0, 10, 5, 1), r); - - // Bottom right - r = cameraRegions.getMeteringRectangleForPoint(cameraRegions.getMaxBoundaries(), 1.0, 1.0); - assertEquals(new MeteringRectangle(89, 44, 10, 5, 1), r); - - // Top left - r = cameraRegions.getMeteringRectangleForPoint(cameraRegions.getMaxBoundaries(), 0.0, 1.0); - assertEquals(new MeteringRectangle(0, 44, 10, 5, 1), r); - - // Top right - r = cameraRegions.getMeteringRectangleForPoint(cameraRegions.getMaxBoundaries(), 1.0, 0.0); - assertEquals(new MeteringRectangle(89, 0, 10, 5, 1), r); - } - - @Test(expected = AssertionError.class) - public void constructor_should_throw_for_0_width_boundary() { - new CameraRegions(new Size(0, 50)); - } - - @Test(expected = AssertionError.class) - public void constructor_should_throw_for_0_height_boundary() { - new CameraRegions(new Size(100, 0)); - } - - @Test - public void constructor_should_initialize() { - CameraRegions cr = new CameraRegions(new Size(100, 50)); - assertEquals(new Size(100, 50), cr.getMaxBoundaries()); - assertNull(cr.getAEMeteringRectangle()); - assertNull(cr.getAFMeteringRectangle()); - } - - @Test - public void setAutoExposureMeteringRectangleFromPoint_should_set_aeMeteringRectangle_for_point() { - CameraRegions cr = new CameraRegions(new Size(100, 50)); - cr.setAutoExposureMeteringRectangleFromPoint(0, 0); - assertEquals(new MeteringRectangle(0, 0, 10, 5, 1), cr.getAEMeteringRectangle()); - } - - @Test - public void resetAutoExposureMeteringRectangle_should_reset_aeMeteringRectangle() { - CameraRegions cr = new CameraRegions(new Size(100, 50)); - cr.setAutoExposureMeteringRectangleFromPoint(0, 0); - assertNotNull(cr.getAEMeteringRectangle()); - cr.resetAutoExposureMeteringRectangle(); - assertNull(cr.getAEMeteringRectangle()); - } - - @Test - public void setAutoFocusMeteringRectangleFromPoint_should_set_afMeteringRectangle_for_point() { - CameraRegions cr = new CameraRegions(new Size(100, 50)); - cr.setAutoFocusMeteringRectangleFromPoint(0, 0); - assertEquals(new MeteringRectangle(0, 0, 10, 5, 1), cr.getAFMeteringRectangle()); - } - - @Test - public void resetAutoFocusMeteringRectangle_should_reset_afMeteringRectangle() { - CameraRegions cr = new CameraRegions(new Size(100, 50)); - cr.setAutoFocusMeteringRectangleFromPoint(0, 0); - assertNotNull(cr.getAFMeteringRectangle()); - cr.resetAutoFocusMeteringRectangle(); - assertNull(cr.getAFMeteringRectangle()); - } -} +//// Copyright 2019 The Chromium Authors. All rights reserved. +//// Use of this source code is governed by a BSD-style license that can be +//// found in the LICENSE file. +// +//package io.flutter.plugins.camera; +// +//import static org.junit.Assert.assertEquals; +//import static org.junit.Assert.assertNotNull; +//import static org.junit.Assert.assertNull; +// +//import android.hardware.camera2.params.MeteringRectangle; +//import android.util.Size; +//import org.junit.Before; +//import org.junit.Test; +//import org.junit.runner.RunWith; +//import org.robolectric.RobolectricTestRunner; +// +//@RunWith(RobolectricTestRunner.class) +//public class CameraRegionsTest { +// +// CameraRegions cameraRegions; +// +// @Before +// public void setUp() { +// this.cameraRegions = new CameraRegions(new Size(100, 50)); +// } +// +// @Test(expected = AssertionError.class) +// public void getMeteringRectangleForPoint_should_throw_for_x_upper_bound() { +// cameraRegions.getMeteringRectangleForPoint(new Size(10, 10), 1.5, 0); +// } +// +// @Test(expected = AssertionError.class) +// public void getMeteringRectangleForPoint_should_throw_for_x_lower_bound() { +// cameraRegions.getMeteringRectangleForPoint(new Size(10, 10), -0.5, 0); +// } +// +// @Test(expected = AssertionError.class) +// public void getMeteringRectangleForPoint_should_throw_for_y_upper_bound() { +// cameraRegions.getMeteringRectangleForPoint(new Size(10, 10), 0, 1.5); +// } +// +// @Test(expected = AssertionError.class) +// public void getMeteringRectangleForPoint_should_throw_for_y_lower_bound() { +// cameraRegions.getMeteringRectangleForPoint(new Size(10, 10), 0, -0.5); +// } +// +// @Test(expected = IllegalStateException.class) +// public void getMeteringRectangleForPoint_should_throw_for_null_boundaries() { +// cameraRegions.getMeteringRectangleForPoint(null, 0, -0); +// } +// +// @Test +// public void getMeteringRectangleForPoint_should_return_valid_MeteringRectangle() { +// MeteringRectangle r; +// // Center +// r = cameraRegions.getMeteringRectangleForPoint(cameraRegions.getMaxBoundaries(), 0.5, 0.5); +// assertEquals(new MeteringRectangle(45, 23, 10, 5, 1), r); +// +// // Top left +// r = cameraRegions.getMeteringRectangleForPoint(cameraRegions.getMaxBoundaries(), 0.0, 0.0); +// assertEquals(new MeteringRectangle(0, 0, 10, 5, 1), r); +// +// // Bottom right +// r = cameraRegions.getMeteringRectangleForPoint(cameraRegions.getMaxBoundaries(), 1.0, 1.0); +// assertEquals(new MeteringRectangle(89, 44, 10, 5, 1), r); +// +// // Top left +// r = cameraRegions.getMeteringRectangleForPoint(cameraRegions.getMaxBoundaries(), 0.0, 1.0); +// assertEquals(new MeteringRectangle(0, 44, 10, 5, 1), r); +// +// // Top right +// r = cameraRegions.getMeteringRectangleForPoint(cameraRegions.getMaxBoundaries(), 1.0, 0.0); +// assertEquals(new MeteringRectangle(89, 0, 10, 5, 1), r); +// } +// +// @Test(expected = AssertionError.class) +// public void constructor_should_throw_for_0_width_boundary() { +// new CameraRegions(new Size(0, 50)); +// } +// +// @Test(expected = AssertionError.class) +// public void constructor_should_throw_for_0_height_boundary() { +// new CameraRegions(new Size(100, 0)); +// } +// +// @Test +// public void constructor_should_initialize() { +// CameraRegions cr = new CameraRegions(new Size(100, 50)); +// assertEquals(new Size(100, 50), cr.getMaxBoundaries()); +// assertNull(cr.getAEMeteringRectangle()); +// assertNull(cr.getAFMeteringRectangle()); +// } +// +// @Test +// public void setAutoExposureMeteringRectangleFromPoint_should_set_aeMeteringRectangle_for_point() { +// CameraRegions cr = new CameraRegions(new Size(100, 50)); +// cr.setAutoExposureMeteringRectangleFromPoint(0, 0); +// assertEquals(new MeteringRectangle(0, 0, 10, 5, 1), cr.getAEMeteringRectangle()); +// } +// +// @Test +// public void resetAutoExposureMeteringRectangle_should_reset_aeMeteringRectangle() { +// CameraRegions cr = new CameraRegions(new Size(100, 50)); +// cr.setAutoExposureMeteringRectangleFromPoint(0, 0); +// assertNotNull(cr.getAEMeteringRectangle()); +// cr.resetAutoExposureMeteringRectangle(); +// assertNull(cr.getAEMeteringRectangle()); +// } +// +// @Test +// public void setAutoFocusMeteringRectangleFromPoint_should_set_afMeteringRectangle_for_point() { +// CameraRegions cr = new CameraRegions(new Size(100, 50)); +// cr.setAutoFocusMeteringRectangleFromPoint(0, 0); +// assertEquals(new MeteringRectangle(0, 0, 10, 5, 1), cr.getAFMeteringRectangle()); +// } +// +// @Test +// public void resetAutoFocusMeteringRectangle_should_reset_afMeteringRectangle() { +// CameraRegions cr = new CameraRegions(new Size(100, 50)); +// cr.setAutoFocusMeteringRectangleFromPoint(0, 0); +// assertNotNull(cr.getAFMeteringRectangle()); +// cr.resetAutoFocusMeteringRectangle(); +// assertNull(cr.getAFMeteringRectangle()); +// } +//} 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 d0c930796486..897fdb5162eb 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 @@ -1,82 +1,74 @@ +// Copyright 2019 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + package io.flutter.plugins.camera; -import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNotNull; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.mockStatic; +import static org.mockito.Mockito.never; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; import android.app.Activity; import android.hardware.camera2.CameraAccessException; -import android.media.CamcorderProfile; -import io.flutter.plugins.camera.types.ResolutionPreset; +import io.flutter.plugins.camera.features.CameraFeatureFactory; +import io.flutter.plugins.camera.features.resolution.ResolutionPreset; import io.flutter.view.TextureRegistry; import org.junit.Test; -import org.junit.runner.RunWith; -import org.mockito.MockedStatic; -import org.robolectric.RobolectricTestRunner; -@RunWith(RobolectricTestRunner.class) public class CameraTest { @Test public void should_create_camera_plugin() throws CameraAccessException { final Activity mockActivity = mock(Activity.class); - final TextureRegistry.SurfaceTextureEntry flutterTextureMock = + final TextureRegistry.SurfaceTextureEntry mockFlutterTexture = mock(TextureRegistry.SurfaceTextureEntry.class); - final DartMessenger dartMessengerMock = mock(DartMessenger.class); + final CameraFeatureFactory mockCameraFeatureFactory = mock(CameraFeatureFactory.class); + final DartMessenger mockDartMessenger = mock(DartMessenger.class); + final CameraProperties mockCameraProperties = mock(CameraProperties.class); final String cameraName = "1"; final ResolutionPreset resolutionPreset = ResolutionPreset.high; final boolean enableAudio = false; - // Mocks - final CamcorderProfile mockCamcorderProfile = mock(CamcorderProfile.class); - final CameraProperties mockCameraProperties = mock(CameraProperties.class); - final DeviceOrientationManager mockDeviceOrientationManager = - mock(DeviceOrientationManager.class); - - try (MockedStatic mockCameraUtils = mockStatic(CameraUtils.class)) { - mockCameraUtils - .when( - () -> - CameraUtils.getBestAvailableCamcorderProfileForResolutionPreset( - cameraName, resolutionPreset)) - .thenReturn(mockCamcorderProfile); - - mockCamcorderProfile.videoFrameHeight = 480; - mockCamcorderProfile.videoFrameWidth = 640; - when(mockCameraProperties.getLensFacing()).thenReturn(0); - when(mockCameraProperties.getSensorOrientation()).thenReturn(0); - when(mockCameraProperties.getCameraName()).thenReturn(cameraName); - when(mockCameraProperties.getControlAutoFocusAvailableModes()) - .thenReturn(new int[] {0, 1, 2}); - when(mockCameraProperties.getControlAutoExposureAvailableTargetFpsRanges()).thenReturn(null); - - Camera camera = null; - try (MockedStatic mockOrientationManagerFactory = - mockStatic(DeviceOrientationManager.class)) { - mockOrientationManagerFactory - .when(() -> DeviceOrientationManager.create(mockActivity, dartMessengerMock, true, 0)) - .thenReturn(mockDeviceOrientationManager); + when(mockCameraProperties.getCameraName()).thenReturn(cameraName); - camera = + Camera camera = new Camera( mockActivity, - flutterTextureMock, - dartMessengerMock, + mockFlutterTexture, + mockCameraFeatureFactory, + mockDartMessenger, mockCameraProperties, resolutionPreset, enableAudio); - } - assertNotNull("should create a camera", camera); - assertEquals( - "should be in preview state from the start", - camera.getState(), - CameraState.STATE_PREVIEW); - verify(mockDeviceOrientationManager, times(1)).start(); - } + verify(mockCameraFeatureFactory, times(1)) + .createAutoFocusFeature(mockCameraProperties, false); + verify(mockCameraFeatureFactory, times(1)) + .createExposureLockFeature(mockCameraProperties); + verify(mockCameraFeatureFactory, times(1)) + .createExposurePointFeature(eq(mockCameraProperties), any()); + verify(mockCameraFeatureFactory, times(1)) + .createExposureOffsetFeature(mockCameraProperties); + verify(mockCameraFeatureFactory, times(1)) + .createFlashFeature(mockCameraProperties); + verify(mockCameraFeatureFactory, times(1)) + .createFocusPointFeature(eq(mockCameraProperties), any()); + verify(mockCameraFeatureFactory, times(1)) + .createFpsRangeFeature(mockCameraProperties); + verify(mockCameraFeatureFactory, times(1)) + .createNoiseReductionFeature(mockCameraProperties); + verify(mockCameraFeatureFactory, times(1)) + .createResolutionFeature(mockCameraProperties, resolutionPreset, cameraName); + verify(mockCameraFeatureFactory, times(1)) + .createSensorOrientationFeature(mockCameraProperties, mockActivity, mockDartMessenger); + verify(mockCameraFeatureFactory, times(1)) + .createZoomLevelFeature(mockCameraProperties); + verify(mockCameraFeatureFactory, never()).createRegionBoundariesFeature(any(), any()); + assertNotNull("should create a camera", camera); } } diff --git a/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/CameraZoomTest.java b/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/CameraZoomTest.java deleted file mode 100644 index 8f05da71b5c5..000000000000 --- a/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/CameraZoomTest.java +++ /dev/null @@ -1,125 +0,0 @@ -// Copyright 2019 The Chromium Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -package io.flutter.plugins.camera; - -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertFalse; -import static org.junit.Assert.assertNotNull; -import static org.junit.Assert.assertNull; -import static org.junit.Assert.assertTrue; - -import android.graphics.Rect; -import org.junit.Test; -import org.junit.runner.RunWith; -import org.robolectric.RobolectricTestRunner; - -@RunWith(RobolectricTestRunner.class) -public class CameraZoomTest { - - @Test - public void ctor_when_parameters_are_valid() { - final Rect sensorSize = new Rect(0, 0, 0, 0); - final Float maxZoom = 4.0f; - final CameraZoom cameraZoom = new CameraZoom(sensorSize, maxZoom); - - assertNotNull(cameraZoom); - assertTrue(cameraZoom.hasSupport); - assertEquals(4.0f, cameraZoom.maxZoom, 0); - assertEquals(1.0f, CameraZoom.DEFAULT_ZOOM_FACTOR, 0); - } - - @Test - public void ctor_when_sensor_size_is_null() { - final Rect sensorSize = null; - final Float maxZoom = 4.0f; - final CameraZoom cameraZoom = new CameraZoom(sensorSize, maxZoom); - - assertNotNull(cameraZoom); - assertFalse(cameraZoom.hasSupport); - assertEquals(cameraZoom.maxZoom, 1.0f, 0); - } - - @Test - public void ctor_when_max_zoom_is_null() { - final Rect sensorSize = new Rect(0, 0, 0, 0); - final Float maxZoom = null; - final CameraZoom cameraZoom = new CameraZoom(sensorSize, maxZoom); - - assertNotNull(cameraZoom); - assertFalse(cameraZoom.hasSupport); - assertEquals(cameraZoom.maxZoom, 1.0f, 0); - } - - @Test - public void ctor_when_max_zoom_is_smaller_then_default_zoom_factor() { - final Rect sensorSize = new Rect(0, 0, 0, 0); - final Float maxZoom = 0.5f; - final CameraZoom cameraZoom = new CameraZoom(sensorSize, maxZoom); - - assertNotNull(cameraZoom); - assertFalse(cameraZoom.hasSupport); - assertEquals(cameraZoom.maxZoom, 1.0f, 0); - } - - @Test - public void setZoom_when_no_support_should_not_set_scaler_crop_region() { - final CameraZoom cameraZoom = new CameraZoom(null, null); - final Rect computedZoom = cameraZoom.computeZoom(2f); - - assertNull(computedZoom); - } - - @Test - public void setZoom_when_sensor_size_equals_zero_should_return_crop_region_of_zero() { - final Rect sensorSize = new Rect(0, 0, 0, 0); - final CameraZoom cameraZoom = new CameraZoom(sensorSize, 20f); - final Rect computedZoom = cameraZoom.computeZoom(18f); - - assertNotNull(computedZoom); - assertEquals(computedZoom.left, 0); - assertEquals(computedZoom.top, 0); - assertEquals(computedZoom.right, 0); - assertEquals(computedZoom.bottom, 0); - } - - @Test - public void setZoom_when_sensor_size_is_valid_should_return_crop_region() { - final Rect sensorSize = new Rect(0, 0, 100, 100); - final CameraZoom cameraZoom = new CameraZoom(sensorSize, 20f); - final Rect computedZoom = cameraZoom.computeZoom(18f); - - assertNotNull(computedZoom); - assertEquals(computedZoom.left, 48); - assertEquals(computedZoom.top, 48); - assertEquals(computedZoom.right, 52); - assertEquals(computedZoom.bottom, 52); - } - - @Test - public void setZoom_when_zoom_is_greater_then_max_zoom_clamp_to_max_zoom() { - final Rect sensorSize = new Rect(0, 0, 100, 100); - final CameraZoom cameraZoom = new CameraZoom(sensorSize, 10f); - final Rect computedZoom = cameraZoom.computeZoom(25f); - - assertNotNull(computedZoom); - assertEquals(computedZoom.left, 45); - assertEquals(computedZoom.top, 45); - assertEquals(computedZoom.right, 55); - assertEquals(computedZoom.bottom, 55); - } - - @Test - public void setZoom_when_zoom_is_smaller_then_min_zoom_clamp_to_min_zoom() { - final Rect sensorSize = new Rect(0, 0, 100, 100); - final CameraZoom cameraZoom = new CameraZoom(sensorSize, 10f); - final Rect computedZoom = cameraZoom.computeZoom(0.5f); - - assertNotNull(computedZoom); - assertEquals(computedZoom.left, 0); - assertEquals(computedZoom.top, 0); - assertEquals(computedZoom.right, 100); - assertEquals(computedZoom.bottom, 100); - } -} diff --git a/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/DartMessengerTest.java b/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/DartMessengerTest.java index 44ba6040a467..d83910683fd4 100644 --- a/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/DartMessengerTest.java +++ b/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/DartMessengerTest.java @@ -12,8 +12,8 @@ import io.flutter.plugin.common.BinaryMessenger; import io.flutter.plugin.common.MethodCall; import io.flutter.plugin.common.StandardMethodCodec; -import io.flutter.plugins.camera.types.ExposureMode; -import io.flutter.plugins.camera.types.FocusMode; +import io.flutter.plugins.camera.features.autofocus.FocusMode; +import io.flutter.plugins.camera.features.exposurelock.ExposureMode; import java.nio.ByteBuffer; import java.util.ArrayList; import java.util.List; diff --git a/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/PictureCaptureRequestTest.java b/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/PictureCaptureRequestTest.java deleted file mode 100644 index cef547f889b8..000000000000 --- a/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/PictureCaptureRequestTest.java +++ /dev/null @@ -1,225 +0,0 @@ -// Copyright 2019 The Chromium Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -package io.flutter.plugins.camera; - -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertFalse; -import static org.junit.Assert.assertTrue; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.mockStatic; -import static org.mockito.Mockito.never; -import static org.mockito.Mockito.times; -import static org.mockito.Mockito.verify; - -import io.flutter.plugin.common.MethodChannel; -import io.flutter.plugins.camera.PictureCaptureRequest.TimeoutHandler; -import org.junit.Test; -import org.junit.runner.RunWith; -import org.mockito.MockedStatic; -import org.robolectric.RobolectricTestRunner; - -@RunWith(RobolectricTestRunner.class) -public class PictureCaptureRequestTest { - - @Test - public void state_is_idle_by_default() { - PictureCaptureRequest req = PictureCaptureRequest.create(null, null, null); - assertEquals("Default state is idle", req.getState(), PictureCaptureRequestState.STATE_IDLE); - } - - @Test - public void setState_sets_state() { - PictureCaptureRequest req = PictureCaptureRequest.create(null, null, null); - req.setState(PictureCaptureRequestState.STATE_WAITING_FOCUS); - assertEquals( - "State is focusing", req.getState(), PictureCaptureRequestState.STATE_WAITING_FOCUS); - req.setState(PictureCaptureRequestState.STATE_WAITING_PRECAPTURE_START); - assertEquals( - "State is preCapture", - req.getState(), - PictureCaptureRequestState.STATE_WAITING_PRECAPTURE_START); - req.setState(PictureCaptureRequestState.STATE_WAITING_PRECAPTURE_DONE); - assertEquals( - "State is waitingPreCaptureReady", - req.getState(), - PictureCaptureRequestState.STATE_WAITING_PRECAPTURE_DONE); - req.setState(PictureCaptureRequestState.STATE_CAPTURING); - assertEquals( - "State is awaitingPreCapture", req.getState(), PictureCaptureRequestState.STATE_CAPTURING); - } - - @Test - public void setState_sends_camera_error_event_When_already_finished() { - DartMessenger mockMessenger = mock(DartMessenger.class); - PictureCaptureRequest req = PictureCaptureRequest.create(null, null, mockMessenger); - - // Make sure state is set to finished - req.setState(PictureCaptureRequestState.STATE_FINISHED); - - // Try to update state - req.setState(PictureCaptureRequestState.STATE_CAPTURING); - - verify(mockMessenger, times(1)).sendCameraErrorEvent("Request has already been finished"); - assertEquals("State is finished", req.getState(), PictureCaptureRequestState.STATE_FINISHED); - } - - @Test - public void setState_resets_timeout() { - try (MockedStatic mockTimeoutFactory = mockStatic(TimeoutHandler.class)) { - PictureCaptureRequest.TimeoutHandler mockTimeoutHandler = - mock(PictureCaptureRequest.TimeoutHandler.class); - - mockTimeoutFactory.when(TimeoutHandler::create).thenReturn(mockTimeoutHandler); - - PictureCaptureRequest req = PictureCaptureRequest.create(null, null, null); - req.setState(PictureCaptureRequestState.STATE_WAITING_FOCUS); - req.setState(PictureCaptureRequestState.STATE_WAITING_PRECAPTURE_START); - req.setState(PictureCaptureRequestState.STATE_WAITING_PRECAPTURE_DONE); - req.setState(PictureCaptureRequestState.STATE_CAPTURING); - verify(mockTimeoutHandler, times(4)).resetTimeout(any()); - verify(mockTimeoutHandler, never()).clearTimeout(any()); - } - } - - @Test - public void setState_clears_timeout() { - try (MockedStatic mockTimeoutFactory = mockStatic(TimeoutHandler.class)) { - PictureCaptureRequest.TimeoutHandler mockTimeoutHandler = - mock(PictureCaptureRequest.TimeoutHandler.class); - - mockTimeoutFactory.when(TimeoutHandler::create).thenReturn(mockTimeoutHandler); - - PictureCaptureRequest req = PictureCaptureRequest.create(null, null, null); - req.setState(PictureCaptureRequestState.STATE_IDLE); - req.setState(PictureCaptureRequestState.STATE_FINISHED); - req = PictureCaptureRequest.create(null, null, null); - req.setState(PictureCaptureRequestState.STATE_ERROR); - verify(mockTimeoutHandler, never()).resetTimeout(any()); - verify(mockTimeoutHandler, times(3)).clearTimeout(any()); - } - } - - @Test - public void finish_sets_result_and_state() { - // Setup - MethodChannel.Result mockResult = mock(MethodChannel.Result.class); - PictureCaptureRequest req = PictureCaptureRequest.create(mockResult, null, null); - // Act - req.finish("/test/path"); - // Test - verify(mockResult).success("/test/path"); - assertEquals("State is finished", req.getState(), PictureCaptureRequestState.STATE_FINISHED); - } - - @Test - public void finish_clears_timeout() { - try (MockedStatic mockTimeoutFactory = mockStatic(TimeoutHandler.class)) { - PictureCaptureRequest.TimeoutHandler mockTimeoutHandler = - mock(PictureCaptureRequest.TimeoutHandler.class); - - mockTimeoutFactory.when(TimeoutHandler::create).thenReturn(mockTimeoutHandler); - - MethodChannel.Result mockResult = mock(MethodChannel.Result.class); - PictureCaptureRequest req = PictureCaptureRequest.create(mockResult, null, null); - req.finish("/test/path"); - verify(mockTimeoutHandler, never()).resetTimeout(any()); - verify(mockTimeoutHandler).clearTimeout(any()); - } - } - - @Test - public void isFinished_is_true_When_state_is_finished_or_error() { - // Setup - PictureCaptureRequest req = PictureCaptureRequest.create(null, null, null); - // Test false states - req.setState(PictureCaptureRequestState.STATE_IDLE); - assertFalse(req.isFinished()); - req.setState(PictureCaptureRequestState.STATE_WAITING_PRECAPTURE_START); - assertFalse(req.isFinished()); - req.setState(PictureCaptureRequestState.STATE_WAITING_PRECAPTURE_DONE); - assertFalse(req.isFinished()); - // Test true states - req.setState(PictureCaptureRequestState.STATE_FINISHED); - assertTrue(req.isFinished()); - req = PictureCaptureRequest.create(null, null, null); // Refresh - req.setState(PictureCaptureRequestState.STATE_ERROR); - assertTrue(req.isFinished()); - } - - @Test - public void finish_returns_When_in_error_state() { - MethodChannel.Result mockResult = mock(MethodChannel.Result.class); - PictureCaptureRequest req = PictureCaptureRequest.create(mockResult, null, null); - - // Make sure state is set to error - req.setState(PictureCaptureRequestState.STATE_ERROR); - - req.finish("/test/path"); - - assertEquals("State is error", req.getState(), PictureCaptureRequestState.STATE_ERROR); - verify(mockResult, never()).success(any()); - } - - @Test(expected = IllegalStateException.class) - public void finish_throws_When_already_finished() { - // Setup - PictureCaptureRequest req = PictureCaptureRequest.create(null, null, null); - req.setState(PictureCaptureRequestState.STATE_FINISHED); - // Act - req.finish("/test/path"); - } - - @Test - public void error_sets_result_and_state() { - // Setup - MethodChannel.Result mockResult = mock(MethodChannel.Result.class); - PictureCaptureRequest req = PictureCaptureRequest.create(mockResult, null, null); - // Act - req.error("ERROR_CODE", "Error Message", null); - // Test - verify(mockResult).error("ERROR_CODE", "Error Message", null); - assertEquals("State is error", req.getState(), PictureCaptureRequestState.STATE_ERROR); - } - - @Test - public void error_clears_timeout() { - try (MockedStatic mockTimeoutFactory = mockStatic(TimeoutHandler.class)) { - PictureCaptureRequest.TimeoutHandler mockTimeoutHandler = - mock(PictureCaptureRequest.TimeoutHandler.class); - - mockTimeoutFactory.when(TimeoutHandler::create).thenReturn(mockTimeoutHandler); - - MethodChannel.Result mockResult = mock(MethodChannel.Result.class); - PictureCaptureRequest req = PictureCaptureRequest.create(mockResult, null, null); - req.error("ERROR_CODE", "Error Message", null); - verify(mockTimeoutHandler, never()).resetTimeout(any()); - verify(mockTimeoutHandler).clearTimeout(any()); - } - } - - @Test - public void error_returns_When_in_error_state() { - MethodChannel.Result mockResult = mock(MethodChannel.Result.class); - PictureCaptureRequest req = PictureCaptureRequest.create(mockResult, null, null); - - // Make sure state is set to error - req.setState(PictureCaptureRequestState.STATE_ERROR); - - req.error("ERROR_CODE", "Error Message", null); - - assertEquals("State is error", req.getState(), PictureCaptureRequestState.STATE_ERROR); - verify(mockResult, never()).error(any(), any(), any()); - } - - @Test(expected = IllegalStateException.class) - public void error_throws_When_already_finished() { - // Setup - PictureCaptureRequest req = PictureCaptureRequest.create(null, null, null); - req.setState(PictureCaptureRequestState.STATE_FINISHED); - // Act - req.error(null, null, null); - } -} diff --git a/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/media/MediaRecorderBuilderTest.java b/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/media/MediaRecorderBuilderTest.java index 401a337fa43f..81cb0ba7d42e 100644 --- a/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/media/MediaRecorderBuilderTest.java +++ b/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/media/MediaRecorderBuilderTest.java @@ -30,7 +30,7 @@ public void build_Should_set_values_in_correct_order_When_audio_is_disabled() th CamcorderProfile recorderProfile = getEmptyCamcorderProfile(); String outputFilePath = "mock_video_file_path"; int mediaOrientation = 1; - MediaRecorder recorder = null; + MediaRecorder recorder; try (MockedStatic mockMediaRecorderFactory = mockStatic(MediaRecorderFactory.class)) { @@ -60,10 +60,8 @@ public void build_Should_set_values_in_correct_order_When_audio_is_disabled() th @Test public void build_Should_set_values_in_correct_order_When_audio_is_enabled() throws IOException { - MediaRecorder recorder = null; + MediaRecorder recorder; CamcorderProfile recorderProfile = getEmptyCamcorderProfile(); - MediaRecorderBuilder.MediaRecorderFactory mockFactory = - mock(MediaRecorderBuilder.MediaRecorderFactory.class); String outputFilePath = "mock_video_file_path"; int mediaOrientation = 1; diff --git a/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/types/ExposureModeTest.java b/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/types/ExposureModeTest.java index 63810f0b5684..dddbdf9d9027 100644 --- a/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/types/ExposureModeTest.java +++ b/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/types/ExposureModeTest.java @@ -6,6 +6,7 @@ import static org.junit.Assert.assertEquals; +import io.flutter.plugins.camera.features.exposurelock.ExposureMode; import org.junit.Test; public class ExposureModeTest { diff --git a/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/types/FlashModeTest.java b/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/types/FlashModeTest.java index 1f5f0c6272ed..4b146e81e041 100644 --- a/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/types/FlashModeTest.java +++ b/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/types/FlashModeTest.java @@ -6,6 +6,7 @@ import static org.junit.Assert.assertEquals; +import io.flutter.plugins.camera.features.flash.FlashMode; import org.junit.Test; public class FlashModeTest { diff --git a/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/types/FocusModeTest.java b/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/types/FocusModeTest.java index 4aa6fadf776b..13119589c673 100644 --- a/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/types/FocusModeTest.java +++ b/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/types/FocusModeTest.java @@ -6,6 +6,7 @@ import static org.junit.Assert.assertEquals; +import io.flutter.plugins.camera.features.autofocus.FocusMode; import org.junit.Test; public class FocusModeTest { From 109ae75b703fd979da57d0c08ff853b18c99c195 Mon Sep 17 00:00:00 2001 From: Maurits van Beusekom Date: Mon, 8 Mar 2021 15:07:50 +0100 Subject: [PATCH 077/114] Added test coverage to auto focus feature --- .../features/autofocus/AutoFocusFeature.java | 2 +- .../autofocus/AutoFocusFeatureTest.java | 163 ++++++++++++++++++ 2 files changed, 164 insertions(+), 1 deletion(-) create mode 100644 packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/features/autofocus/AutoFocusFeatureTest.java diff --git a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/autofocus/AutoFocusFeature.java b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/autofocus/AutoFocusFeature.java index f3cbaca8f5d6..afb9801e312b 100644 --- a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/autofocus/AutoFocusFeature.java +++ b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/autofocus/AutoFocusFeature.java @@ -24,7 +24,7 @@ public AutoFocusFeature(CameraProperties cameraProperties, boolean recordingVide @Override public String getDebugName() { - return "AutoFocus"; + return "AutoFocusFeature"; } @Override diff --git a/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/features/autofocus/AutoFocusFeatureTest.java b/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/features/autofocus/AutoFocusFeatureTest.java new file mode 100644 index 000000000000..88461bc20ff3 --- /dev/null +++ b/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/features/autofocus/AutoFocusFeatureTest.java @@ -0,0 +1,163 @@ +package io.flutter.plugins.camera.features.autofocus; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import android.hardware.camera2.CameraCharacteristics; +import android.hardware.camera2.CaptureRequest; +import io.flutter.plugins.camera.CameraProperties; +import org.junit.Test; + +public class AutoFocusFeatureTest { + private static final int[] FOCUS_MODES_ONLY_OFF = new int[] { CameraCharacteristics.CONTROL_AF_MODE_OFF }; + private static final int[] FOCUS_MODES = new int[] { CameraCharacteristics.CONTROL_AF_MODE_OFF, CameraCharacteristics.CONTROL_AF_MODE_AUTO }; + + @Test + public void getDebugName_should_return_the_name_of_the_feature() { + CameraProperties mockCameraProperties = mock(CameraProperties.class); + AutoFocusFeature autoFocusFeature = new AutoFocusFeature(mockCameraProperties, false); + + assertEquals("AutoFocusFeature", autoFocusFeature.getDebugName()); + } + + @Test + public void getValue_should_return_auto_if_not_set() { + CameraProperties mockCameraProperties = mock(CameraProperties.class); + AutoFocusFeature autoFocusFeature = new AutoFocusFeature(mockCameraProperties, false); + + assertEquals(FocusMode.auto, autoFocusFeature.getValue()); + } + + @Test + public void getValue_should_echo_the_set_value() { + CameraProperties mockCameraProperties = mock(CameraProperties.class); + AutoFocusFeature autoFocusFeature = new AutoFocusFeature(mockCameraProperties, false); + FocusMode expectedValue = FocusMode.locked; + + autoFocusFeature.setValue(expectedValue); + FocusMode actualValue = autoFocusFeature.getValue(); + + assertEquals(expectedValue, actualValue); + } + + @Test + public void checkIsSupported_should_return_false_when_minimum_focus_distance_is_zero() { + CameraProperties mockCameraProperties = mock(CameraProperties.class); + AutoFocusFeature autoFocusFeature = new AutoFocusFeature(mockCameraProperties, false); + + when(mockCameraProperties.getControlAutoFocusAvailableModes()).thenReturn(FOCUS_MODES); + when(mockCameraProperties.getLensInfoMinimumFocusDistance()).thenReturn(0.0F); + + assertFalse(autoFocusFeature.checkIsSupported()); + } + + @Test + public void checkIsSupported_should_return_false_when_minimum_focus_distance_is_null() { + CameraProperties mockCameraProperties = mock(CameraProperties.class); + AutoFocusFeature autoFocusFeature = new AutoFocusFeature(mockCameraProperties, false); + + when(mockCameraProperties.getControlAutoFocusAvailableModes()).thenReturn(FOCUS_MODES); + when(mockCameraProperties.getLensInfoMinimumFocusDistance()).thenReturn(null); + + assertFalse(autoFocusFeature.checkIsSupported()); + } + + @Test + public void checkIsSupport_should_return_false_when_no_focus_modes_are_available() { + CameraProperties mockCameraProperties = mock(CameraProperties.class); + AutoFocusFeature autoFocusFeature = new AutoFocusFeature(mockCameraProperties, false); + + when(mockCameraProperties.getControlAutoFocusAvailableModes()).thenReturn(new int[] {}); + when(mockCameraProperties.getLensInfoMinimumFocusDistance()).thenReturn(1.0F); + + assertFalse(autoFocusFeature.checkIsSupported()); + } + + @Test + public void checkIsSupport_should_return_false_when_only_focus_off_is_available() { + CameraProperties mockCameraProperties = mock(CameraProperties.class); + AutoFocusFeature autoFocusFeature = new AutoFocusFeature(mockCameraProperties, false); + + when(mockCameraProperties.getControlAutoFocusAvailableModes()).thenReturn(FOCUS_MODES_ONLY_OFF); + when(mockCameraProperties.getLensInfoMinimumFocusDistance()).thenReturn(1.0F); + + assertFalse(autoFocusFeature.checkIsSupported()); + } + + @Test + public void checkIsSupport_should_return_true_when_only_multiple_focus_modes_are_available() { + CameraProperties mockCameraProperties = mock(CameraProperties.class); + AutoFocusFeature autoFocusFeature = new AutoFocusFeature(mockCameraProperties, false); + + when(mockCameraProperties.getControlAutoFocusAvailableModes()).thenReturn(FOCUS_MODES); + when(mockCameraProperties.getLensInfoMinimumFocusDistance()).thenReturn(1.0F); + + assertTrue(autoFocusFeature.checkIsSupported()); + } + + @Test + public void updateBuilder_should_return_when_checkIsSupported_is_false() { + CameraProperties mockCameraProperties = mock(CameraProperties.class); + CaptureRequest.Builder mockBuilder = mock(CaptureRequest.Builder.class); + AutoFocusFeature autoFocusFeature = new AutoFocusFeature(mockCameraProperties, false); + + when(mockCameraProperties.getControlAutoFocusAvailableModes()).thenReturn(FOCUS_MODES); + when(mockCameraProperties.getLensInfoMinimumFocusDistance()).thenReturn(0.0F); + + autoFocusFeature.updateBuilder(mockBuilder); + + verify(mockBuilder, never()).set(any(), any()); + } + + @Test + public void updateBuilder_should_set_control_mode_to_auto_when_focus_is_locked() { + CameraProperties mockCameraProperties = mock(CameraProperties.class); + CaptureRequest.Builder mockBuilder = mock(CaptureRequest.Builder.class); + AutoFocusFeature autoFocusFeature = new AutoFocusFeature(mockCameraProperties, false); + + when(mockCameraProperties.getControlAutoFocusAvailableModes()).thenReturn(FOCUS_MODES); + when(mockCameraProperties.getLensInfoMinimumFocusDistance()).thenReturn(1.0F); + + autoFocusFeature.setValue(FocusMode.locked); + autoFocusFeature.updateBuilder(mockBuilder); + + verify(mockBuilder, times(1)).set(CaptureRequest.CONTROL_AF_MODE, CaptureRequest.CONTROL_AF_MODE_AUTO); + } + + @Test + public void updateBuilder_should_set_control_mode_to_continuous_video_when_focus_is_auto_and_recording_video() { + CameraProperties mockCameraProperties = mock(CameraProperties.class); + CaptureRequest.Builder mockBuilder = mock(CaptureRequest.Builder.class); + AutoFocusFeature autoFocusFeature = new AutoFocusFeature(mockCameraProperties, true); + + when(mockCameraProperties.getControlAutoFocusAvailableModes()).thenReturn(FOCUS_MODES); + when(mockCameraProperties.getLensInfoMinimumFocusDistance()).thenReturn(1.0F); + + autoFocusFeature.setValue(FocusMode.auto); + autoFocusFeature.updateBuilder(mockBuilder); + + verify(mockBuilder, times(1)).set(CaptureRequest.CONTROL_AF_MODE, CaptureRequest.CONTROL_AF_MODE_CONTINUOUS_VIDEO); + } + + @Test + public void updateBuilder_should_set_control_mode_to_continuous_video_when_focus_is_auto_and_not_recording_video() { + CameraProperties mockCameraProperties = mock(CameraProperties.class); + CaptureRequest.Builder mockBuilder = mock(CaptureRequest.Builder.class); + AutoFocusFeature autoFocusFeature = new AutoFocusFeature(mockCameraProperties, false); + + when(mockCameraProperties.getControlAutoFocusAvailableModes()).thenReturn(FOCUS_MODES); + when(mockCameraProperties.getLensInfoMinimumFocusDistance()).thenReturn(1.0F); + + autoFocusFeature.setValue(FocusMode.auto); + autoFocusFeature.updateBuilder(mockBuilder); + + verify(mockBuilder, times(1)).set(CaptureRequest.CONTROL_AF_MODE, CaptureRequest.CONTROL_AF_MODE_CONTINUOUS_PICTURE); + } +} From 826c7ed3b4dfb13c21eebdf1e32c1a03d1852f2a Mon Sep 17 00:00:00 2001 From: Maurits van Beusekom Date: Mon, 8 Mar 2021 16:03:30 +0100 Subject: [PATCH 078/114] Added test coverage to exposure lock feature --- .../exposurelock/ExposureLockFeature.java | 2 +- .../autofocus}/FocusModeTest.java | 3 +- .../exposurelock/ExposureLockFeatureTest.java | 75 +++++++++++++++++++ .../exposurelock}/ExposureModeTest.java | 3 +- 4 files changed, 78 insertions(+), 5 deletions(-) rename packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/{types => features/autofocus}/FocusModeTest.java (90%) create mode 100644 packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/features/exposurelock/ExposureLockFeatureTest.java rename packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/{types => features/exposurelock}/ExposureModeTest.java (90%) diff --git a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/exposurelock/ExposureLockFeature.java b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/exposurelock/ExposureLockFeature.java index 00bfabda8a03..0c918c12154d 100644 --- a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/exposurelock/ExposureLockFeature.java +++ b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/exposurelock/ExposureLockFeature.java @@ -21,7 +21,7 @@ public ExposureLockFeature(CameraProperties cameraProperties) { @Override public String getDebugName() { - return "ExposureLock"; + return "ExposureLockFeature"; } @Override diff --git a/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/types/FocusModeTest.java b/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/features/autofocus/FocusModeTest.java similarity index 90% rename from packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/types/FocusModeTest.java rename to packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/features/autofocus/FocusModeTest.java index 13119589c673..c959bba62cc7 100644 --- a/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/types/FocusModeTest.java +++ b/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/features/autofocus/FocusModeTest.java @@ -2,11 +2,10 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -package io.flutter.plugins.camera.types; +package io.flutter.plugins.camera.features.autofocus; import static org.junit.Assert.assertEquals; -import io.flutter.plugins.camera.features.autofocus.FocusMode; import org.junit.Test; public class FocusModeTest { diff --git a/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/features/exposurelock/ExposureLockFeatureTest.java b/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/features/exposurelock/ExposureLockFeatureTest.java new file mode 100644 index 000000000000..06e19f275acd --- /dev/null +++ b/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/features/exposurelock/ExposureLockFeatureTest.java @@ -0,0 +1,75 @@ +package io.flutter.plugins.camera.features.exposurelock; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; + +import android.hardware.camera2.CaptureRequest; +import io.flutter.plugins.camera.CameraProperties; +import org.junit.Test; + +public class ExposureLockFeatureTest { + @Test + public void getDebugName_should_return_the_name_of_the_feature() { + CameraProperties mockCameraProperties = mock(CameraProperties.class); + ExposureLockFeature exposureLockFeature = new ExposureLockFeature(mockCameraProperties); + + assertEquals("ExposureLockFeature", exposureLockFeature.getDebugName()); + } + + @Test + public void getValue_should_return_auto_if_not_set() { + CameraProperties mockCameraProperties = mock(CameraProperties.class); + ExposureLockFeature exposureLockFeature = new ExposureLockFeature(mockCameraProperties); + + assertEquals(ExposureMode.auto, exposureLockFeature.getValue()); + } + + @Test + public void getValue_should_echo_the_set_value() { + CameraProperties mockCameraProperties = mock(CameraProperties.class); + ExposureLockFeature exposureLockFeature = new ExposureLockFeature(mockCameraProperties); + ExposureMode expectedValue = ExposureMode.locked; + + exposureLockFeature.setValue(expectedValue); + ExposureMode actualValue = exposureLockFeature.getValue(); + + assertEquals(expectedValue, actualValue); + } + + @Test + public void checkIsSupported_should_return_true() { + CameraProperties mockCameraProperties = mock(CameraProperties.class); + ExposureLockFeature exposureLockFeature = new ExposureLockFeature(mockCameraProperties); + + assertTrue(exposureLockFeature.checkIsSupported()); + } + + @Test + public void updateBuilder_should_set_control_ae_lock_to_false_when_auto_exposure_is_set_to_auto() { + CameraProperties mockCameraProperties = mock(CameraProperties.class); + CaptureRequest.Builder mockBuilder = mock(CaptureRequest.Builder.class); + ExposureLockFeature exposureLockFeature = new ExposureLockFeature(mockCameraProperties); + + + exposureLockFeature.setValue(ExposureMode.auto); + exposureLockFeature.updateBuilder(mockBuilder); + + verify(mockBuilder, times(1)).set(CaptureRequest.CONTROL_AE_LOCK, false); + } + + @Test + public void updateBuilder_should_set_control_ae_lock_to_false_when_auto_exposure_is_set_to_locked() { + CameraProperties mockCameraProperties = mock(CameraProperties.class); + CaptureRequest.Builder mockBuilder = mock(CaptureRequest.Builder.class); + ExposureLockFeature exposureLockFeature = new ExposureLockFeature(mockCameraProperties); + + + exposureLockFeature.setValue(ExposureMode.locked); + exposureLockFeature.updateBuilder(mockBuilder); + + verify(mockBuilder, times(1)).set(CaptureRequest.CONTROL_AE_LOCK, true); + } +} diff --git a/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/types/ExposureModeTest.java b/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/features/exposurelock/ExposureModeTest.java similarity index 90% rename from packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/types/ExposureModeTest.java rename to packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/features/exposurelock/ExposureModeTest.java index dddbdf9d9027..fb36601a4a4c 100644 --- a/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/types/ExposureModeTest.java +++ b/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/features/exposurelock/ExposureModeTest.java @@ -2,11 +2,10 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -package io.flutter.plugins.camera.types; +package io.flutter.plugins.camera.features.exposurelock; import static org.junit.Assert.assertEquals; -import io.flutter.plugins.camera.features.exposurelock.ExposureMode; import org.junit.Test; public class ExposureModeTest { From 5ea549e6472c315f50e4d50f2aa7d4e8895a9db4 Mon Sep 17 00:00:00 2001 From: Maurits van Beusekom Date: Tue, 9 Mar 2021 08:41:22 +0100 Subject: [PATCH 079/114] Added test coverage to exposure offset feature --- .../plugins/camera/CameraProperties.java | 8 +- .../exposureoffset/ExposureOffsetFeature.java | 8 +- .../ExposureOffsetFeatureTest.java | 85 +++++++++++++++++++ 3 files changed, 92 insertions(+), 9 deletions(-) create mode 100644 packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/features/exposureoffset/ExposureOffsetFeatureTest.java diff --git a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/CameraProperties.java b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/CameraProperties.java index 35b676e3235c..03105de2d95e 100644 --- a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/CameraProperties.java +++ b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/CameraProperties.java @@ -21,7 +21,7 @@ public interface CameraProperties { Range getControlAutoExposureCompensationRange(); - Rational getControlAutoExposureCompensationStep(); + double getControlAutoExposureCompensationStep(); int[] getControlAutoFocusAvailableModes(); @@ -78,8 +78,10 @@ public Range getControlAutoExposureCompensationRange() { } @Override - public Rational getControlAutoExposureCompensationStep() { - return cameraCharacteristics.get(CameraCharacteristics.CONTROL_AE_COMPENSATION_STEP); + public double getControlAutoExposureCompensationStep() { + Rational rational = cameraCharacteristics.get(CameraCharacteristics.CONTROL_AE_COMPENSATION_STEP); + + return rational == null ? 0.0 : rational.doubleValue(); } @Override diff --git a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/exposureoffset/ExposureOffsetFeature.java b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/exposureoffset/ExposureOffsetFeature.java index aba9dfb18ac0..a78e180fb14e 100644 --- a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/exposureoffset/ExposureOffsetFeature.java +++ b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/exposureoffset/ExposureOffsetFeature.java @@ -7,7 +7,6 @@ import android.hardware.camera2.CaptureRequest; import android.util.Log; import android.util.Range; -import android.util.Rational; import io.flutter.plugins.camera.CameraProperties; import io.flutter.plugins.camera.features.CameraFeature; @@ -28,9 +27,7 @@ public ExposureOffsetFeature(CameraProperties cameraProperties) { } @Override - public String getDebugName() { - return "ExposureOffset"; - } + public String getDebugName() { return "ExposureOffsetFeature"; } @Override public ExposureOffsetValue getValue() { @@ -94,7 +91,6 @@ private double getMaxExposureOffset() { * @return */ public double getExposureOffsetStepSize() { - Rational stepSize = cameraProperties.getControlAutoExposureCompensationStep(); - return stepSize == null ? 0.0 : stepSize.doubleValue(); + return cameraProperties.getControlAutoExposureCompensationStep(); } } diff --git a/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/features/exposureoffset/ExposureOffsetFeatureTest.java b/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/features/exposureoffset/ExposureOffsetFeatureTest.java new file mode 100644 index 000000000000..d822d8ec0d18 --- /dev/null +++ b/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/features/exposureoffset/ExposureOffsetFeatureTest.java @@ -0,0 +1,85 @@ +package io.flutter.plugins.camera.features.exposureoffset; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import android.hardware.camera2.CaptureRequest; +import io.flutter.plugins.camera.CameraProperties; +import org.junit.Test; + +public class ExposureOffsetFeatureTest { + private static final ExposureOffsetValue DEFAULT_EXPOSURE_OFFSET_VALUE = new ExposureOffsetValue(0, 0, 0); + + @Test + public void getDebugName_should_return_the_name_of_the_feature() { + CameraProperties mockCameraProperties = mock(CameraProperties.class); + ExposureOffsetFeature exposureOffsetFeature = new ExposureOffsetFeature(mockCameraProperties); + + assertEquals("ExposureOffsetFeature", exposureOffsetFeature.getDebugName()); + } + + @Test + public void getValue_should_return_auto_if_not_set() { + CameraProperties mockCameraProperties = mock(CameraProperties.class); + ExposureOffsetFeature exposureOffsetFeature = new ExposureOffsetFeature(mockCameraProperties); + + final ExposureOffsetValue actualValue = exposureOffsetFeature.getValue(); + + assertEquals(DEFAULT_EXPOSURE_OFFSET_VALUE.min, actualValue.min, 0); + assertEquals(DEFAULT_EXPOSURE_OFFSET_VALUE.max, actualValue.max, 0); + assertEquals(DEFAULT_EXPOSURE_OFFSET_VALUE.value, actualValue.value, 0); + } + + @Test + public void getValue_should_echo_the_set_value() { + CameraProperties mockCameraProperties = mock(CameraProperties.class); + ExposureOffsetFeature exposureOffsetFeature = new ExposureOffsetFeature(mockCameraProperties); + ExposureOffsetValue expectedValue = new ExposureOffsetValue(1, 2, 4); + + when(mockCameraProperties.getControlAutoExposureCompensationStep()).thenReturn(0.5); + + exposureOffsetFeature.setValue(expectedValue); + ExposureOffsetValue actualValue = exposureOffsetFeature.getValue(); + + assertEquals(expectedValue.min, actualValue.min, 1); + assertEquals(expectedValue.max, actualValue.max, 2); + assertEquals(expectedValue.value, actualValue.value, 8); + } + + @Test + public void getExposureOffsetStepSize_should_return_the_control_exposure_compensation_step_value() { + CameraProperties mockCameraProperties = mock(CameraProperties.class); + ExposureOffsetFeature exposureOffsetFeature = new ExposureOffsetFeature(mockCameraProperties); + + when(mockCameraProperties.getControlAutoExposureCompensationStep()).thenReturn(0.5); + + assertEquals(0.5, exposureOffsetFeature.getExposureOffsetStepSize(), 0); + } + + @Test + public void checkIsSupported_should_return_true() { + CameraProperties mockCameraProperties = mock(CameraProperties.class); + ExposureOffsetFeature exposureOffsetFeature = new ExposureOffsetFeature(mockCameraProperties); + + assertTrue(exposureOffsetFeature.checkIsSupported()); + } + + @Test + public void updateBuilder_should_set_control_ae_exposure_compensation_to_offset() { + CameraProperties mockCameraProperties = mock(CameraProperties.class); + CaptureRequest.Builder mockBuilder = mock(CaptureRequest.Builder.class); + ExposureOffsetFeature exposureOffsetFeature = new ExposureOffsetFeature(mockCameraProperties); + ExposureOffsetValue expectedValue = new ExposureOffsetValue(1, 2, 4); + + when(mockCameraProperties.getControlAutoExposureCompensationStep()).thenReturn(0.5); + + exposureOffsetFeature.setValue(expectedValue); + exposureOffsetFeature.updateBuilder(mockBuilder); + + verify(mockBuilder, times(1)).set(CaptureRequest.CONTROL_AE_EXPOSURE_COMPENSATION, 8); + } +} From 36b724939115243cbf34d20367a9da8f53cb142a Mon Sep 17 00:00:00 2001 From: Maurits van Beusekom Date: Tue, 9 Mar 2021 10:18:13 +0100 Subject: [PATCH 080/114] Added test coverage to exposure point feature --- .../exposurepoint/ExposurePointFeature.java | 12 +- .../ExposurePointFeatureTest.java | 177 ++++++++++++++++++ 2 files changed, 183 insertions(+), 6 deletions(-) create mode 100644 packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/features/exposurepoint/ExposurePointFeatureTest.java diff --git a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/exposurepoint/ExposurePointFeature.java b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/exposurepoint/ExposurePointFeature.java index d62b6d3b4690..0307b8883de0 100644 --- a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/exposurepoint/ExposurePointFeature.java +++ b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/exposurepoint/ExposurePointFeature.java @@ -7,6 +7,7 @@ import android.hardware.camera2.CaptureRequest; import android.hardware.camera2.params.MeteringRectangle; import android.util.Log; +import androidx.annotation.NonNull; import io.flutter.plugins.camera.CameraProperties; import io.flutter.plugins.camera.features.CameraFeature; import io.flutter.plugins.camera.features.Point; @@ -26,7 +27,7 @@ public ExposurePointFeature(CameraProperties cameraProperties, Callable 0; - return supported; + return supportedRegions != null && supportedRegions > 0; } @Override @@ -65,14 +65,14 @@ public void updateBuilder(CaptureRequest.Builder requestBuilder) { Log.i("Camera", "updateExposurePoint | currentSetting: " + currentSetting); - MeteringRectangle aeRect = null; + MeteringRectangle aeRect; try { aeRect = getCameraRegions.call().getAEMeteringRectangle(); requestBuilder.set( CaptureRequest.CONTROL_AE_REGIONS, aeRect == null ? null - : new MeteringRectangle[] {getCameraRegions.call().getAEMeteringRectangle()}); + : new MeteringRectangle[] { aeRect }); } catch (Exception e) { e.printStackTrace(); } diff --git a/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/features/exposurepoint/ExposurePointFeatureTest.java b/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/features/exposurepoint/ExposurePointFeatureTest.java new file mode 100644 index 000000000000..7455741e00b0 --- /dev/null +++ b/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/features/exposurepoint/ExposurePointFeatureTest.java @@ -0,0 +1,177 @@ +package io.flutter.plugins.camera.features.exposurepoint; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import android.hardware.camera2.CaptureRequest; +import android.hardware.camera2.params.MeteringRectangle; +import io.flutter.plugins.camera.CameraProperties; +import io.flutter.plugins.camera.features.Point; +import io.flutter.plugins.camera.features.regionboundaries.CameraRegions; +import org.junit.Test; + +public class ExposurePointFeatureTest { + @Test + public void getDebugName_should_return_the_name_of_the_feature() { + CameraProperties mockCameraProperties = mock(CameraProperties.class); + ExposurePointFeature exposurePointFeature = new ExposurePointFeature(mockCameraProperties, () -> null); + + assertEquals("ExposurePointFeature", exposurePointFeature.getDebugName()); + } + + @Test + public void getValue_should_return_default_point_if_not_set() { + CameraProperties mockCameraProperties = mock(CameraProperties.class); + ExposurePointFeature exposurePointFeature = new ExposurePointFeature(mockCameraProperties, () -> null); + Point expectedPoint = new Point(0.0, 0.0); + Point actualPoint = exposurePointFeature.getValue(); + + assertEquals(expectedPoint.x, actualPoint.x); + assertEquals(expectedPoint.y, actualPoint.y); + } + + @Test + public void getValue_should_echo_the_set_value() { + CameraProperties mockCameraProperties = mock(CameraProperties.class); + CameraRegions mockCameraRegions = mock(CameraRegions.class); + ExposurePointFeature exposurePointFeature = new ExposurePointFeature(mockCameraProperties, () -> mockCameraRegions); + Point expectedPoint = new Point(0.0, 0.0); + + exposurePointFeature.setValue(expectedPoint); + Point actualPoint = exposurePointFeature.getValue(); + + assertEquals(expectedPoint, actualPoint); + } + + @Test + public void setValue_should_reset_point_when_x_coord_is_null() { + CameraProperties mockCameraProperties = mock(CameraProperties.class); + CameraRegions mockCameraRegions = mock(CameraRegions.class); + ExposurePointFeature exposurePointFeature = new ExposurePointFeature(mockCameraProperties, () -> mockCameraRegions); + + exposurePointFeature.setValue(new Point(null, 0.0)); + + verify(mockCameraRegions, times(1)).resetAutoExposureMeteringRectangle(); + } + + @Test + public void setValue_should_reset_point_when_y_coord_is_null() { + CameraProperties mockCameraProperties = mock(CameraProperties.class); + CameraRegions mockCameraRegions = mock(CameraRegions.class); + ExposurePointFeature exposurePointFeature = new ExposurePointFeature(mockCameraProperties, () -> mockCameraRegions); + + exposurePointFeature.setValue(new Point(0.0, null)); + + verify(mockCameraRegions, times(1)).resetAutoExposureMeteringRectangle(); + } + + @Test + public void setValue_should_reset_point_when_valid_coords_are_supplied() { + CameraProperties mockCameraProperties = mock(CameraProperties.class); + CameraRegions mockCameraRegions = mock(CameraRegions.class); + ExposurePointFeature exposurePointFeature = new ExposurePointFeature(mockCameraProperties, () -> mockCameraRegions); + Point point = new Point(0.0, 0.0); + + exposurePointFeature.setValue(point); + + verify(mockCameraRegions, times(1)).setAutoExposureMeteringRectangleFromPoint(point.x, point.y); + } + + @Test + public void checkIsSupported_should_return_false_when_max_regions_is_null() { + CameraProperties mockCameraProperties = mock(CameraProperties.class); + ExposurePointFeature exposurePointFeature = new ExposurePointFeature(mockCameraProperties, () -> null); + + when(mockCameraProperties.getControlMaxRegionsAutoExposure()).thenReturn(null); + + assertFalse(exposurePointFeature.checkIsSupported()); + } + + @Test + public void checkIsSupported_should_return_false_when_max_regions_is_zero() { + CameraProperties mockCameraProperties = mock(CameraProperties.class); + ExposurePointFeature exposurePointFeature = new ExposurePointFeature(mockCameraProperties, () -> null); + + when(mockCameraProperties.getControlMaxRegionsAutoExposure()).thenReturn(0); + + assertFalse(exposurePointFeature.checkIsSupported()); + } + + @Test + public void checkIsSupported_should_return_true_when_max_regions_is_bigger_then_zero() { + CameraProperties mockCameraProperties = mock(CameraProperties.class); + ExposurePointFeature exposurePointFeature = new ExposurePointFeature(mockCameraProperties, () -> null); + + when(mockCameraProperties.getControlMaxRegionsAutoExposure()).thenReturn(1); + + assertTrue(exposurePointFeature.checkIsSupported()); + } + + @Test + public void updateBuilder_should_return_when_checkIsSupported_is_false() { + CameraProperties mockCameraProperties = mock(CameraProperties.class); + CameraRegions mockCameraRegions = mock(CameraRegions.class); + ExposurePointFeature exposurePointFeature = new ExposurePointFeature(mockCameraProperties, () -> mockCameraRegions); + + when(mockCameraProperties.getControlMaxRegionsAutoExposure()).thenReturn(0); + + exposurePointFeature.updateBuilder(null); + + verify(mockCameraRegions, never()).getAEMeteringRectangle(); + } + + @Test + public void updateBuilder_should_set_ae_regions_to_null_when_ae_metering_rectangle_is_null() { + CameraProperties mockCameraProperties = mock(CameraProperties.class); + CameraRegions mockCameraRegions = mock(CameraRegions.class); + CaptureRequest.Builder mockBuilder = mock(CaptureRequest.Builder.class); + ExposurePointFeature exposurePointFeature = new ExposurePointFeature(mockCameraProperties, () -> mockCameraRegions); + + when(mockCameraProperties.getControlMaxRegionsAutoExposure()).thenReturn(1); + when(mockCameraRegions.getAEMeteringRectangle()).thenReturn(null); + + exposurePointFeature.updateBuilder(mockBuilder); + + verify(mockBuilder, times(1)).set(CaptureRequest.CONTROL_AE_REGIONS, null); + } + + @Test + public void updateBuilder_should_set_ae_regions_with_metering_rectangle() { + CameraProperties mockCameraProperties = mock(CameraProperties.class); + CameraRegions mockCameraRegions = mock(CameraRegions.class); + CaptureRequest.Builder mockBuilder = mock(CaptureRequest.Builder.class); + ExposurePointFeature exposurePointFeature = new ExposurePointFeature(mockCameraProperties, () -> mockCameraRegions); + MeteringRectangle meteringRectangle = new MeteringRectangle(0,0,0,0,0); + + when(mockCameraProperties.getControlMaxRegionsAutoExposure()).thenReturn(1); + when(mockCameraRegions.getAEMeteringRectangle()).thenReturn(meteringRectangle); + + exposurePointFeature.updateBuilder(mockBuilder); + + verify(mockBuilder, times(1)).set(eq(CaptureRequest.CONTROL_AE_REGIONS), any(MeteringRectangle[].class)); + } + + @Test + public void updateBuilder_should_silently_fail_when_exception_occurs() { + CameraProperties mockCameraProperties = mock(CameraProperties.class); + CameraRegions mockCameraRegions = mock(CameraRegions.class); + CaptureRequest.Builder mockBuilder = mock(CaptureRequest.Builder.class); + ExposurePointFeature exposurePointFeature = new ExposurePointFeature(mockCameraProperties, () -> mockCameraRegions); + MeteringRectangle meteringRectangle = new MeteringRectangle(0,0,0,0,0); + + when(mockCameraProperties.getControlMaxRegionsAutoExposure()).thenReturn(1); + when(mockCameraRegions.getAEMeteringRectangle()).thenThrow(new IllegalArgumentException()); + + exposurePointFeature.updateBuilder(mockBuilder); + + verify(mockBuilder, never()).set(any(), any()); + } +} From 7c701cd9958178dfa82c1dbb900113a402a6b10b Mon Sep 17 00:00:00 2001 From: Maurits van Beusekom Date: Tue, 9 Mar 2021 10:38:55 +0100 Subject: [PATCH 081/114] Added test coverage to flash feature --- .../camera/features/flash/FlashFeature.java | 5 +- .../features/flash/FlashFeatureTest.java | 149 ++++++++++++++++++ 2 files changed, 151 insertions(+), 3 deletions(-) create mode 100644 packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/features/flash/FlashFeatureTest.java diff --git a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/flash/FlashFeature.java b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/flash/FlashFeature.java index b0ad2dd48037..14132383d3a4 100644 --- a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/flash/FlashFeature.java +++ b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/flash/FlashFeature.java @@ -18,7 +18,7 @@ public FlashFeature(CameraProperties cameraProperties) { @Override public String getDebugName() { - return "Flash"; + return "FlashFeature"; } @Override @@ -34,8 +34,7 @@ public void setValue(FlashMode value) { @Override public boolean checkIsSupported() { Boolean available = cameraProperties.getFlashInfoAvailable(); - final boolean supported = available != null && available; - return supported; + return available != null && available; } @Override diff --git a/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/features/flash/FlashFeatureTest.java b/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/features/flash/FlashFeatureTest.java new file mode 100644 index 000000000000..cf303fa67b2c --- /dev/null +++ b/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/features/flash/FlashFeatureTest.java @@ -0,0 +1,149 @@ +package io.flutter.plugins.camera.features.flash; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import android.hardware.camera2.CaptureRequest; +import io.flutter.plugins.camera.CameraProperties; +import org.junit.Test; + +public class FlashFeatureTest { + @Test + public void getDebugName_should_return_the_name_of_the_feature() { + CameraProperties mockCameraProperties = mock(CameraProperties.class); + FlashFeature flashFeature = new FlashFeature(mockCameraProperties); + + assertEquals("FlashFeature", flashFeature.getDebugName()); + } + + @Test + public void getValue_should_return_auto_if_not_set() { + CameraProperties mockCameraProperties = mock(CameraProperties.class); + FlashFeature flashFeature = new FlashFeature(mockCameraProperties); + + assertEquals(FlashMode.auto, flashFeature.getValue()); + } + + @Test + public void getValue_should_echo_the_set_value() { + CameraProperties mockCameraProperties = mock(CameraProperties.class); + FlashFeature flashFeature = new FlashFeature(mockCameraProperties); + FlashMode expectedValue = FlashMode.torch; + + flashFeature.setValue(expectedValue); + FlashMode actualValue = flashFeature.getValue(); + + assertEquals(expectedValue, actualValue); + } + + @Test + public void checkIsSupported_should_return_false_when_flash_info_available_is_null() { + CameraProperties mockCameraProperties = mock(CameraProperties.class); + FlashFeature flashFeature = new FlashFeature(mockCameraProperties); + + when(mockCameraProperties.getFlashInfoAvailable()).thenReturn(null); + + assertFalse(flashFeature.checkIsSupported()); + } + + @Test + public void checkIsSupported_should_return_false_when_flash_info_available_is_false() { + CameraProperties mockCameraProperties = mock(CameraProperties.class); + FlashFeature flashFeature = new FlashFeature(mockCameraProperties); + + when(mockCameraProperties.getFlashInfoAvailable()).thenReturn(false); + + assertFalse(flashFeature.checkIsSupported()); + } + + @Test + public void checkIsSupported_should_return_true_when_flash_info_available_is_true() { + CameraProperties mockCameraProperties = mock(CameraProperties.class); + FlashFeature flashFeature = new FlashFeature(mockCameraProperties); + + when(mockCameraProperties.getFlashInfoAvailable()).thenReturn(true); + + assertTrue(flashFeature.checkIsSupported()); + } + + @Test + public void updateBuilder_should_return_when_checkIsSupported_is_false() { + CameraProperties mockCameraProperties = mock(CameraProperties.class); + CaptureRequest.Builder mockBuilder = mock(CaptureRequest.Builder.class); + FlashFeature flashFeature = new FlashFeature(mockCameraProperties); + + when(mockCameraProperties.getFlashInfoAvailable()).thenReturn(false); + + flashFeature.updateBuilder(mockBuilder); + + verify(mockBuilder, never()).set(any(), any()); + } + + @Test + public void updateBuilder_should_set_ae_mode_and_flash_mode_when_flash_mode_is_off() { + CameraProperties mockCameraProperties = mock(CameraProperties.class); + CaptureRequest.Builder mockBuilder = mock(CaptureRequest.Builder.class); + FlashFeature flashFeature = new FlashFeature(mockCameraProperties); + + when(mockCameraProperties.getFlashInfoAvailable()).thenReturn(true); + + flashFeature.setValue(FlashMode.off); + flashFeature.updateBuilder(mockBuilder); + + verify(mockBuilder, times(1)).set(CaptureRequest.CONTROL_AE_MODE, CaptureRequest.CONTROL_AE_MODE_ON); + verify(mockBuilder, times(1)).set(CaptureRequest.FLASH_MODE, CaptureRequest.FLASH_MODE_OFF); + } + + @Test + public void updateBuilder_should_set_ae_mode_and_flash_mode_when_flash_mode_is_always() { + CameraProperties mockCameraProperties = mock(CameraProperties.class); + CaptureRequest.Builder mockBuilder = mock(CaptureRequest.Builder.class); + FlashFeature flashFeature = new FlashFeature(mockCameraProperties); + + when(mockCameraProperties.getFlashInfoAvailable()).thenReturn(true); + + flashFeature.setValue(FlashMode.always); + flashFeature.updateBuilder(mockBuilder); + + verify(mockBuilder, times(1)).set(CaptureRequest.CONTROL_AE_MODE, CaptureRequest.CONTROL_AE_MODE_ON_ALWAYS_FLASH); + verify(mockBuilder, times(1)).set(CaptureRequest.FLASH_MODE, CaptureRequest.FLASH_MODE_OFF); + } + + @Test + public void updateBuilder_should_set_ae_mode_and_flash_mode_when_flash_mode_is_torch() { + CameraProperties mockCameraProperties = mock(CameraProperties.class); + CaptureRequest.Builder mockBuilder = mock(CaptureRequest.Builder.class); + FlashFeature flashFeature = new FlashFeature(mockCameraProperties); + + when(mockCameraProperties.getFlashInfoAvailable()).thenReturn(true); + + flashFeature.setValue(FlashMode.torch); + flashFeature.updateBuilder(mockBuilder); + + verify(mockBuilder, times(1)).set(CaptureRequest.CONTROL_AE_MODE, CaptureRequest.CONTROL_AE_MODE_ON); + verify(mockBuilder, times(1)).set(CaptureRequest.FLASH_MODE, CaptureRequest.FLASH_MODE_TORCH); + } + + @Test + public void updateBuilder_should_set_ae_mode_and_flash_mode_when_flash_mode_is_auto() { + CameraProperties mockCameraProperties = mock(CameraProperties.class); + CaptureRequest.Builder mockBuilder = mock(CaptureRequest.Builder.class); + FlashFeature flashFeature = new FlashFeature(mockCameraProperties); + + when(mockCameraProperties.getFlashInfoAvailable()).thenReturn(true); + + flashFeature.setValue(FlashMode.auto); + flashFeature.updateBuilder(mockBuilder); + + verify(mockBuilder, times(1)) + .set(CaptureRequest.CONTROL_AE_MODE, CaptureRequest.CONTROL_AE_MODE_ON_AUTO_FLASH); + verify(mockBuilder, times(1)).set(CaptureRequest.FLASH_MODE, CaptureRequest.FLASH_MODE_OFF); + } +} From a106307ee946fd1c76b8b45d5539e184226fc722 Mon Sep 17 00:00:00 2001 From: Maurits van Beusekom Date: Tue, 9 Mar 2021 10:48:55 +0100 Subject: [PATCH 082/114] Added test coverage to focus point feature --- .../focuspoint/FocusPointFeature.java | 2 +- .../focuspoint/FocusPointFeatureTest.java | 112 ++++++++++++++++++ 2 files changed, 113 insertions(+), 1 deletion(-) create mode 100644 packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/features/focuspoint/FocusPointFeatureTest.java diff --git a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/focuspoint/FocusPointFeature.java b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/focuspoint/FocusPointFeature.java index 7c12c9686823..dd5bc6b84478 100644 --- a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/focuspoint/FocusPointFeature.java +++ b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/focuspoint/FocusPointFeature.java @@ -24,7 +24,7 @@ public FocusPointFeature(CameraProperties cameraProperties, Callable null); + + assertEquals("FocusPointFeature", focusPointFeature.getDebugName()); + } + + @Test + public void getValue_should_return_default_point_if_not_set() { + CameraProperties mockCameraProperties = mock(CameraProperties.class); + FocusPointFeature focusPointFeature = new FocusPointFeature(mockCameraProperties, () -> null); + Point expectedPoint = new Point(0.0, 0.0); + Point actualPoint = focusPointFeature.getValue(); + + assertEquals(expectedPoint.x, actualPoint.x); + assertEquals(expectedPoint.y, actualPoint.y); + } + + @Test + public void getValue_should_echo_the_set_value() { + CameraProperties mockCameraProperties = mock(CameraProperties.class); + CameraRegions mockCameraRegions = mock(CameraRegions.class); + FocusPointFeature focusPointFeature = new FocusPointFeature(mockCameraProperties, () -> mockCameraRegions); + Point expectedPoint = new Point(0.0, 0.0); + + focusPointFeature.setValue(expectedPoint); + Point actualPoint = focusPointFeature.getValue(); + + assertEquals(expectedPoint, actualPoint); + } + + @Test + public void setValue_should_reset_point_when_x_coord_is_null() { + CameraProperties mockCameraProperties = mock(CameraProperties.class); + CameraRegions mockCameraRegions = mock(CameraRegions.class); + FocusPointFeature focusPointFeature = new FocusPointFeature(mockCameraProperties, () -> mockCameraRegions); + + focusPointFeature.setValue(new Point(null, 0.0)); + + verify(mockCameraRegions, times(1)).resetAutoFocusMeteringRectangle(); + } + + @Test + public void setValue_should_reset_point_when_y_coord_is_null() { + CameraProperties mockCameraProperties = mock(CameraProperties.class); + CameraRegions mockCameraRegions = mock(CameraRegions.class); + FocusPointFeature focusPointFeature = new FocusPointFeature(mockCameraProperties, () -> mockCameraRegions); + + focusPointFeature.setValue(new Point(0.0, null)); + + verify(mockCameraRegions, times(1)).resetAutoFocusMeteringRectangle(); + } + + @Test + public void setValue_should_reset_point_when_valid_coords_are_supplied() { + CameraProperties mockCameraProperties = mock(CameraProperties.class); + CameraRegions mockCameraRegions = mock(CameraRegions.class); + FocusPointFeature focusPointFeature = new FocusPointFeature(mockCameraProperties, () -> mockCameraRegions); + Point point = new Point(0.0, 0.0); + + focusPointFeature.setValue(point); + + verify(mockCameraRegions, times(1)).setAutoFocusMeteringRectangleFromPoint(point.x, point.y); + } + + @Test + public void checkIsSupported_should_return_false_when_max_regions_is_null() { + CameraProperties mockCameraProperties = mock(CameraProperties.class); + FocusPointFeature focusPointFeature = new FocusPointFeature(mockCameraProperties, () -> null); + + when(mockCameraProperties.getControlMaxRegionsAutoFocus()).thenReturn(null); + + assertFalse(focusPointFeature.checkIsSupported()); + } + + @Test + public void checkIsSupported_should_return_false_when_max_regions_is_zero() { + CameraProperties mockCameraProperties = mock(CameraProperties.class); + FocusPointFeature focusPointFeature = new FocusPointFeature(mockCameraProperties, () -> null); + + when(mockCameraProperties.getControlMaxRegionsAutoFocus()).thenReturn(0); + + assertFalse(focusPointFeature.checkIsSupported()); + } + + @Test + public void checkIsSupported_should_return_true_when_max_regions_is_bigger_then_zero() { + CameraProperties mockCameraProperties = mock(CameraProperties.class); + FocusPointFeature focusPointFeature = new FocusPointFeature(mockCameraProperties, () -> null); + + when(mockCameraProperties.getControlMaxRegionsAutoFocus()).thenReturn(1); + + assertTrue(focusPointFeature.checkIsSupported()); + } +} From 17e3df6a81d8a34cb128f82d92f98cccd7c3fc19 Mon Sep 17 00:00:00 2001 From: Maurits van Beusekom Date: Tue, 9 Mar 2021 17:50:37 +0100 Subject: [PATCH 083/114] Added test coverage to FPS range feature --- .../features/fpsrange/FpsRangeFeature.java | 2 +- .../fpsrange/FpsRangeFeatureTest.java | 88 +++++++++++++++++++ 2 files changed, 89 insertions(+), 1 deletion(-) create mode 100644 packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/features/fpsrange/FpsRangeFeatureTest.java diff --git a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/fpsrange/FpsRangeFeature.java b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/fpsrange/FpsRangeFeature.java index 5055007b8f7f..c3c20abfd71f 100644 --- a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/fpsrange/FpsRangeFeature.java +++ b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/fpsrange/FpsRangeFeature.java @@ -41,7 +41,7 @@ public FpsRangeFeature(CameraProperties cameraProperties) { @Override public String getDebugName() { - return "FpsRange"; + return "FpsRangeFeature"; } @Override diff --git a/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/features/fpsrange/FpsRangeFeatureTest.java b/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/features/fpsrange/FpsRangeFeatureTest.java new file mode 100644 index 000000000000..f495e57d27ff --- /dev/null +++ b/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/features/fpsrange/FpsRangeFeatureTest.java @@ -0,0 +1,88 @@ +package io.flutter.plugins.camera.features.fpsrange; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import android.hardware.camera2.CaptureRequest; +import android.util.Range; +import io.flutter.plugins.camera.CameraProperties; +import org.junit.Test; + +public class FpsRangeFeatureTest { + @Test + public void ctor_should_initialize_fps_range_with_highest_upper_value_from_range_array() { + FpsRangeFeature fpsRangeFeature = createTestInstance(); + assertEquals(13, (int) fpsRangeFeature.getValue().getUpper()); + } + + @Test + public void getDebugName_should_return_the_name_of_the_feature() { + FpsRangeFeature fpsRangeFeature = createTestInstance(); + assertEquals("FpsRangeFeature", fpsRangeFeature.getDebugName()); + } + + @Test + public void getValue_should_return_highest_upper_range_if_not_set() { + CameraProperties mockCameraProperties = mock(CameraProperties.class); + FpsRangeFeature fpsRangeFeature = createTestInstance(); + + assertEquals(13, (int)fpsRangeFeature.getValue().getUpper()); + } + + @Test + public void getValue_should_echo_the_set_value() { + CameraProperties mockCameraProperties = mock(CameraProperties.class); + FpsRangeFeature fpsRangeFeature = new FpsRangeFeature(mockCameraProperties); + @SuppressWarnings("unchecked") + Range expectedValue = mock(Range.class); + + fpsRangeFeature.setValue(expectedValue); + Range actualValue = fpsRangeFeature.getValue(); + + assertEquals(expectedValue, actualValue); + } + + @Test + public void checkIsSupported_should_return_true() { + FpsRangeFeature fpsRangeFeature = createTestInstance(); + assertTrue(fpsRangeFeature.checkIsSupported()); + } + + @Test + @SuppressWarnings("unchecked") + public void updateBuilder_should_set_ae_target_fps_range() { + CaptureRequest.Builder mockBuilder = mock(CaptureRequest.Builder.class); + FpsRangeFeature fpsRangeFeature = createTestInstance(); + + fpsRangeFeature.updateBuilder(mockBuilder); + + verify(mockBuilder).set(eq(CaptureRequest.CONTROL_AE_TARGET_FPS_RANGE), any(Range.class)); + } + + private static FpsRangeFeature createTestInstance() { + @SuppressWarnings("unchecked") + Range rangeOne = mock(Range.class); + @SuppressWarnings("unchecked") + Range rangeTwo = mock(Range.class); + @SuppressWarnings("unchecked") + Range rangeThree = mock(Range.class); + + when(rangeOne.getUpper()).thenReturn(11); + when(rangeTwo.getUpper()).thenReturn(12); + when(rangeThree.getUpper()).thenReturn(13); + + @SuppressWarnings("unchecked") + Range[] ranges = new Range[] { rangeOne, rangeTwo, rangeThree }; + + CameraProperties cameraProperties = mock(CameraProperties.class); + + when(cameraProperties.getControlAutoExposureAvailableTargetFpsRanges()).thenReturn(ranges); + + return new FpsRangeFeature(cameraProperties); + } +} From 722ad7d539725c4dbc98161649f79fc200bee78c Mon Sep 17 00:00:00 2001 From: Maurits van Beusekom Date: Wed, 10 Mar 2021 11:57:12 +0100 Subject: [PATCH 084/114] Added test coverage to noise reduction feature --- .../noisereduction/NoiseReductionFeature.java | 23 ++- .../noisereduction/NoiseReductionMode.java | 8 +- .../autofocus/AutoFocusFeatureTest.java | 4 + .../exposurelock/ExposureLockFeatureTest.java | 4 + .../ExposureOffsetFeatureTest.java | 4 + .../ExposurePointFeatureTest.java | 4 + .../features/flash/FlashFeatureTest.java | 4 + .../focuspoint/FocusPointFeatureTest.java | 4 + .../fpsrange/FpsRangeFeatureTest.java | 4 + .../NoiseReductionFeatureTest.java | 144 ++++++++++++++++++ .../plugins/camera/utils/TestUtils.java | 21 +++ 11 files changed, 220 insertions(+), 4 deletions(-) create mode 100644 packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/features/noisereduction/NoiseReductionFeatureTest.java create mode 100644 packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/utils/TestUtils.java diff --git a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/noisereduction/NoiseReductionFeature.java b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/noisereduction/NoiseReductionFeature.java index d5206e457c3c..8d2093035b06 100644 --- a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/noisereduction/NoiseReductionFeature.java +++ b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/noisereduction/NoiseReductionFeature.java @@ -5,9 +5,12 @@ package io.flutter.plugins.camera.features.noisereduction; import android.hardware.camera2.CaptureRequest; +import android.os.Build.VERSION; +import android.os.Build.VERSION_CODES; import android.util.Log; import io.flutter.plugins.camera.CameraProperties; import io.flutter.plugins.camera.features.CameraFeature; +import java.util.HashMap; /** * This can either be enabled or disabled. Only full capability devices can set this to off. Legacy @@ -15,7 +18,21 @@ * https://developer.android.com/reference/android/hardware/camera2/CameraCharacteristics#NOISE_REDUCTION_AVAILABLE_NOISE_REDUCTION_MODES */ public class NoiseReductionFeature extends CameraFeature { - private NoiseReductionMode currentSetting; + private NoiseReductionMode currentSetting = NoiseReductionMode.fast; + + private static final HashMap NOISE_REDUCTION_MODES = new HashMap<>(); + static { + NOISE_REDUCTION_MODES.put(NoiseReductionMode.off, CaptureRequest.NOISE_REDUCTION_MODE_OFF); + NOISE_REDUCTION_MODES.put(NoiseReductionMode.fast, CaptureRequest.NOISE_REDUCTION_MODE_FAST); + NOISE_REDUCTION_MODES.put(NoiseReductionMode.highQuality, CaptureRequest.NOISE_REDUCTION_MODE_HIGH_QUALITY); + if (VERSION.SDK_INT >= VERSION_CODES.M) { + NOISE_REDUCTION_MODES + .put(NoiseReductionMode.minimal, CaptureRequest.NOISE_REDUCTION_MODE_MINIMAL); + NOISE_REDUCTION_MODES.put(NoiseReductionMode.zeroShutterLag, + CaptureRequest.NOISE_REDUCTION_MODE_ZERO_SHUTTER_LAG); + } + } + public NoiseReductionFeature(CameraProperties cameraProperties) { super(cameraProperties); @@ -23,7 +40,7 @@ public NoiseReductionFeature(CameraProperties cameraProperties) { @Override public String getDebugName() { - return "NoiseReduction"; + return "NoiseReductionFeature"; } @Override @@ -66,6 +83,6 @@ public void updateBuilder(CaptureRequest.Builder requestBuilder) { // Always use fast mode. requestBuilder.set( - CaptureRequest.NOISE_REDUCTION_MODE, CaptureRequest.NOISE_REDUCTION_MODE_FAST); + CaptureRequest.NOISE_REDUCTION_MODE, NOISE_REDUCTION_MODES.get(currentSetting)); } } diff --git a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/noisereduction/NoiseReductionMode.java b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/noisereduction/NoiseReductionMode.java index 51fd3bb05741..b837f21e5de7 100644 --- a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/noisereduction/NoiseReductionMode.java +++ b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/noisereduction/NoiseReductionMode.java @@ -6,7 +6,13 @@ /** Only supports fast mode for now. */ public enum NoiseReductionMode { - fast("fast"); + off("off"), + fast("fast"), + highQuality("highQuality"), + minimal("minimal"), + zeroShutterLag("zeroShutterLag"); + + private final String strValue; diff --git a/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/features/autofocus/AutoFocusFeatureTest.java b/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/features/autofocus/AutoFocusFeatureTest.java index 88461bc20ff3..d785b7d6b123 100644 --- a/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/features/autofocus/AutoFocusFeatureTest.java +++ b/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/features/autofocus/AutoFocusFeatureTest.java @@ -1,3 +1,7 @@ +// Copyright 2019 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + package io.flutter.plugins.camera.features.autofocus; import static org.junit.Assert.assertEquals; diff --git a/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/features/exposurelock/ExposureLockFeatureTest.java b/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/features/exposurelock/ExposureLockFeatureTest.java index 06e19f275acd..9cb9ab3fa6d6 100644 --- a/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/features/exposurelock/ExposureLockFeatureTest.java +++ b/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/features/exposurelock/ExposureLockFeatureTest.java @@ -1,3 +1,7 @@ +// Copyright 2019 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + package io.flutter.plugins.camera.features.exposurelock; import static org.junit.Assert.assertEquals; diff --git a/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/features/exposureoffset/ExposureOffsetFeatureTest.java b/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/features/exposureoffset/ExposureOffsetFeatureTest.java index d822d8ec0d18..b45b09c54ca4 100644 --- a/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/features/exposureoffset/ExposureOffsetFeatureTest.java +++ b/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/features/exposureoffset/ExposureOffsetFeatureTest.java @@ -1,3 +1,7 @@ +// Copyright 2019 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + package io.flutter.plugins.camera.features.exposureoffset; import static org.junit.Assert.assertEquals; diff --git a/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/features/exposurepoint/ExposurePointFeatureTest.java b/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/features/exposurepoint/ExposurePointFeatureTest.java index 7455741e00b0..085248985f7b 100644 --- a/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/features/exposurepoint/ExposurePointFeatureTest.java +++ b/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/features/exposurepoint/ExposurePointFeatureTest.java @@ -1,3 +1,7 @@ +// Copyright 2019 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + package io.flutter.plugins.camera.features.exposurepoint; import static org.junit.Assert.assertEquals; diff --git a/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/features/flash/FlashFeatureTest.java b/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/features/flash/FlashFeatureTest.java index cf303fa67b2c..1bf972daacc6 100644 --- a/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/features/flash/FlashFeatureTest.java +++ b/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/features/flash/FlashFeatureTest.java @@ -1,3 +1,7 @@ +// Copyright 2019 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + package io.flutter.plugins.camera.features.flash; import static org.junit.Assert.assertEquals; diff --git a/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/features/focuspoint/FocusPointFeatureTest.java b/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/features/focuspoint/FocusPointFeatureTest.java index 9ea8570c65d0..6733e6186aca 100644 --- a/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/features/focuspoint/FocusPointFeatureTest.java +++ b/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/features/focuspoint/FocusPointFeatureTest.java @@ -1,3 +1,7 @@ +// Copyright 2019 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + package io.flutter.plugins.camera.features.focuspoint; import static org.junit.Assert.assertEquals; diff --git a/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/features/fpsrange/FpsRangeFeatureTest.java b/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/features/fpsrange/FpsRangeFeatureTest.java index f495e57d27ff..512541a48cfb 100644 --- a/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/features/fpsrange/FpsRangeFeatureTest.java +++ b/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/features/fpsrange/FpsRangeFeatureTest.java @@ -1,3 +1,7 @@ +// Copyright 2019 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + package io.flutter.plugins.camera.features.fpsrange; import static org.junit.Assert.assertEquals; diff --git a/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/features/noisereduction/NoiseReductionFeatureTest.java b/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/features/noisereduction/NoiseReductionFeatureTest.java new file mode 100644 index 000000000000..3ea600dc539e --- /dev/null +++ b/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/features/noisereduction/NoiseReductionFeatureTest.java @@ -0,0 +1,144 @@ +// Copyright 2019 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +package io.flutter.plugins.camera.features.noisereduction; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import android.hardware.camera2.CaptureRequest; +import android.os.Build.VERSION; +import io.flutter.plugins.camera.CameraProperties; +import io.flutter.plugins.camera.utils.TestUtils; +import org.junit.Assert; +import org.junit.BeforeClass; +import org.junit.Test; + +public class NoiseReductionFeatureTest { + @BeforeClass + public static void beforeClass() { + // Make sure the VERSION.SDK_INT field returns 23, to allow using all available + // noise reduction modes in tests. + try { + TestUtils.setFinalStatic(VERSION.class.getField("SDK_INT"), 23); + } catch (Exception e) { + Assert.fail("Unable to set SDK_INT"); + } + } + + @Test + public void getDebugName_should_return_the_name_of_the_feature() { + CameraProperties mockCameraProperties = mock(CameraProperties.class); + NoiseReductionFeature noiseReductionFeature = new NoiseReductionFeature(mockCameraProperties); + + assertEquals("NoiseReductionFeature", noiseReductionFeature.getDebugName()); + } + + @Test + public void getValue_should_return_fast_if_not_set() { + CameraProperties mockCameraProperties = mock(CameraProperties.class); + NoiseReductionFeature noiseReductionFeature = new NoiseReductionFeature(mockCameraProperties); + + assertEquals(NoiseReductionMode.fast, noiseReductionFeature.getValue()); + } + + @Test + public void getValue_should_echo_the_set_value() { + CameraProperties mockCameraProperties = mock(CameraProperties.class); + NoiseReductionFeature noiseReductionFeature = new NoiseReductionFeature(mockCameraProperties); + NoiseReductionMode expectedValue = NoiseReductionMode.fast; + + noiseReductionFeature.setValue(expectedValue); + NoiseReductionMode actualValue = noiseReductionFeature.getValue(); + + assertEquals(expectedValue, actualValue); + } + + @Test + public void checkIsSupported_should_return_false_when_available_noise_reduction_modes_is_null() { + CameraProperties mockCameraProperties = mock(CameraProperties.class); + NoiseReductionFeature noiseReductionFeature = new NoiseReductionFeature(mockCameraProperties); + + when(mockCameraProperties.getAvailableNoiseReductionModes()).thenReturn(null); + + assertFalse(noiseReductionFeature.checkIsSupported()); + } + + @Test + public void checkIsSupported_should_return_false_when_available_noise_reduction_modes_returns_an_empty_array() { + CameraProperties mockCameraProperties = mock(CameraProperties.class); + NoiseReductionFeature noiseReductionFeature = new NoiseReductionFeature(mockCameraProperties); + + when(mockCameraProperties.getAvailableNoiseReductionModes()).thenReturn(new int[] { }); + + assertFalse(noiseReductionFeature.checkIsSupported()); + } + + @Test + public void checkIsSupported_should_return_true_when_available_noise_reduction_modes_returns_at_least_one_item() { + CameraProperties mockCameraProperties = mock(CameraProperties.class); + NoiseReductionFeature noiseReductionFeature = new NoiseReductionFeature(mockCameraProperties); + + when(mockCameraProperties.getAvailableNoiseReductionModes()).thenReturn(new int[] { 1 }); + + assertTrue(noiseReductionFeature.checkIsSupported()); + } + + @Test + public void updateBuilder_should_return_when_checkIsSupported_is_false() { + CameraProperties mockCameraProperties = mock(CameraProperties.class); + CaptureRequest.Builder mockBuilder = mock(CaptureRequest.Builder.class); + NoiseReductionFeature noiseReductionFeature = new NoiseReductionFeature(mockCameraProperties); + + when(mockCameraProperties.getAvailableNoiseReductionModes()).thenReturn(new int[] { }); + + noiseReductionFeature.updateBuilder(mockBuilder); + + verify(mockBuilder, never()).set(any(), any()); + } + + @Test + public void updateBuilder_should_set_noise_reduction_mode_off_when_off() { + testUpdateBuilderWith(NoiseReductionMode.off, CaptureRequest.NOISE_REDUCTION_MODE_OFF); + } + + @Test + public void updateBuilder_should_set_noise_reduction_mode_fast_when_fast() { + testUpdateBuilderWith(NoiseReductionMode.fast, CaptureRequest.NOISE_REDUCTION_MODE_FAST); + } + + @Test + public void updateBuilder_should_set_noise_reduction_mode_high_quality_when_high_quality() { + testUpdateBuilderWith(NoiseReductionMode.highQuality, CaptureRequest.NOISE_REDUCTION_MODE_HIGH_QUALITY); + } + + @Test + public void updateBuilder_should_set_noise_reduction_mode_minimal_when_minimal() { + testUpdateBuilderWith(NoiseReductionMode.minimal, CaptureRequest.NOISE_REDUCTION_MODE_MINIMAL); + } + + @Test + public void updateBuilder_should_set_noise_reduction_mode_zero_shutter_lag_when_zero_shutter_lag() { + testUpdateBuilderWith(NoiseReductionMode.zeroShutterLag, CaptureRequest.NOISE_REDUCTION_MODE_ZERO_SHUTTER_LAG); + } + + private static void testUpdateBuilderWith(NoiseReductionMode mode, int expectedResult) { + CameraProperties mockCameraProperties = mock(CameraProperties.class); + CaptureRequest.Builder mockBuilder = mock(CaptureRequest.Builder.class); + NoiseReductionFeature noiseReductionFeature = new NoiseReductionFeature(mockCameraProperties); + + when(mockCameraProperties.getAvailableNoiseReductionModes()).thenReturn(new int[] { 1 }); + + noiseReductionFeature.setValue(mode); + noiseReductionFeature.updateBuilder(mockBuilder); + verify(mockBuilder, times(1)).set(CaptureRequest.NOISE_REDUCTION_MODE, expectedResult); + } +} diff --git a/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/utils/TestUtils.java b/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/utils/TestUtils.java new file mode 100644 index 000000000000..142f88bc34b4 --- /dev/null +++ b/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/utils/TestUtils.java @@ -0,0 +1,21 @@ +// Copyright 2019 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +package io.flutter.plugins.camera.utils; + +import java.lang.reflect.Field; +import java.lang.reflect.Modifier; + +public class TestUtils { + public static void setFinalStatic(Field field, Object newValue) + throws NoSuchFieldException, IllegalAccessException { + field.setAccessible(true); + + Field modifiersField = Field.class.getDeclaredField("modifiers"); + modifiersField.setAccessible(true); + modifiersField.setInt(field, field.getModifiers() & ~Modifier.FINAL); + + field.set(null, newValue); + } +} From 92029e345964607c59266ee79e53ff63ecb1a4bd Mon Sep 17 00:00:00 2001 From: Andrew Coutts Date: Wed, 10 Mar 2021 06:24:59 -0500 Subject: [PATCH 085/114] Ensure the camera UI can handle when there's too many camera toggle options to fit horizontally on screen --- packages/camera/camera/example/lib/main.dart | 67 ++++++++++---------- 1 file changed, 35 insertions(+), 32 deletions(-) diff --git a/packages/camera/camera/example/lib/main.dart b/packages/camera/camera/example/lib/main.dart index 2bfeda1ca60e..40245b3fcda0 100644 --- a/packages/camera/camera/example/lib/main.dart +++ b/packages/camera/camera/example/lib/main.dart @@ -155,11 +155,17 @@ class _CameraExampleHomeState extends State _modeControlRowWidget(), Padding( padding: const EdgeInsets.all(5.0), - child: Row( - mainAxisAlignment: MainAxisAlignment.start, + child: Stack( children: [ - _cameraTogglesRowWidget(), - _thumbnailWidget(), + SingleChildScrollView( + scrollDirection: Axis.horizontal, + padding: EdgeInsets.only(right: 90), + child: _cameraTogglesRowWidget(), + ), + Positioned( + right: 0, + child: _thumbnailWidget(), + ), ], ), ), @@ -221,35 +227,32 @@ class _CameraExampleHomeState extends State Widget _thumbnailWidget() { final VideoPlayerController? localVideoController = videoController; - return Expanded( - child: Align( - alignment: Alignment.centerRight, - child: Row( - mainAxisSize: MainAxisSize.min, - children: [ - localVideoController == null && imageFile == null - ? Container() - : SizedBox( - child: (localVideoController == null) - ? Image.file(File(imageFile!.path)) - : Container( - child: Center( - child: AspectRatio( - aspectRatio: - localVideoController.value.size != null - ? localVideoController - .value.aspectRatio - : 1.0, - child: VideoPlayer(localVideoController)), - ), - decoration: BoxDecoration( - border: Border.all(color: Colors.pink)), + return Align( + alignment: Alignment.centerRight, + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + localVideoController == null && imageFile == null + ? Container() + : SizedBox( + child: (localVideoController == null) + ? Image.file(File(imageFile!.path)) + : Container( + child: Center( + child: AspectRatio( + aspectRatio: + localVideoController.value.size != null + ? localVideoController.value.aspectRatio + : 1.0, + child: VideoPlayer(localVideoController)), ), - width: 64.0, - height: 64.0, - ), - ], - ), + decoration: BoxDecoration( + border: Border.all(color: Colors.pink)), + ), + width: 64.0, + height: 64.0, + ), + ], ), ); } From 2ddc5b81a834ad0c5a4bbe604a87e40dadbd415b Mon Sep 17 00:00:00 2001 From: Andrew Coutts Date: Wed, 10 Mar 2021 06:33:30 -0500 Subject: [PATCH 086/114] Added timeouts for focusing and metering to ensure picture capture routine will always be executed --- .../io/flutter/plugins/camera/Camera.java | 72 +++++----- .../plugins/camera/CameraCaptureCallback.java | 47 ++++--- .../plugins/camera/CameraProperties.java | 3 +- .../plugins/camera/PictureCaptureRequest.java | 123 +++++------------- .../io/flutter/plugins/camera/Timeout.java | 36 +++++ .../camera/features/CameraFeature.java | 1 - .../camera/features/CameraFeatureFactory.java | 54 ++++---- .../features/CameraFeatureFactoryImpl.java | 9 +- .../exposureoffset/ExposureOffsetFeature.java | 4 +- .../exposurepoint/ExposurePointFeature.java | 7 +- .../focuspoint/FocusPointFeature.java | 3 +- .../RegionBoundariesFeature.java | 3 +- .../io/flutter/plugins/camera/CameraTest.java | 37 +++--- .../autofocus/AutoFocusFeatureTest.java | 23 +++- .../exposurelock/ExposureLockFeatureTest.java | 8 +- .../ExposureOffsetFeatureTest.java | 6 +- .../ExposurePointFeatureTest.java | 46 ++++--- .../features/flash/FlashFeatureTest.java | 9 +- .../focuspoint/FocusPointFeatureTest.java | 12 +- .../fpsrange/FpsRangeFeatureTest.java | 4 +- 20 files changed, 260 insertions(+), 247 deletions(-) create mode 100644 packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/Timeout.java 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 f51c8d1756a4..27284acb16c7 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 @@ -129,8 +129,6 @@ class Camera implements CameraCaptureCallback.CameraCaptureStateListener { private final Activity activity; /** A {@link CameraCaptureSession.CaptureCallback} that handles events related to JPEG capture. */ private final CameraCaptureCallback mCaptureCallback; - /** This manages the state of the camera and the current capture request. */ - PictureCaptureRequest pictureCaptureRequest; /** A {@link Handler} for running tasks in the background. */ private Handler mBackgroundHandler; /** @@ -166,6 +164,9 @@ public void onImageAvailable(ImageReader reader) { private File videoRecordingFile; + /** Holds the current picture capture request and its related timeouts */ + private PictureCaptureRequest pictureCaptureRequest; + public Camera( final Activity activity, final SurfaceTextureEntry flutterTexture, @@ -198,7 +199,8 @@ public Camera( cameraFeatureFactory.createAutoFocusFeature(cameraProperties, false)); put( CameraFeatures.sensorOrientation, - cameraFeatureFactory.createSensorOrientationFeature(cameraProperties, activity, dartMessenger)); + cameraFeatureFactory.createSensorOrientationFeature( + cameraProperties, activity, dartMessenger)); put( CameraFeatures.exposureLock, cameraFeatureFactory.createExposureLockFeature(cameraProperties)); @@ -207,13 +209,13 @@ public Camera( cameraFeatureFactory.createExposureOffsetFeature(cameraProperties)); put( CameraFeatures.exposurePoint, - cameraFeatureFactory.createExposurePointFeature(cameraProperties, () -> getCameraRegions())); + cameraFeatureFactory.createExposurePointFeature( + cameraProperties, () -> getCameraRegions())); put( CameraFeatures.focusPoint, - cameraFeatureFactory.createFocusPointFeature(cameraProperties, () -> getCameraRegions())); - put( - CameraFeatures.flash, - cameraFeatureFactory.createFlashFeature(cameraProperties)); + cameraFeatureFactory.createFocusPointFeature( + cameraProperties, () -> getCameraRegions())); + put(CameraFeatures.flash, cameraFeatureFactory.createFlashFeature(cameraProperties)); put( CameraFeatures.fpsRange, cameraFeatureFactory.createFpsRangeFeature(cameraProperties)); @@ -245,11 +247,6 @@ public void onPrecapture() { runPrecaptureSequence(); } - @Override - public void onPrecaptureTimeout() { - unlockAutoFocus(); - } - /** * Update the builder settings with all of our available features. * @@ -313,12 +310,10 @@ public void onOpened(@NonNull CameraDevice device) { try { startPreview(); - final boolean isExposurePointSupported = cameraFeatures - .get(CameraFeatures.exposurePoint) - .checkIsSupported(); - final boolean isFocusPointSupported = cameraFeatures - .get(CameraFeatures.focusPoint) - .checkIsSupported(); + final boolean isExposurePointSupported = + cameraFeatures.get(CameraFeatures.exposurePoint).checkIsSupported(); + final boolean isFocusPointSupported = + cameraFeatures.get(CameraFeatures.focusPoint).checkIsSupported(); dartMessenger.sendCameraInitializedEvent( getPreviewSize().getWidth(), @@ -517,7 +512,7 @@ public void takePicture(@NonNull final Result result) { final File file = File.createTempFile("CAP", ".jpg", outputDir); // Start a new capture - pictureCaptureRequest = PictureCaptureRequest.create(result, file, dartMessenger); + pictureCaptureRequest = new PictureCaptureRequest(result, dartMessenger, file, 3000, 3000); mCaptureCallback.setPictureCaptureRequest(pictureCaptureRequest); } catch (IOException | SecurityException e) { pictureCaptureRequest.error("cannotCreateFile", e.getMessage(), null); @@ -527,7 +522,8 @@ public void takePicture(@NonNull final Result result) { // Listen for picture being taken pictureImageReader.setOnImageAvailableListener(mOnImageAvailableListener, mBackgroundHandler); - final AutoFocusFeature autoFocusFeature = (AutoFocusFeature) cameraFeatures.get(CameraFeatures.autoFocus); + final AutoFocusFeature autoFocusFeature = + (AutoFocusFeature) cameraFeatures.get(CameraFeatures.autoFocus); final boolean isAutoFocusSupported = autoFocusFeature.checkIsSupported(); if (isAutoFocusSupported && autoFocusFeature.getValue() == FocusMode.auto) { runPictureAutoFocus(); @@ -694,15 +690,12 @@ private void lockAutoFocus() { CaptureRequest.CONTROL_AF_TRIGGER, CaptureRequest.CONTROL_AF_TRIGGER_START); try { - captureSession.capture( - mPreviewRequestBuilder.build(), - null, - mBackgroundHandler); - } catch (CameraAccessException e) { - Log.i(TAG, "Error unlocking focus: " + e.getMessage()); - dartMessenger.sendCameraErrorEvent(e.getMessage()); - return; - } + captureSession.capture(mPreviewRequestBuilder.build(), null, mBackgroundHandler); + } catch (CameraAccessException e) { + Log.i(TAG, "Error unlocking focus: " + e.getMessage()); + dartMessenger.sendCameraErrorEvent(e.getMessage()); + return; + } } /** Cancel and reset auto focus state and refresh the preview session. */ @@ -881,7 +874,8 @@ public CameraRegions getCameraRegions() { * @param y new y. */ public void setExposurePoint(@NonNull final Result result, Double x, Double y) { - ((ExposurePointFeature) cameraFeatures.get(CameraFeatures.exposurePoint)).setValue(new Point(x, y)); + ((ExposurePointFeature) cameraFeatures.get(CameraFeatures.exposurePoint)) + .setValue(new Point(x, y)); cameraFeatures.get(CameraFeatures.exposurePoint).updateBuilder(mPreviewRequestBuilder); refreshPreviewCaptureSession( @@ -906,7 +900,8 @@ public double getMinExposureOffset() { /** Return the exposure offset step size to dart. */ public double getExposureOffsetStepSize() { - final ExposureOffsetFeature val = (ExposureOffsetFeature) cameraFeatures.get(CameraFeatures.exposureOffset); + final ExposureOffsetFeature val = + (ExposureOffsetFeature) cameraFeatures.get(CameraFeatures.exposureOffset); return val.getExposureOffsetStepSize(); } @@ -932,11 +927,11 @@ public void setFocusMode(@NonNull final Result result, FocusMode newMode) { // Set AF state to idle again mPreviewRequestBuilder.set( - CaptureRequest.CONTROL_AF_TRIGGER, CameraMetadata.CONTROL_AF_TRIGGER_IDLE); + CaptureRequest.CONTROL_AF_TRIGGER, CameraMetadata.CONTROL_AF_TRIGGER_IDLE); try { captureSession.setRepeatingRequest( - mPreviewRequestBuilder.build(), null, mBackgroundHandler); + mPreviewRequestBuilder.build(), null, mBackgroundHandler); } catch (CameraAccessException e) { result.error("setFocusModeFailed", "Error setting focus mode: " + e.getMessage(), null); } @@ -984,7 +979,8 @@ public void setExposureOffset(@NonNull final Result result, double offset) { } public float getMaxZoomLevel() { - final ZoomLevelFeature zoomLevel = (ZoomLevelFeature) cameraFeatures.get(CameraFeatures.zoomLevel); + final ZoomLevelFeature zoomLevel = + (ZoomLevelFeature) cameraFeatures.get(CameraFeatures.zoomLevel); return zoomLevel.getCameraZoom().maxZoom; } @@ -1004,7 +1000,8 @@ Size getCaptureSize() { /** Shortcut to get current recording profile. */ CamcorderProfile getRecordingProfile() { - return ((ResolutionFeature) cameraFeatures.get(CameraFeatures.resolution)).getRecordingProfile(); + return ((ResolutionFeature) cameraFeatures.get(CameraFeatures.resolution)) + .getRecordingProfile(); } /** Shortut to get deviceOrientationListener. */ @@ -1020,7 +1017,8 @@ DeviceOrientationManager getDeviceOrientationManager() { * @param zoom new value. */ public void setZoomLevel(@NonNull final Result result, float zoom) throws CameraAccessException { - final ZoomLevelFeature zoomLevel = (ZoomLevelFeature) cameraFeatures.get(CameraFeatures.zoomLevel); + final ZoomLevelFeature zoomLevel = + (ZoomLevelFeature) cameraFeatures.get(CameraFeatures.zoomLevel); float maxZoom = zoomLevel.getCameraZoom().maxZoom; float minZoom = CameraZoom.DEFAULT_ZOOM_FACTOR; diff --git a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/CameraCaptureCallback.java b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/CameraCaptureCallback.java index a9e6e6a98c6f..87753ca4ae99 100644 --- a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/CameraCaptureCallback.java +++ b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/CameraCaptureCallback.java @@ -13,28 +13,20 @@ import androidx.annotation.NonNull; class CameraCaptureCallback extends CaptureCallback { - interface CameraCaptureStateListener { - void onConverged(); - - void onPrecapture(); - - void onPrecaptureTimeout(); - } - private final CameraCaptureStateListener cameraStateListener; private CameraState cameraState; private PictureCaptureRequest pictureCaptureRequest; - public static CameraCaptureCallback create( - @NonNull CameraCaptureStateListener cameraStateListener) { - return new CameraCaptureCallback(cameraStateListener); - } - private CameraCaptureCallback(@NonNull CameraCaptureStateListener cameraStateListener) { cameraState = CameraState.STATE_PREVIEW; this.cameraStateListener = cameraStateListener; } + public static CameraCaptureCallback create( + @NonNull CameraCaptureStateListener cameraStateListener) { + return new CameraCaptureCallback(cameraStateListener); + } + public CameraState getCameraState() { return cameraState; } @@ -73,9 +65,16 @@ private void process(CaptureResult result) { if (afState == null) { return; } else if (afState == CaptureRequest.CONTROL_AF_STATE_FOCUSED_LOCKED - || afState == CaptureRequest.CONTROL_AF_STATE_NOT_FOCUSED_LOCKED) { + || afState == CaptureRequest.CONTROL_AF_STATE_NOT_FOCUSED_LOCKED + + // Move forward in case of a focusing timeout. + || pictureCaptureRequest.preCaptureFocusing.getIsExpired()) { // CONTROL_AE_STATE can be null on some devices + if (pictureCaptureRequest.preCaptureFocusing.getIsExpired()) { + Log.i("Camera", "Focus timeout, moving on with capture"); + } + if (aeState == null || aeState == CaptureRequest.CONTROL_AE_STATE_CONVERGED) { cameraStateListener.onConverged(); } else { @@ -91,7 +90,15 @@ private void process(CaptureResult result) { if (aeState == null || aeState == CaptureResult.CONTROL_AE_STATE_CONVERGED || aeState == CaptureResult.CONTROL_AE_STATE_PRECAPTURE - || aeState == CaptureResult.CONTROL_AE_STATE_FLASH_REQUIRED) { + || aeState == CaptureResult.CONTROL_AE_STATE_FLASH_REQUIRED + + // Move forward in case of a metering timeout. + || pictureCaptureRequest.preCaptureMetering.getIsExpired()) { + + if (pictureCaptureRequest.preCaptureMetering.getIsExpired()) { + Log.i("Camera", "Metering timeout, moving on with capture"); + } + setCameraState(CameraState.STATE_WAITING_PRECAPTURE_DONE); } break; @@ -102,10 +109,6 @@ private void process(CaptureResult result) { // CONTROL_AE_STATE can be null on some devices if (aeState == null || aeState != CaptureResult.CONTROL_AE_STATE_PRECAPTURE) { cameraStateListener.onConverged(); - } else if (pictureCaptureRequest != null - && pictureCaptureRequest.hitPreCaptureTimeout()) { - // Log.i(TAG, "===> Hit precapture timeout"); - cameraStateListener.onPrecaptureTimeout(); } break; } @@ -127,4 +130,10 @@ public void onCaptureCompleted( @NonNull TotalCaptureResult result) { process(result); } + + interface CameraCaptureStateListener { + void onConverged(); + + void onPrecapture(); + } } diff --git a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/CameraProperties.java b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/CameraProperties.java index 03105de2d95e..56c025436230 100644 --- a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/CameraProperties.java +++ b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/CameraProperties.java @@ -79,7 +79,8 @@ public Range getControlAutoExposureCompensationRange() { @Override public double getControlAutoExposureCompensationStep() { - Rational rational = cameraCharacteristics.get(CameraCharacteristics.CONTROL_AE_COMPENSATION_STEP); + Rational rational = + cameraCharacteristics.get(CameraCharacteristics.CONTROL_AE_COMPENSATION_STEP); return rational == null ? 0.0 : rational.doubleValue(); } diff --git a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/PictureCaptureRequest.java b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/PictureCaptureRequest.java index 032e9289a4c5..acf1576459cf 100644 --- a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/PictureCaptureRequest.java +++ b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/PictureCaptureRequest.java @@ -1,71 +1,43 @@ -// Copyright 2019 The Chromium Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - package io.flutter.plugins.camera; -import android.os.Handler; -import android.os.Looper; -import android.os.SystemClock; import androidx.annotation.Nullable; import io.flutter.plugin.common.MethodChannel; import java.io.File; /** - * This is where we store the state of the camera. This conveniently allows us to handle capture - * results and send results back to flutter so we can handle errors. - * - *

It also handles a capture timeout so if a capture doesn't happen within 5 seconds it will - * return an error to dart. + * Holds the temporary things associated with an image capture like the file, the dartMessenger to + * send errors, and the dart result to send the image file back as the capture result. */ -class PictureCaptureRequest { - - /** Timeout for the pre-capture sequence. */ - private static final long PRECAPTURE_TIMEOUT_MS = 1000; - +public class PictureCaptureRequest { + /** The file for saving the capture. */ + public final File file; /** - * This is the output file for the curent capture. The file is created in Camera and passed here - * as reference to it. + * The timeout related to pre-capture focusing. Will ensure that we reach focus in a reasonable + * amount of time. */ - final File file; - + public final Timeout preCaptureFocusing; + /** + * The the timeout related to pre-capture metering. Will ensure that we reach a metering result in + * a reasonable amount of time. + */ + public final Timeout preCaptureMetering; /** Dart method chanel result. */ private final MethodChannel.Result result; - - /** Timeout handler. */ - private final TimeoutHandler timeoutHandler; /** To send errors back to dart */ private final DartMessenger dartMessenger; - /** - * The time that the most recent capture started at. Used to check if the current capture request - * has timed out. - */ - private long preCaptureStartTime; - - /** - * Factory method to create a picture capture request. - * - * @param result - * @param file - */ - static PictureCaptureRequest create( - MethodChannel.Result result, File file, DartMessenger dartMessenger) { - return new PictureCaptureRequest(result, file, dartMessenger); - } - - /** - * Private constructor to create a picture capture request. - * - * @param result - * @param file - */ - private PictureCaptureRequest( - MethodChannel.Result result, File file, DartMessenger dartMessenger) { + /** Create a new picture capture request */ + public PictureCaptureRequest( + MethodChannel.Result result, + DartMessenger dartMessenger, + File file, + long preCaptureFocusingTimeoutMs, + long preCaptureMeteringTimeoutMs) { this.result = result; - this.file = file; this.dartMessenger = dartMessenger; - this.timeoutHandler = TimeoutHandler.create(); + this.file = file; + this.preCaptureFocusing = new Timeout(preCaptureFocusingTimeoutMs); + this.preCaptureMetering = new Timeout(preCaptureMeteringTimeoutMs); } /** @@ -77,50 +49,15 @@ public void finish(String absolutePath) { result.success(absolutePath); } - public void error( - String errorCode, @Nullable String errorMessage, @Nullable Object errorDetails) { - result.error(errorCode, errorMessage, errorDetails); - } - /** - * Check if the timeout for the pre-capture sequence has been reached. + * Return an error to dart for this picture capture request. * - * @return true if the timeout is reached; otherwise false is returned. + * @param errorCode error code. + * @param errorMessage error message. + * @param errorDetails error details. */ - public boolean hitPreCaptureTimeout() { - // Log.i("Camera", "hitPreCaptureTimeout | Time elapsed: " + (SystemClock.elapsedRealtime() - preCaptureStartTime)); - return (SystemClock.elapsedRealtime() - preCaptureStartTime) > PRECAPTURE_TIMEOUT_MS; - } - - /** Sets the time the pre-capture sequence started. */ - public void setPreCaptureStartTime() { - preCaptureStartTime = SystemClock.elapsedRealtime(); - } - - /** - * This handles the timeout for capture requests so they return within a reasonable amount of - * time. - */ - static class TimeoutHandler { - private static final int REQUEST_TIMEOUT = 5000; - private final Handler handler; - - public static TimeoutHandler create() { - return new TimeoutHandler(); - } - - private TimeoutHandler() { - this.handler = new Handler(Looper.getMainLooper()); - } - - public void resetTimeout(Runnable runnable) { - // Log.i("Camera", "PictureCaptureRequest | resetting timeout"); - clearTimeout(runnable); - handler.postDelayed(runnable, REQUEST_TIMEOUT); - } - - public void clearTimeout(Runnable runnable) { - handler.removeCallbacks(runnable); - } + public void error( + String errorCode, @Nullable String errorMessage, @Nullable Object errorDetails) { + result.error(errorCode, errorMessage, errorDetails); } } diff --git a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/Timeout.java b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/Timeout.java new file mode 100644 index 000000000000..37e6d66ce3e2 --- /dev/null +++ b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/Timeout.java @@ -0,0 +1,36 @@ +// Copyright 2019 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +package io.flutter.plugins.camera; + +import android.os.SystemClock; + +/** + * This is a simple class for managing a timeout. In the camera we generally keep two timeouts: one + * for focusing and one for pre-capture metering. + * + *

We use timeouts to ensure a picture is always captured within a reasonable amount of time even + * if the settings don't converge and focus can't be locked. + * + *

You generally check the status of the timeout in the CameraCAptureCallback during the capture + * sequence and use it to move to the next state if the timeout has passed. + */ +class Timeout { + + /** The timeout time in milliseconds */ + private final long timeoutMs; + + /** When this timeout was started. Will be used later to check if the timeout has expired yet. */ + private final long timeStarted; + + public Timeout(long timeoutMs) { + this.timeoutMs = timeoutMs; + this.timeStarted = SystemClock.elapsedRealtime(); + } + + /** Will return true when the timeout period has lapsed. */ + boolean getIsExpired() { + return (SystemClock.elapsedRealtime() - timeStarted) > timeoutMs; + } +} diff --git a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/CameraFeature.java b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/CameraFeature.java index 2f96dae2a858..08ff3f043418 100644 --- a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/CameraFeature.java +++ b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/CameraFeature.java @@ -19,7 +19,6 @@ public abstract class CameraFeature { protected final CameraProperties cameraProperties; - protected CameraFeature(@NonNull CameraProperties cameraProperties) { this.cameraProperties = cameraProperties; } diff --git a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/CameraFeatureFactory.java b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/CameraFeatureFactory.java index 2655b6b1f89d..4f0ba41230bc 100644 --- a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/CameraFeatureFactory.java +++ b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/CameraFeatureFactory.java @@ -30,18 +30,19 @@ public interface CameraFeatureFactory { /** * Creates a new instance of the auto focus feature. * - * @param cameraProperties instance of the CameraProperties class containing information about the cameras features. + * @param cameraProperties instance of the CameraProperties class containing information about the + * cameras features. * @param recordingVideo indicates if the camera is currently recording. * @return newly created instance of the AutoFocusFeature class. */ AutoFocusFeature createAutoFocusFeature( - @NonNull CameraProperties cameraProperties, - boolean recordingVideo); + @NonNull CameraProperties cameraProperties, boolean recordingVideo); /** * Creates a new instance of the exposure lock feature. * - * @param cameraProperties instance of the CameraProperties class containing information about the cameras features. + * @param cameraProperties instance of the CameraProperties class containing information about the + * cameras features. * @return newly created instance of the ExposureLockFeature class. */ ExposureLockFeature createExposureLockFeature(@NonNull CameraProperties cameraProperties); @@ -49,7 +50,8 @@ AutoFocusFeature createAutoFocusFeature( /** * Creates a new instance of the exposure offset feature. * - * @param cameraProperties instance of the CameraProperties class containing information about the cameras features. + * @param cameraProperties instance of the CameraProperties class containing information about the + * cameras features. * @return newly created instance of the ExposureOffsetFeature class. */ ExposureOffsetFeature createExposureOffsetFeature(@NonNull CameraProperties cameraProperties); @@ -57,7 +59,8 @@ AutoFocusFeature createAutoFocusFeature( /** * Creates a new instance of the flash feature. * - * @param cameraProperties instance of the CameraProperties class containing information about the cameras features. + * @param cameraProperties instance of the CameraProperties class containing information about the + * cameras features. * @return newly created instance of the FlashFeature class. */ FlashFeature createFlashFeature(@NonNull CameraProperties cameraProperties); @@ -65,7 +68,8 @@ AutoFocusFeature createAutoFocusFeature( /** * Creates a new instance of the resolution feature. * - * @param cameraProperties instance of the CameraProperties class containing information about the cameras features. + * @param cameraProperties instance of the CameraProperties class containing information about the + * cameras features. * @param initialSetting initial resolution preset. * @param cameraName the name of the camera which can be used to identify the camera device. * @return newly created instance of the ResolutionFeature class. @@ -78,18 +82,19 @@ ResolutionFeature createResolutionFeature( /** * Creates a new instance of the focus point feature. * - * @param cameraProperties instance of the CameraProperties class containing information about the cameras features. + * @param cameraProperties instance of the CameraProperties class containing information about the + * cameras features. * @param getCameraRegions function which is used to retrieve the current camera regions. * @return newly created instance of the FocusPointFeature class. */ FocusPointFeature createFocusPointFeature( - @NonNull CameraProperties cameraProperties, - Callable getCameraRegions); + @NonNull CameraProperties cameraProperties, Callable getCameraRegions); /** * Creates a new instance of the FPS range feature. * - * @param cameraProperties instance of the CameraProperties class containing information about the cameras features. + * @param cameraProperties instance of the CameraProperties class containing information about the + * cameras features. * @return newly created instance of the FpsRangeFeature class. */ FpsRangeFeature createFpsRangeFeature(@NonNull CameraProperties cameraProperties); @@ -97,9 +102,11 @@ FocusPointFeature createFocusPointFeature( /** * Creates a new instance of the sensor orientation feature. * - * @param cameraProperties instance of the CameraProperties class containing information about the cameras features. + * @param cameraProperties instance of the CameraProperties class containing information about the + * cameras features. * @param activity current activity associated with the camera plugin. - * @param dartMessenger instance of the DartMessenger class, used to send state updates back to Dart. + * @param dartMessenger instance of the DartMessenger class, used to send state updates back to + * Dart. * @return newly created instance of the SensorOrientationFeature class. */ SensorOrientationFeature createSensorOrientationFeature( @@ -110,7 +117,8 @@ SensorOrientationFeature createSensorOrientationFeature( /** * Creates a new instance of the zoom level feature. * - * @param cameraProperties instance of the CameraProperties class containing information about the cameras features. + * @param cameraProperties instance of the CameraProperties class containing information about the + * cameras features. * @return newly created instance of the ZoomLevelFeature class. */ ZoomLevelFeature createZoomLevelFeature(@NonNull CameraProperties cameraProperties); @@ -118,18 +126,20 @@ SensorOrientationFeature createSensorOrientationFeature( /** * Creates a new instance of the region boundaries feature. * - * @param cameraProperties instance of the CameraProperties class containing information about the cameras features. - * @param requestBuilder instance of the CaptureRequest.Builder class, used to inform the Camera2 API that the settings are updated. + * @param cameraProperties instance of the CameraProperties class containing information about the + * cameras features. + * @param requestBuilder instance of the CaptureRequest.Builder class, used to inform the Camera2 + * API that the settings are updated. * @return newly created instance of the RegionBoundariesFeature class. */ RegionBoundariesFeature createRegionBoundariesFeature( - @NonNull CameraProperties cameraProperties, - @NonNull CaptureRequest.Builder requestBuilder); + @NonNull CameraProperties cameraProperties, @NonNull CaptureRequest.Builder requestBuilder); /** * Creates a new instance of the exposure point feature. * - * @param cameraProperties instance of the CameraProperties class containing information about the cameras features. + * @param cameraProperties instance of the CameraProperties class containing information about the + * cameras features. * @param getCameraRegions function which is used to retrieve the current camera regions. * @return newly created instance of the ExposurePointFeature class. */ @@ -140,9 +150,9 @@ ExposurePointFeature createExposurePointFeature( /** * Creates a new instance of the noise reduction feature. * - * @param cameraProperties instance of the CameraProperties class containing information about the cameras features. + * @param cameraProperties instance of the CameraProperties class containing information about the + * cameras features. * @return newly created instance of the NoiseReductionFeature class. */ - NoiseReductionFeature createNoiseReductionFeature( - @NonNull CameraProperties cameraProperties); + NoiseReductionFeature createNoiseReductionFeature(@NonNull CameraProperties cameraProperties); } diff --git a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/CameraFeatureFactoryImpl.java b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/CameraFeatureFactoryImpl.java index b49a907f96ca..59e760a9b65b 100644 --- a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/CameraFeatureFactoryImpl.java +++ b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/CameraFeatureFactoryImpl.java @@ -29,8 +29,7 @@ public class CameraFeatureFactoryImpl implements CameraFeatureFactory { @Override public AutoFocusFeature createAutoFocusFeature( - @NonNull CameraProperties cameraProperties, - boolean recordingVideo) { + @NonNull CameraProperties cameraProperties, boolean recordingVideo) { return new AutoFocusFeature(cameraProperties, recordingVideo); } @@ -60,8 +59,7 @@ public ResolutionFeature createResolutionFeature( @Override public FocusPointFeature createFocusPointFeature( - @NonNull CameraProperties cameraProperties, - Callable getCameraRegions) { + @NonNull CameraProperties cameraProperties, Callable getCameraRegions) { return new FocusPointFeature(cameraProperties, getCameraRegions); } @@ -85,8 +83,7 @@ public ZoomLevelFeature createZoomLevelFeature(@NonNull CameraProperties cameraP @Override public RegionBoundariesFeature createRegionBoundariesFeature( - @NonNull CameraProperties cameraProperties, - @NonNull CaptureRequest.Builder requestBuilder) { + @NonNull CameraProperties cameraProperties, @NonNull CaptureRequest.Builder requestBuilder) { return new RegionBoundariesFeature(cameraProperties, requestBuilder); } diff --git a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/exposureoffset/ExposureOffsetFeature.java b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/exposureoffset/ExposureOffsetFeature.java index a78e180fb14e..4f799d6c3656 100644 --- a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/exposureoffset/ExposureOffsetFeature.java +++ b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/exposureoffset/ExposureOffsetFeature.java @@ -27,7 +27,9 @@ public ExposureOffsetFeature(CameraProperties cameraProperties) { } @Override - public String getDebugName() { return "ExposureOffsetFeature"; } + public String getDebugName() { + return "ExposureOffsetFeature"; + } @Override public ExposureOffsetValue getValue() { diff --git a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/exposurepoint/ExposurePointFeature.java b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/exposurepoint/ExposurePointFeature.java index 0307b8883de0..3fa910703d59 100644 --- a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/exposurepoint/ExposurePointFeature.java +++ b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/exposurepoint/ExposurePointFeature.java @@ -20,7 +20,8 @@ public class ExposurePointFeature extends CameraFeature { private final Callable getCameraRegions; private Point currentSetting = new Point(0.0, 0.0); - public ExposurePointFeature(CameraProperties cameraProperties, Callable getCameraRegions) { + public ExposurePointFeature( + CameraProperties cameraProperties, Callable getCameraRegions) { super(cameraProperties); this.getCameraRegions = getCameraRegions; } @@ -70,9 +71,7 @@ public void updateBuilder(CaptureRequest.Builder requestBuilder) { aeRect = getCameraRegions.call().getAEMeteringRectangle(); requestBuilder.set( CaptureRequest.CONTROL_AE_REGIONS, - aeRect == null - ? null - : new MeteringRectangle[] { aeRect }); + aeRect == null ? null : new MeteringRectangle[] {aeRect}); } catch (Exception e) { e.printStackTrace(); } diff --git a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/focuspoint/FocusPointFeature.java b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/focuspoint/FocusPointFeature.java index dd5bc6b84478..f234d4db0a01 100644 --- a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/focuspoint/FocusPointFeature.java +++ b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/focuspoint/FocusPointFeature.java @@ -17,7 +17,8 @@ public class FocusPointFeature extends CameraFeature { private final Callable getCameraRegions; private Point currentSetting = new Point(0.0, 0.0); - public FocusPointFeature(CameraProperties cameraProperties, Callable getCameraRegions) { + public FocusPointFeature( + CameraProperties cameraProperties, Callable getCameraRegions) { super(cameraProperties); this.getCameraRegions = getCameraRegions; } diff --git a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/regionboundaries/RegionBoundariesFeature.java b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/regionboundaries/RegionBoundariesFeature.java index 5b5264784bd5..e393e0d3f783 100644 --- a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/regionboundaries/RegionBoundariesFeature.java +++ b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/regionboundaries/RegionBoundariesFeature.java @@ -21,8 +21,7 @@ public class RegionBoundariesFeature extends CameraFeature { private CameraRegions cameraRegions; public RegionBoundariesFeature( - CameraProperties cameraProperties, - CaptureRequest.Builder requestBuilder) { + CameraProperties cameraProperties, CaptureRequest.Builder requestBuilder) { super(cameraProperties); // No distortion correction support 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 897fdb5162eb..c5e30cc79494 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 @@ -37,37 +37,30 @@ public void should_create_camera_plugin() throws CameraAccessException { when(mockCameraProperties.getCameraName()).thenReturn(cameraName); Camera camera = - new Camera( - mockActivity, - mockFlutterTexture, - mockCameraFeatureFactory, - mockDartMessenger, - mockCameraProperties, - resolutionPreset, - enableAudio); + new Camera( + mockActivity, + mockFlutterTexture, + mockCameraFeatureFactory, + mockDartMessenger, + mockCameraProperties, + resolutionPreset, + enableAudio); - verify(mockCameraFeatureFactory, times(1)) - .createAutoFocusFeature(mockCameraProperties, false); - verify(mockCameraFeatureFactory, times(1)) - .createExposureLockFeature(mockCameraProperties); + verify(mockCameraFeatureFactory, times(1)).createAutoFocusFeature(mockCameraProperties, false); + verify(mockCameraFeatureFactory, times(1)).createExposureLockFeature(mockCameraProperties); verify(mockCameraFeatureFactory, times(1)) .createExposurePointFeature(eq(mockCameraProperties), any()); - verify(mockCameraFeatureFactory, times(1)) - .createExposureOffsetFeature(mockCameraProperties); - verify(mockCameraFeatureFactory, times(1)) - .createFlashFeature(mockCameraProperties); + verify(mockCameraFeatureFactory, times(1)).createExposureOffsetFeature(mockCameraProperties); + verify(mockCameraFeatureFactory, times(1)).createFlashFeature(mockCameraProperties); verify(mockCameraFeatureFactory, times(1)) .createFocusPointFeature(eq(mockCameraProperties), any()); - verify(mockCameraFeatureFactory, times(1)) - .createFpsRangeFeature(mockCameraProperties); - verify(mockCameraFeatureFactory, times(1)) - .createNoiseReductionFeature(mockCameraProperties); + verify(mockCameraFeatureFactory, times(1)).createFpsRangeFeature(mockCameraProperties); + verify(mockCameraFeatureFactory, times(1)).createNoiseReductionFeature(mockCameraProperties); verify(mockCameraFeatureFactory, times(1)) .createResolutionFeature(mockCameraProperties, resolutionPreset, cameraName); verify(mockCameraFeatureFactory, times(1)) .createSensorOrientationFeature(mockCameraProperties, mockActivity, mockDartMessenger); - verify(mockCameraFeatureFactory, times(1)) - .createZoomLevelFeature(mockCameraProperties); + verify(mockCameraFeatureFactory, times(1)).createZoomLevelFeature(mockCameraProperties); verify(mockCameraFeatureFactory, never()).createRegionBoundariesFeature(any(), any()); assertNotNull("should create a camera", camera); } diff --git a/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/features/autofocus/AutoFocusFeatureTest.java b/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/features/autofocus/AutoFocusFeatureTest.java index 88461bc20ff3..f3b88a0ee595 100644 --- a/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/features/autofocus/AutoFocusFeatureTest.java +++ b/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/features/autofocus/AutoFocusFeatureTest.java @@ -16,8 +16,12 @@ import org.junit.Test; public class AutoFocusFeatureTest { - private static final int[] FOCUS_MODES_ONLY_OFF = new int[] { CameraCharacteristics.CONTROL_AF_MODE_OFF }; - private static final int[] FOCUS_MODES = new int[] { CameraCharacteristics.CONTROL_AF_MODE_OFF, CameraCharacteristics.CONTROL_AF_MODE_AUTO }; + private static final int[] FOCUS_MODES_ONLY_OFF = + new int[] {CameraCharacteristics.CONTROL_AF_MODE_OFF}; + private static final int[] FOCUS_MODES = + new int[] { + CameraCharacteristics.CONTROL_AF_MODE_OFF, CameraCharacteristics.CONTROL_AF_MODE_AUTO + }; @Test public void getDebugName_should_return_the_name_of_the_feature() { @@ -128,11 +132,13 @@ public void updateBuilder_should_set_control_mode_to_auto_when_focus_is_locked() autoFocusFeature.setValue(FocusMode.locked); autoFocusFeature.updateBuilder(mockBuilder); - verify(mockBuilder, times(1)).set(CaptureRequest.CONTROL_AF_MODE, CaptureRequest.CONTROL_AF_MODE_AUTO); + verify(mockBuilder, times(1)) + .set(CaptureRequest.CONTROL_AF_MODE, CaptureRequest.CONTROL_AF_MODE_AUTO); } @Test - public void updateBuilder_should_set_control_mode_to_continuous_video_when_focus_is_auto_and_recording_video() { + public void + updateBuilder_should_set_control_mode_to_continuous_video_when_focus_is_auto_and_recording_video() { CameraProperties mockCameraProperties = mock(CameraProperties.class); CaptureRequest.Builder mockBuilder = mock(CaptureRequest.Builder.class); AutoFocusFeature autoFocusFeature = new AutoFocusFeature(mockCameraProperties, true); @@ -143,11 +149,13 @@ public void updateBuilder_should_set_control_mode_to_continuous_video_when_focus autoFocusFeature.setValue(FocusMode.auto); autoFocusFeature.updateBuilder(mockBuilder); - verify(mockBuilder, times(1)).set(CaptureRequest.CONTROL_AF_MODE, CaptureRequest.CONTROL_AF_MODE_CONTINUOUS_VIDEO); + verify(mockBuilder, times(1)) + .set(CaptureRequest.CONTROL_AF_MODE, CaptureRequest.CONTROL_AF_MODE_CONTINUOUS_VIDEO); } @Test - public void updateBuilder_should_set_control_mode_to_continuous_video_when_focus_is_auto_and_not_recording_video() { + public void + updateBuilder_should_set_control_mode_to_continuous_video_when_focus_is_auto_and_not_recording_video() { CameraProperties mockCameraProperties = mock(CameraProperties.class); CaptureRequest.Builder mockBuilder = mock(CaptureRequest.Builder.class); AutoFocusFeature autoFocusFeature = new AutoFocusFeature(mockCameraProperties, false); @@ -158,6 +166,7 @@ public void updateBuilder_should_set_control_mode_to_continuous_video_when_focus autoFocusFeature.setValue(FocusMode.auto); autoFocusFeature.updateBuilder(mockBuilder); - verify(mockBuilder, times(1)).set(CaptureRequest.CONTROL_AF_MODE, CaptureRequest.CONTROL_AF_MODE_CONTINUOUS_PICTURE); + verify(mockBuilder, times(1)) + .set(CaptureRequest.CONTROL_AF_MODE, CaptureRequest.CONTROL_AF_MODE_CONTINUOUS_PICTURE); } } diff --git a/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/features/exposurelock/ExposureLockFeatureTest.java b/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/features/exposurelock/ExposureLockFeatureTest.java index 06e19f275acd..24277193fa52 100644 --- a/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/features/exposurelock/ExposureLockFeatureTest.java +++ b/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/features/exposurelock/ExposureLockFeatureTest.java @@ -48,12 +48,12 @@ public void checkIsSupported_should_return_true() { } @Test - public void updateBuilder_should_set_control_ae_lock_to_false_when_auto_exposure_is_set_to_auto() { + public void + updateBuilder_should_set_control_ae_lock_to_false_when_auto_exposure_is_set_to_auto() { CameraProperties mockCameraProperties = mock(CameraProperties.class); CaptureRequest.Builder mockBuilder = mock(CaptureRequest.Builder.class); ExposureLockFeature exposureLockFeature = new ExposureLockFeature(mockCameraProperties); - exposureLockFeature.setValue(ExposureMode.auto); exposureLockFeature.updateBuilder(mockBuilder); @@ -61,12 +61,12 @@ public void updateBuilder_should_set_control_ae_lock_to_false_when_auto_exposure } @Test - public void updateBuilder_should_set_control_ae_lock_to_false_when_auto_exposure_is_set_to_locked() { + public void + updateBuilder_should_set_control_ae_lock_to_false_when_auto_exposure_is_set_to_locked() { CameraProperties mockCameraProperties = mock(CameraProperties.class); CaptureRequest.Builder mockBuilder = mock(CaptureRequest.Builder.class); ExposureLockFeature exposureLockFeature = new ExposureLockFeature(mockCameraProperties); - exposureLockFeature.setValue(ExposureMode.locked); exposureLockFeature.updateBuilder(mockBuilder); diff --git a/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/features/exposureoffset/ExposureOffsetFeatureTest.java b/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/features/exposureoffset/ExposureOffsetFeatureTest.java index d822d8ec0d18..cfdca9974d0f 100644 --- a/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/features/exposureoffset/ExposureOffsetFeatureTest.java +++ b/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/features/exposureoffset/ExposureOffsetFeatureTest.java @@ -12,7 +12,8 @@ import org.junit.Test; public class ExposureOffsetFeatureTest { - private static final ExposureOffsetValue DEFAULT_EXPOSURE_OFFSET_VALUE = new ExposureOffsetValue(0, 0, 0); + private static final ExposureOffsetValue DEFAULT_EXPOSURE_OFFSET_VALUE = + new ExposureOffsetValue(0, 0, 0); @Test public void getDebugName_should_return_the_name_of_the_feature() { @@ -51,7 +52,8 @@ public void getValue_should_echo_the_set_value() { } @Test - public void getExposureOffsetStepSize_should_return_the_control_exposure_compensation_step_value() { + public void + getExposureOffsetStepSize_should_return_the_control_exposure_compensation_step_value() { CameraProperties mockCameraProperties = mock(CameraProperties.class); ExposureOffsetFeature exposureOffsetFeature = new ExposureOffsetFeature(mockCameraProperties); diff --git a/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/features/exposurepoint/ExposurePointFeatureTest.java b/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/features/exposurepoint/ExposurePointFeatureTest.java index 7455741e00b0..df9789229b29 100644 --- a/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/features/exposurepoint/ExposurePointFeatureTest.java +++ b/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/features/exposurepoint/ExposurePointFeatureTest.java @@ -22,7 +22,8 @@ public class ExposurePointFeatureTest { @Test public void getDebugName_should_return_the_name_of_the_feature() { CameraProperties mockCameraProperties = mock(CameraProperties.class); - ExposurePointFeature exposurePointFeature = new ExposurePointFeature(mockCameraProperties, () -> null); + ExposurePointFeature exposurePointFeature = + new ExposurePointFeature(mockCameraProperties, () -> null); assertEquals("ExposurePointFeature", exposurePointFeature.getDebugName()); } @@ -30,7 +31,8 @@ public void getDebugName_should_return_the_name_of_the_feature() { @Test public void getValue_should_return_default_point_if_not_set() { CameraProperties mockCameraProperties = mock(CameraProperties.class); - ExposurePointFeature exposurePointFeature = new ExposurePointFeature(mockCameraProperties, () -> null); + ExposurePointFeature exposurePointFeature = + new ExposurePointFeature(mockCameraProperties, () -> null); Point expectedPoint = new Point(0.0, 0.0); Point actualPoint = exposurePointFeature.getValue(); @@ -42,7 +44,8 @@ public void getValue_should_return_default_point_if_not_set() { public void getValue_should_echo_the_set_value() { CameraProperties mockCameraProperties = mock(CameraProperties.class); CameraRegions mockCameraRegions = mock(CameraRegions.class); - ExposurePointFeature exposurePointFeature = new ExposurePointFeature(mockCameraProperties, () -> mockCameraRegions); + ExposurePointFeature exposurePointFeature = + new ExposurePointFeature(mockCameraProperties, () -> mockCameraRegions); Point expectedPoint = new Point(0.0, 0.0); exposurePointFeature.setValue(expectedPoint); @@ -55,7 +58,8 @@ public void getValue_should_echo_the_set_value() { public void setValue_should_reset_point_when_x_coord_is_null() { CameraProperties mockCameraProperties = mock(CameraProperties.class); CameraRegions mockCameraRegions = mock(CameraRegions.class); - ExposurePointFeature exposurePointFeature = new ExposurePointFeature(mockCameraProperties, () -> mockCameraRegions); + ExposurePointFeature exposurePointFeature = + new ExposurePointFeature(mockCameraProperties, () -> mockCameraRegions); exposurePointFeature.setValue(new Point(null, 0.0)); @@ -66,7 +70,8 @@ public void setValue_should_reset_point_when_x_coord_is_null() { public void setValue_should_reset_point_when_y_coord_is_null() { CameraProperties mockCameraProperties = mock(CameraProperties.class); CameraRegions mockCameraRegions = mock(CameraRegions.class); - ExposurePointFeature exposurePointFeature = new ExposurePointFeature(mockCameraProperties, () -> mockCameraRegions); + ExposurePointFeature exposurePointFeature = + new ExposurePointFeature(mockCameraProperties, () -> mockCameraRegions); exposurePointFeature.setValue(new Point(0.0, null)); @@ -77,7 +82,8 @@ public void setValue_should_reset_point_when_y_coord_is_null() { public void setValue_should_reset_point_when_valid_coords_are_supplied() { CameraProperties mockCameraProperties = mock(CameraProperties.class); CameraRegions mockCameraRegions = mock(CameraRegions.class); - ExposurePointFeature exposurePointFeature = new ExposurePointFeature(mockCameraProperties, () -> mockCameraRegions); + ExposurePointFeature exposurePointFeature = + new ExposurePointFeature(mockCameraProperties, () -> mockCameraRegions); Point point = new Point(0.0, 0.0); exposurePointFeature.setValue(point); @@ -88,7 +94,8 @@ public void setValue_should_reset_point_when_valid_coords_are_supplied() { @Test public void checkIsSupported_should_return_false_when_max_regions_is_null() { CameraProperties mockCameraProperties = mock(CameraProperties.class); - ExposurePointFeature exposurePointFeature = new ExposurePointFeature(mockCameraProperties, () -> null); + ExposurePointFeature exposurePointFeature = + new ExposurePointFeature(mockCameraProperties, () -> null); when(mockCameraProperties.getControlMaxRegionsAutoExposure()).thenReturn(null); @@ -98,7 +105,8 @@ public void checkIsSupported_should_return_false_when_max_regions_is_null() { @Test public void checkIsSupported_should_return_false_when_max_regions_is_zero() { CameraProperties mockCameraProperties = mock(CameraProperties.class); - ExposurePointFeature exposurePointFeature = new ExposurePointFeature(mockCameraProperties, () -> null); + ExposurePointFeature exposurePointFeature = + new ExposurePointFeature(mockCameraProperties, () -> null); when(mockCameraProperties.getControlMaxRegionsAutoExposure()).thenReturn(0); @@ -108,7 +116,8 @@ public void checkIsSupported_should_return_false_when_max_regions_is_zero() { @Test public void checkIsSupported_should_return_true_when_max_regions_is_bigger_then_zero() { CameraProperties mockCameraProperties = mock(CameraProperties.class); - ExposurePointFeature exposurePointFeature = new ExposurePointFeature(mockCameraProperties, () -> null); + ExposurePointFeature exposurePointFeature = + new ExposurePointFeature(mockCameraProperties, () -> null); when(mockCameraProperties.getControlMaxRegionsAutoExposure()).thenReturn(1); @@ -119,7 +128,8 @@ public void checkIsSupported_should_return_true_when_max_regions_is_bigger_then_ public void updateBuilder_should_return_when_checkIsSupported_is_false() { CameraProperties mockCameraProperties = mock(CameraProperties.class); CameraRegions mockCameraRegions = mock(CameraRegions.class); - ExposurePointFeature exposurePointFeature = new ExposurePointFeature(mockCameraProperties, () -> mockCameraRegions); + ExposurePointFeature exposurePointFeature = + new ExposurePointFeature(mockCameraProperties, () -> mockCameraRegions); when(mockCameraProperties.getControlMaxRegionsAutoExposure()).thenReturn(0); @@ -133,7 +143,8 @@ public void updateBuilder_should_set_ae_regions_to_null_when_ae_metering_rectang CameraProperties mockCameraProperties = mock(CameraProperties.class); CameraRegions mockCameraRegions = mock(CameraRegions.class); CaptureRequest.Builder mockBuilder = mock(CaptureRequest.Builder.class); - ExposurePointFeature exposurePointFeature = new ExposurePointFeature(mockCameraProperties, () -> mockCameraRegions); + ExposurePointFeature exposurePointFeature = + new ExposurePointFeature(mockCameraProperties, () -> mockCameraRegions); when(mockCameraProperties.getControlMaxRegionsAutoExposure()).thenReturn(1); when(mockCameraRegions.getAEMeteringRectangle()).thenReturn(null); @@ -148,15 +159,17 @@ public void updateBuilder_should_set_ae_regions_with_metering_rectangle() { CameraProperties mockCameraProperties = mock(CameraProperties.class); CameraRegions mockCameraRegions = mock(CameraRegions.class); CaptureRequest.Builder mockBuilder = mock(CaptureRequest.Builder.class); - ExposurePointFeature exposurePointFeature = new ExposurePointFeature(mockCameraProperties, () -> mockCameraRegions); - MeteringRectangle meteringRectangle = new MeteringRectangle(0,0,0,0,0); + ExposurePointFeature exposurePointFeature = + new ExposurePointFeature(mockCameraProperties, () -> mockCameraRegions); + MeteringRectangle meteringRectangle = new MeteringRectangle(0, 0, 0, 0, 0); when(mockCameraProperties.getControlMaxRegionsAutoExposure()).thenReturn(1); when(mockCameraRegions.getAEMeteringRectangle()).thenReturn(meteringRectangle); exposurePointFeature.updateBuilder(mockBuilder); - verify(mockBuilder, times(1)).set(eq(CaptureRequest.CONTROL_AE_REGIONS), any(MeteringRectangle[].class)); + verify(mockBuilder, times(1)) + .set(eq(CaptureRequest.CONTROL_AE_REGIONS), any(MeteringRectangle[].class)); } @Test @@ -164,8 +177,9 @@ public void updateBuilder_should_silently_fail_when_exception_occurs() { CameraProperties mockCameraProperties = mock(CameraProperties.class); CameraRegions mockCameraRegions = mock(CameraRegions.class); CaptureRequest.Builder mockBuilder = mock(CaptureRequest.Builder.class); - ExposurePointFeature exposurePointFeature = new ExposurePointFeature(mockCameraProperties, () -> mockCameraRegions); - MeteringRectangle meteringRectangle = new MeteringRectangle(0,0,0,0,0); + ExposurePointFeature exposurePointFeature = + new ExposurePointFeature(mockCameraProperties, () -> mockCameraRegions); + MeteringRectangle meteringRectangle = new MeteringRectangle(0, 0, 0, 0, 0); when(mockCameraProperties.getControlMaxRegionsAutoExposure()).thenReturn(1); when(mockCameraRegions.getAEMeteringRectangle()).thenThrow(new IllegalArgumentException()); diff --git a/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/features/flash/FlashFeatureTest.java b/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/features/flash/FlashFeatureTest.java index cf303fa67b2c..cee31a2f91a8 100644 --- a/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/features/flash/FlashFeatureTest.java +++ b/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/features/flash/FlashFeatureTest.java @@ -97,7 +97,8 @@ public void updateBuilder_should_set_ae_mode_and_flash_mode_when_flash_mode_is_o flashFeature.setValue(FlashMode.off); flashFeature.updateBuilder(mockBuilder); - verify(mockBuilder, times(1)).set(CaptureRequest.CONTROL_AE_MODE, CaptureRequest.CONTROL_AE_MODE_ON); + verify(mockBuilder, times(1)) + .set(CaptureRequest.CONTROL_AE_MODE, CaptureRequest.CONTROL_AE_MODE_ON); verify(mockBuilder, times(1)).set(CaptureRequest.FLASH_MODE, CaptureRequest.FLASH_MODE_OFF); } @@ -112,7 +113,8 @@ public void updateBuilder_should_set_ae_mode_and_flash_mode_when_flash_mode_is_a flashFeature.setValue(FlashMode.always); flashFeature.updateBuilder(mockBuilder); - verify(mockBuilder, times(1)).set(CaptureRequest.CONTROL_AE_MODE, CaptureRequest.CONTROL_AE_MODE_ON_ALWAYS_FLASH); + verify(mockBuilder, times(1)) + .set(CaptureRequest.CONTROL_AE_MODE, CaptureRequest.CONTROL_AE_MODE_ON_ALWAYS_FLASH); verify(mockBuilder, times(1)).set(CaptureRequest.FLASH_MODE, CaptureRequest.FLASH_MODE_OFF); } @@ -127,7 +129,8 @@ public void updateBuilder_should_set_ae_mode_and_flash_mode_when_flash_mode_is_t flashFeature.setValue(FlashMode.torch); flashFeature.updateBuilder(mockBuilder); - verify(mockBuilder, times(1)).set(CaptureRequest.CONTROL_AE_MODE, CaptureRequest.CONTROL_AE_MODE_ON); + verify(mockBuilder, times(1)) + .set(CaptureRequest.CONTROL_AE_MODE, CaptureRequest.CONTROL_AE_MODE_ON); verify(mockBuilder, times(1)).set(CaptureRequest.FLASH_MODE, CaptureRequest.FLASH_MODE_TORCH); } diff --git a/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/features/focuspoint/FocusPointFeatureTest.java b/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/features/focuspoint/FocusPointFeatureTest.java index 9ea8570c65d0..dffa51ce380c 100644 --- a/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/features/focuspoint/FocusPointFeatureTest.java +++ b/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/features/focuspoint/FocusPointFeatureTest.java @@ -37,7 +37,8 @@ public void getValue_should_return_default_point_if_not_set() { public void getValue_should_echo_the_set_value() { CameraProperties mockCameraProperties = mock(CameraProperties.class); CameraRegions mockCameraRegions = mock(CameraRegions.class); - FocusPointFeature focusPointFeature = new FocusPointFeature(mockCameraProperties, () -> mockCameraRegions); + FocusPointFeature focusPointFeature = + new FocusPointFeature(mockCameraProperties, () -> mockCameraRegions); Point expectedPoint = new Point(0.0, 0.0); focusPointFeature.setValue(expectedPoint); @@ -50,7 +51,8 @@ public void getValue_should_echo_the_set_value() { public void setValue_should_reset_point_when_x_coord_is_null() { CameraProperties mockCameraProperties = mock(CameraProperties.class); CameraRegions mockCameraRegions = mock(CameraRegions.class); - FocusPointFeature focusPointFeature = new FocusPointFeature(mockCameraProperties, () -> mockCameraRegions); + FocusPointFeature focusPointFeature = + new FocusPointFeature(mockCameraProperties, () -> mockCameraRegions); focusPointFeature.setValue(new Point(null, 0.0)); @@ -61,7 +63,8 @@ public void setValue_should_reset_point_when_x_coord_is_null() { public void setValue_should_reset_point_when_y_coord_is_null() { CameraProperties mockCameraProperties = mock(CameraProperties.class); CameraRegions mockCameraRegions = mock(CameraRegions.class); - FocusPointFeature focusPointFeature = new FocusPointFeature(mockCameraProperties, () -> mockCameraRegions); + FocusPointFeature focusPointFeature = + new FocusPointFeature(mockCameraProperties, () -> mockCameraRegions); focusPointFeature.setValue(new Point(0.0, null)); @@ -72,7 +75,8 @@ public void setValue_should_reset_point_when_y_coord_is_null() { public void setValue_should_reset_point_when_valid_coords_are_supplied() { CameraProperties mockCameraProperties = mock(CameraProperties.class); CameraRegions mockCameraRegions = mock(CameraRegions.class); - FocusPointFeature focusPointFeature = new FocusPointFeature(mockCameraProperties, () -> mockCameraRegions); + FocusPointFeature focusPointFeature = + new FocusPointFeature(mockCameraProperties, () -> mockCameraRegions); Point point = new Point(0.0, 0.0); focusPointFeature.setValue(point); diff --git a/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/features/fpsrange/FpsRangeFeatureTest.java b/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/features/fpsrange/FpsRangeFeatureTest.java index f495e57d27ff..22d9f4aeae3e 100644 --- a/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/features/fpsrange/FpsRangeFeatureTest.java +++ b/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/features/fpsrange/FpsRangeFeatureTest.java @@ -31,7 +31,7 @@ public void getValue_should_return_highest_upper_range_if_not_set() { CameraProperties mockCameraProperties = mock(CameraProperties.class); FpsRangeFeature fpsRangeFeature = createTestInstance(); - assertEquals(13, (int)fpsRangeFeature.getValue().getUpper()); + assertEquals(13, (int) fpsRangeFeature.getValue().getUpper()); } @Test @@ -77,7 +77,7 @@ private static FpsRangeFeature createTestInstance() { when(rangeThree.getUpper()).thenReturn(13); @SuppressWarnings("unchecked") - Range[] ranges = new Range[] { rangeOne, rangeTwo, rangeThree }; + Range[] ranges = new Range[] {rangeOne, rangeTwo, rangeThree}; CameraProperties cameraProperties = mock(CameraProperties.class); From 50c8754de6851af524c6156580babfe84b8244c7 Mon Sep 17 00:00:00 2001 From: Andrew Coutts Date: Wed, 10 Mar 2021 07:45:13 -0500 Subject: [PATCH 087/114] Factory constructor for PictureCaptureRequest and Timeout --- .../io/flutter/plugins/camera/Camera.java | 2 +- .../plugins/camera/PictureCaptureRequest.java | 22 ++++++++++++++++--- .../io/flutter/plugins/camera/Timeout.java | 11 +++++++++- 3 files changed, 30 insertions(+), 5 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 27284acb16c7..7a234c79b27a 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 @@ -512,7 +512,7 @@ public void takePicture(@NonNull final Result result) { final File file = File.createTempFile("CAP", ".jpg", outputDir); // Start a new capture - pictureCaptureRequest = new PictureCaptureRequest(result, dartMessenger, file, 3000, 3000); + pictureCaptureRequest = PictureCaptureRequest.create(result, dartMessenger, file, 3000, 3000); mCaptureCallback.setPictureCaptureRequest(pictureCaptureRequest); } catch (IOException | SecurityException e) { pictureCaptureRequest.error("cannotCreateFile", e.getMessage(), null); diff --git a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/PictureCaptureRequest.java b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/PictureCaptureRequest.java index acf1576459cf..982d330e898f 100644 --- a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/PictureCaptureRequest.java +++ b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/PictureCaptureRequest.java @@ -26,8 +26,24 @@ public class PictureCaptureRequest { /** To send errors back to dart */ private final DartMessenger dartMessenger; + /** + * Factory method to create a picture capture request. + * + * @param result + * @param file + */ + static PictureCaptureRequest create( + MethodChannel.Result result, + DartMessenger dartMessenger, + File file, + long preCaptureFocusingTimeoutMs, + long preCaptureMeteringTimeoutMs) { + return new PictureCaptureRequest( + result, dartMessenger, file, preCaptureFocusingTimeoutMs, preCaptureMeteringTimeoutMs); + } + /** Create a new picture capture request */ - public PictureCaptureRequest( + private PictureCaptureRequest( MethodChannel.Result result, DartMessenger dartMessenger, File file, @@ -36,8 +52,8 @@ public PictureCaptureRequest( this.result = result; this.dartMessenger = dartMessenger; this.file = file; - this.preCaptureFocusing = new Timeout(preCaptureFocusingTimeoutMs); - this.preCaptureMetering = new Timeout(preCaptureMeteringTimeoutMs); + this.preCaptureFocusing = Timeout.create(preCaptureFocusingTimeoutMs); + this.preCaptureMetering = Timeout.create(preCaptureMeteringTimeoutMs); } /** diff --git a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/Timeout.java b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/Timeout.java index 37e6d66ce3e2..fa5e9b706cc5 100644 --- a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/Timeout.java +++ b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/Timeout.java @@ -24,7 +24,16 @@ class Timeout { /** When this timeout was started. Will be used later to check if the timeout has expired yet. */ private final long timeStarted; - public Timeout(long timeoutMs) { + static Timeout create(long timeoutMs) { + return new Timeout(timeoutMs); + } + + /** + * Create a new timeout. + * + * @param timeoutMs the time in milliseconds for this timeout to lapse. + */ + private Timeout(long timeoutMs) { this.timeoutMs = timeoutMs; this.timeStarted = SystemClock.elapsedRealtime(); } From 944b2a7ea4f7daafa98aa4a94170cbc9eb269bf3 Mon Sep 17 00:00:00 2001 From: Andrew Coutts Date: Wed, 10 Mar 2021 07:48:18 -0500 Subject: [PATCH 088/114] Comments fix on picture capture request / timeout factory constructors --- .../plugins/camera/PictureCaptureRequest.java | 10 +++++--- .../io/flutter/plugins/camera/Timeout.java | 6 +++++ .../noisereduction/NoiseReductionFeature.java | 13 ++++++----- .../noisereduction/NoiseReductionMode.java | 2 -- .../NoiseReductionFeatureTest.java | 23 +++++++++++-------- 5 files changed, 34 insertions(+), 20 deletions(-) diff --git a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/PictureCaptureRequest.java b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/PictureCaptureRequest.java index 982d330e898f..582ed2ba0b69 100644 --- a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/PictureCaptureRequest.java +++ b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/PictureCaptureRequest.java @@ -27,10 +27,14 @@ public class PictureCaptureRequest { private final DartMessenger dartMessenger; /** - * Factory method to create a picture capture request. + * Factory method to create a picture capture request * - * @param result - * @param file + * @param result dart result. + * @param dartMessenger dart messenger. + * @param file file to capture into. + * @param preCaptureFocusingTimeoutMs focusing timeout milliseconds. + * @param preCaptureMeteringTimeoutMs metering timeout milliseconds. + * @return returns a new PictureCaptureRequest. */ static PictureCaptureRequest create( MethodChannel.Result result, diff --git a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/Timeout.java b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/Timeout.java index fa5e9b706cc5..083fee80fa74 100644 --- a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/Timeout.java +++ b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/Timeout.java @@ -24,6 +24,12 @@ class Timeout { /** When this timeout was started. Will be used later to check if the timeout has expired yet. */ private final long timeStarted; + /** + * Factory method to create a new Timeout. + * + * @param timeoutMs timeout to use. + * @return returns a new Timeout. + */ static Timeout create(long timeoutMs) { return new Timeout(timeoutMs); } diff --git a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/noisereduction/NoiseReductionFeature.java b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/noisereduction/NoiseReductionFeature.java index 8d2093035b06..b24fda4bdd8d 100644 --- a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/noisereduction/NoiseReductionFeature.java +++ b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/noisereduction/NoiseReductionFeature.java @@ -21,19 +21,20 @@ public class NoiseReductionFeature extends CameraFeature { private NoiseReductionMode currentSetting = NoiseReductionMode.fast; private static final HashMap NOISE_REDUCTION_MODES = new HashMap<>(); + static { NOISE_REDUCTION_MODES.put(NoiseReductionMode.off, CaptureRequest.NOISE_REDUCTION_MODE_OFF); NOISE_REDUCTION_MODES.put(NoiseReductionMode.fast, CaptureRequest.NOISE_REDUCTION_MODE_FAST); - NOISE_REDUCTION_MODES.put(NoiseReductionMode.highQuality, CaptureRequest.NOISE_REDUCTION_MODE_HIGH_QUALITY); + NOISE_REDUCTION_MODES.put( + NoiseReductionMode.highQuality, CaptureRequest.NOISE_REDUCTION_MODE_HIGH_QUALITY); if (VERSION.SDK_INT >= VERSION_CODES.M) { - NOISE_REDUCTION_MODES - .put(NoiseReductionMode.minimal, CaptureRequest.NOISE_REDUCTION_MODE_MINIMAL); - NOISE_REDUCTION_MODES.put(NoiseReductionMode.zeroShutterLag, - CaptureRequest.NOISE_REDUCTION_MODE_ZERO_SHUTTER_LAG); + NOISE_REDUCTION_MODES.put( + NoiseReductionMode.minimal, CaptureRequest.NOISE_REDUCTION_MODE_MINIMAL); + NOISE_REDUCTION_MODES.put( + NoiseReductionMode.zeroShutterLag, CaptureRequest.NOISE_REDUCTION_MODE_ZERO_SHUTTER_LAG); } } - public NoiseReductionFeature(CameraProperties cameraProperties) { super(cameraProperties); } diff --git a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/noisereduction/NoiseReductionMode.java b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/noisereduction/NoiseReductionMode.java index b837f21e5de7..5219fd4c046e 100644 --- a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/noisereduction/NoiseReductionMode.java +++ b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/noisereduction/NoiseReductionMode.java @@ -12,8 +12,6 @@ public enum NoiseReductionMode { minimal("minimal"), zeroShutterLag("zeroShutterLag"); - - private final String strValue; NoiseReductionMode(String strValue) { diff --git a/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/features/noisereduction/NoiseReductionFeatureTest.java b/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/features/noisereduction/NoiseReductionFeatureTest.java index 3ea600dc539e..f123fb57cf5d 100644 --- a/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/features/noisereduction/NoiseReductionFeatureTest.java +++ b/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/features/noisereduction/NoiseReductionFeatureTest.java @@ -73,21 +73,23 @@ public void checkIsSupported_should_return_false_when_available_noise_reduction_ } @Test - public void checkIsSupported_should_return_false_when_available_noise_reduction_modes_returns_an_empty_array() { + public void + checkIsSupported_should_return_false_when_available_noise_reduction_modes_returns_an_empty_array() { CameraProperties mockCameraProperties = mock(CameraProperties.class); NoiseReductionFeature noiseReductionFeature = new NoiseReductionFeature(mockCameraProperties); - when(mockCameraProperties.getAvailableNoiseReductionModes()).thenReturn(new int[] { }); + when(mockCameraProperties.getAvailableNoiseReductionModes()).thenReturn(new int[] {}); assertFalse(noiseReductionFeature.checkIsSupported()); } @Test - public void checkIsSupported_should_return_true_when_available_noise_reduction_modes_returns_at_least_one_item() { + public void + checkIsSupported_should_return_true_when_available_noise_reduction_modes_returns_at_least_one_item() { CameraProperties mockCameraProperties = mock(CameraProperties.class); NoiseReductionFeature noiseReductionFeature = new NoiseReductionFeature(mockCameraProperties); - when(mockCameraProperties.getAvailableNoiseReductionModes()).thenReturn(new int[] { 1 }); + when(mockCameraProperties.getAvailableNoiseReductionModes()).thenReturn(new int[] {1}); assertTrue(noiseReductionFeature.checkIsSupported()); } @@ -98,7 +100,7 @@ public void updateBuilder_should_return_when_checkIsSupported_is_false() { CaptureRequest.Builder mockBuilder = mock(CaptureRequest.Builder.class); NoiseReductionFeature noiseReductionFeature = new NoiseReductionFeature(mockCameraProperties); - when(mockCameraProperties.getAvailableNoiseReductionModes()).thenReturn(new int[] { }); + when(mockCameraProperties.getAvailableNoiseReductionModes()).thenReturn(new int[] {}); noiseReductionFeature.updateBuilder(mockBuilder); @@ -117,7 +119,8 @@ public void updateBuilder_should_set_noise_reduction_mode_fast_when_fast() { @Test public void updateBuilder_should_set_noise_reduction_mode_high_quality_when_high_quality() { - testUpdateBuilderWith(NoiseReductionMode.highQuality, CaptureRequest.NOISE_REDUCTION_MODE_HIGH_QUALITY); + testUpdateBuilderWith( + NoiseReductionMode.highQuality, CaptureRequest.NOISE_REDUCTION_MODE_HIGH_QUALITY); } @Test @@ -126,8 +129,10 @@ public void updateBuilder_should_set_noise_reduction_mode_minimal_when_minimal() } @Test - public void updateBuilder_should_set_noise_reduction_mode_zero_shutter_lag_when_zero_shutter_lag() { - testUpdateBuilderWith(NoiseReductionMode.zeroShutterLag, CaptureRequest.NOISE_REDUCTION_MODE_ZERO_SHUTTER_LAG); + public void + updateBuilder_should_set_noise_reduction_mode_zero_shutter_lag_when_zero_shutter_lag() { + testUpdateBuilderWith( + NoiseReductionMode.zeroShutterLag, CaptureRequest.NOISE_REDUCTION_MODE_ZERO_SHUTTER_LAG); } private static void testUpdateBuilderWith(NoiseReductionMode mode, int expectedResult) { @@ -135,7 +140,7 @@ private static void testUpdateBuilderWith(NoiseReductionMode mode, int expectedR CaptureRequest.Builder mockBuilder = mock(CaptureRequest.Builder.class); NoiseReductionFeature noiseReductionFeature = new NoiseReductionFeature(mockCameraProperties); - when(mockCameraProperties.getAvailableNoiseReductionModes()).thenReturn(new int[] { 1 }); + when(mockCameraProperties.getAvailableNoiseReductionModes()).thenReturn(new int[] {1}); noiseReductionFeature.setValue(mode); noiseReductionFeature.updateBuilder(mockBuilder); From e45c958d2939db784e72a7003eda3f4fcaf0fe52 Mon Sep 17 00:00:00 2001 From: Maurits van Beusekom Date: Wed, 10 Mar 2021 14:42:34 +0100 Subject: [PATCH 089/114] Added test coverage to region boundries feature --- .../RegionBoundariesFeature.java | 35 ++- .../NoiseReductionFeatureTest.java | 17 +- .../RegionBoundariesFeatureTest.java | 224 ++++++++++++++++++ 3 files changed, 254 insertions(+), 22 deletions(-) create mode 100644 packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/features/regionboundaries/RegionBoundariesFeatureTest.java diff --git a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/regionboundaries/RegionBoundariesFeature.java b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/regionboundaries/RegionBoundariesFeature.java index e393e0d3f783..f6fec4db1d6f 100644 --- a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/regionboundaries/RegionBoundariesFeature.java +++ b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/regionboundaries/RegionBoundariesFeature.java @@ -17,8 +17,8 @@ * CaptureRequestBuilder for which we can read the distortion correction settings from. */ public class RegionBoundariesFeature extends CameraFeature { + private final CameraRegions cameraRegions; private Size currentSetting; - private CameraRegions cameraRegions; public RegionBoundariesFeature( CameraProperties cameraProperties, CaptureRequest.Builder requestBuilder) { @@ -28,33 +28,30 @@ public RegionBoundariesFeature( if (android.os.Build.VERSION.SDK_INT < Build.VERSION_CODES.P || !supportsDistortionCorrection()) { setValue(cameraProperties.getSensorInfoPixelArraySize()); - } - - // Get the current distortion correction mode - Integer distortionCorrectionMode = null; - if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.P) { - distortionCorrectionMode = requestBuilder.get(CaptureRequest.DISTORTION_CORRECTION_MODE); - } - - // Return the correct boundaries depending on the mode - android.graphics.Rect rect; - if (distortionCorrectionMode == null - || distortionCorrectionMode == CaptureRequest.DISTORTION_CORRECTION_MODE_OFF) { - rect = cameraProperties.getSensorInfoPreCorrectionActiveArraySize(); } else { - rect = cameraProperties.getSensorInfoActiveArraySize(); + // Get the current distortion correction mode + Integer distortionCorrectionMode = requestBuilder.get(CaptureRequest.DISTORTION_CORRECTION_MODE); + + // Return the correct boundaries depending on the mode + android.graphics.Rect rect; + if (distortionCorrectionMode == null + || distortionCorrectionMode == CaptureRequest.DISTORTION_CORRECTION_MODE_OFF) { + rect = cameraProperties.getSensorInfoPreCorrectionActiveArraySize(); + } else { + rect = cameraProperties.getSensorInfoActiveArraySize(); + } + + // Set new region size + setValue(rect == null ? null : new Size(rect.width(), rect.height())); } - // Set new region size - setValue(rect == null ? null : new Size(rect.width(), rect.height())); - // Create new camera regions using new size cameraRegions = new CameraRegions(currentSetting); } @Override public String getDebugName() { - return "RegionBoundaries"; + return "RegionBoundariesFeature"; } @Override diff --git a/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/features/noisereduction/NoiseReductionFeatureTest.java b/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/features/noisereduction/NoiseReductionFeatureTest.java index 3ea600dc539e..1647f5659f2b 100644 --- a/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/features/noisereduction/NoiseReductionFeatureTest.java +++ b/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/features/noisereduction/NoiseReductionFeatureTest.java @@ -18,13 +18,14 @@ import android.os.Build.VERSION; import io.flutter.plugins.camera.CameraProperties; import io.flutter.plugins.camera.utils.TestUtils; +import org.junit.After; import org.junit.Assert; -import org.junit.BeforeClass; +import org.junit.Before; import org.junit.Test; public class NoiseReductionFeatureTest { - @BeforeClass - public static void beforeClass() { + @Before + public void before() { // Make sure the VERSION.SDK_INT field returns 23, to allow using all available // noise reduction modes in tests. try { @@ -34,6 +35,16 @@ public static void beforeClass() { } } + @After + public void after() { + // Make sure we reset the VERSION.SDK_INT field to it's original value. + try { + TestUtils.setFinalStatic(VERSION.class.getField("SDK_INT"), 0); + } catch (Exception e) { + Assert.fail("Unable to set SDK_INT"); + } + } + @Test public void getDebugName_should_return_the_name_of_the_feature() { CameraProperties mockCameraProperties = mock(CameraProperties.class); diff --git a/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/features/regionboundaries/RegionBoundariesFeatureTest.java b/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/features/regionboundaries/RegionBoundariesFeatureTest.java new file mode 100644 index 000000000000..7c23bf3cbad5 --- /dev/null +++ b/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/features/regionboundaries/RegionBoundariesFeatureTest.java @@ -0,0 +1,224 @@ +package io.flutter.plugins.camera.features.regionboundaries; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import android.hardware.camera2.CaptureRequest; +import android.os.Build.VERSION; +import android.os.Build.VERSION_CODES; +import android.util.Size; +import io.flutter.plugins.camera.CameraProperties; +import io.flutter.plugins.camera.utils.TestUtils; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; + +public class RegionBoundariesFeatureTest { + private Size mockSize; + + @Before + public void before() { + mockSize = mock(Size.class); + + when(mockSize.getHeight()).thenReturn(640); + when(mockSize.getWidth()).thenReturn(480); + } + + @Test + public void ctor_should_initialize_with_sensor_info_pixel_array_size_when_running_pre_android_p() { + updateSdkVersion(VERSION_CODES.O_MR1); + + try { + CameraProperties mockCameraProperties = mock(CameraProperties.class); + CaptureRequest.Builder mockBuilder = mock(CaptureRequest.Builder.class); + + when(mockCameraProperties.getSensorInfoPixelArraySize()).thenReturn(mockSize); + + RegionBoundariesFeature regionBoundariesFeature = new RegionBoundariesFeature(mockCameraProperties, mockBuilder); + + assertEquals(mockSize, regionBoundariesFeature.getValue()); + verify(mockCameraProperties, never()).getSensorInfoPreCorrectionActiveArraySize(); + verify(mockCameraProperties, never()).getSensorInfoActiveArraySize(); + } + finally { + updateSdkVersion(0); + } + } + + @Test + public void ctor_should_initialize_with_sensor_info_pixel_array_size_when_distortion_correction_is_null() { + updateSdkVersion(VERSION_CODES.P); + + try { + CameraProperties mockCameraProperties = mock(CameraProperties.class); + CaptureRequest.Builder mockBuilder = mock(CaptureRequest.Builder.class); + + when(mockCameraProperties.getDistortionCorrectionAvailableModes()).thenReturn(null); + when(mockCameraProperties.getSensorInfoPixelArraySize()).thenReturn(mockSize); + + RegionBoundariesFeature regionBoundariesFeature = new RegionBoundariesFeature(mockCameraProperties, mockBuilder); + + assertEquals(mockSize, regionBoundariesFeature.getValue()); + verify(mockCameraProperties, never()).getSensorInfoPreCorrectionActiveArraySize(); + verify(mockCameraProperties, never()).getSensorInfoActiveArraySize(); + } + finally { + updateSdkVersion(0); + } + } + + @Test + public void ctor_should_initialize_with_sensor_info_pixel_array_size_when_distortion_correction_is_off() { + updateSdkVersion(VERSION_CODES.P); + + try { + CameraProperties mockCameraProperties = mock(CameraProperties.class); + CaptureRequest.Builder mockBuilder = mock(CaptureRequest.Builder.class); + + when(mockCameraProperties.getDistortionCorrectionAvailableModes()).thenReturn(new int[] { CaptureRequest.DISTORTION_CORRECTION_MODE_OFF }); + when(mockCameraProperties.getSensorInfoPixelArraySize()).thenReturn(mockSize); + + RegionBoundariesFeature regionBoundariesFeature = new RegionBoundariesFeature(mockCameraProperties, mockBuilder); + + assertEquals(mockSize, regionBoundariesFeature.getValue()); + verify(mockCameraProperties, never()).getSensorInfoPreCorrectionActiveArraySize(); + verify(mockCameraProperties, never()).getSensorInfoActiveArraySize(); + } + finally { + updateSdkVersion(0); + } + } + + @Test + public void ctor_should_initialize_with_sensor_info_pre_correction_active_array_size_when_distortion_correction_mode_is_set_to_null() { + updateSdkVersion(VERSION_CODES.P); + + try { + CameraProperties mockCameraProperties = mock(CameraProperties.class); + CaptureRequest.Builder mockBuilder = mock(CaptureRequest.Builder.class); + + when(mockCameraProperties.getDistortionCorrectionAvailableModes()).thenReturn(new int[] { + CaptureRequest.DISTORTION_CORRECTION_MODE_OFF, + CaptureRequest.DISTORTION_CORRECTION_MODE_FAST + }); + + when(mockBuilder.get(CaptureRequest.DISTORTION_CORRECTION_MODE)).thenReturn(null); + when(mockCameraProperties.getSensorInfoPreCorrectionActiveArraySize()).thenReturn(null); + + RegionBoundariesFeature regionBoundariesFeature = new RegionBoundariesFeature(mockCameraProperties, mockBuilder); + + assertNull(regionBoundariesFeature.getValue()); + verify(mockCameraProperties, never()).getSensorInfoPixelArraySize(); + verify(mockCameraProperties, never()).getSensorInfoActiveArraySize(); + } + finally { + updateSdkVersion(0); + } + } + + @Test + public void ctor_should_initialize_with_sensor_info_pre_correction_active_array_size_when_distortion_correction_mode_is_set_off() { + updateSdkVersion(VERSION_CODES.P); + + try { + CameraProperties mockCameraProperties = mock(CameraProperties.class); + CaptureRequest.Builder mockBuilder = mock(CaptureRequest.Builder.class); + + when(mockCameraProperties.getDistortionCorrectionAvailableModes()).thenReturn(new int[] { + CaptureRequest.DISTORTION_CORRECTION_MODE_OFF, + CaptureRequest.DISTORTION_CORRECTION_MODE_FAST + }); + + when(mockBuilder.get(CaptureRequest.DISTORTION_CORRECTION_MODE)).thenReturn(CaptureRequest.DISTORTION_CORRECTION_MODE_OFF); + when(mockCameraProperties.getSensorInfoPreCorrectionActiveArraySize()).thenReturn(null); + + RegionBoundariesFeature regionBoundariesFeature = new RegionBoundariesFeature(mockCameraProperties, mockBuilder); + + assertNull(regionBoundariesFeature.getValue()); + verify(mockCameraProperties, never()).getSensorInfoPixelArraySize(); + verify(mockCameraProperties, never()).getSensorInfoActiveArraySize(); + } + finally { + updateSdkVersion(0); + } + } + + @Test + public void ctor_should_initialize_with_sensor_info_active_array_size_when_distortion_correction_mode_is_set() { + updateSdkVersion(VERSION_CODES.P); + + try { + CameraProperties mockCameraProperties = mock(CameraProperties.class); + CaptureRequest.Builder mockBuilder = mock(CaptureRequest.Builder.class); + + when(mockCameraProperties.getDistortionCorrectionAvailableModes()).thenReturn(new int[] { + CaptureRequest.DISTORTION_CORRECTION_MODE_OFF, + CaptureRequest.DISTORTION_CORRECTION_MODE_FAST + }); + + when(mockBuilder.get(CaptureRequest.DISTORTION_CORRECTION_MODE)).thenReturn(CaptureRequest.DISTORTION_CORRECTION_MODE_FAST); + when(mockCameraProperties.getSensorInfoActiveArraySize()).thenReturn(null); + + RegionBoundariesFeature regionBoundariesFeature = new RegionBoundariesFeature(mockCameraProperties, mockBuilder); + + assertNull(regionBoundariesFeature.getValue()); + verify(mockCameraProperties, never()).getSensorInfoPixelArraySize(); + verify(mockCameraProperties, never()).getSensorInfoPreCorrectionActiveArraySize(); + } + finally { + updateSdkVersion(0); + } + } + + + @Test + public void getDebugName_should_return_the_name_of_the_feature() { + CameraProperties mockCameraProperties = mock(CameraProperties.class); + CaptureRequest.Builder mockBuilder = mock(CaptureRequest.Builder.class); + RegionBoundariesFeature regionBoundariesFeature = new RegionBoundariesFeature(mockCameraProperties, mockBuilder); + + assertEquals("RegionBoundariesFeature", regionBoundariesFeature.getDebugName()); + } + + @Test + public void getValue_should_return_null_if_not_set() { + CameraProperties mockCameraProperties = mock(CameraProperties.class); + CaptureRequest.Builder mockBuilder = mock(CaptureRequest.Builder.class); + RegionBoundariesFeature regionBoundariesFeature = new RegionBoundariesFeature(mockCameraProperties, mockBuilder); + + assertNull(regionBoundariesFeature.getValue()); + } + + @Test + public void getValue_should_echo_setValue() { + CameraProperties mockCameraProperties = mock(CameraProperties.class); + CaptureRequest.Builder mockBuilder = mock(CaptureRequest.Builder.class); + RegionBoundariesFeature regionBoundariesFeature = new RegionBoundariesFeature(mockCameraProperties, mockBuilder); + + regionBoundariesFeature.setValue(mockSize); + + assertEquals(mockSize, regionBoundariesFeature.getValue()); + } + + @Test + public void checkIsSupport_returns_true() { + CameraProperties mockCameraProperties = mock(CameraProperties.class); + CaptureRequest.Builder mockBuilder = mock(CaptureRequest.Builder.class); + RegionBoundariesFeature regionBoundariesFeature = new RegionBoundariesFeature(mockCameraProperties, mockBuilder); + + assertTrue(regionBoundariesFeature.checkIsSupported()); + } + + private static void updateSdkVersion(int version) { + try { + TestUtils.setFinalStatic(VERSION.class.getField("SDK_INT"), version); + } catch (Exception e) { + Assert.fail("Unable to update SDK version"); + } + } +} From c7c543fb2b9b50c4e7d80145f11cb74c5a748f05 Mon Sep 17 00:00:00 2001 From: Maurits van Beusekom Date: Wed, 10 Mar 2021 14:46:22 +0100 Subject: [PATCH 090/114] Fix formatting --- .../noisereduction/NoiseReductionFeature.java | 13 ++- .../noisereduction/NoiseReductionMode.java | 2 - .../RegionBoundariesFeature.java | 3 +- .../NoiseReductionFeatureTest.java | 23 ++-- .../RegionBoundariesFeatureTest.java | 106 ++++++++++-------- 5 files changed, 85 insertions(+), 62 deletions(-) diff --git a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/noisereduction/NoiseReductionFeature.java b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/noisereduction/NoiseReductionFeature.java index 8d2093035b06..b24fda4bdd8d 100644 --- a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/noisereduction/NoiseReductionFeature.java +++ b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/noisereduction/NoiseReductionFeature.java @@ -21,19 +21,20 @@ public class NoiseReductionFeature extends CameraFeature { private NoiseReductionMode currentSetting = NoiseReductionMode.fast; private static final HashMap NOISE_REDUCTION_MODES = new HashMap<>(); + static { NOISE_REDUCTION_MODES.put(NoiseReductionMode.off, CaptureRequest.NOISE_REDUCTION_MODE_OFF); NOISE_REDUCTION_MODES.put(NoiseReductionMode.fast, CaptureRequest.NOISE_REDUCTION_MODE_FAST); - NOISE_REDUCTION_MODES.put(NoiseReductionMode.highQuality, CaptureRequest.NOISE_REDUCTION_MODE_HIGH_QUALITY); + NOISE_REDUCTION_MODES.put( + NoiseReductionMode.highQuality, CaptureRequest.NOISE_REDUCTION_MODE_HIGH_QUALITY); if (VERSION.SDK_INT >= VERSION_CODES.M) { - NOISE_REDUCTION_MODES - .put(NoiseReductionMode.minimal, CaptureRequest.NOISE_REDUCTION_MODE_MINIMAL); - NOISE_REDUCTION_MODES.put(NoiseReductionMode.zeroShutterLag, - CaptureRequest.NOISE_REDUCTION_MODE_ZERO_SHUTTER_LAG); + NOISE_REDUCTION_MODES.put( + NoiseReductionMode.minimal, CaptureRequest.NOISE_REDUCTION_MODE_MINIMAL); + NOISE_REDUCTION_MODES.put( + NoiseReductionMode.zeroShutterLag, CaptureRequest.NOISE_REDUCTION_MODE_ZERO_SHUTTER_LAG); } } - public NoiseReductionFeature(CameraProperties cameraProperties) { super(cameraProperties); } diff --git a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/noisereduction/NoiseReductionMode.java b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/noisereduction/NoiseReductionMode.java index b837f21e5de7..5219fd4c046e 100644 --- a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/noisereduction/NoiseReductionMode.java +++ b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/noisereduction/NoiseReductionMode.java @@ -12,8 +12,6 @@ public enum NoiseReductionMode { minimal("minimal"), zeroShutterLag("zeroShutterLag"); - - private final String strValue; NoiseReductionMode(String strValue) { diff --git a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/regionboundaries/RegionBoundariesFeature.java b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/regionboundaries/RegionBoundariesFeature.java index f6fec4db1d6f..9470eacb21c2 100644 --- a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/regionboundaries/RegionBoundariesFeature.java +++ b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/regionboundaries/RegionBoundariesFeature.java @@ -30,7 +30,8 @@ public RegionBoundariesFeature( setValue(cameraProperties.getSensorInfoPixelArraySize()); } else { // Get the current distortion correction mode - Integer distortionCorrectionMode = requestBuilder.get(CaptureRequest.DISTORTION_CORRECTION_MODE); + Integer distortionCorrectionMode = + requestBuilder.get(CaptureRequest.DISTORTION_CORRECTION_MODE); // Return the correct boundaries depending on the mode android.graphics.Rect rect; diff --git a/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/features/noisereduction/NoiseReductionFeatureTest.java b/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/features/noisereduction/NoiseReductionFeatureTest.java index 1647f5659f2b..0b0e41d9c760 100644 --- a/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/features/noisereduction/NoiseReductionFeatureTest.java +++ b/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/features/noisereduction/NoiseReductionFeatureTest.java @@ -84,21 +84,23 @@ public void checkIsSupported_should_return_false_when_available_noise_reduction_ } @Test - public void checkIsSupported_should_return_false_when_available_noise_reduction_modes_returns_an_empty_array() { + public void + checkIsSupported_should_return_false_when_available_noise_reduction_modes_returns_an_empty_array() { CameraProperties mockCameraProperties = mock(CameraProperties.class); NoiseReductionFeature noiseReductionFeature = new NoiseReductionFeature(mockCameraProperties); - when(mockCameraProperties.getAvailableNoiseReductionModes()).thenReturn(new int[] { }); + when(mockCameraProperties.getAvailableNoiseReductionModes()).thenReturn(new int[] {}); assertFalse(noiseReductionFeature.checkIsSupported()); } @Test - public void checkIsSupported_should_return_true_when_available_noise_reduction_modes_returns_at_least_one_item() { + public void + checkIsSupported_should_return_true_when_available_noise_reduction_modes_returns_at_least_one_item() { CameraProperties mockCameraProperties = mock(CameraProperties.class); NoiseReductionFeature noiseReductionFeature = new NoiseReductionFeature(mockCameraProperties); - when(mockCameraProperties.getAvailableNoiseReductionModes()).thenReturn(new int[] { 1 }); + when(mockCameraProperties.getAvailableNoiseReductionModes()).thenReturn(new int[] {1}); assertTrue(noiseReductionFeature.checkIsSupported()); } @@ -109,7 +111,7 @@ public void updateBuilder_should_return_when_checkIsSupported_is_false() { CaptureRequest.Builder mockBuilder = mock(CaptureRequest.Builder.class); NoiseReductionFeature noiseReductionFeature = new NoiseReductionFeature(mockCameraProperties); - when(mockCameraProperties.getAvailableNoiseReductionModes()).thenReturn(new int[] { }); + when(mockCameraProperties.getAvailableNoiseReductionModes()).thenReturn(new int[] {}); noiseReductionFeature.updateBuilder(mockBuilder); @@ -128,7 +130,8 @@ public void updateBuilder_should_set_noise_reduction_mode_fast_when_fast() { @Test public void updateBuilder_should_set_noise_reduction_mode_high_quality_when_high_quality() { - testUpdateBuilderWith(NoiseReductionMode.highQuality, CaptureRequest.NOISE_REDUCTION_MODE_HIGH_QUALITY); + testUpdateBuilderWith( + NoiseReductionMode.highQuality, CaptureRequest.NOISE_REDUCTION_MODE_HIGH_QUALITY); } @Test @@ -137,8 +140,10 @@ public void updateBuilder_should_set_noise_reduction_mode_minimal_when_minimal() } @Test - public void updateBuilder_should_set_noise_reduction_mode_zero_shutter_lag_when_zero_shutter_lag() { - testUpdateBuilderWith(NoiseReductionMode.zeroShutterLag, CaptureRequest.NOISE_REDUCTION_MODE_ZERO_SHUTTER_LAG); + public void + updateBuilder_should_set_noise_reduction_mode_zero_shutter_lag_when_zero_shutter_lag() { + testUpdateBuilderWith( + NoiseReductionMode.zeroShutterLag, CaptureRequest.NOISE_REDUCTION_MODE_ZERO_SHUTTER_LAG); } private static void testUpdateBuilderWith(NoiseReductionMode mode, int expectedResult) { @@ -146,7 +151,7 @@ private static void testUpdateBuilderWith(NoiseReductionMode mode, int expectedR CaptureRequest.Builder mockBuilder = mock(CaptureRequest.Builder.class); NoiseReductionFeature noiseReductionFeature = new NoiseReductionFeature(mockCameraProperties); - when(mockCameraProperties.getAvailableNoiseReductionModes()).thenReturn(new int[] { 1 }); + when(mockCameraProperties.getAvailableNoiseReductionModes()).thenReturn(new int[] {1}); noiseReductionFeature.setValue(mode); noiseReductionFeature.updateBuilder(mockBuilder); diff --git a/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/features/regionboundaries/RegionBoundariesFeatureTest.java b/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/features/regionboundaries/RegionBoundariesFeatureTest.java index 7c23bf3cbad5..cab7a3ee11df 100644 --- a/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/features/regionboundaries/RegionBoundariesFeatureTest.java +++ b/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/features/regionboundaries/RegionBoundariesFeatureTest.java @@ -30,7 +30,8 @@ public void before() { } @Test - public void ctor_should_initialize_with_sensor_info_pixel_array_size_when_running_pre_android_p() { + public void + ctor_should_initialize_with_sensor_info_pixel_array_size_when_running_pre_android_p() { updateSdkVersion(VERSION_CODES.O_MR1); try { @@ -39,19 +40,20 @@ public void ctor_should_initialize_with_sensor_info_pixel_array_size_when_runnin when(mockCameraProperties.getSensorInfoPixelArraySize()).thenReturn(mockSize); - RegionBoundariesFeature regionBoundariesFeature = new RegionBoundariesFeature(mockCameraProperties, mockBuilder); + RegionBoundariesFeature regionBoundariesFeature = + new RegionBoundariesFeature(mockCameraProperties, mockBuilder); assertEquals(mockSize, regionBoundariesFeature.getValue()); verify(mockCameraProperties, never()).getSensorInfoPreCorrectionActiveArraySize(); verify(mockCameraProperties, never()).getSensorInfoActiveArraySize(); - } - finally { + } finally { updateSdkVersion(0); } } @Test - public void ctor_should_initialize_with_sensor_info_pixel_array_size_when_distortion_correction_is_null() { + public void + ctor_should_initialize_with_sensor_info_pixel_array_size_when_distortion_correction_is_null() { updateSdkVersion(VERSION_CODES.P); try { @@ -61,126 +63,139 @@ public void ctor_should_initialize_with_sensor_info_pixel_array_size_when_distor when(mockCameraProperties.getDistortionCorrectionAvailableModes()).thenReturn(null); when(mockCameraProperties.getSensorInfoPixelArraySize()).thenReturn(mockSize); - RegionBoundariesFeature regionBoundariesFeature = new RegionBoundariesFeature(mockCameraProperties, mockBuilder); + RegionBoundariesFeature regionBoundariesFeature = + new RegionBoundariesFeature(mockCameraProperties, mockBuilder); assertEquals(mockSize, regionBoundariesFeature.getValue()); verify(mockCameraProperties, never()).getSensorInfoPreCorrectionActiveArraySize(); verify(mockCameraProperties, never()).getSensorInfoActiveArraySize(); - } - finally { + } finally { updateSdkVersion(0); } } @Test - public void ctor_should_initialize_with_sensor_info_pixel_array_size_when_distortion_correction_is_off() { + public void + ctor_should_initialize_with_sensor_info_pixel_array_size_when_distortion_correction_is_off() { updateSdkVersion(VERSION_CODES.P); try { CameraProperties mockCameraProperties = mock(CameraProperties.class); CaptureRequest.Builder mockBuilder = mock(CaptureRequest.Builder.class); - when(mockCameraProperties.getDistortionCorrectionAvailableModes()).thenReturn(new int[] { CaptureRequest.DISTORTION_CORRECTION_MODE_OFF }); + when(mockCameraProperties.getDistortionCorrectionAvailableModes()) + .thenReturn(new int[] {CaptureRequest.DISTORTION_CORRECTION_MODE_OFF}); when(mockCameraProperties.getSensorInfoPixelArraySize()).thenReturn(mockSize); - RegionBoundariesFeature regionBoundariesFeature = new RegionBoundariesFeature(mockCameraProperties, mockBuilder); + RegionBoundariesFeature regionBoundariesFeature = + new RegionBoundariesFeature(mockCameraProperties, mockBuilder); assertEquals(mockSize, regionBoundariesFeature.getValue()); verify(mockCameraProperties, never()).getSensorInfoPreCorrectionActiveArraySize(); verify(mockCameraProperties, never()).getSensorInfoActiveArraySize(); - } - finally { + } finally { updateSdkVersion(0); } } @Test - public void ctor_should_initialize_with_sensor_info_pre_correction_active_array_size_when_distortion_correction_mode_is_set_to_null() { + public void + ctor_should_initialize_with_sensor_info_pre_correction_active_array_size_when_distortion_correction_mode_is_set_to_null() { updateSdkVersion(VERSION_CODES.P); try { CameraProperties mockCameraProperties = mock(CameraProperties.class); CaptureRequest.Builder mockBuilder = mock(CaptureRequest.Builder.class); - when(mockCameraProperties.getDistortionCorrectionAvailableModes()).thenReturn(new int[] { - CaptureRequest.DISTORTION_CORRECTION_MODE_OFF, - CaptureRequest.DISTORTION_CORRECTION_MODE_FAST - }); + when(mockCameraProperties.getDistortionCorrectionAvailableModes()) + .thenReturn( + new int[] { + CaptureRequest.DISTORTION_CORRECTION_MODE_OFF, + CaptureRequest.DISTORTION_CORRECTION_MODE_FAST + }); when(mockBuilder.get(CaptureRequest.DISTORTION_CORRECTION_MODE)).thenReturn(null); when(mockCameraProperties.getSensorInfoPreCorrectionActiveArraySize()).thenReturn(null); - RegionBoundariesFeature regionBoundariesFeature = new RegionBoundariesFeature(mockCameraProperties, mockBuilder); + RegionBoundariesFeature regionBoundariesFeature = + new RegionBoundariesFeature(mockCameraProperties, mockBuilder); assertNull(regionBoundariesFeature.getValue()); verify(mockCameraProperties, never()).getSensorInfoPixelArraySize(); verify(mockCameraProperties, never()).getSensorInfoActiveArraySize(); - } - finally { + } finally { updateSdkVersion(0); } } @Test - public void ctor_should_initialize_with_sensor_info_pre_correction_active_array_size_when_distortion_correction_mode_is_set_off() { + public void + ctor_should_initialize_with_sensor_info_pre_correction_active_array_size_when_distortion_correction_mode_is_set_off() { updateSdkVersion(VERSION_CODES.P); try { CameraProperties mockCameraProperties = mock(CameraProperties.class); CaptureRequest.Builder mockBuilder = mock(CaptureRequest.Builder.class); - when(mockCameraProperties.getDistortionCorrectionAvailableModes()).thenReturn(new int[] { - CaptureRequest.DISTORTION_CORRECTION_MODE_OFF, - CaptureRequest.DISTORTION_CORRECTION_MODE_FAST - }); + when(mockCameraProperties.getDistortionCorrectionAvailableModes()) + .thenReturn( + new int[] { + CaptureRequest.DISTORTION_CORRECTION_MODE_OFF, + CaptureRequest.DISTORTION_CORRECTION_MODE_FAST + }); - when(mockBuilder.get(CaptureRequest.DISTORTION_CORRECTION_MODE)).thenReturn(CaptureRequest.DISTORTION_CORRECTION_MODE_OFF); + when(mockBuilder.get(CaptureRequest.DISTORTION_CORRECTION_MODE)) + .thenReturn(CaptureRequest.DISTORTION_CORRECTION_MODE_OFF); when(mockCameraProperties.getSensorInfoPreCorrectionActiveArraySize()).thenReturn(null); - RegionBoundariesFeature regionBoundariesFeature = new RegionBoundariesFeature(mockCameraProperties, mockBuilder); + RegionBoundariesFeature regionBoundariesFeature = + new RegionBoundariesFeature(mockCameraProperties, mockBuilder); assertNull(regionBoundariesFeature.getValue()); verify(mockCameraProperties, never()).getSensorInfoPixelArraySize(); verify(mockCameraProperties, never()).getSensorInfoActiveArraySize(); - } - finally { + } finally { updateSdkVersion(0); } } @Test - public void ctor_should_initialize_with_sensor_info_active_array_size_when_distortion_correction_mode_is_set() { + public void + ctor_should_initialize_with_sensor_info_active_array_size_when_distortion_correction_mode_is_set() { updateSdkVersion(VERSION_CODES.P); try { CameraProperties mockCameraProperties = mock(CameraProperties.class); CaptureRequest.Builder mockBuilder = mock(CaptureRequest.Builder.class); - when(mockCameraProperties.getDistortionCorrectionAvailableModes()).thenReturn(new int[] { - CaptureRequest.DISTORTION_CORRECTION_MODE_OFF, - CaptureRequest.DISTORTION_CORRECTION_MODE_FAST - }); + when(mockCameraProperties.getDistortionCorrectionAvailableModes()) + .thenReturn( + new int[] { + CaptureRequest.DISTORTION_CORRECTION_MODE_OFF, + CaptureRequest.DISTORTION_CORRECTION_MODE_FAST + }); - when(mockBuilder.get(CaptureRequest.DISTORTION_CORRECTION_MODE)).thenReturn(CaptureRequest.DISTORTION_CORRECTION_MODE_FAST); + when(mockBuilder.get(CaptureRequest.DISTORTION_CORRECTION_MODE)) + .thenReturn(CaptureRequest.DISTORTION_CORRECTION_MODE_FAST); when(mockCameraProperties.getSensorInfoActiveArraySize()).thenReturn(null); - RegionBoundariesFeature regionBoundariesFeature = new RegionBoundariesFeature(mockCameraProperties, mockBuilder); + RegionBoundariesFeature regionBoundariesFeature = + new RegionBoundariesFeature(mockCameraProperties, mockBuilder); assertNull(regionBoundariesFeature.getValue()); verify(mockCameraProperties, never()).getSensorInfoPixelArraySize(); verify(mockCameraProperties, never()).getSensorInfoPreCorrectionActiveArraySize(); - } - finally { + } finally { updateSdkVersion(0); } } - @Test public void getDebugName_should_return_the_name_of_the_feature() { CameraProperties mockCameraProperties = mock(CameraProperties.class); CaptureRequest.Builder mockBuilder = mock(CaptureRequest.Builder.class); - RegionBoundariesFeature regionBoundariesFeature = new RegionBoundariesFeature(mockCameraProperties, mockBuilder); + RegionBoundariesFeature regionBoundariesFeature = + new RegionBoundariesFeature(mockCameraProperties, mockBuilder); assertEquals("RegionBoundariesFeature", regionBoundariesFeature.getDebugName()); } @@ -189,7 +204,8 @@ public void getDebugName_should_return_the_name_of_the_feature() { public void getValue_should_return_null_if_not_set() { CameraProperties mockCameraProperties = mock(CameraProperties.class); CaptureRequest.Builder mockBuilder = mock(CaptureRequest.Builder.class); - RegionBoundariesFeature regionBoundariesFeature = new RegionBoundariesFeature(mockCameraProperties, mockBuilder); + RegionBoundariesFeature regionBoundariesFeature = + new RegionBoundariesFeature(mockCameraProperties, mockBuilder); assertNull(regionBoundariesFeature.getValue()); } @@ -198,7 +214,8 @@ public void getValue_should_return_null_if_not_set() { public void getValue_should_echo_setValue() { CameraProperties mockCameraProperties = mock(CameraProperties.class); CaptureRequest.Builder mockBuilder = mock(CaptureRequest.Builder.class); - RegionBoundariesFeature regionBoundariesFeature = new RegionBoundariesFeature(mockCameraProperties, mockBuilder); + RegionBoundariesFeature regionBoundariesFeature = + new RegionBoundariesFeature(mockCameraProperties, mockBuilder); regionBoundariesFeature.setValue(mockSize); @@ -209,7 +226,8 @@ public void getValue_should_echo_setValue() { public void checkIsSupport_returns_true() { CameraProperties mockCameraProperties = mock(CameraProperties.class); CaptureRequest.Builder mockBuilder = mock(CaptureRequest.Builder.class); - RegionBoundariesFeature regionBoundariesFeature = new RegionBoundariesFeature(mockCameraProperties, mockBuilder); + RegionBoundariesFeature regionBoundariesFeature = + new RegionBoundariesFeature(mockCameraProperties, mockBuilder); assertTrue(regionBoundariesFeature.checkIsSupported()); } From 45fbc638588064a3bd11f03c72048a7dbc1d1b8f Mon Sep 17 00:00:00 2001 From: Maurits van Beusekom Date: Wed, 10 Mar 2021 15:45:17 +0100 Subject: [PATCH 091/114] Added test coverage to resolution feature --- .../resolution/ResolutionFeature.java | 2 +- .../resolution/ResolutionFeatureTest.java | 186 ++++++++++++++++++ 2 files changed, 187 insertions(+), 1 deletion(-) create mode 100644 packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/features/resolution/ResolutionFeatureTest.java diff --git a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/resolution/ResolutionFeature.java b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/resolution/ResolutionFeature.java index 99de9e67127f..720a91b03ecd 100644 --- a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/resolution/ResolutionFeature.java +++ b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/resolution/ResolutionFeature.java @@ -82,7 +82,7 @@ static Size computeBestPreviewSize(String cameraName, ResolutionPreset preset) { @Override public String getDebugName() { - return "Resolution"; + return "ResolutionFeature"; } @Override diff --git a/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/features/resolution/ResolutionFeatureTest.java b/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/features/resolution/ResolutionFeatureTest.java new file mode 100644 index 000000000000..6ba198728a3e --- /dev/null +++ b/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/features/resolution/ResolutionFeatureTest.java @@ -0,0 +1,186 @@ +package io.flutter.plugins.camera.features.resolution; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.mockStatic; + +import android.media.CamcorderProfile; +import io.flutter.plugins.camera.CameraProperties; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.mockito.MockedStatic; + +public class ResolutionFeatureTest { + private static final String cameraId = "1"; + private CamcorderProfile mockProfileLow; + private MockedStatic mockedStaticProfile; + + @Before + public void before() { + mockedStaticProfile = mockStatic(CamcorderProfile.class); + mockProfileLow = mock(CamcorderProfile.class); + CamcorderProfile mockProfile = mock(CamcorderProfile.class); + + mockedStaticProfile + .when(() -> CamcorderProfile.hasProfile(1, CamcorderProfile.QUALITY_HIGH)) + .thenReturn(true); + mockedStaticProfile + .when(() -> CamcorderProfile.hasProfile(1, CamcorderProfile.QUALITY_2160P)) + .thenReturn(true); + mockedStaticProfile + .when(() -> CamcorderProfile.hasProfile(1, CamcorderProfile.QUALITY_1080P)) + .thenReturn(true); + mockedStaticProfile + .when(() -> CamcorderProfile.hasProfile(1, CamcorderProfile.QUALITY_720P)) + .thenReturn(true); + mockedStaticProfile + .when(() -> CamcorderProfile.hasProfile(1, CamcorderProfile.QUALITY_480P)) + .thenReturn(true); + mockedStaticProfile + .when(() -> CamcorderProfile.hasProfile(1, CamcorderProfile.QUALITY_QVGA)) + .thenReturn(true); + mockedStaticProfile + .when(() -> CamcorderProfile.hasProfile(1, CamcorderProfile.QUALITY_LOW)) + .thenReturn(true); + + mockedStaticProfile + .when(() -> CamcorderProfile.get(1, CamcorderProfile.QUALITY_HIGH)) + .thenReturn(mockProfile); + mockedStaticProfile + .when(() -> CamcorderProfile.get(1, CamcorderProfile.QUALITY_2160P)) + .thenReturn(mockProfile); + mockedStaticProfile + .when(() -> CamcorderProfile.get(1, CamcorderProfile.QUALITY_1080P)) + .thenReturn(mockProfile); + mockedStaticProfile + .when(() -> CamcorderProfile.get(1, CamcorderProfile.QUALITY_720P)) + .thenReturn(mockProfile); + mockedStaticProfile + .when(() -> CamcorderProfile.get(1, CamcorderProfile.QUALITY_480P)) + .thenReturn(mockProfile); + mockedStaticProfile + .when(() -> CamcorderProfile.get(1, CamcorderProfile.QUALITY_QVGA)) + .thenReturn(mockProfile); + mockedStaticProfile + .when(() -> CamcorderProfile.get(1, CamcorderProfile.QUALITY_LOW)) + .thenReturn(mockProfileLow); + } + + @After + public void after() { + mockedStaticProfile.reset(); + mockedStaticProfile.close(); + } + + @Test + public void getDebugName_should_return_the_name_of_the_feature() { + CameraProperties mockCameraProperties = mock(CameraProperties.class); + ResolutionFeature resolutionFeature = + new ResolutionFeature(mockCameraProperties, ResolutionPreset.max, cameraId); + + assertEquals("ResolutionFeature", resolutionFeature.getDebugName()); + } + + @Test + public void getValue_should_return_initial_value_when_not_set() { + CameraProperties mockCameraProperties = mock(CameraProperties.class); + ResolutionFeature resolutionFeature = + new ResolutionFeature(mockCameraProperties, ResolutionPreset.max, cameraId); + + assertEquals(ResolutionPreset.max, resolutionFeature.getValue()); + } + + @Test + public void getValue_should_echo_setValue() { + CameraProperties mockCameraProperties = mock(CameraProperties.class); + ResolutionFeature resolutionFeature = + new ResolutionFeature(mockCameraProperties, ResolutionPreset.max, cameraId); + + resolutionFeature.setValue(ResolutionPreset.high); + + assertEquals(ResolutionPreset.high, resolutionFeature.getValue()); + } + + @Test + public void checkIsSupport_returns_true() { + CameraProperties mockCameraProperties = mock(CameraProperties.class); + ResolutionFeature resolutionFeature = + new ResolutionFeature(mockCameraProperties, ResolutionPreset.max, cameraId); + + assertTrue(resolutionFeature.checkIsSupported()); + } + + @Test + public void getBestAvailableCamcorderProfileForResolutionPreset_should_fall_through() { + mockedStaticProfile + .when(() -> CamcorderProfile.hasProfile(1, CamcorderProfile.QUALITY_HIGH)) + .thenReturn(false); + mockedStaticProfile + .when(() -> CamcorderProfile.hasProfile(1, CamcorderProfile.QUALITY_2160P)) + .thenReturn(false); + mockedStaticProfile + .when(() -> CamcorderProfile.hasProfile(1, CamcorderProfile.QUALITY_1080P)) + .thenReturn(false); + mockedStaticProfile + .when(() -> CamcorderProfile.hasProfile(1, CamcorderProfile.QUALITY_720P)) + .thenReturn(false); + mockedStaticProfile + .when(() -> CamcorderProfile.hasProfile(1, CamcorderProfile.QUALITY_480P)) + .thenReturn(false); + mockedStaticProfile + .when(() -> CamcorderProfile.hasProfile(1, CamcorderProfile.QUALITY_QVGA)) + .thenReturn(false); + mockedStaticProfile + .when(() -> CamcorderProfile.hasProfile(1, CamcorderProfile.QUALITY_LOW)) + .thenReturn(true); + + assertEquals( + mockProfileLow, + ResolutionFeature.getBestAvailableCamcorderProfileForResolutionPreset( + "1", ResolutionPreset.max)); + } + + @Test + public void computeBestPreviewSize_should_use_720P_when_resolution_preset_max() { + ResolutionFeature.computeBestPreviewSize("1", ResolutionPreset.max); + + mockedStaticProfile.verify(() -> CamcorderProfile.get(1, CamcorderProfile.QUALITY_720P)); + } + + @Test + public void computeBestPreviewSize_should_use_720P_when_resolution_preset_ultraHigh() { + ResolutionFeature.computeBestPreviewSize("1", ResolutionPreset.ultraHigh); + + mockedStaticProfile.verify(() -> CamcorderProfile.get(1, CamcorderProfile.QUALITY_720P)); + } + + @Test + public void computeBestPreviewSize_should_use_720P_when_resolution_preset_veryHigh() { + ResolutionFeature.computeBestPreviewSize("1", ResolutionPreset.veryHigh); + + mockedStaticProfile.verify(() -> CamcorderProfile.get(1, CamcorderProfile.QUALITY_720P)); + } + + @Test + public void computeBestPreviewSize_should_use_720P_when_resolution_preset_high() { + ResolutionFeature.computeBestPreviewSize("1", ResolutionPreset.high); + + mockedStaticProfile.verify(() -> CamcorderProfile.get(1, CamcorderProfile.QUALITY_720P)); + } + + @Test + public void computeBestPreviewSize_should_use_480P_when_resolution_preset_medium() { + ResolutionFeature.computeBestPreviewSize("1", ResolutionPreset.medium); + + mockedStaticProfile.verify(() -> CamcorderProfile.get(1, CamcorderProfile.QUALITY_480P)); + } + + @Test + public void computeBestPreviewSize_should_use_QVGA_when_resolution_preset_low() { + ResolutionFeature.computeBestPreviewSize("1", ResolutionPreset.low); + + mockedStaticProfile.verify(() -> CamcorderProfile.get(1, CamcorderProfile.QUALITY_QVGA)); + } +} From a6805296f2649b80c1352f8f49eff30ad97a688f Mon Sep 17 00:00:00 2001 From: Maurits van Beusekom Date: Wed, 10 Mar 2021 16:21:58 +0100 Subject: [PATCH 092/114] Added test coverage to sensor orientation feature --- .../SensorOrientationFeature.java | 2 +- .../SensorOrientationFeatureTest.java | 122 ++++++++++++++++++ 2 files changed, 123 insertions(+), 1 deletion(-) create mode 100644 packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/features/sensororientation/SensorOrientationFeatureTest.java diff --git a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/sensororientation/SensorOrientationFeature.java b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/sensororientation/SensorOrientationFeature.java index 876d35d40196..30c5a30abaa1 100644 --- a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/sensororientation/SensorOrientationFeature.java +++ b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/sensororientation/SensorOrientationFeature.java @@ -33,7 +33,7 @@ public SensorOrientationFeature( @Override public String getDebugName() { - return "SensorOrientation"; + return "SensorOrientationFeature"; } @Override diff --git a/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/features/sensororientation/SensorOrientationFeatureTest.java b/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/features/sensororientation/SensorOrientationFeatureTest.java new file mode 100644 index 000000000000..684099fb8e0d --- /dev/null +++ b/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/features/sensororientation/SensorOrientationFeatureTest.java @@ -0,0 +1,122 @@ +package io.flutter.plugins.camera.features.sensororientation; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.mockStatic; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import android.app.Activity; +import android.hardware.camera2.CameraMetadata; +import io.flutter.embedding.engine.systemchannels.PlatformChannel.DeviceOrientation; +import io.flutter.plugins.camera.CameraProperties; +import io.flutter.plugins.camera.DartMessenger; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.mockito.MockedStatic; + +public class SensorOrientationFeatureTest { + private MockedStatic mockedStaticDeviceOrientationManager; + private Activity mockActivity; + private CameraProperties mockCameraProperties; + private DartMessenger mockDartMessenger; + private DeviceOrientationManager mockDeviceOrientationManager; + + @Before + public void before() { + mockedStaticDeviceOrientationManager = mockStatic(DeviceOrientationManager.class); + mockActivity = mock(Activity.class); + mockCameraProperties = mock(CameraProperties.class); + mockDartMessenger = mock(DartMessenger.class); + mockDeviceOrientationManager = mock(DeviceOrientationManager.class); + + when(mockCameraProperties.getSensorOrientation()).thenReturn(0); + when(mockCameraProperties.getLensFacing()).thenReturn(CameraMetadata.LENS_FACING_BACK); + + mockedStaticDeviceOrientationManager + .when(() -> DeviceOrientationManager.create(mockActivity, mockDartMessenger, false, 0)) + .thenReturn(mockDeviceOrientationManager); + } + + @After + public void after() { + mockedStaticDeviceOrientationManager.close(); + } + + @Test + public void ctor_should_start_device_orientation_manager() { + SensorOrientationFeature sensorOrientationFeature = + new SensorOrientationFeature(mockCameraProperties, mockActivity, mockDartMessenger); + + verify(mockDeviceOrientationManager, times(1)).start(); + } + + @Test + public void getDebugName_should_return_the_name_of_the_feature() { + SensorOrientationFeature sensorOrientationFeature = + new SensorOrientationFeature(mockCameraProperties, mockActivity, mockDartMessenger); + + assertEquals("SensorOrientationFeature", sensorOrientationFeature.getDebugName()); + } + + @Test + public void getValue_should_return_null_if_not_set() { + SensorOrientationFeature sensorOrientationFeature = + new SensorOrientationFeature(mockCameraProperties, mockActivity, mockDartMessenger); + + assertEquals(0, (int) sensorOrientationFeature.getValue()); + } + + @Test + public void getValue_should_echo_setValue() { + SensorOrientationFeature sensorOrientationFeature = + new SensorOrientationFeature(mockCameraProperties, mockActivity, mockDartMessenger); + + sensorOrientationFeature.setValue(90); + + assertEquals(90, (int) sensorOrientationFeature.getValue()); + } + + @Test + public void checkIsSupport_returns_true() { + SensorOrientationFeature sensorOrientationFeature = + new SensorOrientationFeature(mockCameraProperties, mockActivity, mockDartMessenger); + + assertTrue(sensorOrientationFeature.checkIsSupported()); + } + + @Test + public void + getDeviceOrientationManager_should_return_initialized_DartOrientationManager_instance() { + SensorOrientationFeature sensorOrientationFeature = + new SensorOrientationFeature(mockCameraProperties, mockActivity, mockDartMessenger); + + assertEquals( + mockDeviceOrientationManager, sensorOrientationFeature.getDeviceOrientationManager()); + } + + @Test + public void lockCaptureOrientation_should_lock_to_specified_orientation() { + SensorOrientationFeature sensorOrientationFeature = + new SensorOrientationFeature(mockCameraProperties, mockActivity, mockDartMessenger); + + sensorOrientationFeature.lockCaptureOrientation(DeviceOrientation.PORTRAIT_DOWN); + + assertEquals( + DeviceOrientation.PORTRAIT_DOWN, sensorOrientationFeature.getLockedCaptureOrientation()); + } + + @Test + public void unlockCaptureOrientation_should_set_lock_to_null() { + SensorOrientationFeature sensorOrientationFeature = + new SensorOrientationFeature(mockCameraProperties, mockActivity, mockDartMessenger); + + sensorOrientationFeature.unlockCaptureOrientation(); + + assertNull(sensorOrientationFeature.getLockedCaptureOrientation()); + } +} From 5238d2f2cf3e6f7cea90abc477344e5796624bb8 Mon Sep 17 00:00:00 2001 From: Maurits van Beusekom Date: Wed, 10 Mar 2021 17:11:13 +0100 Subject: [PATCH 093/114] Added test coverage to zoom level feature --- .../camera/features/zoomlevel/CameraZoom.java | 6 +- .../features/zoomlevel/ZoomLevelFeature.java | 6 +- .../zoomlevel/ZoomLevelFeatureTest.java | 97 +++++++++++++++++++ 3 files changed, 105 insertions(+), 4 deletions(-) create mode 100644 packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/features/zoomlevel/ZoomLevelFeatureTest.java diff --git a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/zoomlevel/CameraZoom.java b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/zoomlevel/CameraZoom.java index 8a7909a29b72..e1d1158e2a0e 100644 --- a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/zoomlevel/CameraZoom.java +++ b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/zoomlevel/CameraZoom.java @@ -18,7 +18,11 @@ public final class CameraZoom { public final float maxZoom; public final boolean hasSupport; - public CameraZoom(@Nullable final Rect sensorArraySize, final Float maxZoom) { + public static CameraZoom create(@Nullable final Rect sensorArraySize, final Float maxZoom) { + return new CameraZoom(sensorArraySize, maxZoom); + } + + private CameraZoom(@Nullable final Rect sensorArraySize, final Float maxZoom) { this.sensorSize = sensorArraySize; if (this.sensorSize == null) { diff --git a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/zoomlevel/ZoomLevelFeature.java b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/zoomlevel/ZoomLevelFeature.java index 85499a47a053..e278fe377361 100644 --- a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/zoomlevel/ZoomLevelFeature.java +++ b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/zoomlevel/ZoomLevelFeature.java @@ -12,20 +12,20 @@ /** Exposure offset makes the image brighter or darker. */ public class ZoomLevelFeature extends CameraFeature { + private final CameraZoom cameraZoom; private Float currentSetting = CameraZoom.DEFAULT_ZOOM_FACTOR; - private CameraZoom cameraZoom; public ZoomLevelFeature(CameraProperties cameraProperties) { super(cameraProperties); this.cameraZoom = - new CameraZoom( + CameraZoom.create( cameraProperties.getSensorInfoActiveArraySize(), cameraProperties.getScalerAvailableMaxDigitalZoom()); } @Override public String getDebugName() { - return "ZoomLevel"; + return "ZoomLevelFeature"; } @Override diff --git a/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/features/zoomlevel/ZoomLevelFeatureTest.java b/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/features/zoomlevel/ZoomLevelFeatureTest.java new file mode 100644 index 000000000000..469c5006ffcb --- /dev/null +++ b/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/features/zoomlevel/ZoomLevelFeatureTest.java @@ -0,0 +1,97 @@ +package io.flutter.plugins.camera.features.zoomlevel; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.mockStatic; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import android.graphics.Rect; +import android.hardware.camera2.CaptureRequest; +import io.flutter.plugins.camera.CameraProperties; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.mockito.MockedStatic; + +public class ZoomLevelFeatureTest { + private MockedStatic mockedStaticCameraZoom; + private CameraProperties mockCameraProperties; + private CameraZoom mockCameraZoom; + + @Before + public void before() { + mockedStaticCameraZoom = mockStatic(CameraZoom.class); + mockCameraProperties = mock(CameraProperties.class); + mockCameraZoom = mock(CameraZoom.class); + + mockedStaticCameraZoom.when(() -> CameraZoom.create(any(), any())).thenReturn(mockCameraZoom); + } + + @After + public void after() { + mockedStaticCameraZoom.close(); + } + + @Test + public void ctor_should_initiaze_camera_zoom_instance() { + ZoomLevelFeature zoomLevelFeature = new ZoomLevelFeature(mockCameraProperties); + + verify(mockCameraProperties, times(1)).getSensorInfoActiveArraySize(); + verify(mockCameraProperties, times(1)).getScalerAvailableMaxDigitalZoom(); + assertEquals(mockCameraZoom, zoomLevelFeature.getCameraZoom()); + } + + @Test + public void getDebugName_should_return_the_name_of_the_feature() { + ZoomLevelFeature zoomLevelFeature = new ZoomLevelFeature(mockCameraProperties); + + assertEquals("ZoomLevelFeature", zoomLevelFeature.getDebugName()); + } + + @Test + public void getValue_should_return_null_if_not_set() { + ZoomLevelFeature zoomLevelFeature = new ZoomLevelFeature(mockCameraProperties); + + assertEquals(1.0, (float) zoomLevelFeature.getValue(), 0); + } + + @Test + public void getValue_should_echo_setValue() { + ZoomLevelFeature zoomLevelFeature = new ZoomLevelFeature(mockCameraProperties); + + zoomLevelFeature.setValue(2.3f); + + assertEquals(2.3f, (float) zoomLevelFeature.getValue(), 0); + } + + @Test + public void checkIsSupport_returns_true() { + ZoomLevelFeature zoomLevelFeature = new ZoomLevelFeature(mockCameraProperties); + + assertTrue(zoomLevelFeature.checkIsSupported()); + } + + @Test + public void updateBuilder_should_set_scalar_crop_region_when_checkIsSupport_is_true() { + ZoomLevelFeature zoomLevelFeature = new ZoomLevelFeature(mockCameraProperties); + CaptureRequest.Builder mockBuilder = mock(CaptureRequest.Builder.class); + Rect mockRect = mock(Rect.class); + + when(mockCameraZoom.computeZoom(1.0f)).thenReturn(mockRect); + + zoomLevelFeature.updateBuilder(mockBuilder); + + verify(mockBuilder, times(1)).set(CaptureRequest.SCALER_CROP_REGION, mockRect); + } + + @Test + public void getCameraZoom_should_return_camera_zoom_instance_initialized_in_ctor() { + ZoomLevelFeature zoomLevelFeature = new ZoomLevelFeature(mockCameraProperties); + + assertEquals(mockCameraZoom, zoomLevelFeature.getCameraZoom()); + } +} From 3dba3a3123c01ed1a9de0ffcc03701419213ab45 Mon Sep 17 00:00:00 2001 From: Maurits van Beusekom Date: Wed, 10 Mar 2021 17:46:09 +0100 Subject: [PATCH 094/114] Added test coverage to camera zoom class --- .../features/zoomlevel/CameraZoomTest.java | 125 ++++++++++++++++++ 1 file changed, 125 insertions(+) create mode 100644 packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/features/zoomlevel/CameraZoomTest.java diff --git a/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/features/zoomlevel/CameraZoomTest.java b/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/features/zoomlevel/CameraZoomTest.java new file mode 100644 index 000000000000..9a506b2e6354 --- /dev/null +++ b/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/features/zoomlevel/CameraZoomTest.java @@ -0,0 +1,125 @@ +// Copyright 2019 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +package io.flutter.plugins.camera.features.zoomlevel; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; + +import android.graphics.Rect; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.RobolectricTestRunner; + +@RunWith(RobolectricTestRunner.class) +public class CameraZoomTest { + + @Test + public void ctor_when_parameters_are_valid() { + final Rect sensorSize = new Rect(0, 0, 0, 0); + final Float maxZoom = 4.0f; + final CameraZoom cameraZoom = CameraZoom.create(sensorSize, maxZoom); + + assertNotNull(cameraZoom); + assertTrue(cameraZoom.hasSupport); + assertEquals(4.0f, cameraZoom.maxZoom, 0); + assertEquals(1.0f, CameraZoom.DEFAULT_ZOOM_FACTOR, 0); + } + + @Test + public void ctor_when_sensor_size_is_null() { + final Rect sensorSize = null; + final Float maxZoom = 4.0f; + final CameraZoom cameraZoom = CameraZoom.create(sensorSize, maxZoom); + + assertNotNull(cameraZoom); + assertFalse(cameraZoom.hasSupport); + assertEquals(cameraZoom.maxZoom, 1.0f, 0); + } + + @Test + public void ctor_when_max_zoom_is_null() { + final Rect sensorSize = new Rect(0, 0, 0, 0); + final Float maxZoom = null; + final CameraZoom cameraZoom = CameraZoom.create(sensorSize, maxZoom); + + assertNotNull(cameraZoom); + assertFalse(cameraZoom.hasSupport); + assertEquals(cameraZoom.maxZoom, 1.0f, 0); + } + + @Test + public void ctor_when_max_zoom_is_smaller_then_default_zoom_factor() { + final Rect sensorSize = new Rect(0, 0, 0, 0); + final Float maxZoom = 0.5f; + final CameraZoom cameraZoom = CameraZoom.create(sensorSize, maxZoom); + + assertNotNull(cameraZoom); + assertFalse(cameraZoom.hasSupport); + assertEquals(cameraZoom.maxZoom, 1.0f, 0); + } + + @Test + public void setZoom_when_no_support_should_not_set_scaler_crop_region() { + final CameraZoom cameraZoom = CameraZoom.create(null, null); + final Rect computedZoom = cameraZoom.computeZoom(2f); + + assertNull(computedZoom); + } + + @Test + public void setZoom_when_sensor_size_equals_zero_should_return_crop_region_of_zero() { + final Rect sensorSize = new Rect(0, 0, 0, 0); + final CameraZoom cameraZoom = CameraZoom.create(sensorSize, 20f); + final Rect computedZoom = cameraZoom.computeZoom(18f); + + assertNotNull(computedZoom); + assertEquals(computedZoom.left, 0); + assertEquals(computedZoom.top, 0); + assertEquals(computedZoom.right, 0); + assertEquals(computedZoom.bottom, 0); + } + + @Test + public void setZoom_when_sensor_size_is_valid_should_return_crop_region() { + final Rect sensorSize = new Rect(0, 0, 100, 100); + final CameraZoom cameraZoom = CameraZoom.create(sensorSize, 20f); + final Rect computedZoom = cameraZoom.computeZoom(18f); + + assertNotNull(computedZoom); + assertEquals(computedZoom.left, 48); + assertEquals(computedZoom.top, 48); + assertEquals(computedZoom.right, 52); + assertEquals(computedZoom.bottom, 52); + } + + @Test + public void setZoom_when_zoom_is_greater_then_max_zoom_clamp_to_max_zoom() { + final Rect sensorSize = new Rect(0, 0, 100, 100); + final CameraZoom cameraZoom = CameraZoom.create(sensorSize, 10f); + final Rect computedZoom = cameraZoom.computeZoom(25f); + + assertNotNull(computedZoom); + assertEquals(computedZoom.left, 45); + assertEquals(computedZoom.top, 45); + assertEquals(computedZoom.right, 55); + assertEquals(computedZoom.bottom, 55); + } + + @Test + public void setZoom_when_zoom_is_smaller_then_min_zoom_clamp_to_min_zoom() { + final Rect sensorSize = new Rect(0, 0, 100, 100); + final CameraZoom cameraZoom = CameraZoom.create(sensorSize, 10f); + final Rect computedZoom = cameraZoom.computeZoom(0.5f); + + assertNotNull(computedZoom); + assertEquals(computedZoom.left, 0); + assertEquals(computedZoom.top, 0); + assertEquals(computedZoom.right, 100); + assertEquals(computedZoom.bottom, 100); + } +} \ No newline at end of file From 2de1f92956a117343e6e13658bce2b644670e014 Mon Sep 17 00:00:00 2001 From: Andrew Coutts Date: Wed, 10 Mar 2021 13:53:15 -0500 Subject: [PATCH 095/114] Fix alignment on example app camera selection row items --- packages/camera/camera/example/lib/main.dart | 23 +++++++++++++------- 1 file changed, 15 insertions(+), 8 deletions(-) diff --git a/packages/camera/camera/example/lib/main.dart b/packages/camera/camera/example/lib/main.dart index 40245b3fcda0..11bcbb24f2dc 100644 --- a/packages/camera/camera/example/lib/main.dart +++ b/packages/camera/camera/example/lib/main.dart @@ -157,14 +157,19 @@ class _CameraExampleHomeState extends State padding: const EdgeInsets.all(5.0), child: Stack( children: [ - SingleChildScrollView( - scrollDirection: Axis.horizontal, - padding: EdgeInsets.only(right: 90), - child: _cameraTogglesRowWidget(), + Align( + alignment: Alignment.centerLeft, + child: SingleChildScrollView( + scrollDirection: Axis.horizontal, + padding: EdgeInsets.only(right: 90), + child: _cameraTogglesRowWidget(), + ), ), - Positioned( - right: 0, - child: _thumbnailWidget(), + Positioned.fill( + child: Align( + alignment: Alignment.centerRight, + child: _thumbnailWidget(), + ), ), ], ), @@ -571,7 +576,9 @@ class _CameraExampleHomeState extends State } } - return Row(children: toggles); + return Row( + children: toggles, + ); } String timestamp() => DateTime.now().millisecondsSinceEpoch.toString(); From eaaa5ef9cac603a543963dbf3983c6889bb3f97d Mon Sep 17 00:00:00 2001 From: Andrew Coutts Date: Wed, 10 Mar 2021 14:25:54 -0500 Subject: [PATCH 096/114] Make sure precapture metering timeout works when metering fails to finish in time --- .../java/io/flutter/plugins/camera/Camera.java | 4 ++-- .../plugins/camera/CameraCaptureCallback.java | 16 ++++++++++++++-- .../features/zoomlevel/CameraZoomTest.java | 2 +- 3 files changed, 17 insertions(+), 5 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 7a234c79b27a..0b9cc6e4b533 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 @@ -512,7 +512,7 @@ public void takePicture(@NonNull final Result result) { final File file = File.createTempFile("CAP", ".jpg", outputDir); // Start a new capture - pictureCaptureRequest = PictureCaptureRequest.create(result, dartMessenger, file, 3000, 3000); + pictureCaptureRequest = PictureCaptureRequest.create(result, dartMessenger, file, 3000, 1000); mCaptureCallback.setPictureCaptureRequest(pictureCaptureRequest); } catch (IOException | SecurityException e) { pictureCaptureRequest.error("cannotCreateFile", e.getMessage(), null); @@ -539,7 +539,7 @@ public void takePicture(@NonNull final Result result) { private void runPrecaptureSequence() { Log.i(TAG, "runPrecaptureSequence"); try { - // First set precapture state to idle or else it can hang in STATE_stPRECAPTURE + // First set precapture state to idle or else it can hang in STATE_WAITING_PRECAPTURE_START mPreviewRequestBuilder.set( CaptureRequest.CONTROL_AE_PRECAPTURE_TRIGGER, CaptureRequest.CONTROL_AE_PRECAPTURE_TRIGGER_IDLE); diff --git a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/CameraCaptureCallback.java b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/CameraCaptureCallback.java index 87753ca4ae99..2e989dc02cff 100644 --- a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/CameraCaptureCallback.java +++ b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/CameraCaptureCallback.java @@ -96,7 +96,9 @@ private void process(CaptureResult result) { || pictureCaptureRequest.preCaptureMetering.getIsExpired()) { if (pictureCaptureRequest.preCaptureMetering.getIsExpired()) { - Log.i("Camera", "Metering timeout, moving on with capture"); + Log.i( + "Camera", + "Metering timeout waiting for precapture to start, moving on with capture"); } setCameraState(CameraState.STATE_WAITING_PRECAPTURE_DONE); @@ -107,7 +109,17 @@ private void process(CaptureResult result) { case STATE_WAITING_PRECAPTURE_DONE: { // CONTROL_AE_STATE can be null on some devices - if (aeState == null || aeState != CaptureResult.CONTROL_AE_STATE_PRECAPTURE) { + if (aeState == null + || aeState != CaptureResult.CONTROL_AE_STATE_PRECAPTURE + || pictureCaptureRequest.preCaptureMetering + .getIsExpired() // Some devices like Pixel 5 will hang in pre capture metering + ) { + if (pictureCaptureRequest.preCaptureMetering.getIsExpired()) { + Log.i( + "Camera", + "Metering timeout waiting for precapture to finish, moving on with capture"); + } + cameraStateListener.onConverged(); } break; diff --git a/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/features/zoomlevel/CameraZoomTest.java b/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/features/zoomlevel/CameraZoomTest.java index 9a506b2e6354..878de48547c0 100644 --- a/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/features/zoomlevel/CameraZoomTest.java +++ b/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/features/zoomlevel/CameraZoomTest.java @@ -122,4 +122,4 @@ public void setZoom_when_zoom_is_smaller_then_min_zoom_clamp_to_min_zoom() { assertEquals(computedZoom.right, 100); assertEquals(computedZoom.bottom, 100); } -} \ No newline at end of file +} From 12c8b8c2c73deb01b635eef697b54f0b56a09a90 Mon Sep 17 00:00:00 2001 From: Andrew Coutts Date: Wed, 10 Mar 2021 14:52:35 -0500 Subject: [PATCH 097/114] Update comment --- .../java/io/flutter/plugins/camera/CameraCaptureCallback.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/CameraCaptureCallback.java b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/CameraCaptureCallback.java index 2e989dc02cff..8484512d8f24 100644 --- a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/CameraCaptureCallback.java +++ b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/CameraCaptureCallback.java @@ -112,7 +112,7 @@ private void process(CaptureResult result) { if (aeState == null || aeState != CaptureResult.CONTROL_AE_STATE_PRECAPTURE || pictureCaptureRequest.preCaptureMetering - .getIsExpired() // Some devices like Pixel 5 will hang in pre capture metering + .getIsExpired() // Some devices can hang in precapture metering ) { if (pictureCaptureRequest.preCaptureMetering.getIsExpired()) { Log.i( From a44cdc5ed280d2b50986533146a0a2a7ce8abd51 Mon Sep 17 00:00:00 2001 From: Maurits van Beusekom Date: Thu, 11 Mar 2021 15:58:18 +0100 Subject: [PATCH 098/114] Added test coverage to CameraCaptureCallback class --- .../io/flutter/plugins/camera/Camera.java | 2 +- .../plugins/camera/CameraCaptureCallback.java | 68 ++--- .../plugins/camera/PictureCaptureRequest.java | 8 +- .../io/flutter/plugins/camera/Timeout.java | 2 +- .../CameraCaptureCallbackStatesTest.java | 283 ++++++++++++++++++ .../camera/CameraCaptureCallbackTest.java | 123 ++++++++ .../NoiseReductionFeatureTest.java | 13 +- .../RegionBoundariesFeatureTest.java | 7 +- .../plugins/camera/utils/TestUtils.java | 19 +- 9 files changed, 453 insertions(+), 72 deletions(-) create mode 100644 packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/CameraCaptureCallbackStatesTest.java create mode 100644 packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/CameraCaptureCallbackTest.java 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 0b9cc6e4b533..97d8e75a28dd 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 @@ -512,7 +512,7 @@ public void takePicture(@NonNull final Result result) { final File file = File.createTempFile("CAP", ".jpg", outputDir); // Start a new capture - pictureCaptureRequest = PictureCaptureRequest.create(result, dartMessenger, file, 3000, 1000); + pictureCaptureRequest = PictureCaptureRequest.create(result, file, 3000, 3000); mCaptureCallback.setPictureCaptureRequest(pictureCaptureRequest); } catch (IOException | SecurityException e) { pictureCaptureRequest.error("cannotCreateFile", e.getMessage(), null); diff --git a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/CameraCaptureCallback.java b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/CameraCaptureCallback.java index 8484512d8f24..c6175124aad3 100644 --- a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/CameraCaptureCallback.java +++ b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/CameraCaptureCallback.java @@ -64,69 +64,59 @@ private void process(CaptureResult result) { { if (afState == null) { return; - } else if (afState == CaptureRequest.CONTROL_AF_STATE_FOCUSED_LOCKED - || afState == CaptureRequest.CONTROL_AF_STATE_NOT_FOCUSED_LOCKED - - // Move forward in case of a focusing timeout. - || pictureCaptureRequest.preCaptureFocusing.getIsExpired()) { - // CONTROL_AE_STATE can be null on some devices - - if (pictureCaptureRequest.preCaptureFocusing.getIsExpired()) { - Log.i("Camera", "Focus timeout, moving on with capture"); - } - - if (aeState == null || aeState == CaptureRequest.CONTROL_AE_STATE_CONVERGED) { - cameraStateListener.onConverged(); - } else { - cameraStateListener.onPrecapture(); - } + } else if (afState == CaptureResult.CONTROL_AF_STATE_FOCUSED_LOCKED + || afState == CaptureResult.CONTROL_AF_STATE_NOT_FOCUSED_LOCKED) { + handleWaitingFocusState(aeState); + } else if (pictureCaptureRequest != null && pictureCaptureRequest.preCaptureFocusing.getIsExpired()) { + Log.w("Camera", "Focus timeout, moving on with capture"); + handleWaitingFocusState(aeState); } + break; } - case STATE_WAITING_PRECAPTURE_START: { // CONTROL_AE_STATE can be null on some devices if (aeState == null || aeState == CaptureResult.CONTROL_AE_STATE_CONVERGED || aeState == CaptureResult.CONTROL_AE_STATE_PRECAPTURE - || aeState == CaptureResult.CONTROL_AE_STATE_FLASH_REQUIRED - - // Move forward in case of a metering timeout. - || pictureCaptureRequest.preCaptureMetering.getIsExpired()) { - - if (pictureCaptureRequest.preCaptureMetering.getIsExpired()) { - Log.i( - "Camera", - "Metering timeout waiting for precapture to start, moving on with capture"); - } + || aeState == CaptureResult.CONTROL_AE_STATE_FLASH_REQUIRED) { + setCameraState(CameraState.STATE_WAITING_PRECAPTURE_DONE); + } else if (pictureCaptureRequest != null && pictureCaptureRequest.preCaptureMetering.getIsExpired()) { + Log.w( + "Camera", + "Metering timeout waiting for pre-capture to start, moving on with capture"); setCameraState(CameraState.STATE_WAITING_PRECAPTURE_DONE); } break; } - case STATE_WAITING_PRECAPTURE_DONE: { // CONTROL_AE_STATE can be null on some devices - if (aeState == null - || aeState != CaptureResult.CONTROL_AE_STATE_PRECAPTURE - || pictureCaptureRequest.preCaptureMetering - .getIsExpired() // Some devices can hang in precapture metering - ) { - if (pictureCaptureRequest.preCaptureMetering.getIsExpired()) { - Log.i( - "Camera", - "Metering timeout waiting for precapture to finish, moving on with capture"); - } - + if (aeState == null || aeState != CaptureResult.CONTROL_AE_STATE_PRECAPTURE) { + cameraStateListener.onConverged(); + } else if (pictureCaptureRequest != null && pictureCaptureRequest.preCaptureMetering.getIsExpired()) { + Log.w( + "Camera", + "Metering timeout waiting for pre-capture to finish, moving on with capture"); cameraStateListener.onConverged(); } + break; } } } + private void handleWaitingFocusState(Integer aeState) { + // CONTROL_AE_STATE can be null on some devices + if (aeState == null || aeState == CaptureRequest.CONTROL_AE_STATE_CONVERGED) { + cameraStateListener.onConverged(); + } else { + cameraStateListener.onPrecapture(); + } + } + @Override public void onCaptureProgressed( @NonNull CameraCaptureSession session, diff --git a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/PictureCaptureRequest.java b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/PictureCaptureRequest.java index 582ed2ba0b69..281d3e8cfa1b 100644 --- a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/PictureCaptureRequest.java +++ b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/PictureCaptureRequest.java @@ -23,14 +23,11 @@ public class PictureCaptureRequest { public final Timeout preCaptureMetering; /** Dart method chanel result. */ private final MethodChannel.Result result; - /** To send errors back to dart */ - private final DartMessenger dartMessenger; /** * Factory method to create a picture capture request * * @param result dart result. - * @param dartMessenger dart messenger. * @param file file to capture into. * @param preCaptureFocusingTimeoutMs focusing timeout milliseconds. * @param preCaptureMeteringTimeoutMs metering timeout milliseconds. @@ -38,23 +35,20 @@ public class PictureCaptureRequest { */ static PictureCaptureRequest create( MethodChannel.Result result, - DartMessenger dartMessenger, File file, long preCaptureFocusingTimeoutMs, long preCaptureMeteringTimeoutMs) { return new PictureCaptureRequest( - result, dartMessenger, file, preCaptureFocusingTimeoutMs, preCaptureMeteringTimeoutMs); + result, file, preCaptureFocusingTimeoutMs, preCaptureMeteringTimeoutMs); } /** Create a new picture capture request */ private PictureCaptureRequest( MethodChannel.Result result, - DartMessenger dartMessenger, File file, long preCaptureFocusingTimeoutMs, long preCaptureMeteringTimeoutMs) { this.result = result; - this.dartMessenger = dartMessenger; this.file = file; this.preCaptureFocusing = Timeout.create(preCaptureFocusingTimeoutMs); this.preCaptureMetering = Timeout.create(preCaptureMeteringTimeoutMs); diff --git a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/Timeout.java b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/Timeout.java index 083fee80fa74..3be54f7d56b2 100644 --- a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/Timeout.java +++ b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/Timeout.java @@ -13,7 +13,7 @@ *

We use timeouts to ensure a picture is always captured within a reasonable amount of time even * if the settings don't converge and focus can't be locked. * - *

You generally check the status of the timeout in the CameraCAptureCallback during the capture + *

You generally check the status of the timeout in the CameraCaptureCallback during the capture * sequence and use it to move to the next state if the timeout has passed. */ class Timeout { diff --git a/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/CameraCaptureCallbackStatesTest.java b/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/CameraCaptureCallbackStatesTest.java new file mode 100644 index 000000000000..32586307f8e1 --- /dev/null +++ b/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/CameraCaptureCallbackStatesTest.java @@ -0,0 +1,283 @@ +package io.flutter.plugins.camera; + +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.mockStatic; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import android.hardware.camera2.CameraCaptureSession; +import android.hardware.camera2.CaptureRequest; +import android.hardware.camera2.CaptureResult; +import android.hardware.camera2.CaptureResult.Key; +import android.hardware.camera2.TotalCaptureResult; +import io.flutter.plugin.common.MethodChannel; +import io.flutter.plugins.camera.CameraCaptureCallback.CameraCaptureStateListener; +import io.flutter.plugins.camera.utils.TestUtils; +import java.io.File; +import java.util.HashMap; +import java.util.Map; +import junit.framework.TestCase; +import junit.framework.TestSuite; +import org.mockito.MockedStatic; + +public class CameraCaptureCallbackStatesTest extends TestCase { + private final Integer aeState; + private final Integer afState; + private final CameraState cameraState; + private final boolean isTimedOut; + + private Runnable validate; + + private CameraCaptureCallback cameraCaptureCallback; + private CameraCaptureStateListener mockCaptureStateListener; + private CameraCaptureSession mockCameraCaptureSession; + private CaptureRequest mockCaptureRequest; + private CaptureResult mockPartialCaptureResult; + private TotalCaptureResult mockTotalCaptureResult; + private MockedStatic mockedStaticTimeout; + private Timeout mockTimeout; + + public static TestSuite suite() { + TestSuite suite = new TestSuite(); + + setUpPreviewStateTest(suite); + setUpWaitingFocusTests(suite); + setUpWaitingPreCaptureStartTests(suite); + setUpWaitingPreCaptureDoneTests(suite); + + return suite; + } + + private static void setUpPreviewStateTest(TestSuite suite) { + CameraCaptureCallbackStatesTest previewStateTest = new CameraCaptureCallbackStatesTest("process_should_not_converge_or_pre_capture_when_state_is_preview", CameraState.STATE_PREVIEW, null, null); + previewStateTest.validate = () -> { + verify(previewStateTest.mockCaptureStateListener, never()).onConverged(); + verify(previewStateTest.mockCaptureStateListener, never()).onConverged(); + assertEquals(CameraState.STATE_PREVIEW, previewStateTest.cameraCaptureCallback.getCameraState()); + }; + suite.addTest(previewStateTest); + } + + private static void setUpWaitingFocusTests(TestSuite suite) { + Integer[] actionableAfStates = new Integer[] { + CaptureResult.CONTROL_AF_STATE_FOCUSED_LOCKED, + CaptureResult.CONTROL_AF_STATE_NOT_FOCUSED_LOCKED + }; + + Integer[] nonActionableAfStates = new Integer[] { + CaptureResult.CONTROL_AF_STATE_ACTIVE_SCAN, + CaptureResult.CONTROL_AF_STATE_INACTIVE, + CaptureResult.CONTROL_AF_STATE_PASSIVE_FOCUSED, + CaptureResult.CONTROL_AF_STATE_PASSIVE_SCAN, + CaptureResult.CONTROL_AF_STATE_PASSIVE_UNFOCUSED + }; + + Map aeStatesConvergeMap = new HashMap() {{ + put(null, true); + put(CaptureResult.CONTROL_AE_STATE_CONVERGED, true); + put(CaptureResult.CONTROL_AE_STATE_PRECAPTURE, false); + put(CaptureResult.CONTROL_AE_STATE_LOCKED, false); + put(CaptureResult.CONTROL_AE_STATE_SEARCHING, false); + put(CaptureResult.CONTROL_AE_STATE_INACTIVE, false); + put(CaptureResult.CONTROL_AE_STATE_FLASH_REQUIRED, false); + }}; + + CameraCaptureCallbackStatesTest nullStateTest = new CameraCaptureCallbackStatesTest("process_should_not_converge_or_pre_capture_when_afstate_is_null", CameraState.STATE_WAITING_FOCUS, null, null); + nullStateTest.validate = () -> { + verify(nullStateTest.mockCaptureStateListener, never()).onConverged(); + verify(nullStateTest.mockCaptureStateListener, never()).onConverged(); + assertEquals(CameraState.STATE_WAITING_FOCUS, nullStateTest.cameraCaptureCallback.getCameraState()); + }; + suite.addTest(nullStateTest); + + for (Integer afState : actionableAfStates) { + aeStatesConvergeMap.forEach((aeState, shouldConverge) -> { + CameraCaptureCallbackStatesTest focusLockedTest = new CameraCaptureCallbackStatesTest( + "process_should_converge_when_af_state_is_" + afState + "_and_ae_state_is_" + aeState, + CameraState.STATE_WAITING_FOCUS, + afState, + aeState); + focusLockedTest.validate = () -> { + if (shouldConverge) { + verify(focusLockedTest.mockCaptureStateListener, times(1)).onConverged(); + verify(focusLockedTest.mockCaptureStateListener, never()).onPrecapture(); + } else { + verify(focusLockedTest.mockCaptureStateListener, times(1)).onPrecapture(); + verify(focusLockedTest.mockCaptureStateListener, never()).onConverged(); + } + assertEquals(CameraState.STATE_WAITING_FOCUS, focusLockedTest.cameraCaptureCallback.getCameraState()); + }; + suite.addTest(focusLockedTest); + }); + } + + for (Integer afState : nonActionableAfStates) { + CameraCaptureCallbackStatesTest focusLockedTest = new CameraCaptureCallbackStatesTest( + "process_should_do_nothing_when_af_state_is_" + afState, + CameraState.STATE_WAITING_FOCUS, + afState, + null); + focusLockedTest.validate = () -> { + verify(focusLockedTest.mockCaptureStateListener, never()).onConverged(); + verify(focusLockedTest.mockCaptureStateListener, never()).onPrecapture(); + assertEquals(CameraState.STATE_WAITING_FOCUS, focusLockedTest.cameraCaptureCallback.getCameraState()); + }; + suite.addTest(focusLockedTest); + } + + for (Integer afState : nonActionableAfStates) { + aeStatesConvergeMap.forEach((aeState, shouldConverge) -> { + CameraCaptureCallbackStatesTest focusLockedTest = new CameraCaptureCallbackStatesTest( + "process_should_converge_when_af_state_is_" + afState + "_and_ae_state_is_" + aeState, + CameraState.STATE_WAITING_FOCUS, + afState, + aeState, + true); + focusLockedTest.validate = () -> { + if (shouldConverge) { + verify(focusLockedTest.mockCaptureStateListener, times(1)).onConverged(); + verify(focusLockedTest.mockCaptureStateListener, never()).onPrecapture(); + } else { + verify(focusLockedTest.mockCaptureStateListener, times(1)).onPrecapture(); + verify(focusLockedTest.mockCaptureStateListener, never()).onConverged(); + } + assertEquals(CameraState.STATE_WAITING_FOCUS, focusLockedTest.cameraCaptureCallback.getCameraState()); + }; + suite.addTest(focusLockedTest); + }); + } + } + + private static void setUpWaitingPreCaptureStartTests(TestSuite suite) { + Map cameraStateMap = new HashMap() {{ + put(null, CameraState.STATE_WAITING_PRECAPTURE_DONE); + put(CaptureResult.CONTROL_AE_STATE_INACTIVE, CameraState.STATE_WAITING_PRECAPTURE_START); + put(CaptureResult.CONTROL_AE_STATE_SEARCHING, CameraState.STATE_WAITING_PRECAPTURE_START); + put(CaptureResult.CONTROL_AE_STATE_CONVERGED, CameraState.STATE_WAITING_PRECAPTURE_DONE); + put(CaptureResult.CONTROL_AE_STATE_LOCKED, CameraState.STATE_WAITING_PRECAPTURE_START); + put(CaptureResult.CONTROL_AE_STATE_FLASH_REQUIRED, CameraState.STATE_WAITING_PRECAPTURE_DONE); + put(CaptureResult.CONTROL_AE_STATE_PRECAPTURE, CameraState.STATE_WAITING_PRECAPTURE_DONE); + }}; + + cameraStateMap.forEach((aeState, cameraState) -> { + CameraCaptureCallbackStatesTest testCase = new CameraCaptureCallbackStatesTest("process_should_update_camera_state_to_waiting_pre_capture_done_when_ae_state_is_" + aeState, CameraState.STATE_WAITING_PRECAPTURE_START, null, aeState); + testCase.validate = () -> assertEquals(cameraState, testCase.cameraCaptureCallback.getCameraState()); + suite.addTest(testCase); + }); + + cameraStateMap.forEach((aeState, cameraState) -> { + if (cameraState == CameraState.STATE_WAITING_PRECAPTURE_DONE) { + return; + } + + CameraCaptureCallbackStatesTest testCase = new CameraCaptureCallbackStatesTest("process_should_update_camera_state_to_waiting_pre_capture_done_when_ae_state_is_" + aeState, CameraState.STATE_WAITING_PRECAPTURE_START, null, aeState, true); + testCase.validate = () -> assertEquals(CameraState.STATE_WAITING_PRECAPTURE_DONE, testCase.cameraCaptureCallback.getCameraState()); + suite.addTest(testCase); + }); + } + + private static void setUpWaitingPreCaptureDoneTests(TestSuite suite) { + Integer[] onConvergeStates = new Integer[] { + null, + CaptureResult.CONTROL_AE_STATE_CONVERGED, + CaptureResult.CONTROL_AE_STATE_LOCKED, + CaptureResult.CONTROL_AE_STATE_SEARCHING, + CaptureResult.CONTROL_AE_STATE_INACTIVE, + CaptureResult.CONTROL_AE_STATE_FLASH_REQUIRED, + }; + + for (Integer aeState : onConvergeStates) { + CameraCaptureCallbackStatesTest shouldConvergeTest = new CameraCaptureCallbackStatesTest( + "process_should_converge_when_ae_state_is_" + aeState, + CameraState.STATE_WAITING_PRECAPTURE_DONE, null, null); + shouldConvergeTest.validate = () -> verify(shouldConvergeTest.mockCaptureStateListener, times(1)).onConverged(); + suite.addTest(shouldConvergeTest); + } + + CameraCaptureCallbackStatesTest shouldNotConvergeTest = new CameraCaptureCallbackStatesTest("process_should_not_converge_when_ae_state_is_pre_capture", CameraState.STATE_WAITING_PRECAPTURE_DONE, null, CaptureResult.CONTROL_AE_STATE_PRECAPTURE); + shouldNotConvergeTest.validate = () -> verify(shouldNotConvergeTest.mockCaptureStateListener, never()).onConverged(); + suite.addTest(shouldNotConvergeTest); + + CameraCaptureCallbackStatesTest shouldConvergeWhenTimedOutTest = new CameraCaptureCallbackStatesTest("process_should_not_converge_when_ae_state_is_pre_capture", CameraState.STATE_WAITING_PRECAPTURE_DONE, null, CaptureResult.CONTROL_AE_STATE_PRECAPTURE, true); + shouldConvergeWhenTimedOutTest.validate = () -> verify(shouldConvergeWhenTimedOutTest.mockCaptureStateListener, times(1)).onConverged(); + suite.addTest(shouldConvergeWhenTimedOutTest); + } + + protected CameraCaptureCallbackStatesTest(String name, CameraState cameraState, Integer afState, Integer aeState) { + this(name, cameraState, afState, aeState, false); + } + + protected CameraCaptureCallbackStatesTest(String name, CameraState cameraState, Integer afState, Integer aeState, boolean isTimedOut) { + super(name); + + this.aeState = aeState; + this.afState = afState; + this.cameraState = cameraState; + this.isTimedOut = isTimedOut; + } + + @Override + @SuppressWarnings("unchecked") + protected void setUp() throws Exception { + super.setUp(); + + mockedStaticTimeout = mockStatic(Timeout.class); + mockCaptureStateListener = mock(CameraCaptureStateListener.class); + mockCameraCaptureSession = mock(CameraCaptureSession.class); + mockCaptureRequest = mock(CaptureRequest.class); + mockPartialCaptureResult = mock(CaptureResult.class); + mockTotalCaptureResult = mock(TotalCaptureResult.class); + mockTimeout = mock(Timeout.class); + Key mockAeStateKey = mock(Key.class); + Key mockAfStateKey = mock(Key.class); + + TestUtils.setFinalStatic(CaptureResult.class, "CONTROL_AE_STATE", mockAeStateKey); + TestUtils.setFinalStatic(CaptureResult.class, "CONTROL_AF_STATE", mockAfStateKey); + + mockedStaticTimeout.when(() -> Timeout.create(1000)).thenReturn(mockTimeout); + + cameraCaptureCallback = CameraCaptureCallback.create(mockCaptureStateListener); + } + + @Override + protected void tearDown() throws Exception { + super.tearDown(); + + mockedStaticTimeout.close(); + + TestUtils.setFinalStatic(CaptureResult.class, "CONTROL_AE_STATE", null); + TestUtils.setFinalStatic(CaptureResult.class, "CONTROL_AF_STATE", null); + } + + @Override + protected void runTest() throws Throwable { + when(mockPartialCaptureResult.get(CaptureResult.CONTROL_AF_STATE)).thenReturn(afState); + when(mockPartialCaptureResult.get(CaptureResult.CONTROL_AE_STATE)).thenReturn(aeState); + when(mockTotalCaptureResult.get(CaptureResult.CONTROL_AF_STATE)).thenReturn(afState); + when(mockTotalCaptureResult.get(CaptureResult.CONTROL_AE_STATE)).thenReturn(aeState); + + cameraCaptureCallback.setCameraState(cameraState); + if (isTimedOut) { + configureTimeout(); + cameraCaptureCallback.onCaptureCompleted(mockCameraCaptureSession, mockCaptureRequest, + mockTotalCaptureResult); + } else { + cameraCaptureCallback.onCaptureProgressed(mockCameraCaptureSession, mockCaptureRequest, + mockPartialCaptureResult); + } + + validate.run(); + } + + private void configureTimeout() { + MethodChannel.Result mockResult = mock(MethodChannel.Result.class); + File mockFile = mock(File.class); + + when(mockTimeout.getIsExpired()).thenReturn(true); + + PictureCaptureRequest pictureCaptureRequest = PictureCaptureRequest.create(mockResult, mockFile, 1000, 1000); + cameraCaptureCallback.setPictureCaptureRequest(pictureCaptureRequest); + } +} diff --git a/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/CameraCaptureCallbackTest.java b/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/CameraCaptureCallbackTest.java new file mode 100644 index 000000000000..597783672bcf --- /dev/null +++ b/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/CameraCaptureCallbackTest.java @@ -0,0 +1,123 @@ +package io.flutter.plugins.camera; + +import static org.junit.Assert.assertEquals; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import android.hardware.camera2.CameraCaptureSession; +import android.hardware.camera2.CaptureRequest; +import android.hardware.camera2.CaptureResult; +import android.hardware.camera2.CaptureResult.Key; +import io.flutter.plugins.camera.CameraCaptureCallback.CameraCaptureStateListener; +import io.flutter.plugins.camera.utils.TestUtils; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; + +public class CameraCaptureCallbackTest { + private CameraCaptureStateListener mockCaptureStateListener; + + @Before + public void before() { + mockCaptureStateListener = mock(CameraCaptureStateListener.class); + } + + @Test + public void getCameraState_should_return_camera_preview_if_not_set() { + CameraCaptureCallback captureCallback = CameraCaptureCallback.create(mockCaptureStateListener); + + assertEquals(CameraState.STATE_PREVIEW, captureCallback.getCameraState()); + } + + @Test + public void getCameraState_should_echo_setCameraState_value() { + CameraCaptureCallback captureCallback = CameraCaptureCallback.create(mockCaptureStateListener); + + captureCallback.setCameraState(CameraState.STATE_CAPTURING); + + assertEquals(CameraState.STATE_CAPTURING, captureCallback.getCameraState()); + } + + public static class CameraStateWaitingFocusTests { + private CameraCaptureStateListener mockCaptureStateListener; + private CameraCaptureSession mockCameraCaptureSession; + private CaptureRequest mockCaptureRequest; + private CaptureResult mockCaptureResult; + + @Before + @SuppressWarnings("unchecked") + public void before() { + mockCaptureStateListener = mock(CameraCaptureStateListener.class); + mockCameraCaptureSession = mock(CameraCaptureSession.class); + mockCaptureRequest = mock(CaptureRequest.class); + mockCaptureResult = mock(CaptureResult.class); + Key mockAeStateKey = mock(Key.class); + Key mockAfStateKey = mock(Key.class); + + TestUtils.setFinalStatic(CaptureResult.class, "CONTROL_AE_STATE", mockAeStateKey); + TestUtils.setFinalStatic(CaptureResult.class, "CONTROL_AF_STATE", mockAfStateKey); + } + + @After + public void after() { + TestUtils.setFinalStatic(CaptureResult.class, "CONTROL_AE_STATE", null); + TestUtils.setFinalStatic(CaptureResult.class, "CONTROL_AF_STATE", null); + } + + @Test + public void process_should_converge_when_af_state_is_focus_locked_and_ae_state_is_null() { + CameraCaptureCallback captureCallback = CameraCaptureCallback.create(mockCaptureStateListener); + + when(mockCaptureResult.get(CaptureResult.CONTROL_AF_STATE)).thenReturn(CaptureResult.CONTROL_AF_STATE_FOCUSED_LOCKED); + when(mockCaptureResult.get(CaptureResult.CONTROL_AE_STATE)).thenReturn(null); + + captureCallback.setCameraState(CameraState.STATE_WAITING_FOCUS); + captureCallback.onCaptureProgressed(mockCameraCaptureSession, mockCaptureRequest, mockCaptureResult); + + verify(mockCaptureStateListener, times(1)).onConverged(); + } + + @Test + public void process_should_converge_when_af_state_is_focus_locked_and_ae_state_is_converged() { + CameraCaptureCallback captureCallback = CameraCaptureCallback.create(mockCaptureStateListener); + + when(mockCaptureResult.get(CaptureResult.CONTROL_AF_STATE)).thenReturn(CaptureResult.CONTROL_AF_STATE_FOCUSED_LOCKED); + when(mockCaptureResult.get(CaptureResult.CONTROL_AE_STATE)).thenReturn(CaptureResult.CONTROL_AE_STATE_CONVERGED); + + captureCallback.setCameraState(CameraState.STATE_WAITING_FOCUS); + captureCallback.onCaptureProgressed(mockCameraCaptureSession, mockCaptureRequest, mockCaptureResult); + + verify(mockCaptureStateListener, times(1)).onConverged(); + } + + @Test + public void process_should_converge_when_af_state_is_focus_locked_and_ae_state_is_not_null_and_not_converged() { + CameraCaptureCallback captureCallback = CameraCaptureCallback.create(mockCaptureStateListener); + + when(mockCaptureResult.get(CaptureResult.CONTROL_AF_STATE)).thenReturn(CaptureResult.CONTROL_AF_STATE_FOCUSED_LOCKED); + captureCallback.setCameraState(CameraState.STATE_WAITING_FOCUS); + + when(mockCaptureResult.get(CaptureResult.CONTROL_AE_STATE)).thenReturn(CaptureResult.CONTROL_AE_STATE_LOCKED); + captureCallback.onCaptureProgressed(mockCameraCaptureSession, mockCaptureRequest, mockCaptureResult); + + when(mockCaptureResult.get(CaptureResult.CONTROL_AE_STATE)).thenReturn(CaptureResult.CONTROL_AE_STATE_INACTIVE); + captureCallback.onCaptureProgressed(mockCameraCaptureSession, mockCaptureRequest, mockCaptureResult); + + when(mockCaptureResult.get(CaptureResult.CONTROL_AE_STATE)).thenReturn(CaptureResult.CONTROL_AE_STATE_PRECAPTURE); + captureCallback.onCaptureProgressed(mockCameraCaptureSession, mockCaptureRequest, mockCaptureResult); + + when(mockCaptureResult.get(CaptureResult.CONTROL_AE_STATE)).thenReturn(CaptureResult.CONTROL_AE_STATE_SEARCHING); + captureCallback.onCaptureProgressed(mockCameraCaptureSession, mockCaptureRequest, mockCaptureResult); + + when(mockCaptureResult.get(CaptureResult.CONTROL_AE_STATE)).thenReturn(CaptureResult.CONTROL_AE_STATE_PRECAPTURE); + captureCallback.onCaptureProgressed(mockCameraCaptureSession, mockCaptureRequest, mockCaptureResult); + + when(mockCaptureResult.get(CaptureResult.CONTROL_AE_STATE)).thenReturn(CaptureResult.CONTROL_AE_STATE_FLASH_REQUIRED); + captureCallback.onCaptureProgressed(mockCameraCaptureSession, mockCaptureRequest, mockCaptureResult); + + verify(mockCaptureStateListener, times(6)).onPrecapture(); + } + } +} diff --git a/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/features/noisereduction/NoiseReductionFeatureTest.java b/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/features/noisereduction/NoiseReductionFeatureTest.java index 0b0e41d9c760..a951938550e9 100644 --- a/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/features/noisereduction/NoiseReductionFeatureTest.java +++ b/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/features/noisereduction/NoiseReductionFeatureTest.java @@ -19,7 +19,6 @@ import io.flutter.plugins.camera.CameraProperties; import io.flutter.plugins.camera.utils.TestUtils; import org.junit.After; -import org.junit.Assert; import org.junit.Before; import org.junit.Test; @@ -28,21 +27,13 @@ public class NoiseReductionFeatureTest { public void before() { // Make sure the VERSION.SDK_INT field returns 23, to allow using all available // noise reduction modes in tests. - try { - TestUtils.setFinalStatic(VERSION.class.getField("SDK_INT"), 23); - } catch (Exception e) { - Assert.fail("Unable to set SDK_INT"); - } + TestUtils.setFinalStatic(VERSION.class,"SDK_INT", 23); } @After public void after() { // Make sure we reset the VERSION.SDK_INT field to it's original value. - try { - TestUtils.setFinalStatic(VERSION.class.getField("SDK_INT"), 0); - } catch (Exception e) { - Assert.fail("Unable to set SDK_INT"); - } + TestUtils.setFinalStatic(VERSION.class,"SDK_INT", 0); } @Test diff --git a/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/features/regionboundaries/RegionBoundariesFeatureTest.java b/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/features/regionboundaries/RegionBoundariesFeatureTest.java index cab7a3ee11df..5759b9c49690 100644 --- a/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/features/regionboundaries/RegionBoundariesFeatureTest.java +++ b/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/features/regionboundaries/RegionBoundariesFeatureTest.java @@ -14,7 +14,6 @@ import android.util.Size; import io.flutter.plugins.camera.CameraProperties; import io.flutter.plugins.camera.utils.TestUtils; -import org.junit.Assert; import org.junit.Before; import org.junit.Test; @@ -233,10 +232,6 @@ public void checkIsSupport_returns_true() { } private static void updateSdkVersion(int version) { - try { - TestUtils.setFinalStatic(VERSION.class.getField("SDK_INT"), version); - } catch (Exception e) { - Assert.fail("Unable to update SDK version"); - } + TestUtils.setFinalStatic(VERSION.class,"SDK_INT", version); } } diff --git a/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/utils/TestUtils.java b/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/utils/TestUtils.java index 142f88bc34b4..db4cb90e93b4 100644 --- a/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/utils/TestUtils.java +++ b/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/utils/TestUtils.java @@ -6,16 +6,21 @@ import java.lang.reflect.Field; import java.lang.reflect.Modifier; +import org.junit.Assert; public class TestUtils { - public static void setFinalStatic(Field field, Object newValue) - throws NoSuchFieldException, IllegalAccessException { - field.setAccessible(true); + public static void setFinalStatic(Class classToModify, String fieldName, Object newValue) { + try { + Field field = classToModify.getField(fieldName); + field.setAccessible(true); - Field modifiersField = Field.class.getDeclaredField("modifiers"); - modifiersField.setAccessible(true); - modifiersField.setInt(field, field.getModifiers() & ~Modifier.FINAL); + Field modifiersField = Field.class.getDeclaredField("modifiers"); + modifiersField.setAccessible(true); + modifiersField.setInt(field, field.getModifiers() & ~Modifier.FINAL); - field.set(null, newValue); + field.set(null, newValue); + } catch (Exception e) { + Assert.fail("Unable to mock static field: " + fieldName); + } } } From c541f2fdc26db35405c477734c5a559fffbac58b Mon Sep 17 00:00:00 2001 From: Maurits van Beusekom Date: Thu, 11 Mar 2021 15:59:03 +0100 Subject: [PATCH 099/114] Fix formatting --- .../plugins/camera/CameraCaptureCallback.java | 9 +- .../CameraCaptureCallbackStatesTest.java | 353 +++++++++++------- .../camera/CameraCaptureCallbackTest.java | 76 ++-- .../NoiseReductionFeatureTest.java | 4 +- .../RegionBoundariesFeatureTest.java | 2 +- 5 files changed, 282 insertions(+), 162 deletions(-) diff --git a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/CameraCaptureCallback.java b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/CameraCaptureCallback.java index c6175124aad3..54297fc42f22 100644 --- a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/CameraCaptureCallback.java +++ b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/CameraCaptureCallback.java @@ -67,7 +67,8 @@ private void process(CaptureResult result) { } else if (afState == CaptureResult.CONTROL_AF_STATE_FOCUSED_LOCKED || afState == CaptureResult.CONTROL_AF_STATE_NOT_FOCUSED_LOCKED) { handleWaitingFocusState(aeState); - } else if (pictureCaptureRequest != null && pictureCaptureRequest.preCaptureFocusing.getIsExpired()) { + } else if (pictureCaptureRequest != null + && pictureCaptureRequest.preCaptureFocusing.getIsExpired()) { Log.w("Camera", "Focus timeout, moving on with capture"); handleWaitingFocusState(aeState); } @@ -82,7 +83,8 @@ private void process(CaptureResult result) { || aeState == CaptureResult.CONTROL_AE_STATE_PRECAPTURE || aeState == CaptureResult.CONTROL_AE_STATE_FLASH_REQUIRED) { setCameraState(CameraState.STATE_WAITING_PRECAPTURE_DONE); - } else if (pictureCaptureRequest != null && pictureCaptureRequest.preCaptureMetering.getIsExpired()) { + } else if (pictureCaptureRequest != null + && pictureCaptureRequest.preCaptureMetering.getIsExpired()) { Log.w( "Camera", "Metering timeout waiting for pre-capture to start, moving on with capture"); @@ -96,7 +98,8 @@ private void process(CaptureResult result) { // CONTROL_AE_STATE can be null on some devices if (aeState == null || aeState != CaptureResult.CONTROL_AE_STATE_PRECAPTURE) { cameraStateListener.onConverged(); - } else if (pictureCaptureRequest != null && pictureCaptureRequest.preCaptureMetering.getIsExpired()) { + } else if (pictureCaptureRequest != null + && pictureCaptureRequest.preCaptureMetering.getIsExpired()) { Log.w( "Camera", "Metering timeout waiting for pre-capture to finish, moving on with capture"); diff --git a/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/CameraCaptureCallbackStatesTest.java b/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/CameraCaptureCallbackStatesTest.java index 32586307f8e1..9904e11cf5ab 100644 --- a/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/CameraCaptureCallbackStatesTest.java +++ b/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/CameraCaptureCallbackStatesTest.java @@ -51,165 +51,259 @@ public static TestSuite suite() { } private static void setUpPreviewStateTest(TestSuite suite) { - CameraCaptureCallbackStatesTest previewStateTest = new CameraCaptureCallbackStatesTest("process_should_not_converge_or_pre_capture_when_state_is_preview", CameraState.STATE_PREVIEW, null, null); - previewStateTest.validate = () -> { - verify(previewStateTest.mockCaptureStateListener, never()).onConverged(); - verify(previewStateTest.mockCaptureStateListener, never()).onConverged(); - assertEquals(CameraState.STATE_PREVIEW, previewStateTest.cameraCaptureCallback.getCameraState()); - }; + CameraCaptureCallbackStatesTest previewStateTest = + new CameraCaptureCallbackStatesTest( + "process_should_not_converge_or_pre_capture_when_state_is_preview", + CameraState.STATE_PREVIEW, + null, + null); + previewStateTest.validate = + () -> { + verify(previewStateTest.mockCaptureStateListener, never()).onConverged(); + verify(previewStateTest.mockCaptureStateListener, never()).onConverged(); + assertEquals( + CameraState.STATE_PREVIEW, previewStateTest.cameraCaptureCallback.getCameraState()); + }; suite.addTest(previewStateTest); } private static void setUpWaitingFocusTests(TestSuite suite) { - Integer[] actionableAfStates = new Integer[] { - CaptureResult.CONTROL_AF_STATE_FOCUSED_LOCKED, - CaptureResult.CONTROL_AF_STATE_NOT_FOCUSED_LOCKED - }; - - Integer[] nonActionableAfStates = new Integer[] { - CaptureResult.CONTROL_AF_STATE_ACTIVE_SCAN, - CaptureResult.CONTROL_AF_STATE_INACTIVE, - CaptureResult.CONTROL_AF_STATE_PASSIVE_FOCUSED, - CaptureResult.CONTROL_AF_STATE_PASSIVE_SCAN, - CaptureResult.CONTROL_AF_STATE_PASSIVE_UNFOCUSED - }; - - Map aeStatesConvergeMap = new HashMap() {{ - put(null, true); - put(CaptureResult.CONTROL_AE_STATE_CONVERGED, true); - put(CaptureResult.CONTROL_AE_STATE_PRECAPTURE, false); - put(CaptureResult.CONTROL_AE_STATE_LOCKED, false); - put(CaptureResult.CONTROL_AE_STATE_SEARCHING, false); - put(CaptureResult.CONTROL_AE_STATE_INACTIVE, false); - put(CaptureResult.CONTROL_AE_STATE_FLASH_REQUIRED, false); - }}; - - CameraCaptureCallbackStatesTest nullStateTest = new CameraCaptureCallbackStatesTest("process_should_not_converge_or_pre_capture_when_afstate_is_null", CameraState.STATE_WAITING_FOCUS, null, null); - nullStateTest.validate = () -> { - verify(nullStateTest.mockCaptureStateListener, never()).onConverged(); - verify(nullStateTest.mockCaptureStateListener, never()).onConverged(); - assertEquals(CameraState.STATE_WAITING_FOCUS, nullStateTest.cameraCaptureCallback.getCameraState()); - }; - suite.addTest(nullStateTest); + Integer[] actionableAfStates = + new Integer[] { + CaptureResult.CONTROL_AF_STATE_FOCUSED_LOCKED, + CaptureResult.CONTROL_AF_STATE_NOT_FOCUSED_LOCKED + }; - for (Integer afState : actionableAfStates) { - aeStatesConvergeMap.forEach((aeState, shouldConverge) -> { - CameraCaptureCallbackStatesTest focusLockedTest = new CameraCaptureCallbackStatesTest( - "process_should_converge_when_af_state_is_" + afState + "_and_ae_state_is_" + aeState, - CameraState.STATE_WAITING_FOCUS, - afState, - aeState); - focusLockedTest.validate = () -> { - if (shouldConverge) { - verify(focusLockedTest.mockCaptureStateListener, times(1)).onConverged(); - verify(focusLockedTest.mockCaptureStateListener, never()).onPrecapture(); - } else { - verify(focusLockedTest.mockCaptureStateListener, times(1)).onPrecapture(); - verify(focusLockedTest.mockCaptureStateListener, never()).onConverged(); + Integer[] nonActionableAfStates = + new Integer[] { + CaptureResult.CONTROL_AF_STATE_ACTIVE_SCAN, + CaptureResult.CONTROL_AF_STATE_INACTIVE, + CaptureResult.CONTROL_AF_STATE_PASSIVE_FOCUSED, + CaptureResult.CONTROL_AF_STATE_PASSIVE_SCAN, + CaptureResult.CONTROL_AF_STATE_PASSIVE_UNFOCUSED + }; + + Map aeStatesConvergeMap = + new HashMap() { + { + put(null, true); + put(CaptureResult.CONTROL_AE_STATE_CONVERGED, true); + put(CaptureResult.CONTROL_AE_STATE_PRECAPTURE, false); + put(CaptureResult.CONTROL_AE_STATE_LOCKED, false); + put(CaptureResult.CONTROL_AE_STATE_SEARCHING, false); + put(CaptureResult.CONTROL_AE_STATE_INACTIVE, false); + put(CaptureResult.CONTROL_AE_STATE_FLASH_REQUIRED, false); } - assertEquals(CameraState.STATE_WAITING_FOCUS, focusLockedTest.cameraCaptureCallback.getCameraState()); }; - suite.addTest(focusLockedTest); - }); - } - for (Integer afState : nonActionableAfStates) { - CameraCaptureCallbackStatesTest focusLockedTest = new CameraCaptureCallbackStatesTest( - "process_should_do_nothing_when_af_state_is_" + afState, + CameraCaptureCallbackStatesTest nullStateTest = + new CameraCaptureCallbackStatesTest( + "process_should_not_converge_or_pre_capture_when_afstate_is_null", CameraState.STATE_WAITING_FOCUS, - afState, + null, null); - focusLockedTest.validate = () -> { - verify(focusLockedTest.mockCaptureStateListener, never()).onConverged(); - verify(focusLockedTest.mockCaptureStateListener, never()).onPrecapture(); - assertEquals(CameraState.STATE_WAITING_FOCUS, focusLockedTest.cameraCaptureCallback.getCameraState()); + nullStateTest.validate = + () -> { + verify(nullStateTest.mockCaptureStateListener, never()).onConverged(); + verify(nullStateTest.mockCaptureStateListener, never()).onConverged(); + assertEquals( + CameraState.STATE_WAITING_FOCUS, + nullStateTest.cameraCaptureCallback.getCameraState()); }; - suite.addTest(focusLockedTest); + suite.addTest(nullStateTest); + + for (Integer afState : actionableAfStates) { + aeStatesConvergeMap.forEach( + (aeState, shouldConverge) -> { + CameraCaptureCallbackStatesTest focusLockedTest = + new CameraCaptureCallbackStatesTest( + "process_should_converge_when_af_state_is_" + + afState + + "_and_ae_state_is_" + + aeState, + CameraState.STATE_WAITING_FOCUS, + afState, + aeState); + focusLockedTest.validate = + () -> { + if (shouldConverge) { + verify(focusLockedTest.mockCaptureStateListener, times(1)).onConverged(); + verify(focusLockedTest.mockCaptureStateListener, never()).onPrecapture(); + } else { + verify(focusLockedTest.mockCaptureStateListener, times(1)).onPrecapture(); + verify(focusLockedTest.mockCaptureStateListener, never()).onConverged(); + } + assertEquals( + CameraState.STATE_WAITING_FOCUS, + focusLockedTest.cameraCaptureCallback.getCameraState()); + }; + suite.addTest(focusLockedTest); + }); } for (Integer afState : nonActionableAfStates) { - aeStatesConvergeMap.forEach((aeState, shouldConverge) -> { - CameraCaptureCallbackStatesTest focusLockedTest = new CameraCaptureCallbackStatesTest( - "process_should_converge_when_af_state_is_" + afState + "_and_ae_state_is_" + aeState, - CameraState.STATE_WAITING_FOCUS, - afState, - aeState, - true); - focusLockedTest.validate = () -> { - if (shouldConverge) { - verify(focusLockedTest.mockCaptureStateListener, times(1)).onConverged(); - verify(focusLockedTest.mockCaptureStateListener, never()).onPrecapture(); - } else { - verify(focusLockedTest.mockCaptureStateListener, times(1)).onPrecapture(); + CameraCaptureCallbackStatesTest focusLockedTest = + new CameraCaptureCallbackStatesTest( + "process_should_do_nothing_when_af_state_is_" + afState, + CameraState.STATE_WAITING_FOCUS, + afState, + null); + focusLockedTest.validate = + () -> { verify(focusLockedTest.mockCaptureStateListener, never()).onConverged(); - } - assertEquals(CameraState.STATE_WAITING_FOCUS, focusLockedTest.cameraCaptureCallback.getCameraState()); - }; - suite.addTest(focusLockedTest); - }); + verify(focusLockedTest.mockCaptureStateListener, never()).onPrecapture(); + assertEquals( + CameraState.STATE_WAITING_FOCUS, + focusLockedTest.cameraCaptureCallback.getCameraState()); + }; + suite.addTest(focusLockedTest); + } + + for (Integer afState : nonActionableAfStates) { + aeStatesConvergeMap.forEach( + (aeState, shouldConverge) -> { + CameraCaptureCallbackStatesTest focusLockedTest = + new CameraCaptureCallbackStatesTest( + "process_should_converge_when_af_state_is_" + + afState + + "_and_ae_state_is_" + + aeState, + CameraState.STATE_WAITING_FOCUS, + afState, + aeState, + true); + focusLockedTest.validate = + () -> { + if (shouldConverge) { + verify(focusLockedTest.mockCaptureStateListener, times(1)).onConverged(); + verify(focusLockedTest.mockCaptureStateListener, never()).onPrecapture(); + } else { + verify(focusLockedTest.mockCaptureStateListener, times(1)).onPrecapture(); + verify(focusLockedTest.mockCaptureStateListener, never()).onConverged(); + } + assertEquals( + CameraState.STATE_WAITING_FOCUS, + focusLockedTest.cameraCaptureCallback.getCameraState()); + }; + suite.addTest(focusLockedTest); + }); } } private static void setUpWaitingPreCaptureStartTests(TestSuite suite) { - Map cameraStateMap = new HashMap() {{ - put(null, CameraState.STATE_WAITING_PRECAPTURE_DONE); - put(CaptureResult.CONTROL_AE_STATE_INACTIVE, CameraState.STATE_WAITING_PRECAPTURE_START); - put(CaptureResult.CONTROL_AE_STATE_SEARCHING, CameraState.STATE_WAITING_PRECAPTURE_START); - put(CaptureResult.CONTROL_AE_STATE_CONVERGED, CameraState.STATE_WAITING_PRECAPTURE_DONE); - put(CaptureResult.CONTROL_AE_STATE_LOCKED, CameraState.STATE_WAITING_PRECAPTURE_START); - put(CaptureResult.CONTROL_AE_STATE_FLASH_REQUIRED, CameraState.STATE_WAITING_PRECAPTURE_DONE); - put(CaptureResult.CONTROL_AE_STATE_PRECAPTURE, CameraState.STATE_WAITING_PRECAPTURE_DONE); - }}; - - cameraStateMap.forEach((aeState, cameraState) -> { - CameraCaptureCallbackStatesTest testCase = new CameraCaptureCallbackStatesTest("process_should_update_camera_state_to_waiting_pre_capture_done_when_ae_state_is_" + aeState, CameraState.STATE_WAITING_PRECAPTURE_START, null, aeState); - testCase.validate = () -> assertEquals(cameraState, testCase.cameraCaptureCallback.getCameraState()); - suite.addTest(testCase); - }); - - cameraStateMap.forEach((aeState, cameraState) -> { - if (cameraState == CameraState.STATE_WAITING_PRECAPTURE_DONE) { - return; - } - - CameraCaptureCallbackStatesTest testCase = new CameraCaptureCallbackStatesTest("process_should_update_camera_state_to_waiting_pre_capture_done_when_ae_state_is_" + aeState, CameraState.STATE_WAITING_PRECAPTURE_START, null, aeState, true); - testCase.validate = () -> assertEquals(CameraState.STATE_WAITING_PRECAPTURE_DONE, testCase.cameraCaptureCallback.getCameraState()); - suite.addTest(testCase); - }); + Map cameraStateMap = + new HashMap() { + { + put(null, CameraState.STATE_WAITING_PRECAPTURE_DONE); + put( + CaptureResult.CONTROL_AE_STATE_INACTIVE, + CameraState.STATE_WAITING_PRECAPTURE_START); + put( + CaptureResult.CONTROL_AE_STATE_SEARCHING, + CameraState.STATE_WAITING_PRECAPTURE_START); + put( + CaptureResult.CONTROL_AE_STATE_CONVERGED, + CameraState.STATE_WAITING_PRECAPTURE_DONE); + put(CaptureResult.CONTROL_AE_STATE_LOCKED, CameraState.STATE_WAITING_PRECAPTURE_START); + put( + CaptureResult.CONTROL_AE_STATE_FLASH_REQUIRED, + CameraState.STATE_WAITING_PRECAPTURE_DONE); + put( + CaptureResult.CONTROL_AE_STATE_PRECAPTURE, + CameraState.STATE_WAITING_PRECAPTURE_DONE); + } + }; + + cameraStateMap.forEach( + (aeState, cameraState) -> { + CameraCaptureCallbackStatesTest testCase = + new CameraCaptureCallbackStatesTest( + "process_should_update_camera_state_to_waiting_pre_capture_done_when_ae_state_is_" + + aeState, + CameraState.STATE_WAITING_PRECAPTURE_START, + null, + aeState); + testCase.validate = + () -> assertEquals(cameraState, testCase.cameraCaptureCallback.getCameraState()); + suite.addTest(testCase); + }); + + cameraStateMap.forEach( + (aeState, cameraState) -> { + if (cameraState == CameraState.STATE_WAITING_PRECAPTURE_DONE) { + return; + } + + CameraCaptureCallbackStatesTest testCase = + new CameraCaptureCallbackStatesTest( + "process_should_update_camera_state_to_waiting_pre_capture_done_when_ae_state_is_" + + aeState, + CameraState.STATE_WAITING_PRECAPTURE_START, + null, + aeState, + true); + testCase.validate = + () -> + assertEquals( + CameraState.STATE_WAITING_PRECAPTURE_DONE, + testCase.cameraCaptureCallback.getCameraState()); + suite.addTest(testCase); + }); } private static void setUpWaitingPreCaptureDoneTests(TestSuite suite) { - Integer[] onConvergeStates = new Integer[] { - null, - CaptureResult.CONTROL_AE_STATE_CONVERGED, - CaptureResult.CONTROL_AE_STATE_LOCKED, - CaptureResult.CONTROL_AE_STATE_SEARCHING, - CaptureResult.CONTROL_AE_STATE_INACTIVE, - CaptureResult.CONTROL_AE_STATE_FLASH_REQUIRED, - }; + Integer[] onConvergeStates = + new Integer[] { + null, + CaptureResult.CONTROL_AE_STATE_CONVERGED, + CaptureResult.CONTROL_AE_STATE_LOCKED, + CaptureResult.CONTROL_AE_STATE_SEARCHING, + CaptureResult.CONTROL_AE_STATE_INACTIVE, + CaptureResult.CONTROL_AE_STATE_FLASH_REQUIRED, + }; for (Integer aeState : onConvergeStates) { - CameraCaptureCallbackStatesTest shouldConvergeTest = new CameraCaptureCallbackStatesTest( - "process_should_converge_when_ae_state_is_" + aeState, - CameraState.STATE_WAITING_PRECAPTURE_DONE, null, null); - shouldConvergeTest.validate = () -> verify(shouldConvergeTest.mockCaptureStateListener, times(1)).onConverged(); + CameraCaptureCallbackStatesTest shouldConvergeTest = + new CameraCaptureCallbackStatesTest( + "process_should_converge_when_ae_state_is_" + aeState, + CameraState.STATE_WAITING_PRECAPTURE_DONE, + null, + null); + shouldConvergeTest.validate = + () -> verify(shouldConvergeTest.mockCaptureStateListener, times(1)).onConverged(); suite.addTest(shouldConvergeTest); } - CameraCaptureCallbackStatesTest shouldNotConvergeTest = new CameraCaptureCallbackStatesTest("process_should_not_converge_when_ae_state_is_pre_capture", CameraState.STATE_WAITING_PRECAPTURE_DONE, null, CaptureResult.CONTROL_AE_STATE_PRECAPTURE); - shouldNotConvergeTest.validate = () -> verify(shouldNotConvergeTest.mockCaptureStateListener, never()).onConverged(); + CameraCaptureCallbackStatesTest shouldNotConvergeTest = + new CameraCaptureCallbackStatesTest( + "process_should_not_converge_when_ae_state_is_pre_capture", + CameraState.STATE_WAITING_PRECAPTURE_DONE, + null, + CaptureResult.CONTROL_AE_STATE_PRECAPTURE); + shouldNotConvergeTest.validate = + () -> verify(shouldNotConvergeTest.mockCaptureStateListener, never()).onConverged(); suite.addTest(shouldNotConvergeTest); - CameraCaptureCallbackStatesTest shouldConvergeWhenTimedOutTest = new CameraCaptureCallbackStatesTest("process_should_not_converge_when_ae_state_is_pre_capture", CameraState.STATE_WAITING_PRECAPTURE_DONE, null, CaptureResult.CONTROL_AE_STATE_PRECAPTURE, true); - shouldConvergeWhenTimedOutTest.validate = () -> verify(shouldConvergeWhenTimedOutTest.mockCaptureStateListener, times(1)).onConverged(); + CameraCaptureCallbackStatesTest shouldConvergeWhenTimedOutTest = + new CameraCaptureCallbackStatesTest( + "process_should_not_converge_when_ae_state_is_pre_capture", + CameraState.STATE_WAITING_PRECAPTURE_DONE, + null, + CaptureResult.CONTROL_AE_STATE_PRECAPTURE, + true); + shouldConvergeWhenTimedOutTest.validate = + () -> + verify(shouldConvergeWhenTimedOutTest.mockCaptureStateListener, times(1)).onConverged(); suite.addTest(shouldConvergeWhenTimedOutTest); } - protected CameraCaptureCallbackStatesTest(String name, CameraState cameraState, Integer afState, Integer aeState) { + protected CameraCaptureCallbackStatesTest( + String name, CameraState cameraState, Integer afState, Integer aeState) { this(name, cameraState, afState, aeState, false); } - protected CameraCaptureCallbackStatesTest(String name, CameraState cameraState, Integer afState, Integer aeState, boolean isTimedOut) { + protected CameraCaptureCallbackStatesTest( + String name, CameraState cameraState, Integer afState, Integer aeState, boolean isTimedOut) { super(name); this.aeState = aeState; @@ -261,11 +355,11 @@ protected void runTest() throws Throwable { cameraCaptureCallback.setCameraState(cameraState); if (isTimedOut) { configureTimeout(); - cameraCaptureCallback.onCaptureCompleted(mockCameraCaptureSession, mockCaptureRequest, - mockTotalCaptureResult); + cameraCaptureCallback.onCaptureCompleted( + mockCameraCaptureSession, mockCaptureRequest, mockTotalCaptureResult); } else { - cameraCaptureCallback.onCaptureProgressed(mockCameraCaptureSession, mockCaptureRequest, - mockPartialCaptureResult); + cameraCaptureCallback.onCaptureProgressed( + mockCameraCaptureSession, mockCaptureRequest, mockPartialCaptureResult); } validate.run(); @@ -277,7 +371,8 @@ private void configureTimeout() { when(mockTimeout.getIsExpired()).thenReturn(true); - PictureCaptureRequest pictureCaptureRequest = PictureCaptureRequest.create(mockResult, mockFile, 1000, 1000); + PictureCaptureRequest pictureCaptureRequest = + PictureCaptureRequest.create(mockResult, mockFile, 1000, 1000); cameraCaptureCallback.setPictureCaptureRequest(pictureCaptureRequest); } } diff --git a/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/CameraCaptureCallbackTest.java b/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/CameraCaptureCallbackTest.java index 597783672bcf..21012082b078 100644 --- a/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/CameraCaptureCallbackTest.java +++ b/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/CameraCaptureCallbackTest.java @@ -68,54 +68,76 @@ public void after() { @Test public void process_should_converge_when_af_state_is_focus_locked_and_ae_state_is_null() { - CameraCaptureCallback captureCallback = CameraCaptureCallback.create(mockCaptureStateListener); + CameraCaptureCallback captureCallback = + CameraCaptureCallback.create(mockCaptureStateListener); - when(mockCaptureResult.get(CaptureResult.CONTROL_AF_STATE)).thenReturn(CaptureResult.CONTROL_AF_STATE_FOCUSED_LOCKED); + when(mockCaptureResult.get(CaptureResult.CONTROL_AF_STATE)) + .thenReturn(CaptureResult.CONTROL_AF_STATE_FOCUSED_LOCKED); when(mockCaptureResult.get(CaptureResult.CONTROL_AE_STATE)).thenReturn(null); captureCallback.setCameraState(CameraState.STATE_WAITING_FOCUS); - captureCallback.onCaptureProgressed(mockCameraCaptureSession, mockCaptureRequest, mockCaptureResult); + captureCallback.onCaptureProgressed( + mockCameraCaptureSession, mockCaptureRequest, mockCaptureResult); verify(mockCaptureStateListener, times(1)).onConverged(); } @Test public void process_should_converge_when_af_state_is_focus_locked_and_ae_state_is_converged() { - CameraCaptureCallback captureCallback = CameraCaptureCallback.create(mockCaptureStateListener); + CameraCaptureCallback captureCallback = + CameraCaptureCallback.create(mockCaptureStateListener); - when(mockCaptureResult.get(CaptureResult.CONTROL_AF_STATE)).thenReturn(CaptureResult.CONTROL_AF_STATE_FOCUSED_LOCKED); - when(mockCaptureResult.get(CaptureResult.CONTROL_AE_STATE)).thenReturn(CaptureResult.CONTROL_AE_STATE_CONVERGED); + when(mockCaptureResult.get(CaptureResult.CONTROL_AF_STATE)) + .thenReturn(CaptureResult.CONTROL_AF_STATE_FOCUSED_LOCKED); + when(mockCaptureResult.get(CaptureResult.CONTROL_AE_STATE)) + .thenReturn(CaptureResult.CONTROL_AE_STATE_CONVERGED); captureCallback.setCameraState(CameraState.STATE_WAITING_FOCUS); - captureCallback.onCaptureProgressed(mockCameraCaptureSession, mockCaptureRequest, mockCaptureResult); + captureCallback.onCaptureProgressed( + mockCameraCaptureSession, mockCaptureRequest, mockCaptureResult); verify(mockCaptureStateListener, times(1)).onConverged(); } @Test - public void process_should_converge_when_af_state_is_focus_locked_and_ae_state_is_not_null_and_not_converged() { - CameraCaptureCallback captureCallback = CameraCaptureCallback.create(mockCaptureStateListener); + public void + process_should_converge_when_af_state_is_focus_locked_and_ae_state_is_not_null_and_not_converged() { + CameraCaptureCallback captureCallback = + CameraCaptureCallback.create(mockCaptureStateListener); - when(mockCaptureResult.get(CaptureResult.CONTROL_AF_STATE)).thenReturn(CaptureResult.CONTROL_AF_STATE_FOCUSED_LOCKED); + when(mockCaptureResult.get(CaptureResult.CONTROL_AF_STATE)) + .thenReturn(CaptureResult.CONTROL_AF_STATE_FOCUSED_LOCKED); captureCallback.setCameraState(CameraState.STATE_WAITING_FOCUS); - when(mockCaptureResult.get(CaptureResult.CONTROL_AE_STATE)).thenReturn(CaptureResult.CONTROL_AE_STATE_LOCKED); - captureCallback.onCaptureProgressed(mockCameraCaptureSession, mockCaptureRequest, mockCaptureResult); - - when(mockCaptureResult.get(CaptureResult.CONTROL_AE_STATE)).thenReturn(CaptureResult.CONTROL_AE_STATE_INACTIVE); - captureCallback.onCaptureProgressed(mockCameraCaptureSession, mockCaptureRequest, mockCaptureResult); - - when(mockCaptureResult.get(CaptureResult.CONTROL_AE_STATE)).thenReturn(CaptureResult.CONTROL_AE_STATE_PRECAPTURE); - captureCallback.onCaptureProgressed(mockCameraCaptureSession, mockCaptureRequest, mockCaptureResult); - - when(mockCaptureResult.get(CaptureResult.CONTROL_AE_STATE)).thenReturn(CaptureResult.CONTROL_AE_STATE_SEARCHING); - captureCallback.onCaptureProgressed(mockCameraCaptureSession, mockCaptureRequest, mockCaptureResult); - - when(mockCaptureResult.get(CaptureResult.CONTROL_AE_STATE)).thenReturn(CaptureResult.CONTROL_AE_STATE_PRECAPTURE); - captureCallback.onCaptureProgressed(mockCameraCaptureSession, mockCaptureRequest, mockCaptureResult); - - when(mockCaptureResult.get(CaptureResult.CONTROL_AE_STATE)).thenReturn(CaptureResult.CONTROL_AE_STATE_FLASH_REQUIRED); - captureCallback.onCaptureProgressed(mockCameraCaptureSession, mockCaptureRequest, mockCaptureResult); + when(mockCaptureResult.get(CaptureResult.CONTROL_AE_STATE)) + .thenReturn(CaptureResult.CONTROL_AE_STATE_LOCKED); + captureCallback.onCaptureProgressed( + mockCameraCaptureSession, mockCaptureRequest, mockCaptureResult); + + when(mockCaptureResult.get(CaptureResult.CONTROL_AE_STATE)) + .thenReturn(CaptureResult.CONTROL_AE_STATE_INACTIVE); + captureCallback.onCaptureProgressed( + mockCameraCaptureSession, mockCaptureRequest, mockCaptureResult); + + when(mockCaptureResult.get(CaptureResult.CONTROL_AE_STATE)) + .thenReturn(CaptureResult.CONTROL_AE_STATE_PRECAPTURE); + captureCallback.onCaptureProgressed( + mockCameraCaptureSession, mockCaptureRequest, mockCaptureResult); + + when(mockCaptureResult.get(CaptureResult.CONTROL_AE_STATE)) + .thenReturn(CaptureResult.CONTROL_AE_STATE_SEARCHING); + captureCallback.onCaptureProgressed( + mockCameraCaptureSession, mockCaptureRequest, mockCaptureResult); + + when(mockCaptureResult.get(CaptureResult.CONTROL_AE_STATE)) + .thenReturn(CaptureResult.CONTROL_AE_STATE_PRECAPTURE); + captureCallback.onCaptureProgressed( + mockCameraCaptureSession, mockCaptureRequest, mockCaptureResult); + + when(mockCaptureResult.get(CaptureResult.CONTROL_AE_STATE)) + .thenReturn(CaptureResult.CONTROL_AE_STATE_FLASH_REQUIRED); + captureCallback.onCaptureProgressed( + mockCameraCaptureSession, mockCaptureRequest, mockCaptureResult); verify(mockCaptureStateListener, times(6)).onPrecapture(); } diff --git a/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/features/noisereduction/NoiseReductionFeatureTest.java b/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/features/noisereduction/NoiseReductionFeatureTest.java index a951938550e9..4905c68bce8a 100644 --- a/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/features/noisereduction/NoiseReductionFeatureTest.java +++ b/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/features/noisereduction/NoiseReductionFeatureTest.java @@ -27,13 +27,13 @@ public class NoiseReductionFeatureTest { public void before() { // Make sure the VERSION.SDK_INT field returns 23, to allow using all available // noise reduction modes in tests. - TestUtils.setFinalStatic(VERSION.class,"SDK_INT", 23); + TestUtils.setFinalStatic(VERSION.class, "SDK_INT", 23); } @After public void after() { // Make sure we reset the VERSION.SDK_INT field to it's original value. - TestUtils.setFinalStatic(VERSION.class,"SDK_INT", 0); + TestUtils.setFinalStatic(VERSION.class, "SDK_INT", 0); } @Test diff --git a/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/features/regionboundaries/RegionBoundariesFeatureTest.java b/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/features/regionboundaries/RegionBoundariesFeatureTest.java index 5759b9c49690..1a5d022ffaa7 100644 --- a/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/features/regionboundaries/RegionBoundariesFeatureTest.java +++ b/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/features/regionboundaries/RegionBoundariesFeatureTest.java @@ -232,6 +232,6 @@ public void checkIsSupport_returns_true() { } private static void updateSdkVersion(int version) { - TestUtils.setFinalStatic(VERSION.class,"SDK_INT", version); + TestUtils.setFinalStatic(VERSION.class, "SDK_INT", version); } } From c56da1a0b365554166b407a2ba44c9aa13bba41d Mon Sep 17 00:00:00 2001 From: Maurits van Beusekom Date: Thu, 11 Mar 2021 16:14:13 +0100 Subject: [PATCH 100/114] Restructured test case --- .../CameraCaptureCallbackStatesTest.java | 138 +++++++++--------- 1 file changed, 69 insertions(+), 69 deletions(-) diff --git a/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/CameraCaptureCallbackStatesTest.java b/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/CameraCaptureCallbackStatesTest.java index 9904e11cf5ab..8b7c7d0dace2 100644 --- a/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/CameraCaptureCallbackStatesTest.java +++ b/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/CameraCaptureCallbackStatesTest.java @@ -50,6 +50,74 @@ public static TestSuite suite() { return suite; } + protected CameraCaptureCallbackStatesTest( + String name, CameraState cameraState, Integer afState, Integer aeState) { + this(name, cameraState, afState, aeState, false); + } + + protected CameraCaptureCallbackStatesTest( + String name, CameraState cameraState, Integer afState, Integer aeState, boolean isTimedOut) { + super(name); + + this.aeState = aeState; + this.afState = afState; + this.cameraState = cameraState; + this.isTimedOut = isTimedOut; + } + + @Override + @SuppressWarnings("unchecked") + protected void setUp() throws Exception { + super.setUp(); + + mockedStaticTimeout = mockStatic(Timeout.class); + mockCaptureStateListener = mock(CameraCaptureStateListener.class); + mockCameraCaptureSession = mock(CameraCaptureSession.class); + mockCaptureRequest = mock(CaptureRequest.class); + mockPartialCaptureResult = mock(CaptureResult.class); + mockTotalCaptureResult = mock(TotalCaptureResult.class); + mockTimeout = mock(Timeout.class); + Key mockAeStateKey = mock(Key.class); + Key mockAfStateKey = mock(Key.class); + + TestUtils.setFinalStatic(CaptureResult.class, "CONTROL_AE_STATE", mockAeStateKey); + TestUtils.setFinalStatic(CaptureResult.class, "CONTROL_AF_STATE", mockAfStateKey); + + mockedStaticTimeout.when(() -> Timeout.create(1000)).thenReturn(mockTimeout); + + cameraCaptureCallback = CameraCaptureCallback.create(mockCaptureStateListener); + } + + @Override + protected void tearDown() throws Exception { + super.tearDown(); + + mockedStaticTimeout.close(); + + TestUtils.setFinalStatic(CaptureResult.class, "CONTROL_AE_STATE", null); + TestUtils.setFinalStatic(CaptureResult.class, "CONTROL_AF_STATE", null); + } + + @Override + protected void runTest() throws Throwable { + when(mockPartialCaptureResult.get(CaptureResult.CONTROL_AF_STATE)).thenReturn(afState); + when(mockPartialCaptureResult.get(CaptureResult.CONTROL_AE_STATE)).thenReturn(aeState); + when(mockTotalCaptureResult.get(CaptureResult.CONTROL_AF_STATE)).thenReturn(afState); + when(mockTotalCaptureResult.get(CaptureResult.CONTROL_AE_STATE)).thenReturn(aeState); + + cameraCaptureCallback.setCameraState(cameraState); + if (isTimedOut) { + configureTimeout(); + cameraCaptureCallback.onCaptureCompleted( + mockCameraCaptureSession, mockCaptureRequest, mockTotalCaptureResult); + } else { + cameraCaptureCallback.onCaptureProgressed( + mockCameraCaptureSession, mockCaptureRequest, mockPartialCaptureResult); + } + + validate.run(); + } + private static void setUpPreviewStateTest(TestSuite suite) { CameraCaptureCallbackStatesTest previewStateTest = new CameraCaptureCallbackStatesTest( @@ -95,7 +163,7 @@ private static void setUpWaitingFocusTests(TestSuite suite) { put(CaptureResult.CONTROL_AE_STATE_FLASH_REQUIRED, false); } }; - + CameraCaptureCallbackStatesTest nullStateTest = new CameraCaptureCallbackStatesTest( "process_should_not_converge_or_pre_capture_when_afstate_is_null", @@ -297,74 +365,6 @@ private static void setUpWaitingPreCaptureDoneTests(TestSuite suite) { suite.addTest(shouldConvergeWhenTimedOutTest); } - protected CameraCaptureCallbackStatesTest( - String name, CameraState cameraState, Integer afState, Integer aeState) { - this(name, cameraState, afState, aeState, false); - } - - protected CameraCaptureCallbackStatesTest( - String name, CameraState cameraState, Integer afState, Integer aeState, boolean isTimedOut) { - super(name); - - this.aeState = aeState; - this.afState = afState; - this.cameraState = cameraState; - this.isTimedOut = isTimedOut; - } - - @Override - @SuppressWarnings("unchecked") - protected void setUp() throws Exception { - super.setUp(); - - mockedStaticTimeout = mockStatic(Timeout.class); - mockCaptureStateListener = mock(CameraCaptureStateListener.class); - mockCameraCaptureSession = mock(CameraCaptureSession.class); - mockCaptureRequest = mock(CaptureRequest.class); - mockPartialCaptureResult = mock(CaptureResult.class); - mockTotalCaptureResult = mock(TotalCaptureResult.class); - mockTimeout = mock(Timeout.class); - Key mockAeStateKey = mock(Key.class); - Key mockAfStateKey = mock(Key.class); - - TestUtils.setFinalStatic(CaptureResult.class, "CONTROL_AE_STATE", mockAeStateKey); - TestUtils.setFinalStatic(CaptureResult.class, "CONTROL_AF_STATE", mockAfStateKey); - - mockedStaticTimeout.when(() -> Timeout.create(1000)).thenReturn(mockTimeout); - - cameraCaptureCallback = CameraCaptureCallback.create(mockCaptureStateListener); - } - - @Override - protected void tearDown() throws Exception { - super.tearDown(); - - mockedStaticTimeout.close(); - - TestUtils.setFinalStatic(CaptureResult.class, "CONTROL_AE_STATE", null); - TestUtils.setFinalStatic(CaptureResult.class, "CONTROL_AF_STATE", null); - } - - @Override - protected void runTest() throws Throwable { - when(mockPartialCaptureResult.get(CaptureResult.CONTROL_AF_STATE)).thenReturn(afState); - when(mockPartialCaptureResult.get(CaptureResult.CONTROL_AE_STATE)).thenReturn(aeState); - when(mockTotalCaptureResult.get(CaptureResult.CONTROL_AF_STATE)).thenReturn(afState); - when(mockTotalCaptureResult.get(CaptureResult.CONTROL_AE_STATE)).thenReturn(aeState); - - cameraCaptureCallback.setCameraState(cameraState); - if (isTimedOut) { - configureTimeout(); - cameraCaptureCallback.onCaptureCompleted( - mockCameraCaptureSession, mockCaptureRequest, mockTotalCaptureResult); - } else { - cameraCaptureCallback.onCaptureProgressed( - mockCameraCaptureSession, mockCaptureRequest, mockPartialCaptureResult); - } - - validate.run(); - } - private void configureTimeout() { MethodChannel.Result mockResult = mock(MethodChannel.Result.class); File mockFile = mock(File.class); From 1339834af33025e81c8f1f05381a941771bea21a Mon Sep 17 00:00:00 2001 From: Maurits van Beusekom Date: Thu, 11 Mar 2021 16:32:13 +0100 Subject: [PATCH 101/114] Removed obsolete test script --- .../io/flutter/plugins/camera/Camera.java | 1 - .../CameraCaptureCallbackStatesTest.java | 2 +- .../camera/CameraCaptureCallbackTest.java | 145 ------------------ 3 files changed, 1 insertion(+), 147 deletions(-) delete mode 100644 packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/CameraCaptureCallbackTest.java 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 97d8e75a28dd..3b39e1cdaeef 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 @@ -157,7 +157,6 @@ public void onImageAvailable(ImageReader reader) { private ImageReader imageStreamReader; /** {@link CaptureRequest.Builder} for the camera preview */ private CaptureRequest.Builder mPreviewRequestBuilder; - /** {@link CaptureRequest} generated by {@link #mPreviewRequestBuilder} */ private MediaRecorder mediaRecorder; /** True when recording video. */ private boolean recordingVideo; diff --git a/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/CameraCaptureCallbackStatesTest.java b/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/CameraCaptureCallbackStatesTest.java index 8b7c7d0dace2..1830e1507e07 100644 --- a/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/CameraCaptureCallbackStatesTest.java +++ b/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/CameraCaptureCallbackStatesTest.java @@ -163,7 +163,7 @@ private static void setUpWaitingFocusTests(TestSuite suite) { put(CaptureResult.CONTROL_AE_STATE_FLASH_REQUIRED, false); } }; - + CameraCaptureCallbackStatesTest nullStateTest = new CameraCaptureCallbackStatesTest( "process_should_not_converge_or_pre_capture_when_afstate_is_null", diff --git a/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/CameraCaptureCallbackTest.java b/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/CameraCaptureCallbackTest.java deleted file mode 100644 index 21012082b078..000000000000 --- a/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/CameraCaptureCallbackTest.java +++ /dev/null @@ -1,145 +0,0 @@ -package io.flutter.plugins.camera; - -import static org.junit.Assert.assertEquals; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.times; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.when; - -import android.hardware.camera2.CameraCaptureSession; -import android.hardware.camera2.CaptureRequest; -import android.hardware.camera2.CaptureResult; -import android.hardware.camera2.CaptureResult.Key; -import io.flutter.plugins.camera.CameraCaptureCallback.CameraCaptureStateListener; -import io.flutter.plugins.camera.utils.TestUtils; -import org.junit.After; -import org.junit.Before; -import org.junit.Test; - -public class CameraCaptureCallbackTest { - private CameraCaptureStateListener mockCaptureStateListener; - - @Before - public void before() { - mockCaptureStateListener = mock(CameraCaptureStateListener.class); - } - - @Test - public void getCameraState_should_return_camera_preview_if_not_set() { - CameraCaptureCallback captureCallback = CameraCaptureCallback.create(mockCaptureStateListener); - - assertEquals(CameraState.STATE_PREVIEW, captureCallback.getCameraState()); - } - - @Test - public void getCameraState_should_echo_setCameraState_value() { - CameraCaptureCallback captureCallback = CameraCaptureCallback.create(mockCaptureStateListener); - - captureCallback.setCameraState(CameraState.STATE_CAPTURING); - - assertEquals(CameraState.STATE_CAPTURING, captureCallback.getCameraState()); - } - - public static class CameraStateWaitingFocusTests { - private CameraCaptureStateListener mockCaptureStateListener; - private CameraCaptureSession mockCameraCaptureSession; - private CaptureRequest mockCaptureRequest; - private CaptureResult mockCaptureResult; - - @Before - @SuppressWarnings("unchecked") - public void before() { - mockCaptureStateListener = mock(CameraCaptureStateListener.class); - mockCameraCaptureSession = mock(CameraCaptureSession.class); - mockCaptureRequest = mock(CaptureRequest.class); - mockCaptureResult = mock(CaptureResult.class); - Key mockAeStateKey = mock(Key.class); - Key mockAfStateKey = mock(Key.class); - - TestUtils.setFinalStatic(CaptureResult.class, "CONTROL_AE_STATE", mockAeStateKey); - TestUtils.setFinalStatic(CaptureResult.class, "CONTROL_AF_STATE", mockAfStateKey); - } - - @After - public void after() { - TestUtils.setFinalStatic(CaptureResult.class, "CONTROL_AE_STATE", null); - TestUtils.setFinalStatic(CaptureResult.class, "CONTROL_AF_STATE", null); - } - - @Test - public void process_should_converge_when_af_state_is_focus_locked_and_ae_state_is_null() { - CameraCaptureCallback captureCallback = - CameraCaptureCallback.create(mockCaptureStateListener); - - when(mockCaptureResult.get(CaptureResult.CONTROL_AF_STATE)) - .thenReturn(CaptureResult.CONTROL_AF_STATE_FOCUSED_LOCKED); - when(mockCaptureResult.get(CaptureResult.CONTROL_AE_STATE)).thenReturn(null); - - captureCallback.setCameraState(CameraState.STATE_WAITING_FOCUS); - captureCallback.onCaptureProgressed( - mockCameraCaptureSession, mockCaptureRequest, mockCaptureResult); - - verify(mockCaptureStateListener, times(1)).onConverged(); - } - - @Test - public void process_should_converge_when_af_state_is_focus_locked_and_ae_state_is_converged() { - CameraCaptureCallback captureCallback = - CameraCaptureCallback.create(mockCaptureStateListener); - - when(mockCaptureResult.get(CaptureResult.CONTROL_AF_STATE)) - .thenReturn(CaptureResult.CONTROL_AF_STATE_FOCUSED_LOCKED); - when(mockCaptureResult.get(CaptureResult.CONTROL_AE_STATE)) - .thenReturn(CaptureResult.CONTROL_AE_STATE_CONVERGED); - - captureCallback.setCameraState(CameraState.STATE_WAITING_FOCUS); - captureCallback.onCaptureProgressed( - mockCameraCaptureSession, mockCaptureRequest, mockCaptureResult); - - verify(mockCaptureStateListener, times(1)).onConverged(); - } - - @Test - public void - process_should_converge_when_af_state_is_focus_locked_and_ae_state_is_not_null_and_not_converged() { - CameraCaptureCallback captureCallback = - CameraCaptureCallback.create(mockCaptureStateListener); - - when(mockCaptureResult.get(CaptureResult.CONTROL_AF_STATE)) - .thenReturn(CaptureResult.CONTROL_AF_STATE_FOCUSED_LOCKED); - captureCallback.setCameraState(CameraState.STATE_WAITING_FOCUS); - - when(mockCaptureResult.get(CaptureResult.CONTROL_AE_STATE)) - .thenReturn(CaptureResult.CONTROL_AE_STATE_LOCKED); - captureCallback.onCaptureProgressed( - mockCameraCaptureSession, mockCaptureRequest, mockCaptureResult); - - when(mockCaptureResult.get(CaptureResult.CONTROL_AE_STATE)) - .thenReturn(CaptureResult.CONTROL_AE_STATE_INACTIVE); - captureCallback.onCaptureProgressed( - mockCameraCaptureSession, mockCaptureRequest, mockCaptureResult); - - when(mockCaptureResult.get(CaptureResult.CONTROL_AE_STATE)) - .thenReturn(CaptureResult.CONTROL_AE_STATE_PRECAPTURE); - captureCallback.onCaptureProgressed( - mockCameraCaptureSession, mockCaptureRequest, mockCaptureResult); - - when(mockCaptureResult.get(CaptureResult.CONTROL_AE_STATE)) - .thenReturn(CaptureResult.CONTROL_AE_STATE_SEARCHING); - captureCallback.onCaptureProgressed( - mockCameraCaptureSession, mockCaptureRequest, mockCaptureResult); - - when(mockCaptureResult.get(CaptureResult.CONTROL_AE_STATE)) - .thenReturn(CaptureResult.CONTROL_AE_STATE_PRECAPTURE); - captureCallback.onCaptureProgressed( - mockCameraCaptureSession, mockCaptureRequest, mockCaptureResult); - - when(mockCaptureResult.get(CaptureResult.CONTROL_AE_STATE)) - .thenReturn(CaptureResult.CONTROL_AE_STATE_FLASH_REQUIRED); - captureCallback.onCaptureProgressed( - mockCameraCaptureSession, mockCaptureRequest, mockCaptureResult); - - verify(mockCaptureStateListener, times(6)).onPrecapture(); - } - } -} From ea1d9cdd9e0952a50ec374300869dc0204e7156d Mon Sep 17 00:00:00 2001 From: Maurits van Beusekom Date: Thu, 11 Mar 2021 17:35:05 +0100 Subject: [PATCH 102/114] Reverted some changes that are not required --- .../camera/example/ios/Flutter/Flutter.podspec | 18 ------------------ .../xcshareddata/IDEWorkspaceChecks.plist | 8 -------- .../camera_platform_interface/CHANGELOG.md | 2 +- .../camera_platform_interface/pubspec.yaml | 2 +- 4 files changed, 2 insertions(+), 28 deletions(-) delete mode 100644 packages/camera/camera/example/ios/Flutter/Flutter.podspec delete mode 100644 packages/camera/camera/example/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist diff --git a/packages/camera/camera/example/ios/Flutter/Flutter.podspec b/packages/camera/camera/example/ios/Flutter/Flutter.podspec deleted file mode 100644 index 2c4421cfe51e..000000000000 --- a/packages/camera/camera/example/ios/Flutter/Flutter.podspec +++ /dev/null @@ -1,18 +0,0 @@ -# -# NOTE: This podspec is NOT to be published. It is only used as a local source! -# This is a generated file; do not edit or check into version control. -# - -Pod::Spec.new do |s| - s.name = 'Flutter' - s.version = '1.0.0' - s.summary = 'High-performance, high-fidelity mobile apps.' - s.homepage = 'https://flutter.io' - s.license = { :type => 'MIT' } - s.author = { 'Flutter Dev Team' => 'flutter-dev@googlegroups.com' } - s.source = { :git => 'https://github.com/flutter/engine', :tag => s.version.to_s } - s.ios.deployment_target = '8.0' - # Framework linking is handled by Flutter tooling, not CocoaPods. - # Add a placeholder to satisfy `s.dependency 'Flutter'` plugin podspecs. - s.vendored_frameworks = 'path/to/nothing' -end diff --git a/packages/camera/camera/example/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/packages/camera/camera/example/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist deleted file mode 100644 index 18d981003d68..000000000000 --- a/packages/camera/camera/example/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist +++ /dev/null @@ -1,8 +0,0 @@ - - - - - IDEDidComputeMac32BitWarning - - - diff --git a/packages/camera/camera_platform_interface/CHANGELOG.md b/packages/camera/camera_platform_interface/CHANGELOG.md index 73f708bbc2e1..49214d24d18e 100644 --- a/packages/camera/camera_platform_interface/CHANGELOG.md +++ b/packages/camera/camera_platform_interface/CHANGELOG.md @@ -1,6 +1,6 @@ ## 2.0.1 -- Update platform_plugin_interface version requirement. +* Update platform_plugin_interface version requirement. ## 2.0.0 diff --git a/packages/camera/camera_platform_interface/pubspec.yaml b/packages/camera/camera_platform_interface/pubspec.yaml index dd93dc30b41f..12c5bc48b9ec 100644 --- a/packages/camera/camera_platform_interface/pubspec.yaml +++ b/packages/camera/camera_platform_interface/pubspec.yaml @@ -3,7 +3,7 @@ description: A common platform interface for the camera plugin. homepage: https://github.com/flutter/plugins/tree/master/packages/camera/camera_platform_interface # NOTE: We strongly prefer non-breaking changes, even at the expense of a # less-clean API. See https://flutter.dev/go/platform-interface-breaking-changes -version: 2.0.2 +version: 2.0.1 dependencies: flutter: From b45aa6a82575bfaa93e8203f4191c17cb7979515 Mon Sep 17 00:00:00 2001 From: Maurits van Beusekom Date: Thu, 11 Mar 2021 17:37:22 +0100 Subject: [PATCH 103/114] Resolve deprecation warning --- .../src/main/java/io/flutter/plugins/camera/Camera.java | 8 +++++++- 1 file changed, 7 insertions(+), 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 3b39e1cdaeef..5d034f71731c 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 @@ -33,6 +33,7 @@ import android.util.Log; import android.util.Size; import android.util.SparseIntArray; +import android.view.Display; import android.view.Surface; import androidx.annotation.NonNull; import androidx.annotation.Nullable; @@ -589,7 +590,7 @@ private void takePictureAfterPrecapture() { updateBuilderSettings(stillBuilder); // Orientation - int rotation = activity.getWindowManager().getDefaultDisplay().getRotation(); + int rotation = getDefaultDisplay().getRotation(); stillBuilder.set(CaptureRequest.JPEG_ORIENTATION, getOrientation(rotation)); CameraCaptureSession.CaptureCallback captureCallback = @@ -631,6 +632,11 @@ public void onCaptureCompleted( } } + @SuppressWarnings("deprecated") + private Display getDefaultDisplay() { + return activity.getWindowManager().getDefaultDisplay(); + } + /** Starts a background thread and its {@link Handler}. TODO: call when activity resumed */ private void startBackgroundThread() { mBackgroundThread = new HandlerThread("CameraBackground"); From ba6ea368fc69cf6cfc36e90ecebe7888fc16b6f3 Mon Sep 17 00:00:00 2001 From: Maurits van Beusekom Date: Thu, 11 Mar 2021 17:38:37 +0100 Subject: [PATCH 104/114] Fix formatting warnings --- .../android/src/main/java/io/flutter/plugins/camera/Camera.java | 1 + 1 file changed, 1 insertion(+) 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 5d034f71731c..5e234ed5bb57 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 @@ -158,6 +158,7 @@ public void onImageAvailable(ImageReader reader) { private ImageReader imageStreamReader; /** {@link CaptureRequest.Builder} for the camera preview */ private CaptureRequest.Builder mPreviewRequestBuilder; + private MediaRecorder mediaRecorder; /** True when recording video. */ private boolean recordingVideo; From ed2a02c45c4227dc915d794b346cdc79d1261ffd Mon Sep 17 00:00:00 2001 From: Maurits van Beusekom Date: Thu, 11 Mar 2021 18:40:52 +0100 Subject: [PATCH 105/114] Fix deprecation warning --- .../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 5e234ed5bb57..3250b5202aae 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 @@ -633,7 +633,7 @@ public void onCaptureCompleted( } } - @SuppressWarnings("deprecated") + @SuppressWarnings("deprecation") private Display getDefaultDisplay() { return activity.getWindowManager().getDefaultDisplay(); } From dff6b16b7e84ead88f9b5460c9c9e6d0910d9ec5 Mon Sep 17 00:00:00 2001 From: Maurits van Beusekom Date: Mon, 15 Mar 2021 13:41:32 +0100 Subject: [PATCH 106/114] Renamed variables to confirm to style guide --- .../io/flutter/plugins/camera/Camera.java | 112 +++++++++--------- 1 file changed, 57 insertions(+), 55 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 3250b5202aae..1d31ef7598e9 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 @@ -129,35 +129,35 @@ class Camera implements CameraCaptureCallback.CameraCaptureStateListener { private final CameraProperties cameraProperties; private final Activity activity; /** A {@link CameraCaptureSession.CaptureCallback} that handles events related to JPEG capture. */ - private final CameraCaptureCallback mCaptureCallback; + private final CameraCaptureCallback cameraCaptureCallback; /** A {@link Handler} for running tasks in the background. */ - private Handler mBackgroundHandler; + private Handler backgroundHandler; /** * This a callback object for the {@link ImageReader}. "onImageAvailable" will be called when a * still image is ready to be saved. */ - private final ImageReader.OnImageAvailableListener mOnImageAvailableListener = + private final ImageReader.OnImageAvailableListener onImageAvailableListener = new ImageReader.OnImageAvailableListener() { @Override public void onImageAvailable(ImageReader reader) { Log.i(TAG, "onImageAvailable"); // Use acquireNextImage since our image reader is only for 1 image. - mBackgroundHandler.post( + backgroundHandler.post( new ImageSaver( reader.acquireNextImage(), pictureCaptureRequest.file, pictureCaptureRequest)); - mCaptureCallback.setCameraState(CameraState.STATE_PREVIEW); + cameraCaptureCallback.setCameraState(CameraState.STATE_PREVIEW); } }; /** An additional thread for running tasks that shouldn't block the UI. */ - private HandlerThread mBackgroundThread; + private HandlerThread backgroundHandlerThread; private CameraDevice cameraDevice; private CameraCaptureSession captureSession; private ImageReader pictureImageReader; private ImageReader imageStreamReader; /** {@link CaptureRequest.Builder} for the camera preview */ - private CaptureRequest.Builder mPreviewRequestBuilder; + private CaptureRequest.Builder previewRequestBuilder; private MediaRecorder mediaRecorder; /** True when recording video. */ @@ -232,7 +232,7 @@ public Camera( }; // Create capture callback - mCaptureCallback = CameraCaptureCallback.create(this); + cameraCaptureCallback = CameraCaptureCallback.create(this); // Start background thread. startBackgroundThread(); @@ -373,7 +373,7 @@ public void onError(@NonNull CameraDevice cameraDevice, int errorCode) { dartMessenger.sendCameraErrorEvent(errorDescription); } }, - mBackgroundHandler); + backgroundHandler); } private void createCaptureSession(int templateType, Surface... surfaces) @@ -390,26 +390,26 @@ private void createCaptureSession( closeCaptureSession(); // Create a new capture builder. - mPreviewRequestBuilder = cameraDevice.createCaptureRequest(templateType); + previewRequestBuilder = cameraDevice.createCaptureRequest(templateType); // Build Flutter surface to render to SurfaceTexture surfaceTexture = flutterTexture.surfaceTexture(); surfaceTexture.setDefaultBufferSize(getPreviewSize().getWidth(), getPreviewSize().getHeight()); Surface flutterSurface = new Surface(surfaceTexture); - mPreviewRequestBuilder.addTarget(flutterSurface); + previewRequestBuilder.addTarget(flutterSurface); List remainingSurfaces = Arrays.asList(surfaces); if (templateType != CameraDevice.TEMPLATE_PREVIEW) { // If it is not preview mode, add all surfaces as targets. for (Surface surface : remainingSurfaces) { - mPreviewRequestBuilder.addTarget(surface); + previewRequestBuilder.addTarget(surface); } } // Update camera regions cameraFeatures.put( CameraFeatures.regionBoundaries, - new RegionBoundariesFeature(cameraProperties, mPreviewRequestBuilder)); + new RegionBoundariesFeature(cameraProperties, previewRequestBuilder)); // Prepare the callback CameraCaptureSession.StateCallback callback = @@ -424,7 +424,7 @@ public void onConfigured(@NonNull CameraCaptureSession session) { captureSession = session; Log.i(TAG, "Updating builder settings"); - updateBuilderSettings(mPreviewRequestBuilder); + updateBuilderSettings(previewRequestBuilder); refreshPreviewCaptureSession( onSuccessCallback, (code, message) -> dartMessenger.sendCameraErrorEvent(message)); @@ -471,7 +471,7 @@ private void createCaptureSessionWithSessionConfig( private void createCaptureSession( List surfaces, CameraCaptureSession.StateCallback callback) throws CameraAccessException { - cameraDevice.createCaptureSession(surfaces, callback, mBackgroundHandler); + cameraDevice.createCaptureSession(surfaces, callback, backgroundHandler); } // Send a repeating request to refresh our capture session. @@ -485,7 +485,7 @@ private void refreshPreviewCaptureSession( try { captureSession.setRepeatingRequest( - mPreviewRequestBuilder.build(), mCaptureCallback, mBackgroundHandler); + previewRequestBuilder.build(), cameraCaptureCallback, backgroundHandler); if (onSuccessCallback != null) { onSuccessCallback.run(); @@ -502,7 +502,7 @@ public void takePicture(@NonNull final Result result) { "takePicture | useAutoFocus: " + cameraFeatures.get(CameraFeatures.autoFocus).getValue()); // Only take one 1 picture at a time. - if (mCaptureCallback.getCameraState() != CameraState.STATE_PREVIEW) { + if (cameraCaptureCallback.getCameraState() != CameraState.STATE_PREVIEW) { result.error("captureAlreadyActive", "Picture is currently already being captured", null); return; } @@ -514,14 +514,14 @@ public void takePicture(@NonNull final Result result) { // Start a new capture pictureCaptureRequest = PictureCaptureRequest.create(result, file, 3000, 3000); - mCaptureCallback.setPictureCaptureRequest(pictureCaptureRequest); + cameraCaptureCallback.setPictureCaptureRequest(pictureCaptureRequest); } catch (IOException | SecurityException e) { pictureCaptureRequest.error("cannotCreateFile", e.getMessage(), null); return; } // Listen for picture being taken - pictureImageReader.setOnImageAvailableListener(mOnImageAvailableListener, mBackgroundHandler); + pictureImageReader.setOnImageAvailableListener(onImageAvailableListener, backgroundHandler); final AutoFocusFeature autoFocusFeature = (AutoFocusFeature) cameraFeatures.get(CameraFeatures.autoFocus); @@ -535,30 +535,32 @@ public void takePicture(@NonNull final Result result) { /** * Run the precapture sequence for capturing a still image. This method should be called when we - * get a response in {@link #mCaptureCallback} from lockFocus(). + * get a response in {@link #cameraCaptureCallback} from lockFocus(). */ private void runPrecaptureSequence() { Log.i(TAG, "runPrecaptureSequence"); try { // First set precapture state to idle or else it can hang in STATE_WAITING_PRECAPTURE_START - mPreviewRequestBuilder.set( + previewRequestBuilder.set( CaptureRequest.CONTROL_AE_PRECAPTURE_TRIGGER, CaptureRequest.CONTROL_AE_PRECAPTURE_TRIGGER_IDLE); - captureSession.capture(mPreviewRequestBuilder.build(), mCaptureCallback, mBackgroundHandler); + captureSession.capture(previewRequestBuilder.build(), cameraCaptureCallback, + backgroundHandler); // Repeating request to refresh preview session refreshPreviewCaptureSession( null, (code, message) -> pictureCaptureRequest.error("cameraAccess", message, null)); // Start precapture now - mCaptureCallback.setCameraState(CameraState.STATE_WAITING_PRECAPTURE_START); + cameraCaptureCallback.setCameraState(CameraState.STATE_WAITING_PRECAPTURE_START); - mPreviewRequestBuilder.set( + previewRequestBuilder.set( CaptureRequest.CONTROL_AE_PRECAPTURE_TRIGGER, CaptureRequest.CONTROL_AE_PRECAPTURE_TRIGGER_START); // Trigger one capture to start AE sequence - captureSession.capture(mPreviewRequestBuilder.build(), mCaptureCallback, mBackgroundHandler); + captureSession.capture(previewRequestBuilder.build(), cameraCaptureCallback, + backgroundHandler); } catch (CameraAccessException e) { e.printStackTrace(); @@ -567,11 +569,11 @@ private void runPrecaptureSequence() { /** * Capture a still picture. This method should be called when we get a response in {@link - * #mCaptureCallback} from both lockFocus(). + * #cameraCaptureCallback} from both lockFocus(). */ private void takePictureAfterPrecapture() { Log.i(TAG, "captureStillPicture"); - mCaptureCallback.setCameraState(CameraState.STATE_CAPTURING); + cameraCaptureCallback.setCameraState(CameraState.STATE_CAPTURING); try { if (null == cameraDevice) { @@ -585,7 +587,7 @@ private void takePictureAfterPrecapture() { // Zoom stillBuilder.set( CaptureRequest.SCALER_CROP_REGION, - mPreviewRequestBuilder.get(CaptureRequest.SCALER_CROP_REGION)); + previewRequestBuilder.get(CaptureRequest.SCALER_CROP_REGION)); // Update builder settings updateBuilderSettings(stillBuilder); @@ -627,7 +629,7 @@ public void onCaptureCompleted( captureSession.stopRepeating(); captureSession.abortCaptures(); Log.i(TAG, "sending capture request"); - captureSession.capture(stillBuilder.build(), captureCallback, mBackgroundHandler); + captureSession.capture(stillBuilder.build(), captureCallback, backgroundHandler); } catch (CameraAccessException e) { pictureCaptureRequest.error("cameraAccess", e.getMessage(), null); } @@ -640,21 +642,21 @@ private Display getDefaultDisplay() { /** Starts a background thread and its {@link Handler}. TODO: call when activity resumed */ private void startBackgroundThread() { - mBackgroundThread = new HandlerThread("CameraBackground"); - mBackgroundThread.start(); - mBackgroundHandler = new Handler(mBackgroundThread.getLooper()); + backgroundHandlerThread = new HandlerThread("CameraBackground"); + backgroundHandlerThread.start(); + backgroundHandler = new Handler(backgroundHandlerThread.getLooper()); } /** Stops the background thread and its {@link Handler}. TODO: call when activity paused */ private void stopBackgroundThread() { try { - if (mBackgroundThread != null) { - mBackgroundThread.quitSafely(); - mBackgroundThread.join(); - mBackgroundThread = null; + if (backgroundHandlerThread != null) { + backgroundHandlerThread.quitSafely(); + backgroundHandlerThread.join(); + backgroundHandlerThread = null; } - mBackgroundHandler = null; + backgroundHandler = null; } catch (InterruptedException e) { pictureCaptureRequest.error("cameraAccess", e.getMessage(), null); } @@ -681,7 +683,7 @@ private void runPictureAutoFocus() { Log.i(TAG, "runPictureAutoFocus"); assert (pictureCaptureRequest != null); - mCaptureCallback.setCameraState(CameraState.STATE_WAITING_FOCUS); + cameraCaptureCallback.setCameraState(CameraState.STATE_WAITING_FOCUS); lockAutoFocus(); } @@ -692,11 +694,11 @@ private void lockAutoFocus() { Log.i(TAG, "lockAutoFocus"); // Trigger AF to start - mPreviewRequestBuilder.set( + previewRequestBuilder.set( CaptureRequest.CONTROL_AF_TRIGGER, CaptureRequest.CONTROL_AF_TRIGGER_START); try { - captureSession.capture(mPreviewRequestBuilder.build(), null, mBackgroundHandler); + captureSession.capture(previewRequestBuilder.build(), null, backgroundHandler); } catch (CameraAccessException e) { Log.i(TAG, "Error unlocking focus: " + e.getMessage()); dartMessenger.sendCameraErrorEvent(e.getMessage()); @@ -709,15 +711,15 @@ private void unlockAutoFocus() { Log.i(TAG, "unlockAutoFocus"); try { // Cancel existing AF state - mPreviewRequestBuilder.set( + previewRequestBuilder.set( CaptureRequest.CONTROL_AF_TRIGGER, CameraMetadata.CONTROL_AF_TRIGGER_CANCEL); - captureSession.capture(mPreviewRequestBuilder.build(), null, mBackgroundHandler); + captureSession.capture(previewRequestBuilder.build(), null, backgroundHandler); // Set AF state to idle again - mPreviewRequestBuilder.set( + previewRequestBuilder.set( CaptureRequest.CONTROL_AF_TRIGGER, CameraMetadata.CONTROL_AF_TRIGGER_IDLE); - captureSession.capture(mPreviewRequestBuilder.build(), null, mBackgroundHandler); + captureSession.capture(previewRequestBuilder.build(), null, backgroundHandler); } catch (CameraAccessException e) { Log.i(TAG, "Error unlocking focus: " + e.getMessage()); dartMessenger.sendCameraErrorEvent(e.getMessage()); @@ -835,7 +837,7 @@ public void resumeVideoRecording(@NonNull final Result result) { public void setFlashMode(@NonNull final Result result, FlashMode newMode) { // Save the new flash mode setting ((FlashFeature) cameraFeatures.get(CameraFeatures.flash)).setValue(newMode); - cameraFeatures.get(CameraFeatures.flash).updateBuilder(mPreviewRequestBuilder); + cameraFeatures.get(CameraFeatures.flash).updateBuilder(previewRequestBuilder); refreshPreviewCaptureSession( () -> result.success(null), @@ -850,7 +852,7 @@ public void setFlashMode(@NonNull final Result result, FlashMode newMode) { */ public void setExposureMode(@NonNull final Result result, ExposureMode newMode) { ((ExposureLockFeature) cameraFeatures.get(CameraFeatures.exposureLock)).setValue(newMode); - cameraFeatures.get(CameraFeatures.exposureLock).updateBuilder(mPreviewRequestBuilder); + cameraFeatures.get(CameraFeatures.exposureLock).updateBuilder(previewRequestBuilder); refreshPreviewCaptureSession( () -> result.success(null), @@ -882,7 +884,7 @@ public CameraRegions getCameraRegions() { public void setExposurePoint(@NonNull final Result result, Double x, Double y) { ((ExposurePointFeature) cameraFeatures.get(CameraFeatures.exposurePoint)) .setValue(new Point(x, y)); - cameraFeatures.get(CameraFeatures.exposurePoint).updateBuilder(mPreviewRequestBuilder); + cameraFeatures.get(CameraFeatures.exposurePoint).updateBuilder(previewRequestBuilder); refreshPreviewCaptureSession( () -> result.success(null), @@ -920,7 +922,7 @@ public double getExposureOffsetStepSize() { public void setFocusMode(@NonNull final Result result, FocusMode newMode) { ((AutoFocusFeature) cameraFeatures.get(CameraFeatures.autoFocus)).setValue(newMode); - cameraFeatures.get(CameraFeatures.autoFocus).updateBuilder(mPreviewRequestBuilder); + cameraFeatures.get(CameraFeatures.autoFocus).updateBuilder(previewRequestBuilder); /* * For focus mode we need to do an extra step of actually locking/unlocking the @@ -932,12 +934,12 @@ public void setFocusMode(@NonNull final Result result, FocusMode newMode) { lockAutoFocus(); // Set AF state to idle again - mPreviewRequestBuilder.set( + previewRequestBuilder.set( CaptureRequest.CONTROL_AF_TRIGGER, CameraMetadata.CONTROL_AF_TRIGGER_IDLE); try { captureSession.setRepeatingRequest( - mPreviewRequestBuilder.build(), null, mBackgroundHandler); + previewRequestBuilder.build(), null, backgroundHandler); } catch (CameraAccessException e) { result.error("setFocusModeFailed", "Error setting focus mode: " + e.getMessage(), null); } @@ -961,7 +963,7 @@ public void setFocusMode(@NonNull final Result result, FocusMode newMode) { */ public void setFocusPoint(@NonNull final Result result, Double x, Double y) { ((FocusPointFeature) cameraFeatures.get(CameraFeatures.focusPoint)).setValue(new Point(x, y)); - cameraFeatures.get(CameraFeatures.focusPoint).updateBuilder(mPreviewRequestBuilder); + cameraFeatures.get(CameraFeatures.focusPoint).updateBuilder(previewRequestBuilder); refreshPreviewCaptureSession( () -> result.success(null), @@ -977,7 +979,7 @@ public void setFocusPoint(@NonNull final Result result, Double x, Double y) { public void setExposureOffset(@NonNull final Result result, double offset) { ((ExposureOffsetFeature) cameraFeatures.get(CameraFeatures.exposureOffset)) .setValue(new ExposureOffsetValue(offset)); - cameraFeatures.get(CameraFeatures.exposureOffset).updateBuilder(mPreviewRequestBuilder); + cameraFeatures.get(CameraFeatures.exposureOffset).updateBuilder(previewRequestBuilder); refreshPreviewCaptureSession( () -> result.success(null), @@ -1040,7 +1042,7 @@ public void setZoomLevel(@NonNull final Result result, float zoom) throws Camera } zoomLevel.setValue(zoom); - zoomLevel.updateBuilder(mPreviewRequestBuilder); + zoomLevel.updateBuilder(previewRequestBuilder); refreshPreviewCaptureSession( () -> result.success(null), @@ -1084,7 +1086,7 @@ public void onListen(Object o, EventChannel.EventSink imageStreamSink) { @Override public void onCancel(Object o) { - imageStreamReader.setOnImageAvailableListener(null, mBackgroundHandler); + imageStreamReader.setOnImageAvailableListener(null, backgroundHandler); } }); } @@ -1121,7 +1123,7 @@ private void setImageStreamImageAvailableListener(final EventChannel.EventSink i handler.post(() -> imageStreamSink.success(imageBuffer)); img.close(); }, - mBackgroundHandler); + backgroundHandler); } private void closeCaptureSession() { From cac6b1e95cfabcc765a20face19e5824c43a2eb1 Mon Sep 17 00:00:00 2001 From: Maurits van Beusekom Date: Mon, 15 Mar 2021 13:42:10 +0100 Subject: [PATCH 107/114] Formatted Camera.java --- .../src/main/java/io/flutter/plugins/camera/Camera.java | 8 ++++---- 1 file changed, 4 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 1d31ef7598e9..851d98da4d68 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 @@ -544,8 +544,8 @@ private void runPrecaptureSequence() { previewRequestBuilder.set( CaptureRequest.CONTROL_AE_PRECAPTURE_TRIGGER, CaptureRequest.CONTROL_AE_PRECAPTURE_TRIGGER_IDLE); - captureSession.capture(previewRequestBuilder.build(), cameraCaptureCallback, - backgroundHandler); + captureSession.capture( + previewRequestBuilder.build(), cameraCaptureCallback, backgroundHandler); // Repeating request to refresh preview session refreshPreviewCaptureSession( @@ -559,8 +559,8 @@ private void runPrecaptureSequence() { CaptureRequest.CONTROL_AE_PRECAPTURE_TRIGGER_START); // Trigger one capture to start AE sequence - captureSession.capture(previewRequestBuilder.build(), cameraCaptureCallback, - backgroundHandler); + captureSession.capture( + previewRequestBuilder.build(), cameraCaptureCallback, backgroundHandler); } catch (CameraAccessException e) { e.printStackTrace(); From a04e645f3686a7a91390d71d2d8aa3d4dabae4a2 Mon Sep 17 00:00:00 2001 From: Maurits van Beusekom Date: Mon, 15 Mar 2021 18:30:11 +0100 Subject: [PATCH 108/114] Test if camera calls features --- .../io/flutter/plugins/camera/Camera.java | 17 +- .../plugins/camera/MethodCallHandlerImpl.java | 5 +- .../io/flutter/plugins/camera/CameraTest.java | 361 +++++++++++++++++- 3 files changed, 370 insertions(+), 13 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 851d98da4d68..678bacf3396c 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 @@ -127,6 +127,7 @@ class Camera implements CameraCaptureCallback.CameraCaptureStateListener { private final Context applicationContext; private final DartMessenger dartMessenger; private final CameraProperties cameraProperties; + private final CameraFeatureFactory cameraFeatureFactory; private final Activity activity; /** A {@link CameraCaptureSession.CaptureCallback} that handles events related to JPEG capture. */ private final CameraCaptureCallback cameraCaptureCallback; @@ -186,6 +187,7 @@ public Camera( this.dartMessenger = dartMessenger; this.applicationContext = activity.getApplicationContext(); this.cameraProperties = cameraProperties; + this.cameraFeatureFactory = cameraFeatureFactory; // Setup camera features this.cameraFeatures = @@ -409,7 +411,8 @@ private void createCaptureSession( // Update camera regions cameraFeatures.put( CameraFeatures.regionBoundaries, - new RegionBoundariesFeature(cameraProperties, previewRequestBuilder)); + cameraFeatureFactory.createRegionBoundariesFeature( + cameraProperties, previewRequestBuilder)); // Prepare the callback CameraCaptureSession.StateCallback callback = @@ -878,12 +881,10 @@ public CameraRegions getCameraRegions() { * Set new exposure point from dart. * * @param result Flutter result. - * @param x new x. - * @param y new y. + * @param point The exposure point. */ - public void setExposurePoint(@NonNull final Result result, Double x, Double y) { - ((ExposurePointFeature) cameraFeatures.get(CameraFeatures.exposurePoint)) - .setValue(new Point(x, y)); + public void setExposurePoint(@NonNull final Result result, Point point) { + ((ExposurePointFeature) cameraFeatures.get(CameraFeatures.exposurePoint)).setValue(point); cameraFeatures.get(CameraFeatures.exposurePoint).updateBuilder(previewRequestBuilder); refreshPreviewCaptureSession( @@ -961,8 +962,8 @@ public void setFocusMode(@NonNull final Result result, FocusMode newMode) { * @param x new x. * @param y new y. */ - public void setFocusPoint(@NonNull final Result result, Double x, Double y) { - ((FocusPointFeature) cameraFeatures.get(CameraFeatures.focusPoint)).setValue(new Point(x, y)); + public void setFocusPoint(@NonNull final Result result, Point point) { + ((FocusPointFeature) cameraFeatures.get(CameraFeatures.focusPoint)).setValue(point); cameraFeatures.get(CameraFeatures.focusPoint).updateBuilder(previewRequestBuilder); refreshPreviewCaptureSession( diff --git a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/MethodCallHandlerImpl.java b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/MethodCallHandlerImpl.java index e54c786149cc..9d6e6f762ceb 100644 --- a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/MethodCallHandlerImpl.java +++ b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/MethodCallHandlerImpl.java @@ -16,6 +16,7 @@ import io.flutter.plugin.common.MethodChannel.Result; import io.flutter.plugins.camera.CameraPermissions.PermissionsRegistry; import io.flutter.plugins.camera.features.CameraFeatureFactoryImpl; +import io.flutter.plugins.camera.features.Point; import io.flutter.plugins.camera.features.autofocus.FocusMode; import io.flutter.plugins.camera.features.exposurelock.ExposureMode; import io.flutter.plugins.camera.features.flash.FlashMode; @@ -172,7 +173,7 @@ public void onMethodCall(@NonNull MethodCall call, @NonNull final Result result) y = call.argument("y"); } try { - camera.setExposurePoint(result, x, y); + camera.setExposurePoint(result, new Point(x, y)); } catch (Exception e) { handleException(e, result); } @@ -239,7 +240,7 @@ public void onMethodCall(@NonNull MethodCall call, @NonNull final Result result) y = call.argument("y"); } try { - camera.setFocusPoint(result, x, y); + camera.setFocusPoint(result, new Point(x, y)); } catch (Exception e) { handleException(e, result); } 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 c5e30cc79494..57e35b1fc0f0 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 @@ -4,6 +4,7 @@ package io.flutter.plugins.camera; +import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNotNull; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.eq; @@ -14,22 +15,77 @@ import static org.mockito.Mockito.when; import android.app.Activity; +import android.graphics.Rect; import android.hardware.camera2.CameraAccessException; +import android.hardware.camera2.CaptureRequest.Builder; +import android.media.CamcorderProfile; +import android.util.Size; +import androidx.annotation.NonNull; +import io.flutter.plugin.common.MethodChannel; import io.flutter.plugins.camera.features.CameraFeatureFactory; +import io.flutter.plugins.camera.features.Point; +import io.flutter.plugins.camera.features.autofocus.AutoFocusFeature; +import io.flutter.plugins.camera.features.exposurelock.ExposureLockFeature; +import io.flutter.plugins.camera.features.exposurelock.ExposureMode; +import io.flutter.plugins.camera.features.exposureoffset.ExposureOffsetFeature; +import io.flutter.plugins.camera.features.exposureoffset.ExposureOffsetValue; +import io.flutter.plugins.camera.features.exposurepoint.ExposurePointFeature; +import io.flutter.plugins.camera.features.flash.FlashFeature; +import io.flutter.plugins.camera.features.flash.FlashMode; +import io.flutter.plugins.camera.features.focuspoint.FocusPointFeature; +import io.flutter.plugins.camera.features.fpsrange.FpsRangeFeature; +import io.flutter.plugins.camera.features.noisereduction.NoiseReductionFeature; +import io.flutter.plugins.camera.features.regionboundaries.CameraRegions; +import io.flutter.plugins.camera.features.regionboundaries.RegionBoundariesFeature; +import io.flutter.plugins.camera.features.resolution.ResolutionFeature; import io.flutter.plugins.camera.features.resolution.ResolutionPreset; +import io.flutter.plugins.camera.features.sensororientation.DeviceOrientationManager; +import io.flutter.plugins.camera.features.sensororientation.SensorOrientationFeature; +import io.flutter.plugins.camera.features.zoomlevel.CameraZoom; +import io.flutter.plugins.camera.features.zoomlevel.ZoomLevelFeature; import io.flutter.view.TextureRegistry; +import java.util.concurrent.Callable; +import org.junit.Before; import org.junit.Test; public class CameraTest { + private CameraProperties mockCameraProperties; + private CameraFeatureFactory mockCameraFeatureFactory; + private DartMessenger mockDartMessenger; + private Camera camera; + + @Before + public void before() { + mockCameraProperties = mock(CameraProperties.class); + mockCameraFeatureFactory = new TestCameraFeatureFactory(); + mockDartMessenger = mock(DartMessenger.class); + + final Activity mockActivity = mock(Activity.class); + final TextureRegistry.SurfaceTextureEntry mockFlutterTexture = + mock(TextureRegistry.SurfaceTextureEntry.class); + final String cameraName = "1"; + final ResolutionPreset resolutionPreset = ResolutionPreset.high; + final boolean enableAudio = false; + + when(mockCameraProperties.getCameraName()).thenReturn(cameraName); + + camera = + new Camera( + mockActivity, + mockFlutterTexture, + mockCameraFeatureFactory, + mockDartMessenger, + mockCameraProperties, + resolutionPreset, + enableAudio); + } @Test - public void should_create_camera_plugin() throws CameraAccessException { + public void should_create_camera_plugin_and_set_all_features() { final Activity mockActivity = mock(Activity.class); final TextureRegistry.SurfaceTextureEntry mockFlutterTexture = mock(TextureRegistry.SurfaceTextureEntry.class); final CameraFeatureFactory mockCameraFeatureFactory = mock(CameraFeatureFactory.class); - final DartMessenger mockDartMessenger = mock(DartMessenger.class); - final CameraProperties mockCameraProperties = mock(CameraProperties.class); final String cameraName = "1"; final ResolutionPreset resolutionPreset = ResolutionPreset.high; final boolean enableAudio = false; @@ -64,4 +120,303 @@ public void should_create_camera_plugin() throws CameraAccessException { verify(mockCameraFeatureFactory, never()).createRegionBoundariesFeature(any(), any()); assertNotNull("should create a camera", camera); } + + @Test + public void getCaptureSize() { + ResolutionFeature mockResolutionFeature = + mockCameraFeatureFactory.createResolutionFeature(mockCameraProperties, null, null); + Size mockSize = mock(Size.class); + + when(mockResolutionFeature.getCaptureSize()).thenReturn(mockSize); + + Size actualSize = camera.getCaptureSize(); + + verify(mockResolutionFeature, times(1)).getCaptureSize(); + assertEquals(mockSize, actualSize); + } + + @Test + public void getPreviewSize() { + ResolutionFeature mockResolutionFeature = + mockCameraFeatureFactory.createResolutionFeature(mockCameraProperties, null, null); + Size mockSize = mock(Size.class); + + when(mockResolutionFeature.getPreviewSize()).thenReturn(mockSize); + + Size actualSize = camera.getPreviewSize(); + + verify(mockResolutionFeature, times(1)).getPreviewSize(); + assertEquals(mockSize, actualSize); + } + + @Test + public void getDeviceOrientationManager() { + SensorOrientationFeature mockSensorOrientationFeature = + mockCameraFeatureFactory.createSensorOrientationFeature(mockCameraProperties, null, null); + DeviceOrientationManager mockDeviceOrientationManager = mock(DeviceOrientationManager.class); + + when(mockSensorOrientationFeature.getDeviceOrientationManager()) + .thenReturn(mockDeviceOrientationManager); + + DeviceOrientationManager actualDeviceOrientationManager = camera.getDeviceOrientationManager(); + + verify(mockSensorOrientationFeature, times(1)).getDeviceOrientationManager(); + assertEquals(mockDeviceOrientationManager, actualDeviceOrientationManager); + } + + @Test + public void getExposureOffsetStepSize() { + ExposureOffsetFeature mockExposureOffsetFeature = + mockCameraFeatureFactory.createExposureOffsetFeature(mockCameraProperties); + double stepSize = 2.3; + + when(mockExposureOffsetFeature.getExposureOffsetStepSize()).thenReturn(stepSize); + + double actualSize = camera.getExposureOffsetStepSize(); + + verify(mockExposureOffsetFeature, times(1)).getExposureOffsetStepSize(); + assertEquals(stepSize, actualSize, 0); + } + + @Test + public void getMaxExposureOffset() { + ExposureOffsetFeature mockExposureOffsetFeature = + mockCameraFeatureFactory.createExposureOffsetFeature(mockCameraProperties); + double expectedMaxOffset = 42.0; + ExposureOffsetValue exposureOffsetValue = new ExposureOffsetValue(21.5, 42.0, 0.0); + + when(mockExposureOffsetFeature.getValue()).thenReturn(exposureOffsetValue); + + double actualMaxOffset = camera.getMaxExposureOffset(); + + verify(mockExposureOffsetFeature, times(1)).getValue(); + assertEquals(expectedMaxOffset, actualMaxOffset, 0); + } + + @Test + public void getMinExposureOffset() { + ExposureOffsetFeature mockExposureOffsetFeature = + mockCameraFeatureFactory.createExposureOffsetFeature(mockCameraProperties); + double expectedMinOffset = 21.5; + ExposureOffsetValue exposureOffsetValue = new ExposureOffsetValue(21.5, 42.0, 0.0); + + when(mockExposureOffsetFeature.getValue()).thenReturn(exposureOffsetValue); + + double actualMinOffset = camera.getMinExposureOffset(); + + verify(mockExposureOffsetFeature, times(1)).getValue(); + assertEquals(expectedMinOffset, actualMinOffset, 0); + } + + @Test + public void getMaxZoomLevel() { + ZoomLevelFeature mockZoomLevelFeature = + mockCameraFeatureFactory.createZoomLevelFeature(mockCameraProperties); + Rect sensorRect = mock(Rect.class); + float expectedMaxZoomLevel = 4.2f; + CameraZoom cameraZoom = CameraZoom.create(sensorRect, expectedMaxZoomLevel); + + when(mockZoomLevelFeature.getCameraZoom()).thenReturn(cameraZoom); + + float actualMaxZoomLevel = camera.getMaxZoomLevel(); + + verify(mockZoomLevelFeature, times(1)).getCameraZoom(); + assertEquals(expectedMaxZoomLevel, actualMaxZoomLevel, 0); + } + + @Test + public void getMinZoomLevel() { + float expectedMinZoomLevel = CameraZoom.DEFAULT_ZOOM_FACTOR; + float actualMinZoomLevel = camera.getMinZoomLevel(); + + assertEquals(expectedMinZoomLevel, actualMinZoomLevel, 0); + } + + @Test + public void getRecordingProfile() { + ResolutionFeature mockResolutionFeature = + mockCameraFeatureFactory.createResolutionFeature(mockCameraProperties, null, null); + CamcorderProfile mockCamcorderProfile = mock(CamcorderProfile.class); + + when(mockResolutionFeature.getRecordingProfile()).thenReturn(mockCamcorderProfile); + + CamcorderProfile actualRecordingProfile = camera.getRecordingProfile(); + + verify(mockResolutionFeature, times(1)).getRecordingProfile(); + assertEquals(mockCamcorderProfile, actualRecordingProfile); + } + + @Test + public void setExposureMode_Should_update_exposure_lock_feature_and_update_builder() { + ExposureLockFeature mockExposureLockFeature = + mockCameraFeatureFactory.createExposureLockFeature(mockCameraProperties); + MethodChannel.Result mockResult = mock(MethodChannel.Result.class); + ExposureMode exposureMode = ExposureMode.locked; + + camera.setExposureMode(mockResult, exposureMode); + + verify(mockExposureLockFeature, times(1)).setValue(exposureMode); + verify(mockExposureLockFeature, times(1)).updateBuilder(null); + } + + @Test + public void setExposurePoint_Should_update_exposure_point_feature_and_update_builder() { + ExposurePointFeature mockExposurePointFeature = + mockCameraFeatureFactory.createExposurePointFeature(mockCameraProperties, null); + MethodChannel.Result mockResult = mock(MethodChannel.Result.class); + Point point = new Point(42d, 42d); + + camera.setExposurePoint(mockResult, point); + + verify(mockExposurePointFeature, times(1)).setValue(point); + verify(mockExposurePointFeature, times(1)).updateBuilder(null); + } + + @Test + public void setFlashMode_Should_update_flash_feature_and_update_builder() { + FlashFeature mockFlashFeature = + mockCameraFeatureFactory.createFlashFeature(mockCameraProperties); + MethodChannel.Result mockResult = mock(MethodChannel.Result.class); + FlashMode flashMode = FlashMode.always; + + camera.setFlashMode(mockResult, flashMode); + + verify(mockFlashFeature, times(1)).setValue(flashMode); + verify(mockFlashFeature, times(1)).updateBuilder(null); + } + + @Test + public void setFocusPoint_Should_update_focus_point_feature_and_update_builder() { + FocusPointFeature mockFocusPointFeature = + mockCameraFeatureFactory.createFocusPointFeature(mockCameraProperties, null); + MethodChannel.Result mockResult = mock(MethodChannel.Result.class); + Point point = new Point(42d, 42d); + + camera.setFocusPoint(mockResult, point); + + verify(mockFocusPointFeature, times(1)).setValue(point); + verify(mockFocusPointFeature, times(1)).updateBuilder(null); + } + + @Test + public void setZoomLevel_Should_update_zoom_level_feature_and_update_builder() + throws CameraAccessException { + ZoomLevelFeature mockZoomLevelFeature = + mockCameraFeatureFactory.createZoomLevelFeature(mockCameraProperties); + MethodChannel.Result mockResult = mock(MethodChannel.Result.class); + Rect sensorRect = mock(Rect.class); + CameraZoom cameraZoom = CameraZoom.create(sensorRect, 5.0f); + float zoomLevel = 4.2f; + + when(mockZoomLevelFeature.getCameraZoom()).thenReturn(cameraZoom); + + camera.setZoomLevel(mockResult, zoomLevel); + + verify(mockZoomLevelFeature, times(1)).setValue(zoomLevel); + verify(mockZoomLevelFeature, times(1)).updateBuilder(null); + } + + private static class TestCameraFeatureFactory implements CameraFeatureFactory { + private final AutoFocusFeature mockAutoFocusFeature; + private final ExposureLockFeature mockExposureLockFeature; + private final ExposureOffsetFeature mockExposureOffsetFeature; + private final ExposurePointFeature mockExposurePointFeature; + private final FlashFeature mockFlashFeature; + private final FocusPointFeature mockFocusPointFeature; + private final FpsRangeFeature mockFpsRangeFeature; + private final NoiseReductionFeature mockNoiseReductionFeature; + private final RegionBoundariesFeature mockRegionBoundariesFeature; + private final ResolutionFeature mockResolutionFeature; + private final SensorOrientationFeature mockSensorOrientationFeature; + private final ZoomLevelFeature mockZoomLevelFeature; + + public TestCameraFeatureFactory() { + this.mockAutoFocusFeature = mock(AutoFocusFeature.class); + this.mockExposureLockFeature = mock(ExposureLockFeature.class); + this.mockExposureOffsetFeature = mock(ExposureOffsetFeature.class); + this.mockExposurePointFeature = mock(ExposurePointFeature.class); + this.mockFlashFeature = mock(FlashFeature.class); + this.mockFocusPointFeature = mock(FocusPointFeature.class); + this.mockFpsRangeFeature = mock(FpsRangeFeature.class); + this.mockNoiseReductionFeature = mock(NoiseReductionFeature.class); + this.mockRegionBoundariesFeature = mock(RegionBoundariesFeature.class); + this.mockResolutionFeature = mock(ResolutionFeature.class); + this.mockSensorOrientationFeature = mock(SensorOrientationFeature.class); + this.mockZoomLevelFeature = mock(ZoomLevelFeature.class); + } + + @Override + public AutoFocusFeature createAutoFocusFeature( + @NonNull CameraProperties cameraProperties, boolean recordingVideo) { + return mockAutoFocusFeature; + } + + @Override + public ExposureLockFeature createExposureLockFeature( + @NonNull CameraProperties cameraProperties) { + return mockExposureLockFeature; + } + + @Override + public ExposureOffsetFeature createExposureOffsetFeature( + @NonNull CameraProperties cameraProperties) { + return mockExposureOffsetFeature; + } + + @Override + public FlashFeature createFlashFeature(@NonNull CameraProperties cameraProperties) { + return mockFlashFeature; + } + + @Override + public ResolutionFeature createResolutionFeature( + @NonNull CameraProperties cameraProperties, + ResolutionPreset initialSetting, + String cameraName) { + return mockResolutionFeature; + } + + @Override + public FocusPointFeature createFocusPointFeature( + @NonNull CameraProperties cameraProperties, Callable getCameraRegions) { + return mockFocusPointFeature; + } + + @Override + public FpsRangeFeature createFpsRangeFeature(@NonNull CameraProperties cameraProperties) { + return mockFpsRangeFeature; + } + + @Override + public SensorOrientationFeature createSensorOrientationFeature( + @NonNull CameraProperties cameraProperties, + @NonNull Activity activity, + @NonNull DartMessenger dartMessenger) { + return mockSensorOrientationFeature; + } + + @Override + public ZoomLevelFeature createZoomLevelFeature(@NonNull CameraProperties cameraProperties) { + return mockZoomLevelFeature; + } + + @Override + public RegionBoundariesFeature createRegionBoundariesFeature( + @NonNull CameraProperties cameraProperties, @NonNull Builder requestBuilder) { + return mockRegionBoundariesFeature; + } + + @Override + public ExposurePointFeature createExposurePointFeature( + @NonNull CameraProperties cameraProperties, + @NonNull Callable getCameraRegions) { + return mockExposurePointFeature; + } + + @Override + public NoiseReductionFeature createNoiseReductionFeature( + @NonNull CameraProperties cameraProperties) { + return mockNoiseReductionFeature; + } + } } From 455f9237af9c435f7b67132f0acff20ac9cb6785 Mon Sep 17 00:00:00 2001 From: Maurits van Beusekom Date: Tue, 16 Mar 2021 08:37:13 +0100 Subject: [PATCH 109/114] organize imports --- .../io/flutter/plugins/camera/CameraTest.java | 30 +++++++++++-------- 1 file changed, 17 insertions(+), 13 deletions(-) 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 57e35b1fc0f0..90e923d30483 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 @@ -4,23 +4,20 @@ package io.flutter.plugins.camera; -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertNotNull; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.ArgumentMatchers.eq; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.never; -import static org.mockito.Mockito.times; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.when; - import android.app.Activity; import android.graphics.Rect; import android.hardware.camera2.CameraAccessException; import android.hardware.camera2.CaptureRequest.Builder; import android.media.CamcorderProfile; import android.util.Size; + import androidx.annotation.NonNull; + +import org.junit.Before; +import org.junit.Test; + +import java.util.concurrent.Callable; + import io.flutter.plugin.common.MethodChannel; import io.flutter.plugins.camera.features.CameraFeatureFactory; import io.flutter.plugins.camera.features.Point; @@ -44,9 +41,16 @@ import io.flutter.plugins.camera.features.zoomlevel.CameraZoom; import io.flutter.plugins.camera.features.zoomlevel.ZoomLevelFeature; import io.flutter.view.TextureRegistry; -import java.util.concurrent.Callable; -import org.junit.Before; -import org.junit.Test; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; public class CameraTest { private CameraProperties mockCameraProperties; From 9c11397972edb10fc8259caca19ea7100d91e2fd Mon Sep 17 00:00:00 2001 From: Maurits van Beusekom Date: Tue, 16 Mar 2021 08:44:42 +0100 Subject: [PATCH 110/114] fix formatting --- .../io/flutter/plugins/camera/CameraTest.java | 30 ++++++++----------- 1 file changed, 13 insertions(+), 17 deletions(-) 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 90e923d30483..57e35b1fc0f0 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 @@ -4,20 +4,23 @@ package io.flutter.plugins.camera; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + import android.app.Activity; import android.graphics.Rect; import android.hardware.camera2.CameraAccessException; import android.hardware.camera2.CaptureRequest.Builder; import android.media.CamcorderProfile; import android.util.Size; - import androidx.annotation.NonNull; - -import org.junit.Before; -import org.junit.Test; - -import java.util.concurrent.Callable; - import io.flutter.plugin.common.MethodChannel; import io.flutter.plugins.camera.features.CameraFeatureFactory; import io.flutter.plugins.camera.features.Point; @@ -41,16 +44,9 @@ import io.flutter.plugins.camera.features.zoomlevel.CameraZoom; import io.flutter.plugins.camera.features.zoomlevel.ZoomLevelFeature; import io.flutter.view.TextureRegistry; - -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertNotNull; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.ArgumentMatchers.eq; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.never; -import static org.mockito.Mockito.times; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.when; +import java.util.concurrent.Callable; +import org.junit.Before; +import org.junit.Test; public class CameraTest { private CameraProperties mockCameraProperties; From cf1de786b0ef17621bcedd51d83c2ce4426f8689 Mon Sep 17 00:00:00 2001 From: Maxwell Tang Date: Sat, 3 Apr 2021 10:15:23 +0700 Subject: [PATCH 111/114] missing applying uithread --- .../main/java/io/flutter/plugins/camera/ImageSaver.java | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/ImageSaver.java b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/ImageSaver.java index 61a60c22e46c..647bb93c6f77 100644 --- a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/ImageSaver.java +++ b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/ImageSaver.java @@ -52,7 +52,13 @@ public void run() { }); } catch (IOException e) { - mPictureCaptureRequest.error("IOError", "Failed saving image", null); + handler.post( + new Runnable() { + @Override + public void run() { + mPictureCaptureRequest.error("IOError", "Failed saving image", null); + } + }); } finally { mImage.close(); if (null != output) { From dc1fd4e162658f5636cff3385a72db0d2c770a24 Mon Sep 17 00:00:00 2001 From: Maxwell Tang Date: Sat, 3 Apr 2021 10:38:53 +0700 Subject: [PATCH 112/114] handle captureSession null --- .../src/main/java/io/flutter/plugins/camera/Camera.java | 4 ++++ 1 file changed, 4 insertions(+) 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 678bacf3396c..91f0df33476a 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 @@ -713,6 +713,10 @@ private void lockAutoFocus() { private void unlockAutoFocus() { Log.i(TAG, "unlockAutoFocus"); try { + if (captureSession == null) { + Log.i(TAG, "[unlockAutoFocus] captureSession null, returning"); + return; + } // Cancel existing AF state previewRequestBuilder.set( CaptureRequest.CONTROL_AF_TRIGGER, CameraMetadata.CONTROL_AF_TRIGGER_CANCEL); From 95e104d7f5392a49a7decda32f178215900da8b8 Mon Sep 17 00:00:00 2001 From: Maxwell Tang Date: Mon, 5 Apr 2021 23:18:01 +0700 Subject: [PATCH 113/114] move runOnUIthread to PictureCaptureRequest class --- .../io/flutter/plugins/camera/ImageSaver.java | 27 +++---------------- .../plugins/camera/PictureCaptureRequest.java | 7 +++-- 2 files changed, 8 insertions(+), 26 deletions(-) diff --git a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/ImageSaver.java b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/ImageSaver.java index 647bb93c6f77..5972ca85ea85 100644 --- a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/ImageSaver.java +++ b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/ImageSaver.java @@ -32,9 +32,6 @@ public class ImageSaver implements Runnable { @Override public void run() { - // We need to call the method channel stuff on main thread - final Handler handler = new Handler(Looper.getMainLooper()); - ByteBuffer buffer = mImage.getPlanes()[0].getBuffer(); byte[] bytes = new byte[buffer.remaining()]; buffer.get(bytes); @@ -43,35 +40,17 @@ public void run() { output = new FileOutputStream(mFile); output.write(bytes); - handler.post( - new Runnable() { - @Override - public void run() { - mPictureCaptureRequest.finish(mFile.getAbsolutePath()); - } - }); + mPictureCaptureRequest.finish(mFile.getAbsolutePath()); } catch (IOException e) { - handler.post( - new Runnable() { - @Override - public void run() { - mPictureCaptureRequest.error("IOError", "Failed saving image", null); - } - }); + mPictureCaptureRequest.error("IOError", "Failed saving image", null); } finally { mImage.close(); if (null != output) { try { output.close(); } catch (IOException e) { - handler.post( - new Runnable() { - @Override - public void run() { - mPictureCaptureRequest.error("cameraAccess", e.getMessage(), null); - } - }); + mPictureCaptureRequest.error("cameraAccess", e.getMessage(), null); } } } diff --git a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/PictureCaptureRequest.java b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/PictureCaptureRequest.java index 281d3e8cfa1b..b746058e4417 100644 --- a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/PictureCaptureRequest.java +++ b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/PictureCaptureRequest.java @@ -1,5 +1,8 @@ package io.flutter.plugins.camera; +import android.os.Handler; +import android.os.Looper; + import androidx.annotation.Nullable; import io.flutter.plugin.common.MethodChannel; import java.io.File; @@ -60,7 +63,7 @@ private PictureCaptureRequest( * @param absolutePath */ public void finish(String absolutePath) { - result.success(absolutePath); + new Handler(Looper.getMainLooper()).post(() -> result.success(absolutePath)); } /** @@ -72,6 +75,6 @@ public void finish(String absolutePath) { */ public void error( String errorCode, @Nullable String errorMessage, @Nullable Object errorDetails) { - result.error(errorCode, errorMessage, errorDetails); + new Handler(Looper.getMainLooper()).post(() -> result.error(errorCode, errorMessage, errorDetails)); } } From eea20e3207b6c36a08f97f00c7fbb1202acfda7a Mon Sep 17 00:00:00 2001 From: Andrew Coutts Date: Tue, 25 May 2021 15:09:25 -0400 Subject: [PATCH 114/114] Fix 60fps bug on Pixel 4A preventing image capture from working --- .../camera/features/fpsrange/FpsRangeFeature.java | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/fpsrange/FpsRangeFeature.java b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/fpsrange/FpsRangeFeature.java index c3c20abfd71f..b9b3b3c3ba19 100644 --- a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/fpsrange/FpsRangeFeature.java +++ b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/fpsrange/FpsRangeFeature.java @@ -25,7 +25,13 @@ public FpsRangeFeature(CameraProperties cameraProperties) { for (Range range : ranges) { int upper = range.getUpper(); Log.i("Camera", "[FPS Range Available] is:" + range); - if (upper >= 10) { + + // There is a bug in the Pixel 4A where it cannot support 60fps modes + // even though they are reported as supported by `getControlAutoExposureAvailableTargetFpsRanges`. + // For max device compatibility we will keep FPS under 60 even if they report they are + // capable of achieving 60 fps. + // https://issuetracker.google.com/issues/189237151 + if (upper >= 10 && upper < 60) { if (currentSetting == null || upper > currentSetting.getUpper()) { currentSetting = range; } @@ -66,7 +72,7 @@ public void updateBuilder(CaptureRequest.Builder requestBuilder) { return; } - Log.i("Camera", "updateFpsRange | currentSetting: " + currentSetting); + Log.i("Camera", "FpsRangeFeature | currentSetting: " + currentSetting); requestBuilder.set(CaptureRequest.CONTROL_AE_TARGET_FPS_RANGE, currentSetting); }