From 2b7aa9b12c5cbd57b8225f38670e3ec81f11c671 Mon Sep 17 00:00:00 2001 From: Maurits van Beusekom Date: Thu, 8 Apr 2021 13:11:03 +0200 Subject: [PATCH 01/63] Base classes to support Android camera features Co-authored-by: Andrew Coutts --- .../plugins/camera/CameraProperties.java | 168 ++++++++++++ .../camera/features/CameraFeature.java | 56 ++++ .../camera/CameraPropertiesImplTest.java | 254 ++++++++++++++++++ 3 files changed, 478 insertions(+) create mode 100644 packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/CameraProperties.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/test/java/io/flutter/plugins/camera/CameraPropertiesImplTest.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 new file mode 100644 index 000000000000..1a8bb742d811 --- /dev/null +++ b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/CameraProperties.java @@ -0,0 +1,168 @@ +// Copyright 2013 The Flutter 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; +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; + +/** + * An interface allowing access to the different characteristics of the device's camera. + */ +public interface CameraProperties { + String getCameraName(); + + Range[] getControlAutoExposureAvailableTargetFpsRanges(); + + Range getControlAutoExposureCompensationRange(); + + double 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(); + + int getHardwareLevel(); + + int[] getAvailableNoiseReductionModes(); +} + +/** + * Implementation of the @see CameraProperties interface using the @see android.hardware.camera2.CameraCharacteristics + * class to access the different characteristics. + */ +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 double getControlAutoExposureCompensationStep() { + Rational rational = + cameraCharacteristics.get(CameraCharacteristics.CONTROL_AE_COMPENSATION_STEP); + + return rational == null ? 0.0 : rational.doubleValue(); + } + + @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); + } + + @Override + 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); + } +} \ No newline at end of file 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..618c637f7589 --- /dev/null +++ b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/CameraFeature.java @@ -0,0 +1,56 @@ +// Copyright 2013 The Flutter 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; + +/** + * 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 abstract class CameraFeature { + protected final CameraProperties cameraProperties; + + protected CameraFeature(@NonNull CameraProperties cameraProperties) { + this.cameraProperties = cameraProperties; + } + + /** Debug name for this feature. */ + public abstract String getDebugName(); + + /** + * Get the current value of this feature's setting. + * + * @return + */ + public abstract T getValue(); + + /** + * Set a new value for this feature's setting. + * + * @param value + */ + public abstract void setValue(T value); + + /** + * Returns whether or not this feature is supported. + * + * @return + */ + public abstract boolean checkIsSupported(); + + /** + * Update the setting in a provided request builder. + * + * @param requestBuilder + */ + public abstract void updateBuilder(CaptureRequest.Builder requestBuilder); +} \ No newline at end of file diff --git a/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/CameraPropertiesImplTest.java b/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/CameraPropertiesImplTest.java new file mode 100644 index 000000000000..189bb2cd61fb --- /dev/null +++ b/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/CameraPropertiesImplTest.java @@ -0,0 +1,254 @@ +// Copyright 2013 The Flutter 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; +import android.hardware.camera2.CameraAccessException; +import android.hardware.camera2.CameraCharacteristics; +import android.hardware.camera2.CameraManager; +import android.util.Range; +import android.util.Rational; +import android.util.Size; + +import org.junit.Before; +import org.junit.Test; + +import static org.junit.Assert.assertArrayEquals; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.fail; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +public class CameraPropertiesImplTest { + private static final String CAMERA_NAME = "test_camera"; + private final CameraCharacteristics mockCharacteristics = mock(CameraCharacteristics.class); + private final CameraManager mockCameraManager = mock(CameraManager.class); + + private CameraPropertiesImpl cameraProperties; + + @Before + public void before() { + try { + when(mockCameraManager.getCameraCharacteristics(CAMERA_NAME)).thenReturn(mockCharacteristics); + cameraProperties = new CameraPropertiesImpl(CAMERA_NAME, mockCameraManager); + } catch (CameraAccessException e) { + fail(); + } + } + + @Test + public void ctor_Should_return_valid_instance() throws CameraAccessException { + verify(mockCameraManager, times(1)).getCameraCharacteristics(CAMERA_NAME); + assertNotNull(cameraProperties); + } + + @Test + @SuppressWarnings("unchecked") + public void getControlAutoExposureAvailableTargetFpsRangesTest() { + Range mockRange = mock(Range.class); + Range[] mockRanges = new Range[] { mockRange }; + when(mockCharacteristics.get(CameraCharacteristics.CONTROL_AE_AVAILABLE_TARGET_FPS_RANGES)).thenReturn(mockRanges); + + Range[] actualRanges = cameraProperties.getControlAutoExposureAvailableTargetFpsRanges(); + + verify(mockCharacteristics, times(1)).get(CameraCharacteristics.CONTROL_AE_AVAILABLE_TARGET_FPS_RANGES); + assertArrayEquals(actualRanges, mockRanges); + } + + @Test + @SuppressWarnings("unchecked") + public void getControlAutoExposureCompensationRangeTest() { + Range mockRange = mock(Range.class); + when(mockCharacteristics.get(CameraCharacteristics.CONTROL_AE_COMPENSATION_RANGE)).thenReturn(mockRange); + + Range actualRange = cameraProperties.getControlAutoExposureCompensationRange(); + + verify(mockCharacteristics, times(1)).get(CameraCharacteristics.CONTROL_AE_COMPENSATION_RANGE); + assertEquals(actualRange, mockRange); + } + + @Test + public void getControlAutoExposureCompensationStep_Should_return_double_When_rational_is_not_null() { + double expectedStep = 3.1415926535; + Rational mockRational = mock(Rational.class); + + when(mockCharacteristics.get(CameraCharacteristics.CONTROL_AE_COMPENSATION_STEP)).thenReturn(mockRational); + when(mockRational.doubleValue()).thenReturn(expectedStep); + + double actualSteps = cameraProperties.getControlAutoExposureCompensationStep(); + + verify(mockCharacteristics, times(1)).get(CameraCharacteristics.CONTROL_AE_COMPENSATION_STEP); + assertEquals(actualSteps, expectedStep, 0); + } + + @Test + public void getControlAutoExposureCompensationStep_Should_return_zero_When_rational_is_null() { + double expectedStep = 0.0; + + when(mockCharacteristics.get(CameraCharacteristics.CONTROL_AE_COMPENSATION_STEP)).thenReturn(null); + + double actualSteps = cameraProperties.getControlAutoExposureCompensationStep(); + + verify(mockCharacteristics, times(1)).get(CameraCharacteristics.CONTROL_AE_COMPENSATION_STEP); + assertEquals(actualSteps, expectedStep, 0); + } + + @Test + public void getControlAutoFocusAvailableModesTest() { + int[] expectedAutoFocusModes = new int[] {0, 1, 2}; + when(mockCharacteristics.get(CameraCharacteristics.CONTROL_AF_AVAILABLE_MODES)).thenReturn(expectedAutoFocusModes); + + int[] actualAutoFocusModes = cameraProperties.getControlAutoFocusAvailableModes(); + + verify(mockCharacteristics, times(1)).get(CameraCharacteristics.CONTROL_AF_AVAILABLE_MODES); + assertEquals(actualAutoFocusModes, expectedAutoFocusModes); + } + + @Test + public void getControlMaxRegionsAutoExposureTest() { + int expectedRegions = 42; + when(mockCharacteristics.get(CameraCharacteristics.CONTROL_MAX_REGIONS_AE)).thenReturn(expectedRegions); + + int actualRegions = cameraProperties.getControlMaxRegionsAutoExposure(); + + verify(mockCharacteristics, times(1)).get(CameraCharacteristics.CONTROL_MAX_REGIONS_AE); + assertEquals(actualRegions, expectedRegions); + } + + @Test + public void getControlMaxRegionsAutoFocusTest() { + int expectedRegions = 42; + when(mockCharacteristics.get(CameraCharacteristics.CONTROL_MAX_REGIONS_AF)).thenReturn(expectedRegions); + + int actualRegions = cameraProperties.getControlMaxRegionsAutoFocus(); + + verify(mockCharacteristics, times(1)).get(CameraCharacteristics.CONTROL_MAX_REGIONS_AF); + assertEquals(actualRegions, expectedRegions); + } + + @Test + public void getDistortionCorrectionAvailableModesTest() { + int[] expectedCorrectionModes = new int[] {0, 1, 2}; + when(mockCharacteristics.get(CameraCharacteristics.DISTORTION_CORRECTION_AVAILABLE_MODES)).thenReturn(expectedCorrectionModes); + + int[] actualCorrectionModes = cameraProperties.getDistortionCorrectionAvailableModes(); + + verify(mockCharacteristics, times(1)).get(CameraCharacteristics.DISTORTION_CORRECTION_AVAILABLE_MODES); + assertEquals(actualCorrectionModes, expectedCorrectionModes); + } + + @Test + public void getFlashInfoAvailableTest() { + boolean expectedAvailability = true; + when(mockCharacteristics.get(CameraCharacteristics.FLASH_INFO_AVAILABLE)).thenReturn(expectedAvailability); + + boolean actualAvailability = cameraProperties.getFlashInfoAvailable(); + + verify(mockCharacteristics, times(1)).get(CameraCharacteristics.FLASH_INFO_AVAILABLE); + assertEquals(actualAvailability, expectedAvailability); + } + + @Test + public void getLensFacingTest() { + int expectedFacing = 42; + when(mockCharacteristics.get(CameraCharacteristics.LENS_FACING)).thenReturn(expectedFacing); + + int actualFacing = cameraProperties.getLensFacing(); + + verify(mockCharacteristics, times(1)).get(CameraCharacteristics.LENS_FACING); + assertEquals(actualFacing, expectedFacing); + } + + @Test + public void getLensInfoMinimumFocusDistanceTest() { + Float expectedFocusDistance = new Float(3.14); + when(mockCharacteristics.get(CameraCharacteristics.LENS_INFO_MINIMUM_FOCUS_DISTANCE)).thenReturn(expectedFocusDistance); + + Float actualFocusDistance = cameraProperties.getLensInfoMinimumFocusDistance(); + + verify(mockCharacteristics, times(1)).get(CameraCharacteristics.LENS_INFO_MINIMUM_FOCUS_DISTANCE); + assertEquals(actualFocusDistance, expectedFocusDistance); + } + + @Test + public void getScalerAvailableMaxDigitalZoomTest() { + Float expectedDigitalZoom = new Float(3.14); + when(mockCharacteristics.get(CameraCharacteristics.SCALER_AVAILABLE_MAX_DIGITAL_ZOOM)).thenReturn(expectedDigitalZoom); + + Float actualDigitalZoom = cameraProperties.getScalerAvailableMaxDigitalZoom(); + + verify(mockCharacteristics, times(1)).get(CameraCharacteristics.SCALER_AVAILABLE_MAX_DIGITAL_ZOOM); + assertEquals(actualDigitalZoom, expectedDigitalZoom); + } + + @Test + public void getSensorInfoActiveArraySizeTest() { + Rect expectedArraySize = mock(Rect.class); + when(mockCharacteristics.get(CameraCharacteristics.SENSOR_INFO_ACTIVE_ARRAY_SIZE)).thenReturn(expectedArraySize); + + Rect actualArraySize = cameraProperties.getSensorInfoActiveArraySize(); + + verify(mockCharacteristics, times(1)).get(CameraCharacteristics.SENSOR_INFO_ACTIVE_ARRAY_SIZE); + assertEquals(actualArraySize, expectedArraySize); + } + + @Test + public void getSensorInfoPixelArraySizeTest() { + Size expectedArraySize = mock(Size.class); + when(mockCharacteristics.get(CameraCharacteristics.SENSOR_INFO_PIXEL_ARRAY_SIZE)).thenReturn(expectedArraySize); + + Size actualArraySize = cameraProperties.getSensorInfoPixelArraySize(); + + verify(mockCharacteristics, times(1)).get(CameraCharacteristics.SENSOR_INFO_PIXEL_ARRAY_SIZE); + assertEquals(actualArraySize, expectedArraySize); + } + + @Test + public void getSensorInfoPreCorrectionActiveArraySize() { + Rect expectedArraySize = mock(Rect.class); + when(mockCharacteristics.get(CameraCharacteristics.SENSOR_INFO_PRE_CORRECTION_ACTIVE_ARRAY_SIZE)).thenReturn(expectedArraySize); + + Rect actualArraySize = cameraProperties.getSensorInfoPreCorrectionActiveArraySize(); + + verify(mockCharacteristics, times(1)).get(CameraCharacteristics.SENSOR_INFO_PRE_CORRECTION_ACTIVE_ARRAY_SIZE); + assertEquals(actualArraySize, expectedArraySize); + } + + @Test + public void getSensorOrientationTest() { + int expectedOrientation = 42; + when(mockCharacteristics.get(CameraCharacteristics.SENSOR_ORIENTATION)).thenReturn(expectedOrientation); + + int actualOrientation = cameraProperties.getSensorOrientation(); + + verify(mockCharacteristics, times(1)).get(CameraCharacteristics.SENSOR_ORIENTATION); + assertEquals(actualOrientation, expectedOrientation); + } + + @Test + public void getHardwareLevelTest() { + int expectedLevel = 42; + when(mockCharacteristics.get(CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL)).thenReturn(expectedLevel); + + int actualLevel = cameraProperties.getHardwareLevel(); + + verify(mockCharacteristics, times(1)).get(CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL); + assertEquals(actualLevel, expectedLevel); + } + + @Test + public void getAvailableNoiseReductionModesTest() { + int[] expectedReductionModes = new int[] {0, 1, 2}; + when(mockCharacteristics.get(CameraCharacteristics.NOISE_REDUCTION_AVAILABLE_NOISE_REDUCTION_MODES)).thenReturn(expectedReductionModes); + + int[] actualReductionModes = cameraProperties.getAvailableNoiseReductionModes(); + + verify(mockCharacteristics, times(1)).get(CameraCharacteristics.NOISE_REDUCTION_AVAILABLE_NOISE_REDUCTION_MODES); + assertEquals(actualReductionModes, expectedReductionModes); + } +} From f7807420ba1963eb937104a05d7cca1519774dea Mon Sep 17 00:00:00 2001 From: Maurits van Beusekom Date: Thu, 8 Apr 2021 13:51:58 +0200 Subject: [PATCH 02/63] Fixed formatting --- .../plugins/camera/CameraProperties.java | 256 +++++----- .../camera/features/CameraFeature.java | 74 +-- .../camera/CameraPropertiesImplTest.java | 482 +++++++++--------- 3 files changed, 418 insertions(+), 394 deletions(-) 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 1a8bb742d811..6ed550bc233e 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 @@ -14,155 +14,153 @@ import android.util.Size; import androidx.annotation.RequiresApi; -/** - * An interface allowing access to the different characteristics of the device's camera. - */ +/** An interface allowing access to the different characteristics of the device's camera. */ public interface CameraProperties { - String getCameraName(); + String getCameraName(); - Range[] getControlAutoExposureAvailableTargetFpsRanges(); + Range[] getControlAutoExposureAvailableTargetFpsRanges(); - Range getControlAutoExposureCompensationRange(); + Range getControlAutoExposureCompensationRange(); - double getControlAutoExposureCompensationStep(); + double getControlAutoExposureCompensationStep(); - int[] getControlAutoFocusAvailableModes(); + int[] getControlAutoFocusAvailableModes(); - Integer getControlMaxRegionsAutoExposure(); + Integer getControlMaxRegionsAutoExposure(); - Integer getControlMaxRegionsAutoFocus(); + Integer getControlMaxRegionsAutoFocus(); - int[] getDistortionCorrectionAvailableModes(); + int[] getDistortionCorrectionAvailableModes(); - Boolean getFlashInfoAvailable(); + Boolean getFlashInfoAvailable(); - int getLensFacing(); + int getLensFacing(); - Float getLensInfoMinimumFocusDistance(); + Float getLensInfoMinimumFocusDistance(); - Float getScalerAvailableMaxDigitalZoom(); + Float getScalerAvailableMaxDigitalZoom(); - Rect getSensorInfoActiveArraySize(); + Rect getSensorInfoActiveArraySize(); - Size getSensorInfoPixelArraySize(); + Size getSensorInfoPixelArraySize(); - Rect getSensorInfoPreCorrectionActiveArraySize(); + Rect getSensorInfoPreCorrectionActiveArraySize(); - int getSensorOrientation(); + int getSensorOrientation(); - int getHardwareLevel(); + int getHardwareLevel(); - int[] getAvailableNoiseReductionModes(); + int[] getAvailableNoiseReductionModes(); } /** - * Implementation of the @see CameraProperties interface using the @see android.hardware.camera2.CameraCharacteristics - * class to access the different characteristics. + * Implementation of the @see CameraProperties interface using the @see + * android.hardware.camera2.CameraCharacteristics class to access the different characteristics. */ 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 double getControlAutoExposureCompensationStep() { - Rational rational = - cameraCharacteristics.get(CameraCharacteristics.CONTROL_AE_COMPENSATION_STEP); - - return rational == null ? 0.0 : rational.doubleValue(); - } - - @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); - } - - @Override - 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); - } -} \ No newline at end of file + 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 double getControlAutoExposureCompensationStep() { + Rational rational = + cameraCharacteristics.get(CameraCharacteristics.CONTROL_AE_COMPENSATION_STEP); + + return rational == null ? 0.0 : rational.doubleValue(); + } + + @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); + } + + @Override + 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/CameraFeature.java b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/CameraFeature.java index 618c637f7589..39ecc8f92a39 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 @@ -17,40 +17,40 @@ * @param */ public abstract class CameraFeature { - protected final CameraProperties cameraProperties; - - protected CameraFeature(@NonNull CameraProperties cameraProperties) { - this.cameraProperties = cameraProperties; - } - - /** Debug name for this feature. */ - public abstract String getDebugName(); - - /** - * Get the current value of this feature's setting. - * - * @return - */ - public abstract T getValue(); - - /** - * Set a new value for this feature's setting. - * - * @param value - */ - public abstract void setValue(T value); - - /** - * Returns whether or not this feature is supported. - * - * @return - */ - public abstract boolean checkIsSupported(); - - /** - * Update the setting in a provided request builder. - * - * @param requestBuilder - */ - public abstract void updateBuilder(CaptureRequest.Builder requestBuilder); -} \ No newline at end of file + protected final CameraProperties cameraProperties; + + protected CameraFeature(@NonNull CameraProperties cameraProperties) { + this.cameraProperties = cameraProperties; + } + + /** Debug name for this feature. */ + public abstract String getDebugName(); + + /** + * Get the current value of this feature's setting. + * + * @return + */ + public abstract T getValue(); + + /** + * Set a new value for this feature's setting. + * + * @param value + */ + public abstract void setValue(T value); + + /** + * Returns whether or not this feature is supported. + * + * @return + */ + public abstract boolean checkIsSupported(); + + /** + * Update the setting in a provided request builder. + * + * @param requestBuilder + */ + public abstract void updateBuilder(CaptureRequest.Builder requestBuilder); +} diff --git a/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/CameraPropertiesImplTest.java b/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/CameraPropertiesImplTest.java index 189bb2cd61fb..2c0381744191 100644 --- a/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/CameraPropertiesImplTest.java +++ b/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/CameraPropertiesImplTest.java @@ -4,6 +4,15 @@ package io.flutter.plugins.camera; +import static org.junit.Assert.assertArrayEquals; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.fail; +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.graphics.Rect; import android.hardware.camera2.CameraAccessException; import android.hardware.camera2.CameraCharacteristics; @@ -11,244 +20,261 @@ import android.util.Range; import android.util.Rational; import android.util.Size; - import org.junit.Before; import org.junit.Test; -import static org.junit.Assert.assertArrayEquals; -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertNotNull; -import static org.junit.Assert.fail; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.times; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.when; - public class CameraPropertiesImplTest { - private static final String CAMERA_NAME = "test_camera"; - private final CameraCharacteristics mockCharacteristics = mock(CameraCharacteristics.class); - private final CameraManager mockCameraManager = mock(CameraManager.class); - - private CameraPropertiesImpl cameraProperties; - - @Before - public void before() { - try { - when(mockCameraManager.getCameraCharacteristics(CAMERA_NAME)).thenReturn(mockCharacteristics); - cameraProperties = new CameraPropertiesImpl(CAMERA_NAME, mockCameraManager); - } catch (CameraAccessException e) { - fail(); - } - } - - @Test - public void ctor_Should_return_valid_instance() throws CameraAccessException { - verify(mockCameraManager, times(1)).getCameraCharacteristics(CAMERA_NAME); - assertNotNull(cameraProperties); - } - - @Test - @SuppressWarnings("unchecked") - public void getControlAutoExposureAvailableTargetFpsRangesTest() { - Range mockRange = mock(Range.class); - Range[] mockRanges = new Range[] { mockRange }; - when(mockCharacteristics.get(CameraCharacteristics.CONTROL_AE_AVAILABLE_TARGET_FPS_RANGES)).thenReturn(mockRanges); - - Range[] actualRanges = cameraProperties.getControlAutoExposureAvailableTargetFpsRanges(); - - verify(mockCharacteristics, times(1)).get(CameraCharacteristics.CONTROL_AE_AVAILABLE_TARGET_FPS_RANGES); - assertArrayEquals(actualRanges, mockRanges); - } - - @Test - @SuppressWarnings("unchecked") - public void getControlAutoExposureCompensationRangeTest() { - Range mockRange = mock(Range.class); - when(mockCharacteristics.get(CameraCharacteristics.CONTROL_AE_COMPENSATION_RANGE)).thenReturn(mockRange); - - Range actualRange = cameraProperties.getControlAutoExposureCompensationRange(); - - verify(mockCharacteristics, times(1)).get(CameraCharacteristics.CONTROL_AE_COMPENSATION_RANGE); - assertEquals(actualRange, mockRange); - } - - @Test - public void getControlAutoExposureCompensationStep_Should_return_double_When_rational_is_not_null() { - double expectedStep = 3.1415926535; - Rational mockRational = mock(Rational.class); - - when(mockCharacteristics.get(CameraCharacteristics.CONTROL_AE_COMPENSATION_STEP)).thenReturn(mockRational); - when(mockRational.doubleValue()).thenReturn(expectedStep); - - double actualSteps = cameraProperties.getControlAutoExposureCompensationStep(); - - verify(mockCharacteristics, times(1)).get(CameraCharacteristics.CONTROL_AE_COMPENSATION_STEP); - assertEquals(actualSteps, expectedStep, 0); - } - - @Test - public void getControlAutoExposureCompensationStep_Should_return_zero_When_rational_is_null() { - double expectedStep = 0.0; - - when(mockCharacteristics.get(CameraCharacteristics.CONTROL_AE_COMPENSATION_STEP)).thenReturn(null); - - double actualSteps = cameraProperties.getControlAutoExposureCompensationStep(); - - verify(mockCharacteristics, times(1)).get(CameraCharacteristics.CONTROL_AE_COMPENSATION_STEP); - assertEquals(actualSteps, expectedStep, 0); - } - - @Test - public void getControlAutoFocusAvailableModesTest() { - int[] expectedAutoFocusModes = new int[] {0, 1, 2}; - when(mockCharacteristics.get(CameraCharacteristics.CONTROL_AF_AVAILABLE_MODES)).thenReturn(expectedAutoFocusModes); - - int[] actualAutoFocusModes = cameraProperties.getControlAutoFocusAvailableModes(); - - verify(mockCharacteristics, times(1)).get(CameraCharacteristics.CONTROL_AF_AVAILABLE_MODES); - assertEquals(actualAutoFocusModes, expectedAutoFocusModes); - } - - @Test - public void getControlMaxRegionsAutoExposureTest() { - int expectedRegions = 42; - when(mockCharacteristics.get(CameraCharacteristics.CONTROL_MAX_REGIONS_AE)).thenReturn(expectedRegions); - - int actualRegions = cameraProperties.getControlMaxRegionsAutoExposure(); - - verify(mockCharacteristics, times(1)).get(CameraCharacteristics.CONTROL_MAX_REGIONS_AE); - assertEquals(actualRegions, expectedRegions); - } - - @Test - public void getControlMaxRegionsAutoFocusTest() { - int expectedRegions = 42; - when(mockCharacteristics.get(CameraCharacteristics.CONTROL_MAX_REGIONS_AF)).thenReturn(expectedRegions); - - int actualRegions = cameraProperties.getControlMaxRegionsAutoFocus(); - - verify(mockCharacteristics, times(1)).get(CameraCharacteristics.CONTROL_MAX_REGIONS_AF); - assertEquals(actualRegions, expectedRegions); - } - - @Test - public void getDistortionCorrectionAvailableModesTest() { - int[] expectedCorrectionModes = new int[] {0, 1, 2}; - when(mockCharacteristics.get(CameraCharacteristics.DISTORTION_CORRECTION_AVAILABLE_MODES)).thenReturn(expectedCorrectionModes); - - int[] actualCorrectionModes = cameraProperties.getDistortionCorrectionAvailableModes(); - - verify(mockCharacteristics, times(1)).get(CameraCharacteristics.DISTORTION_CORRECTION_AVAILABLE_MODES); - assertEquals(actualCorrectionModes, expectedCorrectionModes); - } - - @Test - public void getFlashInfoAvailableTest() { - boolean expectedAvailability = true; - when(mockCharacteristics.get(CameraCharacteristics.FLASH_INFO_AVAILABLE)).thenReturn(expectedAvailability); - - boolean actualAvailability = cameraProperties.getFlashInfoAvailable(); - - verify(mockCharacteristics, times(1)).get(CameraCharacteristics.FLASH_INFO_AVAILABLE); - assertEquals(actualAvailability, expectedAvailability); - } - - @Test - public void getLensFacingTest() { - int expectedFacing = 42; - when(mockCharacteristics.get(CameraCharacteristics.LENS_FACING)).thenReturn(expectedFacing); - - int actualFacing = cameraProperties.getLensFacing(); - - verify(mockCharacteristics, times(1)).get(CameraCharacteristics.LENS_FACING); - assertEquals(actualFacing, expectedFacing); - } - - @Test - public void getLensInfoMinimumFocusDistanceTest() { - Float expectedFocusDistance = new Float(3.14); - when(mockCharacteristics.get(CameraCharacteristics.LENS_INFO_MINIMUM_FOCUS_DISTANCE)).thenReturn(expectedFocusDistance); - - Float actualFocusDistance = cameraProperties.getLensInfoMinimumFocusDistance(); - - verify(mockCharacteristics, times(1)).get(CameraCharacteristics.LENS_INFO_MINIMUM_FOCUS_DISTANCE); - assertEquals(actualFocusDistance, expectedFocusDistance); - } - - @Test - public void getScalerAvailableMaxDigitalZoomTest() { - Float expectedDigitalZoom = new Float(3.14); - when(mockCharacteristics.get(CameraCharacteristics.SCALER_AVAILABLE_MAX_DIGITAL_ZOOM)).thenReturn(expectedDigitalZoom); - - Float actualDigitalZoom = cameraProperties.getScalerAvailableMaxDigitalZoom(); - - verify(mockCharacteristics, times(1)).get(CameraCharacteristics.SCALER_AVAILABLE_MAX_DIGITAL_ZOOM); - assertEquals(actualDigitalZoom, expectedDigitalZoom); - } - - @Test - public void getSensorInfoActiveArraySizeTest() { - Rect expectedArraySize = mock(Rect.class); - when(mockCharacteristics.get(CameraCharacteristics.SENSOR_INFO_ACTIVE_ARRAY_SIZE)).thenReturn(expectedArraySize); - - Rect actualArraySize = cameraProperties.getSensorInfoActiveArraySize(); - - verify(mockCharacteristics, times(1)).get(CameraCharacteristics.SENSOR_INFO_ACTIVE_ARRAY_SIZE); - assertEquals(actualArraySize, expectedArraySize); - } - - @Test - public void getSensorInfoPixelArraySizeTest() { - Size expectedArraySize = mock(Size.class); - when(mockCharacteristics.get(CameraCharacteristics.SENSOR_INFO_PIXEL_ARRAY_SIZE)).thenReturn(expectedArraySize); - - Size actualArraySize = cameraProperties.getSensorInfoPixelArraySize(); - - verify(mockCharacteristics, times(1)).get(CameraCharacteristics.SENSOR_INFO_PIXEL_ARRAY_SIZE); - assertEquals(actualArraySize, expectedArraySize); + private static final String CAMERA_NAME = "test_camera"; + private final CameraCharacteristics mockCharacteristics = mock(CameraCharacteristics.class); + private final CameraManager mockCameraManager = mock(CameraManager.class); + + private CameraPropertiesImpl cameraProperties; + + @Before + public void before() { + try { + when(mockCameraManager.getCameraCharacteristics(CAMERA_NAME)).thenReturn(mockCharacteristics); + cameraProperties = new CameraPropertiesImpl(CAMERA_NAME, mockCameraManager); + } catch (CameraAccessException e) { + fail(); } + } - @Test - public void getSensorInfoPreCorrectionActiveArraySize() { - Rect expectedArraySize = mock(Rect.class); - when(mockCharacteristics.get(CameraCharacteristics.SENSOR_INFO_PRE_CORRECTION_ACTIVE_ARRAY_SIZE)).thenReturn(expectedArraySize); - - Rect actualArraySize = cameraProperties.getSensorInfoPreCorrectionActiveArraySize(); + @Test + public void ctor_Should_return_valid_instance() throws CameraAccessException { + verify(mockCameraManager, times(1)).getCameraCharacteristics(CAMERA_NAME); + assertNotNull(cameraProperties); + } + + @Test + @SuppressWarnings("unchecked") + public void getControlAutoExposureAvailableTargetFpsRangesTest() { + Range mockRange = mock(Range.class); + Range[] mockRanges = new Range[] {mockRange}; + when(mockCharacteristics.get(CameraCharacteristics.CONTROL_AE_AVAILABLE_TARGET_FPS_RANGES)) + .thenReturn(mockRanges); + + Range[] actualRanges = + cameraProperties.getControlAutoExposureAvailableTargetFpsRanges(); + + verify(mockCharacteristics, times(1)) + .get(CameraCharacteristics.CONTROL_AE_AVAILABLE_TARGET_FPS_RANGES); + assertArrayEquals(actualRanges, mockRanges); + } + + @Test + @SuppressWarnings("unchecked") + public void getControlAutoExposureCompensationRangeTest() { + Range mockRange = mock(Range.class); + when(mockCharacteristics.get(CameraCharacteristics.CONTROL_AE_COMPENSATION_RANGE)) + .thenReturn(mockRange); + + Range actualRange = cameraProperties.getControlAutoExposureCompensationRange(); + + verify(mockCharacteristics, times(1)).get(CameraCharacteristics.CONTROL_AE_COMPENSATION_RANGE); + assertEquals(actualRange, mockRange); + } + + @Test + public void + getControlAutoExposureCompensationStep_Should_return_double_When_rational_is_not_null() { + double expectedStep = 3.1415926535; + Rational mockRational = mock(Rational.class); + + when(mockCharacteristics.get(CameraCharacteristics.CONTROL_AE_COMPENSATION_STEP)) + .thenReturn(mockRational); + when(mockRational.doubleValue()).thenReturn(expectedStep); - verify(mockCharacteristics, times(1)).get(CameraCharacteristics.SENSOR_INFO_PRE_CORRECTION_ACTIVE_ARRAY_SIZE); - assertEquals(actualArraySize, expectedArraySize); - } - - @Test - public void getSensorOrientationTest() { - int expectedOrientation = 42; - when(mockCharacteristics.get(CameraCharacteristics.SENSOR_ORIENTATION)).thenReturn(expectedOrientation); + double actualSteps = cameraProperties.getControlAutoExposureCompensationStep(); + + verify(mockCharacteristics, times(1)).get(CameraCharacteristics.CONTROL_AE_COMPENSATION_STEP); + assertEquals(actualSteps, expectedStep, 0); + } + + @Test + public void getControlAutoExposureCompensationStep_Should_return_zero_When_rational_is_null() { + double expectedStep = 0.0; + + when(mockCharacteristics.get(CameraCharacteristics.CONTROL_AE_COMPENSATION_STEP)) + .thenReturn(null); + + double actualSteps = cameraProperties.getControlAutoExposureCompensationStep(); + + verify(mockCharacteristics, times(1)).get(CameraCharacteristics.CONTROL_AE_COMPENSATION_STEP); + assertEquals(actualSteps, expectedStep, 0); + } + + @Test + public void getControlAutoFocusAvailableModesTest() { + int[] expectedAutoFocusModes = new int[] {0, 1, 2}; + when(mockCharacteristics.get(CameraCharacteristics.CONTROL_AF_AVAILABLE_MODES)) + .thenReturn(expectedAutoFocusModes); + + int[] actualAutoFocusModes = cameraProperties.getControlAutoFocusAvailableModes(); + + verify(mockCharacteristics, times(1)).get(CameraCharacteristics.CONTROL_AF_AVAILABLE_MODES); + assertEquals(actualAutoFocusModes, expectedAutoFocusModes); + } + + @Test + public void getControlMaxRegionsAutoExposureTest() { + int expectedRegions = 42; + when(mockCharacteristics.get(CameraCharacteristics.CONTROL_MAX_REGIONS_AE)) + .thenReturn(expectedRegions); + + int actualRegions = cameraProperties.getControlMaxRegionsAutoExposure(); + + verify(mockCharacteristics, times(1)).get(CameraCharacteristics.CONTROL_MAX_REGIONS_AE); + assertEquals(actualRegions, expectedRegions); + } + + @Test + public void getControlMaxRegionsAutoFocusTest() { + int expectedRegions = 42; + when(mockCharacteristics.get(CameraCharacteristics.CONTROL_MAX_REGIONS_AF)) + .thenReturn(expectedRegions); + + int actualRegions = cameraProperties.getControlMaxRegionsAutoFocus(); + + verify(mockCharacteristics, times(1)).get(CameraCharacteristics.CONTROL_MAX_REGIONS_AF); + assertEquals(actualRegions, expectedRegions); + } + + @Test + public void getDistortionCorrectionAvailableModesTest() { + int[] expectedCorrectionModes = new int[] {0, 1, 2}; + when(mockCharacteristics.get(CameraCharacteristics.DISTORTION_CORRECTION_AVAILABLE_MODES)) + .thenReturn(expectedCorrectionModes); + + int[] actualCorrectionModes = cameraProperties.getDistortionCorrectionAvailableModes(); + + verify(mockCharacteristics, times(1)) + .get(CameraCharacteristics.DISTORTION_CORRECTION_AVAILABLE_MODES); + assertEquals(actualCorrectionModes, expectedCorrectionModes); + } - int actualOrientation = cameraProperties.getSensorOrientation(); + @Test + public void getFlashInfoAvailableTest() { + boolean expectedAvailability = true; + when(mockCharacteristics.get(CameraCharacteristics.FLASH_INFO_AVAILABLE)) + .thenReturn(expectedAvailability); + + boolean actualAvailability = cameraProperties.getFlashInfoAvailable(); - verify(mockCharacteristics, times(1)).get(CameraCharacteristics.SENSOR_ORIENTATION); - assertEquals(actualOrientation, expectedOrientation); - } + verify(mockCharacteristics, times(1)).get(CameraCharacteristics.FLASH_INFO_AVAILABLE); + assertEquals(actualAvailability, expectedAvailability); + } - @Test - public void getHardwareLevelTest() { - int expectedLevel = 42; - when(mockCharacteristics.get(CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL)).thenReturn(expectedLevel); + @Test + public void getLensFacingTest() { + int expectedFacing = 42; + when(mockCharacteristics.get(CameraCharacteristics.LENS_FACING)).thenReturn(expectedFacing); + + int actualFacing = cameraProperties.getLensFacing(); + + verify(mockCharacteristics, times(1)).get(CameraCharacteristics.LENS_FACING); + assertEquals(actualFacing, expectedFacing); + } + + @Test + public void getLensInfoMinimumFocusDistanceTest() { + Float expectedFocusDistance = new Float(3.14); + when(mockCharacteristics.get(CameraCharacteristics.LENS_INFO_MINIMUM_FOCUS_DISTANCE)) + .thenReturn(expectedFocusDistance); + + Float actualFocusDistance = cameraProperties.getLensInfoMinimumFocusDistance(); - int actualLevel = cameraProperties.getHardwareLevel(); + verify(mockCharacteristics, times(1)) + .get(CameraCharacteristics.LENS_INFO_MINIMUM_FOCUS_DISTANCE); + assertEquals(actualFocusDistance, expectedFocusDistance); + } + + @Test + public void getScalerAvailableMaxDigitalZoomTest() { + Float expectedDigitalZoom = new Float(3.14); + when(mockCharacteristics.get(CameraCharacteristics.SCALER_AVAILABLE_MAX_DIGITAL_ZOOM)) + .thenReturn(expectedDigitalZoom); - verify(mockCharacteristics, times(1)).get(CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL); - assertEquals(actualLevel, expectedLevel); - } - - @Test - public void getAvailableNoiseReductionModesTest() { - int[] expectedReductionModes = new int[] {0, 1, 2}; - when(mockCharacteristics.get(CameraCharacteristics.NOISE_REDUCTION_AVAILABLE_NOISE_REDUCTION_MODES)).thenReturn(expectedReductionModes); - - int[] actualReductionModes = cameraProperties.getAvailableNoiseReductionModes(); - - verify(mockCharacteristics, times(1)).get(CameraCharacteristics.NOISE_REDUCTION_AVAILABLE_NOISE_REDUCTION_MODES); - assertEquals(actualReductionModes, expectedReductionModes); - } + Float actualDigitalZoom = cameraProperties.getScalerAvailableMaxDigitalZoom(); + + verify(mockCharacteristics, times(1)) + .get(CameraCharacteristics.SCALER_AVAILABLE_MAX_DIGITAL_ZOOM); + assertEquals(actualDigitalZoom, expectedDigitalZoom); + } + + @Test + public void getSensorInfoActiveArraySizeTest() { + Rect expectedArraySize = mock(Rect.class); + when(mockCharacteristics.get(CameraCharacteristics.SENSOR_INFO_ACTIVE_ARRAY_SIZE)) + .thenReturn(expectedArraySize); + + Rect actualArraySize = cameraProperties.getSensorInfoActiveArraySize(); + + verify(mockCharacteristics, times(1)).get(CameraCharacteristics.SENSOR_INFO_ACTIVE_ARRAY_SIZE); + assertEquals(actualArraySize, expectedArraySize); + } + + @Test + public void getSensorInfoPixelArraySizeTest() { + Size expectedArraySize = mock(Size.class); + when(mockCharacteristics.get(CameraCharacteristics.SENSOR_INFO_PIXEL_ARRAY_SIZE)) + .thenReturn(expectedArraySize); + + Size actualArraySize = cameraProperties.getSensorInfoPixelArraySize(); + + verify(mockCharacteristics, times(1)).get(CameraCharacteristics.SENSOR_INFO_PIXEL_ARRAY_SIZE); + assertEquals(actualArraySize, expectedArraySize); + } + + @Test + public void getSensorInfoPreCorrectionActiveArraySize() { + Rect expectedArraySize = mock(Rect.class); + when(mockCharacteristics.get( + CameraCharacteristics.SENSOR_INFO_PRE_CORRECTION_ACTIVE_ARRAY_SIZE)) + .thenReturn(expectedArraySize); + + Rect actualArraySize = cameraProperties.getSensorInfoPreCorrectionActiveArraySize(); + + verify(mockCharacteristics, times(1)) + .get(CameraCharacteristics.SENSOR_INFO_PRE_CORRECTION_ACTIVE_ARRAY_SIZE); + assertEquals(actualArraySize, expectedArraySize); + } + + @Test + public void getSensorOrientationTest() { + int expectedOrientation = 42; + when(mockCharacteristics.get(CameraCharacteristics.SENSOR_ORIENTATION)) + .thenReturn(expectedOrientation); + + int actualOrientation = cameraProperties.getSensorOrientation(); + + verify(mockCharacteristics, times(1)).get(CameraCharacteristics.SENSOR_ORIENTATION); + assertEquals(actualOrientation, expectedOrientation); + } + + @Test + public void getHardwareLevelTest() { + int expectedLevel = 42; + when(mockCharacteristics.get(CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL)) + .thenReturn(expectedLevel); + + int actualLevel = cameraProperties.getHardwareLevel(); + + verify(mockCharacteristics, times(1)).get(CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL); + assertEquals(actualLevel, expectedLevel); + } + + @Test + public void getAvailableNoiseReductionModesTest() { + int[] expectedReductionModes = new int[] {0, 1, 2}; + when(mockCharacteristics.get( + CameraCharacteristics.NOISE_REDUCTION_AVAILABLE_NOISE_REDUCTION_MODES)) + .thenReturn(expectedReductionModes); + + int[] actualReductionModes = cameraProperties.getAvailableNoiseReductionModes(); + + verify(mockCharacteristics, times(1)) + .get(CameraCharacteristics.NOISE_REDUCTION_AVAILABLE_NOISE_REDUCTION_MODES); + assertEquals(actualReductionModes, expectedReductionModes); + } } From 76bc5bd2febb87319c678aa4c96cbcfe69ca32ed Mon Sep 17 00:00:00 2001 From: Maurits van Beusekom Date: Tue, 20 Apr 2021 13:21:38 +0200 Subject: [PATCH 03/63] Applied feedback from PR --- .../plugins/camera/CameraProperties.java | 184 ++++++++++++++++++ .../camera/features/CameraFeature.java | 17 +- 2 files changed, 194 insertions(+), 7 deletions(-) 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 6ed550bc233e..a69ddd0410d4 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 @@ -16,40 +16,224 @@ /** An interface allowing access to the different characteristics of the device's camera. */ public interface CameraProperties { + + /** + * Returns the name (or identifier) of the camera device. + * + * @return String The name of the camera device. + */ String getCameraName(); + /** + * Returns the list of frame rate ranges for @see android.control.aeTargetFpsRange supported by + * this camera device. + * + *

By default maps to the @see + * android.hardware.camera2.CameraCharacteristics#CONTROL_AE_TARGET_FPS_RANGE key. + * + * @return android.util.Range[] List of frame rate ranges supported by this camera + * device. + */ Range[] getControlAutoExposureAvailableTargetFpsRanges(); + /** + * Returns the maximum and minimum exposure compensation values for @see + * android.control.aeExposureCompensation, in counts of @see android.control.aeCompensationStep, + * that are supported by this camera device. + * + *

By default maps to the @see + * android.hardware.camera2.CameraCharacteristics#CONTROL_AE_COMPENSATION_RANGE key. + * + * @return android.util.Range Maximum and minimum exposure compensation supported by this + * camera device. + */ Range getControlAutoExposureCompensationRange(); + /** + * Returns the smallest step by which the exposure compensation can be changed. + * + *

By default maps to the @see + * android.hardware.camera2.CameraCharacteristics#CONTROL_AE_COMPENSATION_STEP key. + * + * @return double Smallest step by which the exposure compensation can be changed. + */ double getControlAutoExposureCompensationStep(); + /** + * Returns a list of auto-focus modes for @see android.control.afMode that are supported by this + * camera device. + * + *

By default maps to the @see + * android.hardware.camera2.CameraCharacteristics#CONTROL_AF_AVAILABLE_MODES key. + * + * @return int[] List of auto-focus modes supported by this camera device. + */ int[] getControlAutoFocusAvailableModes(); + /** + * Returns the maximum number of metering regions that can be used by the auto-exposure routine. + * + *

By default maps to the @see + * android.hardware.camera2.CameraCharacteristics#CONTROL_MAX_REGIONS_AE key. + * + * @return Integer Maximum number of metering regions that can be used by the auto-exposure + * routine. + */ Integer getControlMaxRegionsAutoExposure(); + /** + * Returns the maximum number of metering regions that can be used by the auto-focus routine. + * + *

By default maps to the @see + * android.hardware.camera2.CameraCharacteristics#CONTROL_MAX_REGIONS_AF key. + * + * @return Integer Maximum number of metering regions that can be used by the auto-focus routine. + */ Integer getControlMaxRegionsAutoFocus(); + /** + * Returns a list of distortion correction modes for @see android.distortionCorrection.mode that + * are supported by this camera device. + * + *

By default maps to the @see + * android.hardware.camera2.CameraCharacteristics#DISTORTION_CORRECTION_AVAILABLE_MODES key. + * + * @return int[] List of distortion correction modes supported by this camera device. + */ + @RequiresApi(api = VERSION_CODES.P) int[] getDistortionCorrectionAvailableModes(); + /** + * Returns whether this camera device has a flash unit. + * + *

By default maps to the @see + * android.hardware.camera2.CameraCharacteristics#FLASH_INFO_AVAILABLE key. + * + * @return Boolean Whether this camera device has a flash unit. + */ Boolean getFlashInfoAvailable(); + /** + * Returns the direction the camera faces relative to device screen. + * + *

Possible values: + * + *

    + *
  • @see android.hardware.camera2.CameraMetadata.LENS_FACING_FRONT + *
  • @see android.hardware.camera2.CameraMetadata.LENS_FACING_BACK + *
  • @see android.hardware.camera2.CameraMetadata.LENS_FACING_EXTERNAL + *
+ * + * By default maps to the @see android.hardware.camera2.CameraCharacteristics.LENS_FACING key. + * + * @return int Direction the camera faces relative to device screen. + */ int getLensFacing(); + /** + * Returns the shortest distance from front most surface of the lens that can be brought into + * sharp focus. + * + *

By default maps to the @see + * android.hardware.camera2.CameraCharacteristics#LENS_INFO_MINIMUM_FOCUS_DISTANCE key. + * + * @return Float Shortest distance from front most surface of the lens that can be brought into + * sharp focus. + */ Float getLensInfoMinimumFocusDistance(); + /** + * Returns the maximum ratio between both active area width and crop region width, and active area + * height and crop region height, for @see android.scaler.cropRegion. + * + *

By default maps to the @see + * android.hardware.camera2.CameraCharacteristics#SCALER_AVAILABLE_MAX_DIGITAL_ZOOM key. + * + * @return Float Maximum ratio between both active area width and crop region width, and active + * area height and crop region height + */ Float getScalerAvailableMaxDigitalZoom(); + /** + * Returns the area of the image sensor which corresponds to active pixels after any geometric + * distortion correction has been applied. + * + *

By default maps to the @see + * android.hardware.camera2.CameraCharacteristics#SENSOR_INFO_ACTIVE_ARRAY_SIZE key. + * + * @return android.graphics.Rect area of the image sensor which corresponds to active pixels after + * any geometric distortion correction has been applied. + */ Rect getSensorInfoActiveArraySize(); + /** + * Returns the dimensions of the full pixel array, possibly including black calibration pixels. + * + *

By default maps to the @see + * android.hardware.camera2.CameraCharacteristics#SENSOR_INFO_PIXEL_ARRAY_SIZE key. + * + * @return android.util.Size Dimensions of the full pixel array, possibly including black + * calibration pixels. + */ Size getSensorInfoPixelArraySize(); + /** + * Returns the area of the image sensor which corresponds to active pixels prior to the + * application of any geometric distortion correction. + * + *

By default maps to the @see + * android.hardware.camera2.CameraCharacteristics#SENSOR_INFO_PRE_CORRECTION_ACTIVE_ARRAY_SIZE + * key. + * + * @return android.graphics.Rect Area of the image sensor which corresponds to active pixels prior + * to the application of any geometric distortion correction. + */ + @RequiresApi(api = VERSION_CODES.M) Rect getSensorInfoPreCorrectionActiveArraySize(); + /** + * Returns the clockwise angle through which the output image needs to be rotated to be upright on + * the device screen in its native orientation. + * + *

By default maps to the @see + * android.hardware.camera2.CameraCharacteristics#SENSOR_ORIENTATION key. + * + * @return int Clockwise angle through which the output image needs to be rotated to be upright on + * the device screen in its native orientation. + */ int getSensorOrientation(); + /** + * Returns a level which generally classifies the overall set of the camera device functionality. + * + *

Possible values: + * + *

    + *
  • @see android.hardware.camera2.CameraMetadata.INFO_SUPPORTED_HARDWARE_LEVEL_LEGACY + *
  • @see android.hardware.camera2.CameraMetadata.INFO_SUPPORTED_HARDWARE_LEVEL_LIMITED + *
  • @see android.hardware.camera2.CameraMetadata.INFO_SUPPORTED_HARDWARE_LEVEL_FULL + *
  • @see android.hardware.camera2.CameraMetadata.INFO_SUPPORTED_HARDWARE_LEVEL_LEVEL_3 + *
  • @see android.hardware.camera2.CameraMetadata.INFO_SUPPORTED_HARDWARE_LEVEL_EXTERNAL + *
+ * + * By default maps to the @see + * android.hardware.camera2.CameraCharacteristics#INFO_SUPPORTED_HARDWARE_LEVEL key. + * + * @return int Level which generally classifies the overall set of the camera device + * functionality. + */ int getHardwareLevel(); + /** + * Returns a list of noise reduction modes for @see android.noiseReduction.mode that are supported + * by this camera device. + * + *

By default maps to the @see + * android.hardware.camera2.CameraCharacteristics#NOISE_REDUCTION_AVAILABLE_NOISE_REDUCTION_MODES + * key. + * + * @return int[] List of noise reduction modes that are supported by this camera device. + */ int[] getAvailableNoiseReductionModes(); } 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 39ecc8f92a39..ad800f5e1163 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 @@ -27,30 +27,33 @@ protected CameraFeature(@NonNull CameraProperties cameraProperties) { public abstract String getDebugName(); /** - * Get the current value of this feature's setting. + * Gets the current value of this feature's setting. * - * @return + * @return Current value of this feature's setting. */ public abstract T getValue(); /** - * Set a new value for this feature's setting. + * Sets a new value for this feature's setting. * - * @param value + * @param value New value for this feature's setting. */ public abstract void setValue(T value); /** * Returns whether or not this feature is supported. * - * @return + *

When the feature is not supported any {@see #value} is simply ignored by the camera plugin. + * + * @return boolean Whether or not this feature is supported. */ public abstract boolean checkIsSupported(); /** - * Update the setting in a provided request builder. + * Updates the setting in a provided {@see android.hardware.camera2.CaptureRequest.Builder}. * - * @param requestBuilder + * @param requestBuilder A {@see android.hardware.camera2.CaptureRequest.Builder} instance used to + * configure the settings and outputs needed to capture a single image from the camera device. */ public abstract void updateBuilder(CaptureRequest.Builder requestBuilder); } From 0bbed99382b812498486ff64e38970b5d91f8727 Mon Sep 17 00:00:00 2001 From: Maurits van Beusekom Date: Thu, 8 Apr 2021 16:05:54 +0200 Subject: [PATCH 04/63] Added Android FPS range, resolution and sensor orientation features Co-authored-by: Andrew Coutts --- .../flutter/plugins/camera/DartMessenger.java | 4 +- .../features/fpsrange/FpsRangeFeature.java | 69 ++++++ .../resolution/ResolutionFeature.java | 118 ++++++++++ .../features/resolution/ResolutionPreset.java | 15 ++ .../DeviceOrientationManager.java | 214 ++++++++++++++++++ .../SensorOrientationFeature.java | 74 ++++++ .../fpsrange/FpsRangeFeatureTest.java | 92 ++++++++ .../resolution/ResolutionFeatureTest.java | 190 ++++++++++++++++ .../SensorOrientationFeatureTest.java | 126 +++++++++++ 9 files changed, 900 insertions(+), 2 deletions(-) create mode 100644 packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/fpsrange/FpsRangeFeature.java create mode 100644 packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/resolution/ResolutionFeature.java create mode 100644 packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/resolution/ResolutionPreset.java create mode 100644 packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/sensororientation/DeviceOrientationManager.java create mode 100644 packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/sensororientation/SensorOrientationFeature.java create mode 100644 packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/features/fpsrange/FpsRangeFeatureTest.java create mode 100644 packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/features/resolution/ResolutionFeatureTest.java 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/DartMessenger.java b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/DartMessenger.java index aaac1361eb3d..37bfbf294663 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 @@ -16,7 +16,7 @@ import java.util.HashMap; import java.util.Map; -class DartMessenger { +public class DartMessenger { @NonNull private final Handler handler; @Nullable private MethodChannel cameraChannel; @Nullable private MethodChannel deviceChannel; @@ -48,7 +48,7 @@ enum CameraEventType { this.handler = handler; } - 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/features/fpsrange/FpsRangeFeature.java b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/fpsrange/FpsRangeFeature.java new file mode 100644 index 000000000000..67bb85c70a00 --- /dev/null +++ b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/fpsrange/FpsRangeFeature.java @@ -0,0 +1,69 @@ +// Copyright 2013 The Flutter 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; +import android.util.Log; +import android.util.Range; +import io.flutter.plugins.camera.CameraProperties; +import io.flutter.plugins.camera.features.CameraFeature; + +public class FpsRangeFeature extends CameraFeature> { + private Range currentSetting; + + public FpsRangeFeature(CameraProperties cameraProperties) { + super(cameraProperties); + + Log.i("Camera", "getAvailableFpsRange"); + + try { + Range[] ranges = cameraProperties.getControlAutoExposureAvailableTargetFpsRanges(); + + if (ranges != null) { + for (Range range : ranges) { + int upper = range.getUpper(); + 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); + } + } + + @Override + public String getDebugName() { + return "FpsRangeFeature"; + } + + @Override + public Range getValue() { + return currentSetting; + } + + @Override + public void setValue(Range value) { + this.currentSetting = value; + } + + // Always supported + @Override + public boolean checkIsSupported() { + return true; + } + + @Override + public void updateBuilder(CaptureRequest.Builder requestBuilder) { + if (!checkIsSupported()) { + 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/resolution/ResolutionFeature.java b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/resolution/ResolutionFeature.java new file mode 100644 index 000000000000..621fd1e6fba4 --- /dev/null +++ b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/resolution/ResolutionFeature.java @@ -0,0 +1,118 @@ +// Copyright 2013 The Flutter 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; +import android.media.CamcorderProfile; +import android.util.Size; +import io.flutter.plugins.camera.CameraProperties; +import io.flutter.plugins.camera.features.CameraFeature; + +public class ResolutionFeature extends CameraFeature { + private final Size captureSize; + private final Size previewSize; + private final CamcorderProfile recordingProfile; + private ResolutionPreset currentSetting; + + public ResolutionFeature( + CameraProperties cameraProperties, ResolutionPreset initialSetting, String cameraName) { + super(cameraProperties); + setValue(initialSetting); + + // Resolution configuration + recordingProfile = + getBestAvailableCamcorderProfileForResolutionPreset(cameraName, initialSetting); + captureSize = new Size(recordingProfile.videoFrameWidth, recordingProfile.videoFrameHeight); + + previewSize = computeBestPreviewSize(cameraName, initialSetting); + } + + public 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 String getDebugName() { + return "ResolutionFeature"; + } + + @Override + public ResolutionPreset getValue() { + return currentSetting; + } + + @Override + public void setValue(ResolutionPreset value) { + this.currentSetting = value; + } + + // Always supported + @Override + public boolean checkIsSupported() { + return true; + } + + @Override + public void updateBuilder(CaptureRequest.Builder requestBuilder) { + // No-op: when setting a resolution there is no need to update the request builder. + } + + 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/features/resolution/ResolutionPreset.java b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/resolution/ResolutionPreset.java new file mode 100644 index 000000000000..359300305d40 --- /dev/null +++ b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/resolution/ResolutionPreset.java @@ -0,0 +1,15 @@ +// Copyright 2013 The Flutter 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; + +// Mirrors camera.dart +public enum ResolutionPreset { + low, + medium, + high, + veryHigh, + ultraHigh, + max, +} 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 new file mode 100644 index 000000000000..efd7cf54f2e5 --- /dev/null +++ b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/sensororientation/DeviceOrientationManager.java @@ -0,0 +1,214 @@ +// Copyright 2013 The Flutter 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.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; +import android.content.res.Configuration; +import android.hardware.SensorManager; +import android.provider.Settings; +import android.view.Display; +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; + +public class DeviceOrientationManager { + + private static final IntentFilter orientationIntentFilter = + new IntentFilter(Intent.ACTION_CONFIGURATION_CHANGED); + + private final Activity activity; + private final DartMessenger messenger; + private final boolean isFrontFacing; + private final int sensorOrientation; + private PlatformChannel.DeviceOrientation lastOrientation; + private OrientationEventListener orientationEventListener; + private BroadcastReceiver broadcastReceiver; + + /** Factory method to create a device orientation manager. */ + public static DeviceOrientationManager create( + @NonNull Activity activity, + @NonNull DartMessenger messenger, + boolean isFrontFacing, + int sensorOrientation) { + return new DeviceOrientationManager(activity, messenger, isFrontFacing, sensorOrientation); + } + + private DeviceOrientationManager( + @NonNull Activity activity, + @NonNull DartMessenger messenger, + boolean isFrontFacing, + int sensorOrientation) { + this.activity = activity; + this.messenger = messenger; + this.isFrontFacing = isFrontFacing; + this.sensorOrientation = sensorOrientation; + } + + public void start() { + startSensorListener(); + startUIListener(); + } + + public void stop() { + stopSensorListener(); + stopUIListener(); + } + + public int getMediaOrientation() { + return this.getMediaOrientation(this.lastOrientation); + } + + public int getMediaOrientation(PlatformChannel.DeviceOrientation orientation) { + int angle = 0; + + // Fallback to device orientation when the orientation value is null + if (orientation == null) { + orientation = getUIOrientation(); + } + + switch (orientation) { + case PORTRAIT_UP: + angle = 0; + break; + case PORTRAIT_DOWN: + angle = 180; + break; + case LANDSCAPE_LEFT: + angle = 90; + break; + case LANDSCAPE_RIGHT: + angle = 270; + break; + } + if (isFrontFacing) angle *= -1; + return (angle + sensorOrientation + 360) % 360; + } + + private void startSensorListener() { + if (orientationEventListener != null) return; + orientationEventListener = + new OrientationEventListener(activity, SensorManager.SENSOR_DELAY_NORMAL) { + @Override + public void onOrientationChanged(int angle) { + if (!isSystemAutoRotationLocked()) { + PlatformChannel.DeviceOrientation newOrientation = calculateSensorOrientation(angle); + if (!newOrientation.equals(lastOrientation)) { + lastOrientation = newOrientation; + messenger.sendDeviceOrientationChangeEvent(newOrientation); + } + } + } + }; + if (orientationEventListener.canDetectOrientation()) { + orientationEventListener.enable(); + } + } + + private void startUIListener() { + if (broadcastReceiver != null) return; + broadcastReceiver = + new BroadcastReceiver() { + @Override + public void onReceive(Context context, Intent intent) { + if (isSystemAutoRotationLocked()) { + PlatformChannel.DeviceOrientation orientation = getUIOrientation(); + if (!orientation.equals(lastOrientation)) { + lastOrientation = orientation; + messenger.sendDeviceOrientationChangeEvent(orientation); + } + } + } + }; + activity.registerReceiver(broadcastReceiver, orientationIntentFilter); + broadcastReceiver.onReceive(activity, null); + } + + private void stopSensorListener() { + if (orientationEventListener == null) return; + orientationEventListener.disable(); + orientationEventListener = null; + } + + private void stopUIListener() { + if (broadcastReceiver == null) return; + activity.unregisterReceiver(broadcastReceiver); + broadcastReceiver = null; + } + + private boolean isSystemAutoRotationLocked() { + return android.provider.Settings.System.getInt( + activity.getContentResolver(), Settings.System.ACCELEROMETER_ROTATION, 0) + != 1; + } + + private PlatformChannel.DeviceOrientation getUIOrientation() { + final int rotation = getDisplay().getRotation(); + final int orientation = activity.getResources().getConfiguration().orientation; + + switch (orientation) { + case Configuration.ORIENTATION_PORTRAIT: + if (rotation == Surface.ROTATION_0 || rotation == Surface.ROTATION_90) { + return PlatformChannel.DeviceOrientation.PORTRAIT_UP; + } else { + return PlatformChannel.DeviceOrientation.PORTRAIT_DOWN; + } + case Configuration.ORIENTATION_LANDSCAPE: + if (rotation == Surface.ROTATION_0 || rotation == Surface.ROTATION_90) { + return PlatformChannel.DeviceOrientation.LANDSCAPE_LEFT; + } else { + return PlatformChannel.DeviceOrientation.LANDSCAPE_RIGHT; + } + default: + return PlatformChannel.DeviceOrientation.PORTRAIT_UP; + } + } + + private PlatformChannel.DeviceOrientation calculateSensorOrientation(int angle) { + final int tolerance = 45; + angle += tolerance; + + // Orientation is 0 in the default orientation mode. This is portait-mode for phones + // and landscape for tablets. We have to compensate for this by calculating the default + // orientation, and apply an offset accordingly. + int defaultDeviceOrientation = getDeviceDefaultOrientation(); + if (defaultDeviceOrientation == Configuration.ORIENTATION_LANDSCAPE) { + angle += 90; + } + // Determine the orientation + angle = angle % 360; + return new PlatformChannel.DeviceOrientation[] { + PlatformChannel.DeviceOrientation.PORTRAIT_UP, + PlatformChannel.DeviceOrientation.LANDSCAPE_LEFT, + PlatformChannel.DeviceOrientation.PORTRAIT_DOWN, + PlatformChannel.DeviceOrientation.LANDSCAPE_RIGHT, + } + [angle / 90]; + } + + private int getDeviceDefaultOrientation() { + Configuration config = activity.getResources().getConfiguration(); + int rotation = getDisplay().getRotation(); + if (((rotation == Surface.ROTATION_0 || rotation == Surface.ROTATION_180) + && config.orientation == Configuration.ORIENTATION_LANDSCAPE) + || ((rotation == Surface.ROTATION_90 || rotation == Surface.ROTATION_270) + && config.orientation == Configuration.ORIENTATION_PORTRAIT)) { + return Configuration.ORIENTATION_LANDSCAPE; + } else { + return Configuration.ORIENTATION_PORTRAIT; + } + } + + @SuppressWarnings("deprecation") + private Display getDisplay() { + return ((WindowManager) activity.getSystemService(Context.WINDOW_SERVICE)).getDefaultDisplay(); + } +} 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 new file mode 100644 index 000000000000..091d8405e2ff --- /dev/null +++ b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/sensororientation/SensorOrientationFeature.java @@ -0,0 +1,74 @@ +// Copyright 2013 The Flutter 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 SensorOrientationFeature extends CameraFeature { + private Integer currentSetting = 0; + private final DeviceOrientationManager deviceOrientationListener; + private PlatformChannel.DeviceOrientation lockedCaptureOrientation; + + 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(); + } + + @Override + public String getDebugName() { + return "SensorOrientationFeature"; + } + + @Override + public Integer getValue() { + return currentSetting; + } + + @Override + public void setValue(Integer value) { + this.currentSetting = value; + } + + @Override + public boolean checkIsSupported() { + return true; + } + + @Override + public void updateBuilder(CaptureRequest.Builder requestBuilder) { + // Noop: when setting the sensor orientation there is no need to update the request builder. + } + + public DeviceOrientationManager getDeviceOrientationManager() { + return this.deviceOrientationListener; + } + + public void lockCaptureOrientation(PlatformChannel.DeviceOrientation orientation) { + this.lockedCaptureOrientation = orientation; + } + + public void unlockCaptureOrientation() { + this.lockedCaptureOrientation = null; + } + + public PlatformChannel.DeviceOrientation getLockedCaptureOrientation() { + return this.lockedCaptureOrientation; + } +} 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..a74e42afd957 --- /dev/null +++ b/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/features/fpsrange/FpsRangeFeatureTest.java @@ -0,0 +1,92 @@ +// Copyright 2013 The Flutter 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; +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); + } +} 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..f777cb5843c2 --- /dev/null +++ b/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/features/resolution/ResolutionFeatureTest.java @@ -0,0 +1,190 @@ +// Copyright 2013 The Flutter 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 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)); + } +} 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..ce2bb7bb2670 --- /dev/null +++ b/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/features/sensororientation/SensorOrientationFeatureTest.java @@ -0,0 +1,126 @@ +// Copyright 2013 The Flutter 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 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 1ba738d9469b8c6bbcfe5aefb97343f06b3b6021 Mon Sep 17 00:00:00 2001 From: Maurits van Beusekom Date: Fri, 9 Apr 2021 13:59:51 +0200 Subject: [PATCH 05/63] Use mockito-inline --- packages/camera/camera/android/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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' } From 728346afae57e6ec10374146d10c2710d70b5a06 Mon Sep 17 00:00:00 2001 From: Maurits van Beusekom Date: Wed, 26 May 2021 15:22:28 +0200 Subject: [PATCH 06/63] Fix issue Pixel 4A --- .../features/fpsrange/FpsRangeFeature.java | 29 ++++++++++--------- 1 file changed, 15 insertions(+), 14 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 67bb85c70a00..4ec508607093 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 @@ -17,24 +17,25 @@ public FpsRangeFeature(CameraProperties cameraProperties) { super(cameraProperties); Log.i("Camera", "getAvailableFpsRange"); + + Range[] ranges = cameraProperties.getControlAutoExposureAvailableTargetFpsRanges(); - try { - Range[] ranges = cameraProperties.getControlAutoExposureAvailableTargetFpsRanges(); - - if (ranges != null) { - for (Range range : ranges) { - int upper = range.getUpper(); - if (upper >= 10) { - if (currentSetting == null || upper > currentSetting.getUpper()) { - currentSetting = range; - } + if (ranges != null) { + for (Range range : ranges) { + int upper = range.getUpper(); + + // 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; } } } - } catch (Exception e) { - // TODO: maybe just send a dart error back - // pictureCaptureRequest.error("cameraAccess", e.getMessage(), null); - } + } } @Override From 84f5e73e635d3a83210a46190321a1bc81c2f0ef Mon Sep 17 00:00:00 2001 From: Maurits van Beusekom Date: Mon, 31 May 2021 10:56:53 +0200 Subject: [PATCH 07/63] Added API documentation --- .../features/fpsrange/FpsRangeFeature.java | 12 ++- .../resolution/ResolutionFeature.java | 77 +++++++++++++++---- .../DeviceOrientationManager.java | 45 +++++++++++ .../SensorOrientationFeature.java | 30 ++++++++ .../resolution/ResolutionFeatureTest.java | 16 ++-- 5 files changed, 153 insertions(+), 27 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 4ec508607093..c78839bb68c7 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 @@ -5,19 +5,25 @@ 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; +/** + * Controls the frames per seconds (FPS) range configuration on the {@link android.hardware.camera2} + * API. + */ public class FpsRangeFeature extends CameraFeature> { private Range currentSetting; + /** + * Creates a new instance of the {@link FpsRangeFeature}. + * + * @param cameraProperties Collection of characteristics for the current camera device. + */ public FpsRangeFeature(CameraProperties cameraProperties) { super(cameraProperties); - Log.i("Camera", "getAvailableFpsRange"); - Range[] ranges = cameraProperties.getControlAutoExposureAvailableTargetFpsRanges(); if (ranges != null) { 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 621fd1e6fba4..3ef6cd34742f 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 @@ -7,31 +7,50 @@ import android.hardware.camera2.CaptureRequest; import android.media.CamcorderProfile; import android.util.Size; +import androidx.annotation.VisibleForTesting; import io.flutter.plugins.camera.CameraProperties; import io.flutter.plugins.camera.features.CameraFeature; +/** + * Controls the resolutions configuration on the {@link android.hardware.camera2} API. + * + * The {@link ResolutionFeature} is responsible for converting the platform independent + * {@link ResolutionPreset} into a {@link android.media.CamcorderProfile} which contains all the + * properties required to configure the resolution using the {@link android.hardware.camera2} API. + */ public class ResolutionFeature extends CameraFeature { - private final Size captureSize; - private final Size previewSize; - private final CamcorderProfile recordingProfile; + private Size captureSize; + private Size previewSize; + private CamcorderProfile recordingProfile; private ResolutionPreset currentSetting; - + private int cameraId; + + /** + * Creates a new instance of the {@link ResolutionFeature}. + * + * @param cameraProperties Collection of characteristics for the current camera device. + * @param resolutionPreset Platform agnostic enum containing resolution information. + * @param cameraId Camera identifier of the camera for which to configure the resolution. + */ public ResolutionFeature( - CameraProperties cameraProperties, ResolutionPreset initialSetting, String cameraName) { + CameraProperties cameraProperties, ResolutionPreset resolutionPreset, int cameraId) { super(cameraProperties); - setValue(initialSetting); + this.currentSetting = resolutionPreset; + this.cameraId = cameraId; - // Resolution configuration - recordingProfile = - getBestAvailableCamcorderProfileForResolutionPreset(cameraName, initialSetting); - captureSize = new Size(recordingProfile.videoFrameWidth, recordingProfile.videoFrameHeight); - - previewSize = computeBestPreviewSize(cameraName, initialSetting); + configureResolution(resolutionPreset, cameraId); } + /** + * Gets the best possible {@link android.media.CamcorderProfile} for the supplied {@link ResolutionPreset}. + * + * @param cameraId Camera identifier which indicates the device's camera for which to select a {@link android.media.CamcorderProfile}. + * @param preset The {@link ResolutionPreset} for which is to be translated to a {@link android.media.CamcorderProfile}. + * @return The best possible {@link android.media.CamcorderProfile} that matches the supplied {@link ResolutionPreset}. + */ public static CamcorderProfile getBestAvailableCamcorderProfileForResolutionPreset( - String cameraName, ResolutionPreset preset) { - int cameraId = Integer.parseInt(cameraName); + int cameraId, ResolutionPreset preset) { + switch (preset) { // All of these cases deliberately fall through to get the best available profile. case max: @@ -68,16 +87,25 @@ public static CamcorderProfile getBestAvailableCamcorderProfileForResolutionPres } } - static Size computeBestPreviewSize(String cameraName, ResolutionPreset preset) { + @VisibleForTesting + static Size computeBestPreviewSize(int cameraId, ResolutionPreset preset) { if (preset.ordinal() > ResolutionPreset.high.ordinal()) { preset = ResolutionPreset.high; } CamcorderProfile profile = - getBestAvailableCamcorderProfileForResolutionPreset(cameraName, preset); + getBestAvailableCamcorderProfileForResolutionPreset(cameraId, preset); return new Size(profile.videoFrameWidth, profile.videoFrameHeight); } + private void configureResolution(ResolutionPreset resolutionPreset, int cameraId) { + recordingProfile = + getBestAvailableCamcorderProfileForResolutionPreset(cameraId, resolutionPreset); + captureSize = new Size(recordingProfile.videoFrameWidth, recordingProfile.videoFrameHeight); + + previewSize = computeBestPreviewSize(cameraId, resolutionPreset); + } + @Override public String getDebugName() { return "ResolutionFeature"; @@ -91,6 +119,7 @@ public ResolutionPreset getValue() { @Override public void setValue(ResolutionPreset value) { this.currentSetting = value; + configureResolution(currentSetting, cameraId); } // Always supported @@ -104,14 +133,30 @@ public void updateBuilder(CaptureRequest.Builder requestBuilder) { // No-op: when setting a resolution there is no need to update the request builder. } + /** + * Gets the {@link android.media.CamcorderProfile} containing the information to configure the + * resolution using the {@link android.hardware.camera2} API. + * + * @return Resolution information to configure the {@link android.hardware.camera2} API. + */ public CamcorderProfile getRecordingProfile() { return this.recordingProfile; } + /** + * Gets the optimal preview size based on the configured resolution. + * + * @return The optimal preview size. + */ public Size getPreviewSize() { return this.previewSize; } + /** + * Get the optimal capture size based on the configured resolution. + * + * @return The optimal capture size. + */ public Size getCaptureSize() { return this.captureSize; } 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 efd7cf54f2e5..ee54529673d7 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 @@ -20,6 +20,10 @@ import io.flutter.embedding.engine.systemchannels.PlatformChannel; import io.flutter.plugins.camera.DartMessenger; +/** + * Support class to help to determine the media orientation based on the orientation of the + * device. + */ public class DeviceOrientationManager { private static final IntentFilter orientationIntentFilter = @@ -53,20 +57,61 @@ private DeviceOrientationManager( this.sensorOrientation = sensorOrientation; } + /** + * Starts listening to the device's sensors and UI for orientation updates. + * + * When either the sensor or UI listeners indicate the orientation has changed the updated + * orientation is send to the client using the {@link DartMessenger}. + */ public void start() { startSensorListener(); startUIListener(); } + /** + * Stops listening for orientation updates. + */ public void stop() { stopSensorListener(); stopUIListener(); } + /** + * Returns the last captured orientation in degrees based on sensor or UI information. + * + * The orientation is returned in degrees and could be one of the following values: + *

+ *

    + *
  • 0: Indicates the device is currently in portrait.
  • + *
  • 90: Indicates the device is currently in landscape left.
  • + *
  • 180: Indicates the device is currently in portrait down.
  • + *
  • 270: Indicates the device is currently in landscape right.
  • + *
+ *

+ * + * @return The last captured orientation in degrees + */ public int getMediaOrientation() { return this.getMediaOrientation(this.lastOrientation); } + /** + * Returns the device's orientation in degrees based on the supplied {@link + * PlatformChannel.DeviceOrientation} value. + * + *

+ * + *

    + *
  • PORTRAIT_UP: converts to 0 degrees.
  • + *
  • LANDSCAPE_LEFT: converts to 90 degrees.
  • + *
  • PORTRAIT_DOWN: converts to 180 degrees.
  • + *
  • LANDSCAPE_RIGHT: converts to 270 degrees.
  • + *
+ * + * @param orientation The {@link PlatformChannel.DeviceOrientation} value that is to be converted + * into degrees. + * @return The device's orientation in degrees. + */ public int getMediaOrientation(PlatformChannel.DeviceOrientation orientation) { int angle = 0; 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 091d8405e2ff..7c62037a3b59 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 @@ -12,12 +12,23 @@ import io.flutter.plugins.camera.CameraProperties; import io.flutter.plugins.camera.DartMessenger; import io.flutter.plugins.camera.features.CameraFeature; +import io.flutter.plugins.camera.features.resolution.ResolutionFeature; +/** Provides access to the sensor orientation of the camera devices. */ public class SensorOrientationFeature extends CameraFeature { private Integer currentSetting = 0; private final DeviceOrientationManager deviceOrientationListener; private PlatformChannel.DeviceOrientation lockedCaptureOrientation; + /** + * Creates a new instance of the {@link ResolutionFeature}. + * + * @param cameraProperties Collection of characteristics for the current camera device. + * @param activity Current Android {@link android.app.Activity}, used to detect UI orientation + * changes. + * @param dartMessenger Instance of a {@link DartMessenger} used to communicate orientation + * updates back to the client. + */ public SensorOrientationFeature( @NonNull CameraProperties cameraProperties, @NonNull Activity activity, @@ -56,18 +67,37 @@ public void updateBuilder(CaptureRequest.Builder requestBuilder) { // Noop: when setting the sensor orientation there is no need to update the request builder. } + /** + * Gets the instance of the {@link DeviceOrientationManager} used to detect orientation changes. + * @return The instance of the {@link DeviceOrientationManager}. + */ public DeviceOrientationManager getDeviceOrientationManager() { return this.deviceOrientationListener; } + /** + * Lock the capture orientation, indicating that the device orientation should not influence the + * capture orientation. + * + * @param orientation The orientation in which to lock the capture orientation. + */ public void lockCaptureOrientation(PlatformChannel.DeviceOrientation orientation) { this.lockedCaptureOrientation = orientation; } + /** + * Unlock the capture orientation, indicating that the device orientation should be used to + * configure the capture orientation. + */ public void unlockCaptureOrientation() { this.lockedCaptureOrientation = null; } + /** + * Gets the configured locked capture orientation. + * + * @return The configured locked capture orientation. + */ public PlatformChannel.DeviceOrientation getLockedCaptureOrientation() { return this.lockedCaptureOrientation; } 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 index f777cb5843c2..716219246a27 100644 --- 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 @@ -17,7 +17,7 @@ import org.mockito.MockedStatic; public class ResolutionFeatureTest { - private static final String cameraId = "1"; + private static final int cameraId = 1; private CamcorderProfile mockProfileLow; private MockedStatic mockedStaticProfile; @@ -143,47 +143,47 @@ public void getBestAvailableCamcorderProfileForResolutionPreset_should_fall_thro assertEquals( mockProfileLow, ResolutionFeature.getBestAvailableCamcorderProfileForResolutionPreset( - "1", ResolutionPreset.max)); + 1, ResolutionPreset.max)); } @Test public void computeBestPreviewSize_should_use_720P_when_resolution_preset_max() { - ResolutionFeature.computeBestPreviewSize("1", ResolutionPreset.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); + 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); + 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); + 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); + 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); + ResolutionFeature.computeBestPreviewSize(1, ResolutionPreset.low); mockedStaticProfile.verify(() -> CamcorderProfile.get(1, CamcorderProfile.QUALITY_QVGA)); } From f763f771edd970720a413d0929f5aabab9018bd9 Mon Sep 17 00:00:00 2001 From: Maurits van Beusekom Date: Mon, 31 May 2021 16:40:08 +0200 Subject: [PATCH 08/63] Processed feedback on PR --- .../features/fpsrange/FpsRangeFeature.java | 7 +- .../resolution/ResolutionFeature.java | 18 +- .../DeviceOrientationManager.java | 110 ++++++--- .../SensorOrientationFeature.java | 5 +- .../DeviceOrientationManagerTest.java | 210 ++++++++++++++++++ 5 files changed, 307 insertions(+), 43 deletions(-) create mode 100644 packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/features/sensororientation/DeviceOrientationManagerTest.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 c78839bb68c7..1c834990c11b 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 @@ -29,9 +29,10 @@ public FpsRangeFeature(CameraProperties cameraProperties) { if (ranges != null) { for (Range range : ranges) { int upper = range.getUpper(); - + // There is a bug in the Pixel 4A where it cannot support 60fps modes - // even though they are reported as supported by `getControlAutoExposureAvailableTargetFpsRanges`. + // 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 @@ -41,7 +42,7 @@ public FpsRangeFeature(CameraProperties cameraProperties) { } } } - } + } } @Override 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 3ef6cd34742f..d5a0ad6d2374 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 @@ -14,9 +14,9 @@ /** * Controls the resolutions configuration on the {@link android.hardware.camera2} API. * - * The {@link ResolutionFeature} is responsible for converting the platform independent - * {@link ResolutionPreset} into a {@link android.media.CamcorderProfile} which contains all the - * properties required to configure the resolution using the {@link android.hardware.camera2} API. + *

The {@link ResolutionFeature} is responsible for converting the platform independent {@link + * ResolutionPreset} into a {@link android.media.CamcorderProfile} which contains all the properties + * required to configure the resolution using the {@link android.hardware.camera2} API. */ public class ResolutionFeature extends CameraFeature { private Size captureSize; @@ -42,11 +42,15 @@ public ResolutionFeature( } /** - * Gets the best possible {@link android.media.CamcorderProfile} for the supplied {@link ResolutionPreset}. + * Gets the best possible {@link android.media.CamcorderProfile} for the supplied {@link + * ResolutionPreset}. * - * @param cameraId Camera identifier which indicates the device's camera for which to select a {@link android.media.CamcorderProfile}. - * @param preset The {@link ResolutionPreset} for which is to be translated to a {@link android.media.CamcorderProfile}. - * @return The best possible {@link android.media.CamcorderProfile} that matches the supplied {@link ResolutionPreset}. + * @param cameraId Camera identifier which indicates the device's camera for which to select a + * {@link android.media.CamcorderProfile}. + * @param preset The {@link ResolutionPreset} for which is to be translated to a {@link + * android.media.CamcorderProfile}. + * @return The best possible {@link android.media.CamcorderProfile} that matches the supplied + * {@link ResolutionPreset}. */ public static CamcorderProfile getBestAvailableCamcorderProfileForResolutionPreset( int cameraId, ResolutionPreset preset) { 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 ee54529673d7..90608fbd91fb 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 @@ -17,12 +17,12 @@ import android.view.Surface; import android.view.WindowManager; import androidx.annotation.NonNull; +import androidx.annotation.VisibleForTesting; import io.flutter.embedding.engine.systemchannels.PlatformChannel; import io.flutter.plugins.camera.DartMessenger; /** - * Support class to help to determine the media orientation based on the orientation of the - * device. + * Support class to help to determine the media orientation based on the orientation of the device. */ public class DeviceOrientationManager { @@ -58,19 +58,23 @@ private DeviceOrientationManager( } /** - * Starts listening to the device's sensors and UI for orientation updates. + * Starts listening to the device's sensors or UI for orientation updates. * - * When either the sensor or UI listeners indicate the orientation has changed the updated - * orientation is send to the client using the {@link DartMessenger}. + *

When orientation information is updated the new orientation is send to the client using the + * {@link DartMessenger}. This latest value can also be retrieved through the {@link + * #getMediaOrientation()} accessor. + * + *

If the device's ACCELEROMETER_ROTATION setting is enabled the {@link + * DeviceOrientationManager} will report orientation updates based on the sensor information. If + * the ACCELEROMETER_ROTATION is disabled the {@link DeviceOrientationManager} will fallback to + * the deliver orientation updates based on the UI orientation. */ public void start() { startSensorListener(); startUIListener(); } - /** - * Stops listening for orientation updates. - */ + /** Stops listening for orientation updates. */ public void stop() { stopSensorListener(); stopUIListener(); @@ -79,15 +83,14 @@ public void stop() { /** * Returns the last captured orientation in degrees based on sensor or UI information. * - * The orientation is returned in degrees and could be one of the following values: - *

- *

    - *
  • 0: Indicates the device is currently in portrait.
  • - *
  • 90: Indicates the device is currently in landscape left.
  • - *
  • 180: Indicates the device is currently in portrait down.
  • - *
  • 270: Indicates the device is currently in landscape right.
  • - *
- *

+ *

The orientation is returned in degrees and could be one of the following values: + * + *

    + *
  • 0: Indicates the device is currently in portrait. + *
  • 90: Indicates the device is currently in landscape left. + *
  • 180: Indicates the device is currently in portrait down. + *
  • 270: Indicates the device is currently in landscape right. + *
* * @return The last captured orientation in degrees */ @@ -102,10 +105,10 @@ public int getMediaOrientation() { *

* *

    - *
  • PORTRAIT_UP: converts to 0 degrees.
  • - *
  • LANDSCAPE_LEFT: converts to 90 degrees.
  • - *
  • PORTRAIT_DOWN: converts to 180 degrees.
  • - *
  • LANDSCAPE_RIGHT: converts to 270 degrees.
  • + *
  • PORTRAIT_UP: converts to 0 degrees. + *
  • LANDSCAPE_LEFT: converts to 90 degrees. + *
  • PORTRAIT_DOWN: converts to 180 degrees. + *
  • LANDSCAPE_RIGHT: converts to 270 degrees. *
* * @param orientation The {@link PlatformChannel.DeviceOrientation} value that is to be converted @@ -134,12 +137,18 @@ public int getMediaOrientation(PlatformChannel.DeviceOrientation orientation) { angle = 270; break; } - if (isFrontFacing) angle *= -1; + + if (isFrontFacing) { + angle *= -1; + } + return (angle + sensorOrientation + 360) % 360; } private void startSensorListener() { - if (orientationEventListener != null) return; + if (orientationEventListener != null) { + return; + } orientationEventListener = new OrientationEventListener(activity, SensorManager.SENSOR_DELAY_NORMAL) { @Override @@ -159,7 +168,9 @@ public void onOrientationChanged(int angle) { } private void startUIListener() { - if (broadcastReceiver != null) return; + if (broadcastReceiver != null) { + return; + } broadcastReceiver = new BroadcastReceiver() { @Override @@ -178,13 +189,17 @@ public void onReceive(Context context, Intent intent) { } private void stopSensorListener() { - if (orientationEventListener == null) return; + if (orientationEventListener == null) { + return; + } orientationEventListener.disable(); orientationEventListener = null; } private void stopUIListener() { - if (broadcastReceiver == null) return; + if (broadcastReceiver == null) { + return; + } activity.unregisterReceiver(broadcastReceiver); broadcastReceiver = null; } @@ -195,7 +210,15 @@ private boolean isSystemAutoRotationLocked() { != 1; } - private PlatformChannel.DeviceOrientation getUIOrientation() { + /** + * Gets the current user interface orientation. + * + * This method is visible for testing purposes only and should never be used outside this class. + * + * @return The current user interface orientation. + */ + @VisibleForTesting + PlatformChannel.DeviceOrientation getUIOrientation() { final int rotation = getDisplay().getRotation(); final int orientation = activity.getResources().getConfiguration().orientation; @@ -217,11 +240,20 @@ private PlatformChannel.DeviceOrientation getUIOrientation() { } } - private PlatformChannel.DeviceOrientation calculateSensorOrientation(int angle) { + /** + * Calculates the sensor orientation based on the supplied angle. + * + * This method is visible for testing purposes only and should never be used outside this class. + * + * @param angle Orientation angle. + * @return The sensor orientation based on the supplied angle. + */ + @VisibleForTesting + PlatformChannel.DeviceOrientation calculateSensorOrientation(int angle) { final int tolerance = 45; angle += tolerance; - // Orientation is 0 in the default orientation mode. This is portait-mode for phones + // Orientation is 0 in the default orientation mode. This is portrait-mode for phones // and landscape for tablets. We have to compensate for this by calculating the default // orientation, and apply an offset accordingly. int defaultDeviceOrientation = getDeviceDefaultOrientation(); @@ -239,7 +271,15 @@ private PlatformChannel.DeviceOrientation calculateSensorOrientation(int angle) [angle / 90]; } - private int getDeviceDefaultOrientation() { + /** + * Gets the default orientation of the device. + * + * This method is visible for testing purposes only and should never be used outside this class. + * + * @return The default orientation of the device. + */ + @VisibleForTesting + int getDeviceDefaultOrientation() { Configuration config = activity.getResources().getConfiguration(); int rotation = getDisplay().getRotation(); if (((rotation == Surface.ROTATION_0 || rotation == Surface.ROTATION_180) @@ -252,8 +292,16 @@ private int getDeviceDefaultOrientation() { } } + /** + * Gets an instance of the Android {@link android.view.Display}. + * + * This method is visible for testing purposes only and should never be used outside this class. + * + * @return An instance of the Android {@link android.view.Display}. + */ @SuppressWarnings("deprecation") - private Display getDisplay() { + @VisibleForTesting + Display getDisplay() { return ((WindowManager) activity.getSystemService(Context.WINDOW_SERVICE)).getDefaultDisplay(); } } 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 7c62037a3b59..9e316f741805 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 @@ -25,9 +25,9 @@ public class SensorOrientationFeature extends CameraFeature { * * @param cameraProperties Collection of characteristics for the current camera device. * @param activity Current Android {@link android.app.Activity}, used to detect UI orientation - * changes. + * changes. * @param dartMessenger Instance of a {@link DartMessenger} used to communicate orientation - * updates back to the client. + * updates back to the client. */ public SensorOrientationFeature( @NonNull CameraProperties cameraProperties, @@ -69,6 +69,7 @@ public void updateBuilder(CaptureRequest.Builder requestBuilder) { /** * Gets the instance of the {@link DeviceOrientationManager} used to detect orientation changes. + * * @return The instance of the {@link DeviceOrientationManager}. */ public DeviceOrientationManager getDeviceOrientationManager() { diff --git a/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/features/sensororientation/DeviceOrientationManagerTest.java b/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/features/sensororientation/DeviceOrientationManagerTest.java new file mode 100644 index 000000000000..061313f9a3f2 --- /dev/null +++ b/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/features/sensororientation/DeviceOrientationManagerTest.java @@ -0,0 +1,210 @@ +// Copyright 2013 The Flutter 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 static org.junit.Assert.assertEquals; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import android.app.Activity; +import android.content.Context; +import android.content.res.Configuration; +import android.content.res.Resources; +import android.view.Display; +import android.view.Surface; +import android.view.WindowManager; +import io.flutter.embedding.engine.systemchannels.PlatformChannel.DeviceOrientation; +import io.flutter.plugins.camera.DartMessenger; +import org.junit.Before; +import org.junit.Test; + +public class DeviceOrientationManagerTest { + private Activity mockActivity; + private DartMessenger mockDartMessenger; + private WindowManager mockWindowManager; + private Display mockDisplay; + private DeviceOrientationManager deviceOrientationManager; + + @Before + public void before() { + mockActivity = mock(Activity.class); + mockDartMessenger = mock(DartMessenger.class); + mockDisplay = mock(Display.class); + mockWindowManager = mock(WindowManager.class); + + when(mockActivity.getSystemService(Context.WINDOW_SERVICE)).thenReturn(mockWindowManager); + when(mockWindowManager.getDefaultDisplay()).thenReturn(mockDisplay); + + deviceOrientationManager = + DeviceOrientationManager.create(mockActivity, mockDartMessenger, false, 0); + } + + @Test + public void getMediaOrientation_when_natural_screen_orientation_equals_portrait_up() { + int degreesPortraitUp = + deviceOrientationManager.getMediaOrientation(DeviceOrientation.PORTRAIT_UP); + int degreesPortraitDown = + deviceOrientationManager.getMediaOrientation(DeviceOrientation.PORTRAIT_DOWN); + int degreesLandscapeLeft = + deviceOrientationManager.getMediaOrientation(DeviceOrientation.LANDSCAPE_LEFT); + int degreesLandscapeRight = + deviceOrientationManager.getMediaOrientation(DeviceOrientation.LANDSCAPE_RIGHT); + + assertEquals(0, degreesPortraitUp); + assertEquals(90, degreesLandscapeLeft); + assertEquals(180, degreesPortraitDown); + assertEquals(270, degreesLandscapeRight); + } + + @Test + public void getMediaOrientation_when_natural_screen_orientation_equals_landscape_left() { + DeviceOrientationManager orientationManager = + DeviceOrientationManager.create(mockActivity, mockDartMessenger, false, 90); + + int degreesPortraitUp = + orientationManager.getMediaOrientation(DeviceOrientation.PORTRAIT_UP); + int degreesPortraitDown = + orientationManager.getMediaOrientation(DeviceOrientation.PORTRAIT_DOWN); + int degreesLandscapeLeft = + orientationManager.getMediaOrientation(DeviceOrientation.LANDSCAPE_LEFT); + int degreesLandscapeRight = + orientationManager.getMediaOrientation(DeviceOrientation.LANDSCAPE_RIGHT); + + assertEquals(90, degreesPortraitUp); + assertEquals(180, degreesLandscapeLeft); + assertEquals(270, degreesPortraitDown); + assertEquals(0, degreesLandscapeRight); + } + + @Test + public void getMediaOrientation_should_fallback_to_sensor_orientation_when_orientation_is_null() { + setUpUIOrientationMocks(Configuration.ORIENTATION_LANDSCAPE, Surface.ROTATION_0); + + int degrees = + deviceOrientationManager.getMediaOrientation(null); + + assertEquals(90, degrees); + } + + @Test + public void getUIOrientation() { + // Orientation portrait and rotation of 0 should translate to "PORTRAIT_UP". + setUpUIOrientationMocks(Configuration.ORIENTATION_PORTRAIT, Surface.ROTATION_0); + DeviceOrientation uiOrientation = deviceOrientationManager.getUIOrientation(); + assertEquals(DeviceOrientation.PORTRAIT_UP, uiOrientation); + + // Orientation portrait and rotation of 90 should translate to "PORTRAIT_UP". + setUpUIOrientationMocks(Configuration.ORIENTATION_PORTRAIT, Surface.ROTATION_90); + uiOrientation = deviceOrientationManager.getUIOrientation(); + assertEquals(DeviceOrientation.PORTRAIT_UP, uiOrientation); + + // Orientation portrait and rotation of 180 should translate to "PORTRAIT_DOWN". + setUpUIOrientationMocks(Configuration.ORIENTATION_PORTRAIT, Surface.ROTATION_180); + uiOrientation = deviceOrientationManager.getUIOrientation(); + assertEquals(DeviceOrientation.PORTRAIT_DOWN, uiOrientation); + + // Orientation portrait and rotation of 270 should translate to "PORTRAIT_DOWN". + setUpUIOrientationMocks(Configuration.ORIENTATION_PORTRAIT, Surface.ROTATION_270); + uiOrientation = deviceOrientationManager.getUIOrientation(); + assertEquals(DeviceOrientation.PORTRAIT_DOWN, uiOrientation); + + // Orientation landscape and rotation of 0 should translate to "LANDSCAPE_LEFT". + setUpUIOrientationMocks(Configuration.ORIENTATION_LANDSCAPE, Surface.ROTATION_0); + uiOrientation = deviceOrientationManager.getUIOrientation(); + assertEquals(DeviceOrientation.LANDSCAPE_LEFT, uiOrientation); + + // Orientation landscape and rotation of 90 should translate to "LANDSCAPE_LEFT". + setUpUIOrientationMocks(Configuration.ORIENTATION_LANDSCAPE, Surface.ROTATION_90); + uiOrientation = deviceOrientationManager.getUIOrientation(); + assertEquals(DeviceOrientation.LANDSCAPE_LEFT, uiOrientation); + + // Orientation landscape and rotation of 180 should translate to "LANDSCAPE_RIGHT". + setUpUIOrientationMocks(Configuration.ORIENTATION_LANDSCAPE, Surface.ROTATION_180); + uiOrientation = deviceOrientationManager.getUIOrientation(); + assertEquals(DeviceOrientation.LANDSCAPE_RIGHT, uiOrientation); + + // Orientation landscape and rotation of 270 should translate to "LANDSCAPE_RIGHT". + setUpUIOrientationMocks(Configuration.ORIENTATION_LANDSCAPE, Surface.ROTATION_270); + uiOrientation = deviceOrientationManager.getUIOrientation(); + assertEquals(DeviceOrientation.LANDSCAPE_RIGHT, uiOrientation); + + // Orientation undefined should default to "PORTRAIT_UP". + setUpUIOrientationMocks(Configuration.ORIENTATION_UNDEFINED, Surface.ROTATION_0); + uiOrientation = deviceOrientationManager.getUIOrientation(); + assertEquals(DeviceOrientation.PORTRAIT_UP, uiOrientation); + } + + @Test + public void getDeviceDefaultOrientation() { + setUpUIOrientationMocks(Configuration.ORIENTATION_PORTRAIT, Surface.ROTATION_0); + int orientation = deviceOrientationManager.getDeviceDefaultOrientation(); + assertEquals(Configuration.ORIENTATION_PORTRAIT, orientation); + + setUpUIOrientationMocks(Configuration.ORIENTATION_PORTRAIT, Surface.ROTATION_180); + orientation = deviceOrientationManager.getDeviceDefaultOrientation(); + assertEquals(Configuration.ORIENTATION_PORTRAIT, orientation); + + setUpUIOrientationMocks(Configuration.ORIENTATION_PORTRAIT, Surface.ROTATION_90); + orientation = deviceOrientationManager.getDeviceDefaultOrientation(); + assertEquals(Configuration.ORIENTATION_LANDSCAPE, orientation); + + setUpUIOrientationMocks(Configuration.ORIENTATION_PORTRAIT, Surface.ROTATION_270); + orientation = deviceOrientationManager.getDeviceDefaultOrientation(); + assertEquals(Configuration.ORIENTATION_LANDSCAPE, orientation); + + setUpUIOrientationMocks(Configuration.ORIENTATION_LANDSCAPE, Surface.ROTATION_0); + orientation = deviceOrientationManager.getDeviceDefaultOrientation(); + assertEquals(Configuration.ORIENTATION_LANDSCAPE, orientation); + + setUpUIOrientationMocks(Configuration.ORIENTATION_LANDSCAPE, Surface.ROTATION_180); + orientation = deviceOrientationManager.getDeviceDefaultOrientation(); + assertEquals(Configuration.ORIENTATION_LANDSCAPE, orientation); + + setUpUIOrientationMocks(Configuration.ORIENTATION_LANDSCAPE, Surface.ROTATION_90); + orientation = deviceOrientationManager.getDeviceDefaultOrientation(); + assertEquals(Configuration.ORIENTATION_PORTRAIT, orientation); + + setUpUIOrientationMocks(Configuration.ORIENTATION_LANDSCAPE, Surface.ROTATION_270); + orientation = deviceOrientationManager.getDeviceDefaultOrientation(); + assertEquals(Configuration.ORIENTATION_PORTRAIT, orientation); + } + + @Test + public void calculateSensorOrientation() { + setUpUIOrientationMocks(Configuration.ORIENTATION_PORTRAIT, Surface.ROTATION_0); + DeviceOrientation orientation = deviceOrientationManager.calculateSensorOrientation(0); + assertEquals(DeviceOrientation.PORTRAIT_UP, orientation); + + setUpUIOrientationMocks(Configuration.ORIENTATION_PORTRAIT, Surface.ROTATION_0); + orientation = deviceOrientationManager.calculateSensorOrientation(90); + assertEquals(DeviceOrientation.LANDSCAPE_LEFT, orientation); + + setUpUIOrientationMocks(Configuration.ORIENTATION_PORTRAIT, Surface.ROTATION_0); + orientation = deviceOrientationManager.calculateSensorOrientation(180); + assertEquals(DeviceOrientation.PORTRAIT_DOWN, orientation); + + setUpUIOrientationMocks(Configuration.ORIENTATION_PORTRAIT, Surface.ROTATION_0); + orientation = deviceOrientationManager.calculateSensorOrientation(270); + assertEquals(DeviceOrientation.LANDSCAPE_RIGHT, orientation); + } + + private void setUpUIOrientationMocks(int orientation, int rotation) { + Resources mockResources = mock(Resources.class); + Configuration mockConfiguration = mock(Configuration.class); + + when(mockDisplay.getRotation()).thenReturn(rotation); + + mockConfiguration.orientation = orientation; + when(mockActivity.getResources()).thenReturn(mockResources); + when(mockResources.getConfiguration()).thenReturn(mockConfiguration); + } + + @Test + public void getDisplayTest() { + Display display = deviceOrientationManager.getDisplay(); + + assertEquals(mockDisplay, display); + } +} From 4a7c73ad1ea21476124d2a2919831f2f5fb9bad0 Mon Sep 17 00:00:00 2001 From: Maurits van Beusekom Date: Mon, 31 May 2021 16:40:34 +0200 Subject: [PATCH 09/63] Fix formatting --- .../sensororientation/DeviceOrientationManagerTest.java | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/features/sensororientation/DeviceOrientationManagerTest.java b/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/features/sensororientation/DeviceOrientationManagerTest.java index 061313f9a3f2..5dc1c63e15d3 100644 --- a/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/features/sensororientation/DeviceOrientationManagerTest.java +++ b/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/features/sensororientation/DeviceOrientationManagerTest.java @@ -63,8 +63,7 @@ public void getMediaOrientation_when_natural_screen_orientation_equals_landscape DeviceOrientationManager orientationManager = DeviceOrientationManager.create(mockActivity, mockDartMessenger, false, 90); - int degreesPortraitUp = - orientationManager.getMediaOrientation(DeviceOrientation.PORTRAIT_UP); + int degreesPortraitUp = orientationManager.getMediaOrientation(DeviceOrientation.PORTRAIT_UP); int degreesPortraitDown = orientationManager.getMediaOrientation(DeviceOrientation.PORTRAIT_DOWN); int degreesLandscapeLeft = @@ -82,8 +81,7 @@ public void getMediaOrientation_when_natural_screen_orientation_equals_landscape public void getMediaOrientation_should_fallback_to_sensor_orientation_when_orientation_is_null() { setUpUIOrientationMocks(Configuration.ORIENTATION_LANDSCAPE, Surface.ROTATION_0); - int degrees = - deviceOrientationManager.getMediaOrientation(null); + int degrees = deviceOrientationManager.getMediaOrientation(null); assertEquals(90, degrees); } From a8909198f6476935fcbce5e7d952486aa925051b Mon Sep 17 00:00:00 2001 From: Maurits van Beusekom Date: Mon, 31 May 2021 16:47:14 +0200 Subject: [PATCH 10/63] Fix formatting --- .../sensororientation/DeviceOrientationManager.java | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) 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 90608fbd91fb..fc2824c8ab90 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 @@ -213,7 +213,8 @@ private boolean isSystemAutoRotationLocked() { /** * Gets the current user interface orientation. * - * This method is visible for testing purposes only and should never be used outside this class. + *

This method is visible for testing purposes only and should never be used outside this + * class. * * @return The current user interface orientation. */ @@ -243,7 +244,8 @@ PlatformChannel.DeviceOrientation getUIOrientation() { /** * Calculates the sensor orientation based on the supplied angle. * - * This method is visible for testing purposes only and should never be used outside this class. + *

This method is visible for testing purposes only and should never be used outside this + * class. * * @param angle Orientation angle. * @return The sensor orientation based on the supplied angle. @@ -274,7 +276,8 @@ PlatformChannel.DeviceOrientation calculateSensorOrientation(int angle) { /** * Gets the default orientation of the device. * - * This method is visible for testing purposes only and should never be used outside this class. + *

This method is visible for testing purposes only and should never be used outside this + * class. * * @return The default orientation of the device. */ @@ -295,7 +298,8 @@ int getDeviceDefaultOrientation() { /** * Gets an instance of the Android {@link android.view.Display}. * - * This method is visible for testing purposes only and should never be used outside this class. + *

This method is visible for testing purposes only and should never be used outside this + * class. * * @return An instance of the Android {@link android.view.Display}. */ From 55a6702aab3afcb4513ea9d09f38cc8c41d7ea8a Mon Sep 17 00:00:00 2001 From: Maurits van Beusekom Date: Tue, 8 Jun 2021 11:35:00 +0200 Subject: [PATCH 11/63] Only exclude 60 FPS limit for Pixel 4a --- .../features/fpsrange/FpsRangeFeature.java | 35 ++++++++++++------- 1 file changed, 22 insertions(+), 13 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 1c834990c11b..59efd76188f2 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 @@ -5,6 +5,7 @@ package io.flutter.plugins.camera.features.fpsrange; import android.hardware.camera2.CaptureRequest; +import android.os.Build; import android.util.Range; import io.flutter.plugins.camera.CameraProperties; import io.flutter.plugins.camera.features.CameraFeature; @@ -24,27 +25,35 @@ public class FpsRangeFeature extends CameraFeature> { public FpsRangeFeature(CameraProperties cameraProperties) { super(cameraProperties); - Range[] ranges = cameraProperties.getControlAutoExposureAvailableTargetFpsRanges(); + if (isPixel4A()) { + // HACK: 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. Highest working FPS is 30. + // https://issuetracker.google.com/issues/189237151 + currentSetting = new Range<>(30, 30); + } else { + Range[] ranges = cameraProperties.getControlAutoExposureAvailableTargetFpsRanges(); - if (ranges != null) { - for (Range range : ranges) { - int upper = range.getUpper(); + if (ranges != null) { + for (Range range : ranges) { + int upper = range.getUpper(); - // 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; + if (upper >= 10) { + if (currentSetting == null || upper > currentSetting.getUpper()) { + currentSetting = range; + } } } } } } + private boolean isPixel4A() { + return Build.BRAND.equals("google") && Build.MODEL.equals("Pixel 4a"); + } + @Override public String getDebugName() { return "FpsRangeFeature"; From cd5332183df74566428c83cbe79023f3398e63cc Mon Sep 17 00:00:00 2001 From: Maurits van Beusekom Date: Tue, 8 Jun 2021 13:06:37 +0200 Subject: [PATCH 12/63] Removed redundant empty line --- .../plugins/camera/features/resolution/ResolutionFeature.java | 1 - 1 file changed, 1 deletion(-) 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 d5a0ad6d2374..3400505cbc19 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 @@ -106,7 +106,6 @@ private void configureResolution(ResolutionPreset resolutionPreset, int cameraId recordingProfile = getBestAvailableCamcorderProfileForResolutionPreset(cameraId, resolutionPreset); captureSize = new Size(recordingProfile.videoFrameWidth, recordingProfile.videoFrameHeight); - previewSize = computeBestPreviewSize(cameraId, resolutionPreset); } From 35831d3b1313be87c7173da823b10615aab861d8 Mon Sep 17 00:00:00 2001 From: Maurits van Beusekom Date: Tue, 8 Jun 2021 13:07:58 +0200 Subject: [PATCH 13/63] Fixed comment --- .../plugins/camera/features/resolution/ResolutionFeature.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 3400505cbc19..a5469c63359b 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 @@ -156,7 +156,7 @@ public Size getPreviewSize() { } /** - * Get the optimal capture size based on the configured resolution. + * Gets the optimal capture size based on the configured resolution. * * @return The optimal capture size. */ From a9f3142d34fed3d141db3b7032eed5e3179f036a Mon Sep 17 00:00:00 2001 From: Maurits van Beusekom Date: Tue, 8 Jun 2021 14:30:01 +0200 Subject: [PATCH 14/63] Test Pixel 4a workaround --- .../features/fpsrange/FpsRangeFeature.java | 3 ++- .../fpsrange/FpsRangeFeaturePixel4aTest.java | 26 +++++++++++++++++++ .../fpsrange/FpsRangeFeatureTest.java | 16 ++++++++++++ 3 files changed, 44 insertions(+), 1 deletion(-) create mode 100644 packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/features/fpsrange/FpsRangeFeaturePixel4aTest.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 59efd76188f2..812f44f03f1a 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 @@ -15,6 +15,7 @@ * API. */ public class FpsRangeFeature extends CameraFeature> { + private final static Range MAX_PIXEL4A_RANGE = new Range<>(30, 30); private Range currentSetting; /** @@ -32,7 +33,7 @@ public FpsRangeFeature(CameraProperties cameraProperties) { // For max device compatibility we will keep FPS under 60 even if they report they are // capable of achieving 60 fps. Highest working FPS is 30. // https://issuetracker.google.com/issues/189237151 - currentSetting = new Range<>(30, 30); + currentSetting = MAX_PIXEL4A_RANGE; } else { Range[] ranges = cameraProperties.getControlAutoExposureAvailableTargetFpsRanges(); diff --git a/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/features/fpsrange/FpsRangeFeaturePixel4aTest.java b/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/features/fpsrange/FpsRangeFeaturePixel4aTest.java new file mode 100644 index 000000000000..979f3f12e35e --- /dev/null +++ b/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/features/fpsrange/FpsRangeFeaturePixel4aTest.java @@ -0,0 +1,26 @@ +package io.flutter.plugins.camera.features.fpsrange; + +import static org.junit.Assert.assertEquals; +import static org.mockito.Mockito.mock; + +import android.os.Build; +import android.util.Range; +import io.flutter.plugins.camera.CameraProperties; +import io.flutter.plugins.camera.utils.TestUtils; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.RobolectricTestRunner; + +@RunWith(RobolectricTestRunner.class) +public class FpsRangeFeaturePixel4aTest { + @Test + public void ctor_should_initialize_fps_range_with_30_when_device_is_pixel_4a() { + TestUtils.setFinalStatic(Build.class, "BRAND", "google"); + TestUtils.setFinalStatic(Build.class, "MODEL", "Pixel 4a"); + + FpsRangeFeature fpsRangeFeature = new FpsRangeFeature(mock(CameraProperties.class)); + Range range = fpsRangeFeature.getValue(); + assertEquals(30, (int) range.getLower()); + assertEquals(30, (int) range.getUpper()); + } +} 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 a74e42afd957..77937b5e87c6 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 @@ -13,11 +13,27 @@ import static org.mockito.Mockito.when; import android.hardware.camera2.CaptureRequest; +import android.os.Build; import android.util.Range; import io.flutter.plugins.camera.CameraProperties; +import io.flutter.plugins.camera.utils.TestUtils; +import org.junit.After; +import org.junit.Before; import org.junit.Test; public class FpsRangeFeatureTest { + @Before + public void before() { + TestUtils.setFinalStatic(Build.class, "BRAND", "Test Brand"); + TestUtils.setFinalStatic(Build.class, "MODEL", "Test Model"); + } + + @After + public void after() { + TestUtils.setFinalStatic(Build.class, "BRAND", null); + TestUtils.setFinalStatic(Build.class, "MODEL", null); + } + @Test public void ctor_should_initialize_fps_range_with_highest_upper_value_from_range_array() { FpsRangeFeature fpsRangeFeature = createTestInstance(); From 551800e20d70b69b6c5b6dfeaa7b9e3d63344b23 Mon Sep 17 00:00:00 2001 From: Maurits van Beusekom Date: Thu, 10 Jun 2021 10:48:16 +0200 Subject: [PATCH 15/63] Add tests for orientation updates --- .../DeviceOrientationManager.java | 68 ++++++++++--- .../DeviceOrientationManagerTest.java | 95 +++++++++++++++++++ 2 files changed, 148 insertions(+), 15 deletions(-) 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 fc2824c8ab90..2a04caad743a 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 @@ -19,6 +19,7 @@ import androidx.annotation.NonNull; import androidx.annotation.VisibleForTesting; import io.flutter.embedding.engine.systemchannels.PlatformChannel; +import io.flutter.embedding.engine.systemchannels.PlatformChannel.DeviceOrientation; import io.flutter.plugins.camera.DartMessenger; /** @@ -153,13 +154,7 @@ private void startSensorListener() { new OrientationEventListener(activity, SensorManager.SENSOR_DELAY_NORMAL) { @Override public void onOrientationChanged(int angle) { - if (!isSystemAutoRotationLocked()) { - PlatformChannel.DeviceOrientation newOrientation = calculateSensorOrientation(angle); - if (!newOrientation.equals(lastOrientation)) { - lastOrientation = newOrientation; - messenger.sendDeviceOrientationChangeEvent(newOrientation); - } - } + handleSensorOrientationChange(angle); } }; if (orientationEventListener.canDetectOrientation()) { @@ -175,19 +170,62 @@ private void startUIListener() { new BroadcastReceiver() { @Override public void onReceive(Context context, Intent intent) { - if (isSystemAutoRotationLocked()) { - PlatformChannel.DeviceOrientation orientation = getUIOrientation(); - if (!orientation.equals(lastOrientation)) { - lastOrientation = orientation; - messenger.sendDeviceOrientationChangeEvent(orientation); - } - } + handleUIOrientationChange(); } }; activity.registerReceiver(broadcastReceiver, orientationIntentFilter); broadcastReceiver.onReceive(activity, null); } + /** + * Handles orientation changes based on information from the device's sensors. + * + *

This method is visible for testing purposes only and should never be used outside this + * class. + * + * @param angle of the current orientation. + */ + @VisibleForTesting + void handleSensorOrientationChange(int angle) { + if (!isAccelerometerRotationLocked()) { + PlatformChannel.DeviceOrientation orientation = calculateSensorOrientation(angle); + lastOrientation = handleOrientationChange(orientation, lastOrientation, messenger); + } + } + + /** + * Handles orientation changes based on change events triggered by the OrientationIntentFilter. + * + *

This method is visible for testing purposes only and should never be used outside this + * class. + */ + @VisibleForTesting + void handleUIOrientationChange() { + if (isAccelerometerRotationLocked()) { + PlatformChannel.DeviceOrientation orientation = getUIOrientation(); + lastOrientation = handleOrientationChange(orientation, lastOrientation, messenger); + } + } + + /** + * Handles orientation changes coming from either the device's sensors or the + * OrientationIntentFilter. + * + *

This method is visible for testing purposes only and should never be used outside this + * class. + */ + @VisibleForTesting + static DeviceOrientation handleOrientationChange( + DeviceOrientation newOrientation, + DeviceOrientation previousOrientation, + DartMessenger messenger) { + if (!newOrientation.equals(previousOrientation)) { + messenger.sendDeviceOrientationChangeEvent(newOrientation); + } + + return newOrientation; + } + private void stopSensorListener() { if (orientationEventListener == null) { return; @@ -204,7 +242,7 @@ private void stopUIListener() { broadcastReceiver = null; } - private boolean isSystemAutoRotationLocked() { + private boolean isAccelerometerRotationLocked() { return android.provider.Settings.System.getInt( activity.getContentResolver(), Settings.System.ACCELEROMETER_ROTATION, 0) != 1; diff --git a/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/features/sensororientation/DeviceOrientationManagerTest.java b/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/features/sensororientation/DeviceOrientationManagerTest.java index 5dc1c63e15d3..1e45dac46c28 100644 --- a/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/features/sensororientation/DeviceOrientationManagerTest.java +++ b/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/features/sensororientation/DeviceOrientationManagerTest.java @@ -5,13 +5,20 @@ package io.flutter.plugins.camera.features.sensororientation; import static org.junit.Assert.assertEquals; +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.content.Context; import android.content.res.Configuration; import android.content.res.Resources; +import android.provider.Settings; import android.view.Display; import android.view.Surface; import android.view.WindowManager; @@ -19,6 +26,7 @@ import io.flutter.plugins.camera.DartMessenger; import org.junit.Before; import org.junit.Test; +import org.mockito.MockedStatic; public class DeviceOrientationManagerTest { private Activity mockActivity; @@ -86,6 +94,93 @@ public void getMediaOrientation_should_fallback_to_sensor_orientation_when_orien assertEquals(90, degrees); } + @Test + public void handleSensorOrientationChange_should_send_message_when_sensor_access_is_allowed() { + try (MockedStatic mockedSystem = mockStatic(Settings.System.class)) { + mockedSystem + .when( + () -> + Settings.System.getInt(any(), eq(Settings.System.ACCELEROMETER_ROTATION), eq(0))) + .thenReturn(1); + setUpUIOrientationMocks(Configuration.ORIENTATION_PORTRAIT, Surface.ROTATION_0); + + deviceOrientationManager.handleSensorOrientationChange(90); + } + + verify(mockDartMessenger, times(1)).sendDeviceOrientationChangeEvent(DeviceOrientation.LANDSCAPE_LEFT); + + } + + @Test + public void handleSensorOrientationChange_should_send_message_when_sensor_access_is_not_allowed() { + try (MockedStatic mockedSystem = mockStatic(Settings.System.class)) { + mockedSystem + .when( + () -> + Settings.System.getInt(any(), eq(Settings.System.ACCELEROMETER_ROTATION), eq(0))) + .thenReturn(0); + setUpUIOrientationMocks(Configuration.ORIENTATION_PORTRAIT, Surface.ROTATION_0); + + deviceOrientationManager.handleSensorOrientationChange(90); + } + + verify(mockDartMessenger, never()).sendDeviceOrientationChangeEvent(any()); + } + + @Test + public void handleUIOrientationChange_should_send_message_when_sensor_access_is_allowed() { + try (MockedStatic mockedSystem = mockStatic(Settings.System.class)) { + mockedSystem + .when( + () -> + Settings.System.getInt(any(), eq(Settings.System.ACCELEROMETER_ROTATION), eq(0))) + .thenReturn(0); + setUpUIOrientationMocks(Configuration.ORIENTATION_LANDSCAPE, Surface.ROTATION_0); + + deviceOrientationManager.handleUIOrientationChange(); + } + + verify(mockDartMessenger, times(1)).sendDeviceOrientationChangeEvent(DeviceOrientation.LANDSCAPE_LEFT); + } + + @Test + public void handleUIOrientationChange_should_send_message_when_sensor_access_is_not_allowed() { + try (MockedStatic mockedSystem = mockStatic(Settings.System.class)) { + mockedSystem + .when( + () -> + Settings.System.getInt(any(), eq(Settings.System.ACCELEROMETER_ROTATION), eq(0))) + .thenReturn(1); + setUpUIOrientationMocks(Configuration.ORIENTATION_LANDSCAPE, Surface.ROTATION_0); + + deviceOrientationManager.handleUIOrientationChange(); + } + + verify(mockDartMessenger, never()).sendDeviceOrientationChangeEvent(any()); + } + + @Test + public void handleOrientationChange_should_send_message_when_orientation_is_updated() { + DeviceOrientation previousOrientation = DeviceOrientation.PORTRAIT_UP; + DeviceOrientation newOrientation = DeviceOrientation.LANDSCAPE_LEFT; + + DeviceOrientation orientation = DeviceOrientationManager.handleOrientationChange(newOrientation, previousOrientation, mockDartMessenger); + + verify(mockDartMessenger, times(1)).sendDeviceOrientationChangeEvent(newOrientation); + assertEquals(newOrientation, orientation); + } + + @Test + public void handleOrientationChange_should_not_send_message_when_orientation_is_not_updated() { + DeviceOrientation previousOrientation = DeviceOrientation.PORTRAIT_UP; + DeviceOrientation newOrientation = DeviceOrientation.PORTRAIT_UP; + + DeviceOrientation orientation = DeviceOrientationManager.handleOrientationChange(newOrientation, previousOrientation, mockDartMessenger); + + verify(mockDartMessenger, never()).sendDeviceOrientationChangeEvent(any()); + assertEquals(newOrientation, orientation); + } + @Test public void getUIOrientation() { // Orientation portrait and rotation of 0 should translate to "PORTRAIT_UP". From 68cbc56643db4fadceaf0b278e4cab2652bbe427 Mon Sep 17 00:00:00 2001 From: Maurits van Beusekom Date: Thu, 10 Jun 2021 10:55:06 +0200 Subject: [PATCH 16/63] Fix formatting --- .../DeviceOrientationManagerTest.java | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/features/sensororientation/DeviceOrientationManagerTest.java b/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/features/sensororientation/DeviceOrientationManagerTest.java index 1e45dac46c28..6e8d04d20e99 100644 --- a/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/features/sensororientation/DeviceOrientationManagerTest.java +++ b/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/features/sensororientation/DeviceOrientationManagerTest.java @@ -107,12 +107,13 @@ public void handleSensorOrientationChange_should_send_message_when_sensor_access deviceOrientationManager.handleSensorOrientationChange(90); } - verify(mockDartMessenger, times(1)).sendDeviceOrientationChangeEvent(DeviceOrientation.LANDSCAPE_LEFT); - + verify(mockDartMessenger, times(1)) + .sendDeviceOrientationChangeEvent(DeviceOrientation.LANDSCAPE_LEFT); } @Test - public void handleSensorOrientationChange_should_send_message_when_sensor_access_is_not_allowed() { + public void + handleSensorOrientationChange_should_send_message_when_sensor_access_is_not_allowed() { try (MockedStatic mockedSystem = mockStatic(Settings.System.class)) { mockedSystem .when( @@ -140,7 +141,8 @@ public void handleUIOrientationChange_should_send_message_when_sensor_access_is_ deviceOrientationManager.handleUIOrientationChange(); } - verify(mockDartMessenger, times(1)).sendDeviceOrientationChangeEvent(DeviceOrientation.LANDSCAPE_LEFT); + verify(mockDartMessenger, times(1)) + .sendDeviceOrientationChangeEvent(DeviceOrientation.LANDSCAPE_LEFT); } @Test @@ -164,7 +166,9 @@ public void handleOrientationChange_should_send_message_when_orientation_is_upda DeviceOrientation previousOrientation = DeviceOrientation.PORTRAIT_UP; DeviceOrientation newOrientation = DeviceOrientation.LANDSCAPE_LEFT; - DeviceOrientation orientation = DeviceOrientationManager.handleOrientationChange(newOrientation, previousOrientation, mockDartMessenger); + DeviceOrientation orientation = + DeviceOrientationManager.handleOrientationChange( + newOrientation, previousOrientation, mockDartMessenger); verify(mockDartMessenger, times(1)).sendDeviceOrientationChangeEvent(newOrientation); assertEquals(newOrientation, orientation); @@ -175,7 +179,9 @@ public void handleOrientationChange_should_not_send_message_when_orientation_is_ DeviceOrientation previousOrientation = DeviceOrientation.PORTRAIT_UP; DeviceOrientation newOrientation = DeviceOrientation.PORTRAIT_UP; - DeviceOrientation orientation = DeviceOrientationManager.handleOrientationChange(newOrientation, previousOrientation, mockDartMessenger); + DeviceOrientation orientation = + DeviceOrientationManager.handleOrientationChange( + newOrientation, previousOrientation, mockDartMessenger); verify(mockDartMessenger, never()).sendDeviceOrientationChangeEvent(any()); assertEquals(newOrientation, orientation); From 1b137c2af2f899be685b261a52dacb4c9a1365c8 Mon Sep 17 00:00:00 2001 From: Maurits van Beusekom Date: Thu, 10 Jun 2021 11:03:30 +0200 Subject: [PATCH 17/63] Fix formatting --- .../plugins/camera/features/fpsrange/FpsRangeFeature.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 812f44f03f1a..500f2aa28dc2 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 @@ -15,7 +15,7 @@ * API. */ public class FpsRangeFeature extends CameraFeature> { - private final static Range MAX_PIXEL4A_RANGE = new Range<>(30, 30); + private static final Range MAX_PIXEL4A_RANGE = new Range<>(30, 30); private Range currentSetting; /** From 6514a008a8bfae3dd3d0f2ecbe47f40650698eb5 Mon Sep 17 00:00:00 2001 From: Maurits van Beusekom Date: Thu, 10 Jun 2021 11:18:12 +0200 Subject: [PATCH 18/63] Added missing license header --- .../camera/features/fpsrange/FpsRangeFeaturePixel4aTest.java | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/features/fpsrange/FpsRangeFeaturePixel4aTest.java b/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/features/fpsrange/FpsRangeFeaturePixel4aTest.java index 979f3f12e35e..7b6e70fff5b2 100644 --- a/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/features/fpsrange/FpsRangeFeaturePixel4aTest.java +++ b/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/features/fpsrange/FpsRangeFeaturePixel4aTest.java @@ -1,3 +1,7 @@ +// Copyright 2013 The Flutter 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; From baae5f6bc22b72f99d1535e448186d354d44f535 Mon Sep 17 00:00:00 2001 From: BeMacized Date: Thu, 10 Jun 2021 14:59:00 +0200 Subject: [PATCH 19/63] Add feature classes for exposure- and focus point functionality. --- packages/camera/camera/android/build.gradle | 2 +- .../plugins/camera/CameraRegionUtils.java | 138 ++++++++ .../exposurepoint/ExposurePointFeature.java | 61 ++-- .../focuspoint/FocusPointFeature.java | 87 ++++++ .../plugins/camera/types/CameraRegions.java | 199 ------------ .../plugins/camera/CameraRegionUtilsTest.java | 294 ++++++++++++++++++ .../ExposurePointFeatureTest.java | 220 ++++++++----- .../focuspoint/FocusPointFeatureTest.java | 270 ++++++++++++++++ .../types/CameraRegionsFactoryTest.java | 201 ------------ .../camera/types/CameraRegionsTest.java | 114 ------- 10 files changed, 972 insertions(+), 614 deletions(-) create mode 100644 packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/CameraRegionUtils.java create mode 100644 packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/focuspoint/FocusPointFeature.java delete mode 100644 packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/types/CameraRegions.java create mode 100644 packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/CameraRegionUtilsTest.java create mode 100644 packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/features/focuspoint/FocusPointFeatureTest.java delete mode 100644 packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/types/CameraRegionsFactoryTest.java delete mode 100644 packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/types/CameraRegionsTest.java diff --git a/packages/camera/camera/android/build.gradle b/packages/camera/camera/android/build.gradle index 0907c1eeecc9..21b25ad66dea 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-inline:3.5.13' + testImplementation 'org.mockito:mockito-inline:3.11.0' 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/CameraRegionUtils.java b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/CameraRegionUtils.java new file mode 100644 index 000000000000..bc03aae27a03 --- /dev/null +++ b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/CameraRegionUtils.java @@ -0,0 +1,138 @@ +// Copyright 2013 The Flutter 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.annotation.TargetApi; +import android.hardware.camera2.CaptureRequest; +import android.hardware.camera2.params.MeteringRectangle; +import android.os.Build; +import android.util.Size; +import androidx.annotation.NonNull; +import androidx.annotation.VisibleForTesting; +import java.util.Arrays; + +/** + * Utility class offering functions to calculate values regarding the camera boundaries. + * + *

The functions are used to calculate focus and exposure settings. + */ +public final class CameraRegionUtils { + + /** + * Obtains the boundaries for the currently active camera, that can be used for calculating + * MeteringRectangle instances required for setting focus or exposure settings. + * + * @param cameraProperties - Collection of the characteristics for the current camera device. + * @param requestBuilder - The request builder for the current capture request. + * @return The boundaries for the current camera device. + */ + public static Size getCameraBoundaries( + @NonNull CameraProperties cameraProperties, @NonNull CaptureRequest.Builder requestBuilder) { + // No distortion correction support + if (android.os.Build.VERSION.SDK_INT >= Build.VERSION_CODES.P + && supportsDistortionCorrection(cameraProperties)) { + // 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(); + } + + // Return camera boundaries + return rect == null ? null : new Size(rect.width(), rect.height()); + } else { + // Return camera boundaries + return cameraProperties.getSensorInfoPixelArraySize(); + } + } + + /** + * Converts a point into a {@link MeteringRectangle} with the supplied coordinates as the centre + * point. + * + *

Since the Camera API (due to cross-platform constraints) only accepts a point when + * configuring a specific focus or exposure area and Android requires a rectangle to configure + * these settings there is a need to convert the point into a rectangle. This method will create + * the required rectangle with an arbitrarily size that is a 10th of the current viewport and the + * coordinates as the centre point. + * + * @param boundaries - The camera boundaries to calculate the metering rectangle for. + * @param x x - 1 >= coordinate >= 0 + * @param y y - 1 >= coordinate >= 0 + * @return The dimensions of the metering rectangle based on the supplied coordinates and + * boundaries. + */ + public static MeteringRectangle convertPointToMeteringRectangle( + @NonNull Size boundaries, double x, double y) { + assert (boundaries.getWidth() > 0 && boundaries.getHeight() > 0); + assert (x >= 0 && x <= 1); + assert (y >= 0 && y <= 1); + + // Interpolate the target coordinate + int targetX = (int) Math.round(x * ((double) (boundaries.getWidth() - 1))); + int targetY = (int) Math.round(y * ((double) (boundaries.getHeight() - 1))); + // Since the Camera API only allows Determine the dimensions of the metering rectangle (10th of + // the viewport) + int targetWidth = (int) Math.round(((double) boundaries.getWidth()) / 10d); + int targetHeight = (int) Math.round(((double) boundaries.getHeight()) / 10d); + // Adjust target coordinate to represent top-left corner of metering rectangle + targetX -= targetWidth / 2; + targetY -= targetHeight / 2; + // Adjust target coordinate as to not fall out of bounds + if (targetX < 0) targetX = 0; + if (targetY < 0) targetY = 0; + int maxTargetX = boundaries.getWidth() - 1 - targetWidth; + int maxTargetY = boundaries.getHeight() - 1 - targetHeight; + if (targetX > maxTargetX) targetX = maxTargetX; + if (targetY > maxTargetY) targetY = maxTargetY; + + // Build the metering rectangle + return MeteringRectangleFactory.create(targetX, targetY, targetWidth, targetHeight, 1); + } + + @TargetApi(Build.VERSION_CODES.P) + private static boolean supportsDistortionCorrection(CameraProperties cameraProperties) { + 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; + } + + /** Factory class that assists in creating a {@link MeteringRectangle} instance. */ + static class MeteringRectangleFactory { + /** + * Creates a new instance of the {@link MeteringRectangle} class. + * + *

This method is visible for testing purposes only and should never be used outside this * + * class. + * + * @param x coordinate >= 0 + * @param y coordinate >= 0 + * @param width width >= 0 + * @param height height >= 0 + * @param meteringWeight weight between {@value MeteringRectangle#METERING_WEIGHT_MIN} and + * {@value MeteringRectangle#METERING_WEIGHT_MAX} inclusively + * @return new instance of the {@link MeteringRectangle} class. + * @throws IllegalArgumentException if any of the parameters were negative + */ + @VisibleForTesting + public static MeteringRectangle create( + int x, int y, int width, int height, int meteringWeight) { + return new MeteringRectangle(x, y, width, height, meteringWeight); + } + } +} 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 f729d33c8528..905c9922f600 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 @@ -6,28 +6,36 @@ import android.hardware.camera2.CaptureRequest; import android.hardware.camera2.params.MeteringRectangle; -import android.util.Log; -import androidx.annotation.NonNull; +import android.util.Size; import io.flutter.plugins.camera.CameraProperties; +import io.flutter.plugins.camera.CameraRegionUtils; import io.flutter.plugins.camera.features.CameraFeature; import io.flutter.plugins.camera.features.Point; -import io.flutter.plugins.camera.types.CameraRegions; /** Exposure point controls where in the frame exposure metering will come from. */ public class ExposurePointFeature extends CameraFeature { - private final CameraRegions cameraRegions; - private Point currentSetting = new Point(0.0, 0.0); + private Size cameraBoundaries; + private Point exposurePoint; + private MeteringRectangle exposureRectangle; /** * Creates a new instance of the {@link ExposurePointFeature}. * * @param cameraProperties Collection of the characteristics for the current camera device. - * @param cameraRegions Utility class to assist in calculating exposure boundaries. */ - public ExposurePointFeature(CameraProperties cameraProperties, CameraRegions cameraRegions) { + public ExposurePointFeature(CameraProperties cameraProperties) { super(cameraProperties); - this.cameraRegions = cameraRegions; + } + + /** + * Sets the camera boundaries that are required for the exposure point feature to function. + * + * @param cameraBoundaries - The camera boundaries to set. + */ + public void setCameraBoundaries(Size cameraBoundaries) { + this.cameraBoundaries = cameraBoundaries; + this.buildExposureRectangle(); } @Override @@ -37,23 +45,19 @@ public String getDebugName() { @Override public Point getValue() { - return currentSetting; + return exposurePoint; } @Override - public void setValue(@NonNull Point value) { - this.currentSetting = value; - - if (value.x == null || value.y == null) { - cameraRegions.resetAutoExposureMeteringRectangle(); - } else { - cameraRegions.setAutoExposureMeteringRectangleFromPoint(value.x, value.y); - } + public void setValue(Point value) { + this.exposurePoint = value == null || value.x == null || value.y == null ? null : value; + this.buildExposureRectangle(); } // Whether or not this camera can set the exposure point. @Override public boolean checkIsSupported() { + if (cameraBoundaries == null) return false; Integer supportedRegions = cameraProperties.getControlMaxRegionsAutoExposure(); return supportedRegions != null && supportedRegions > 0; } @@ -63,16 +67,21 @@ public void updateBuilder(CaptureRequest.Builder requestBuilder) { if (!checkIsSupported()) { return; } - - MeteringRectangle aeRect = null; - try { - aeRect = cameraRegions.getAEMeteringRectangle(); - } catch (Exception e) { - Log.w("Camera", "Unable to retrieve the Auto Exposure metering rectangle.", e); - } - requestBuilder.set( CaptureRequest.CONTROL_AE_REGIONS, - aeRect == null ? null : new MeteringRectangle[] {aeRect}); + exposureRectangle == null ? null : new MeteringRectangle[] {exposureRectangle}); + } + + private void buildExposureRectangle() { + if (!checkIsSupported()) { + return; + } + if (this.exposurePoint == null) { + this.exposureRectangle = null; + } else { + this.exposureRectangle = + CameraRegionUtils.convertPointToMeteringRectangle( + this.cameraBoundaries, this.exposurePoint.x, this.exposurePoint.y); + } } } 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 new file mode 100644 index 000000000000..8f2941646b4a --- /dev/null +++ b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/focuspoint/FocusPointFeature.java @@ -0,0 +1,87 @@ +// 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; +import android.hardware.camera2.params.MeteringRectangle; +import android.util.Size; +import io.flutter.plugins.camera.CameraProperties; +import io.flutter.plugins.camera.CameraRegionUtils; +import io.flutter.plugins.camera.features.CameraFeature; +import io.flutter.plugins.camera.features.Point; + +/** Focus point controls where in the frame focus will come from. */ +public class FocusPointFeature extends CameraFeature { + + private Size cameraBoundaries; + private Point focusPoint; + private MeteringRectangle focusRectangle; + + /** + * Creates a new instance of the {@link FocusPointFeature}. + * + * @param cameraProperties Collection of the characteristics for the current camera device. + */ + public FocusPointFeature(CameraProperties cameraProperties) { + super(cameraProperties); + } + + /** + * Sets the camera boundaries that are required for the focus point feature to function. + * + * @param cameraBoundaries - The camera boundaries to set. + */ + public void setCameraBoundaries(Size cameraBoundaries) { + this.cameraBoundaries = cameraBoundaries; + this.buildFocusRectangle(); + } + + @Override + public String getDebugName() { + return "FocusPointFeature"; + } + + @Override + public Point getValue() { + return focusPoint; + } + + @Override + public void setValue(Point value) { + this.focusPoint = value == null || value.x == null || value.y == null ? null : value; + this.buildFocusRectangle(); + } + + // Whether or not this camera can set the exposure point. + @Override + public boolean checkIsSupported() { + if (cameraBoundaries == null) return false; + Integer supportedRegions = cameraProperties.getControlMaxRegionsAutoFocus(); + return supportedRegions != null && supportedRegions > 0; + } + + @Override + public void updateBuilder(CaptureRequest.Builder requestBuilder) { + if (!checkIsSupported()) { + return; + } + requestBuilder.set( + CaptureRequest.CONTROL_AF_REGIONS, + focusRectangle == null ? null : new MeteringRectangle[] {focusRectangle}); + } + + private void buildFocusRectangle() { + if (!checkIsSupported()) { + return; + } + if (this.focusPoint == null) { + this.focusRectangle = null; + } else { + this.focusRectangle = + CameraRegionUtils.convertPointToMeteringRectangle( + this.cameraBoundaries, this.focusPoint.x, this.focusPoint.y); + } + } +} diff --git a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/types/CameraRegions.java b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/types/CameraRegions.java deleted file mode 100644 index b86241e78d29..000000000000 --- a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/types/CameraRegions.java +++ /dev/null @@ -1,199 +0,0 @@ -// Copyright 2013 The Flutter 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.types; - -import android.annotation.TargetApi; -import android.hardware.camera2.CaptureRequest; -import android.hardware.camera2.params.MeteringRectangle; -import android.os.Build; -import android.util.Size; -import androidx.annotation.NonNull; -import io.flutter.plugins.camera.CameraProperties; -import java.util.Arrays; - -/** - * Utility class that contains information regarding the camera's regions. - * - *

The regions information is used to calculate focus and exposure settings. - */ -public final class CameraRegions { - - /** Factory class that assists in creating a {@link CameraRegions} instance. */ - public static class Factory { - /** - * Creates a new instance of the {@link CameraRegions} class. - * - *

The {@link CameraProperties} and {@link CaptureRequest.Builder} classed are used to - * determine if the device's camera supports distortion correction mode and calculate the - * correct boundaries based on the outcome. - * - * @param cameraProperties Collection of the characteristics for the current camera device. - * @param requestBuilder CaptureRequest builder containing current target and surface settings. - * @return new instance of the {@link CameraRegions} class. - */ - public static CameraRegions create( - @NonNull CameraProperties cameraProperties, - @NonNull CaptureRequest.Builder requestBuilder) { - Size boundaries; - - // No distortion correction support - if (android.os.Build.VERSION.SDK_INT >= Build.VERSION_CODES.P - && supportsDistortionCorrection(cameraProperties)) { - // 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 - boundaries = rect == null ? null : new Size(rect.width(), rect.height()); - } else { - boundaries = cameraProperties.getSensorInfoPixelArraySize(); - } - - // Create new camera regions using new size - return new CameraRegions(boundaries); - } - - @TargetApi(Build.VERSION_CODES.P) - private static boolean supportsDistortionCorrection(CameraProperties cameraProperties) { - 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 final Size boundaries; - - private MeteringRectangle aeMeteringRectangle; - private MeteringRectangle afMeteringRectangle; - - /** - * Creates a new instance of the {@link CameraRegions} class. - * - * @param boundaries The area of the image sensor. - */ - CameraRegions(Size boundaries) { - assert (boundaries == null || boundaries.getWidth() > 0); - assert (boundaries == null || boundaries.getHeight() > 0); - - this.boundaries = boundaries; - } - - /** - * Gets the {@link MeteringRectangle} on which the auto exposure will be applied. - * - * @return The {@link MeteringRectangle} on which the auto exposure will be applied. - */ - public MeteringRectangle getAEMeteringRectangle() { - return aeMeteringRectangle; - } - - /** - * Gets the {@link MeteringRectangle} on which the auto focus will be applied. - * - * @return The {@link MeteringRectangle} on which the auto focus will be applied. - */ - public MeteringRectangle getAFMeteringRectangle() { - return afMeteringRectangle; - } - - /** - * Gets the area of the image sensor. - * - *

If distortion correction is supported the size corresponds to the active pixels after any - * geometric distortion correction has been applied. If distortion correction is not supported the - * dimensions include the full pixel array, possibly including black calibration pixels. - * - * @return The area of the image sensor. - */ - public Size getBoundaries() { - return this.boundaries; - } - - /** Resets the {@link MeteringRectangle} on which the auto exposure will be applied. */ - public void resetAutoExposureMeteringRectangle() { - this.aeMeteringRectangle = null; - } - - /** - * Sets the coordinates which will form the centre of the exposure rectangle. - * - * @param x x – coordinate >= 0 - * @param y y – coordinate >= 0 - */ - public void setAutoExposureMeteringRectangleFromPoint(double x, double y) { - this.aeMeteringRectangle = convertPointToMeteringRectangle(x, y); - } - - /** Resets the {@link MeteringRectangle} on which the auto focus will be applied. */ - public void resetAutoFocusMeteringRectangle() { - this.afMeteringRectangle = null; - } - - /** - * Sets the coordinates which will form the centre of the focus rectangle. - * - * @param x x – coordinate >= 0 - * @param y y – coordinate >= 0 - */ - public void setAutoFocusMeteringRectangleFromPoint(double x, double y) { - this.afMeteringRectangle = convertPointToMeteringRectangle(x, y); - } - - /** - * Converts a point into a {@link MeteringRectangle} with the supplied coordinates as the centre - * point. - * - *

Since the Camera API (due to cross-platform constraints) only accepts a point when - * configuring a specific focus or exposure area and Android requires a rectangle to configure - * these settings there is a need to convert the point into a rectangle. This method will create - * the required rectangle with an arbitrarily size that is a 10th of the current viewport and the - * coordinates as the centre point. - * - * @param x x - coordinate >= 0 - * @param y y - coordinate >= 0 - * @return The dimensions of the metering rectangle based on the supplied coordinates. - */ - MeteringRectangle convertPointToMeteringRectangle(double x, double y) { - assert (x >= 0 && x <= 1); - assert (y >= 0 && y <= 1); - - // Interpolate the target coordinate - int targetX = (int) Math.round(x * ((double) (boundaries.getWidth() - 1))); - int targetY = (int) Math.round(y * ((double) (boundaries.getHeight() - 1))); - // Since the Camera API only allows Determine the dimensions of the metering rectangle (10th of - // the viewport) - int targetWidth = (int) Math.round(((double) boundaries.getWidth()) / 10d); - int targetHeight = (int) Math.round(((double) boundaries.getHeight()) / 10d); - // Adjust target coordinate to represent top-left corner of metering rectangle - targetX -= targetWidth / 2; - targetY -= targetHeight / 2; - // Adjust target coordinate as to not fall out of bounds - if (targetX < 0) targetX = 0; - if (targetY < 0) targetY = 0; - int maxTargetX = boundaries.getWidth() - 1 - targetWidth; - int maxTargetY = boundaries.getHeight() - 1 - targetHeight; - if (targetX > maxTargetX) targetX = maxTargetX; - if (targetY > maxTargetY) targetY = maxTargetY; - - // Build the metering rectangle - return new MeteringRectangle(targetX, targetY, targetWidth, targetHeight, 1); - } -} diff --git a/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/CameraRegionUtilsTest.java b/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/CameraRegionUtilsTest.java new file mode 100644 index 000000000000..e2255f2c665c --- /dev/null +++ b/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/CameraRegionUtilsTest.java @@ -0,0 +1,294 @@ +package io.flutter.plugins.camera; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.mockStatic; +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.hardware.camera2.params.MeteringRectangle; +import android.os.Build; +import android.util.Size; +import io.flutter.plugins.camera.utils.TestUtils; +import org.junit.Before; +import org.junit.Test; +import org.mockito.MockedStatic; +import org.mockito.invocation.InvocationOnMock; +import org.mockito.stubbing.Answer; + +public class CameraRegionUtilsTest { + + Size mockCameraBoundaries; + + @Before + public void setUp() { + this.mockCameraBoundaries = mock(Size.class); + when(this.mockCameraBoundaries.getWidth()).thenReturn(100); + when(this.mockCameraBoundaries.getHeight()).thenReturn(100); + } + + @Test + public void + getCameraBoundaries_should_return_sensor_info_pixel_array_size_when_running_pre_android_p() { + updateSdkVersion(Build.VERSION_CODES.O_MR1); + + try { + CameraProperties mockCameraProperties = mock(CameraProperties.class); + CaptureRequest.Builder mockBuilder = mock(CaptureRequest.Builder.class); + when(mockCameraProperties.getSensorInfoPixelArraySize()).thenReturn(mockCameraBoundaries); + + Size result = CameraRegionUtils.getCameraBoundaries(mockCameraProperties, mockBuilder); + + assertEquals(mockCameraBoundaries, result); + verify(mockCameraProperties, never()).getSensorInfoPreCorrectionActiveArraySize(); + verify(mockCameraProperties, never()).getSensorInfoActiveArraySize(); + } finally { + updateSdkVersion(0); + } + } + + @Test + public void + getCameraBoundaries_should_return_sensor_info_pixel_array_size_when_distortion_correction_is_null() { + updateSdkVersion(Build.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(mockCameraBoundaries); + + Size result = CameraRegionUtils.getCameraBoundaries(mockCameraProperties, mockBuilder); + + assertEquals(mockCameraBoundaries, result); + verify(mockCameraProperties, never()).getSensorInfoPreCorrectionActiveArraySize(); + verify(mockCameraProperties, never()).getSensorInfoActiveArraySize(); + } finally { + updateSdkVersion(0); + } + } + + @Test + public void + getCameraBoundaries_should_return_sensor_info_pixel_array_size_when_distortion_correction_is_off() { + updateSdkVersion(Build.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(mockCameraBoundaries); + + Size result = CameraRegionUtils.getCameraBoundaries(mockCameraProperties, mockBuilder); + + assertEquals(mockCameraBoundaries, result); + verify(mockCameraProperties, never()).getSensorInfoPreCorrectionActiveArraySize(); + verify(mockCameraProperties, never()).getSensorInfoActiveArraySize(); + } finally { + updateSdkVersion(0); + } + } + + @Test + public void + getCameraBoundaries_should_return_info_pre_correction_active_array_size_when_distortion_correction_mode_is_set_to_null() { + updateSdkVersion(Build.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); + + Size result = CameraRegionUtils.getCameraBoundaries(mockCameraProperties, mockBuilder); + + assertNull(result); + verify(mockCameraProperties, never()).getSensorInfoPixelArraySize(); + verify(mockCameraProperties, never()).getSensorInfoActiveArraySize(); + } finally { + updateSdkVersion(0); + } + } + + @Test + public void + getCameraBoundaries_should_return_info_pre_correction_active_array_size_when_distortion_correction_mode_is_set_to_off() { + updateSdkVersion(Build.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); + + Size result = CameraRegionUtils.getCameraBoundaries(mockCameraProperties, mockBuilder); + + assertNull(result); + verify(mockCameraProperties, never()).getSensorInfoPixelArraySize(); + verify(mockCameraProperties, never()).getSensorInfoActiveArraySize(); + } finally { + updateSdkVersion(0); + } + } + + @Test + public void + getCameraBoundaries_should_return_sensor_info_active_array_size_when_distortion_correction_mode_is_set() { + updateSdkVersion(Build.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); + + Size result = CameraRegionUtils.getCameraBoundaries(mockCameraProperties, mockBuilder); + + assertNull(result); + verify(mockCameraProperties, never()).getSensorInfoPixelArraySize(); + verify(mockCameraProperties, never()).getSensorInfoPreCorrectionActiveArraySize(); + } finally { + updateSdkVersion(0); + } + } + + @Test(expected = AssertionError.class) + public void getMeteringRectangleForPoint_should_throw_for_x_upper_bound() { + CameraRegionUtils.convertPointToMeteringRectangle(this.mockCameraBoundaries, 1.5, 0); + } + + @Test(expected = AssertionError.class) + public void getMeteringRectangleForPoint_should_throw_for_x_lower_bound() { + CameraRegionUtils.convertPointToMeteringRectangle(this.mockCameraBoundaries, -0.5, 0); + } + + @Test(expected = AssertionError.class) + public void getMeteringRectangleForPoint_should_throw_for_y_upper_bound() { + CameraRegionUtils.convertPointToMeteringRectangle(this.mockCameraBoundaries, 0, 1.5); + } + + @Test(expected = AssertionError.class) + public void getMeteringRectangleForPoint_should_throw_for_y_lower_bound() { + CameraRegionUtils.convertPointToMeteringRectangle(this.mockCameraBoundaries, 0, -0.5); + } + + @Test + public void getMeteringRectangleForPoint_should_return_valid_MeteringRectangle() { + try (MockedStatic mockedMeteringRectangleFactory = + mockStatic(CameraRegionUtils.MeteringRectangleFactory.class)) { + + mockedMeteringRectangleFactory + .when( + () -> + CameraRegionUtils.MeteringRectangleFactory.create( + anyInt(), anyInt(), anyInt(), anyInt(), anyInt())) + .thenAnswer( + new Answer() { + @Override + public MeteringRectangle answer(InvocationOnMock createInvocation) + throws Throwable { + MeteringRectangle mockMeteringRectangle = mock(MeteringRectangle.class); + when(mockMeteringRectangle.getX()).thenReturn(createInvocation.getArgument(0)); + when(mockMeteringRectangle.getY()).thenReturn(createInvocation.getArgument(1)); + when(mockMeteringRectangle.getWidth()) + .thenReturn(createInvocation.getArgument(2)); + when(mockMeteringRectangle.getHeight()) + .thenReturn(createInvocation.getArgument(3)); + when(mockMeteringRectangle.getMeteringWeight()) + .thenReturn(createInvocation.getArgument(4)); + when(mockMeteringRectangle.equals(any())) + .thenAnswer( + new Answer() { + @Override + public Boolean answer(InvocationOnMock equalsInvocation) + throws Throwable { + MeteringRectangle otherMockMeteringRectangle = + equalsInvocation.getArgument(0); + return mockMeteringRectangle.getX() + == otherMockMeteringRectangle.getX() + && mockMeteringRectangle.getY() + == otherMockMeteringRectangle.getY() + && mockMeteringRectangle.getWidth() + == otherMockMeteringRectangle.getWidth() + && mockMeteringRectangle.getHeight() + == otherMockMeteringRectangle.getHeight() + && mockMeteringRectangle.getMeteringWeight() + == otherMockMeteringRectangle.getMeteringWeight(); + } + }); + return mockMeteringRectangle; + } + }); + + MeteringRectangle r; + // Center + r = CameraRegionUtils.convertPointToMeteringRectangle(this.mockCameraBoundaries, 0.5, 0.5); + assertTrue(CameraRegionUtils.MeteringRectangleFactory.create(45, 45, 10, 10, 1).equals(r)); + + // Top left + r = CameraRegionUtils.convertPointToMeteringRectangle(this.mockCameraBoundaries, 0.0, 0.0); + assertTrue(CameraRegionUtils.MeteringRectangleFactory.create(0, 0, 10, 10, 1).equals(r)); + + // Bottom right + r = CameraRegionUtils.convertPointToMeteringRectangle(this.mockCameraBoundaries, 1.0, 1.0); + assertTrue(CameraRegionUtils.MeteringRectangleFactory.create(89, 89, 10, 10, 1).equals(r)); + + // Top left + r = CameraRegionUtils.convertPointToMeteringRectangle(this.mockCameraBoundaries, 0.0, 1.0); + assertTrue(CameraRegionUtils.MeteringRectangleFactory.create(0, 89, 10, 10, 1).equals(r)); + + // Top right + r = CameraRegionUtils.convertPointToMeteringRectangle(this.mockCameraBoundaries, 1.0, 0.0); + assertTrue(CameraRegionUtils.MeteringRectangleFactory.create(89, 0, 10, 10, 1).equals(r)); + } + } + + @Test(expected = AssertionError.class) + public void getMeteringRectangleForPoint_should_throw_for_0_width_boundary() { + new io.flutter.plugins.camera.CameraRegions(new Size(0, 50)); + } + + @Test(expected = AssertionError.class) + public void getMeteringRectangleForPoint_should_throw_for_0_height_boundary() { + new io.flutter.plugins.camera.CameraRegions(new Size(100, 0)); + } + + private static void updateSdkVersion(int version) { + TestUtils.setFinalStatic(Build.VERSION.class, "SDK_INT", version); + } +} 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 0aedc59ef635..1c38affa28cc 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 @@ -6,9 +6,9 @@ import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNull; 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; @@ -17,41 +17,36 @@ import android.hardware.camera2.CaptureRequest; import android.hardware.camera2.params.MeteringRectangle; +import android.util.Size; import io.flutter.plugins.camera.CameraProperties; +import io.flutter.plugins.camera.CameraRegionUtils; import io.flutter.plugins.camera.features.Point; -import io.flutter.plugins.camera.types.CameraRegions; import org.junit.Test; +import org.mockito.MockedStatic; +import org.mockito.Mockito; public class ExposurePointFeatureTest { @Test public void getDebugName_should_return_the_name_of_the_feature() { CameraProperties mockCameraProperties = mock(CameraProperties.class); - CameraRegions mockCameraRegions = mock(CameraRegions.class); - ExposurePointFeature exposurePointFeature = - new ExposurePointFeature(mockCameraProperties, mockCameraRegions); + CameraRegionUtils mockCameraRegions = mock(CameraRegionUtils.class); + ExposurePointFeature exposurePointFeature = new ExposurePointFeature(mockCameraProperties); assertEquals("ExposurePointFeature", exposurePointFeature.getDebugName()); } @Test - public void getValue_should_return_default_point_if_not_set() { + public void getValue_should_return_null_if_not_set() { 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 exposurePointFeature = new ExposurePointFeature(mockCameraProperties); Point actualPoint = exposurePointFeature.getValue(); - - assertEquals(expectedPoint.x, actualPoint.x); - assertEquals(expectedPoint.y, actualPoint.y); + assertNull(exposurePointFeature.getValue()); } @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); + ExposurePointFeature exposurePointFeature = new ExposurePointFeature(mockCameraProperties); Point expectedPoint = new Point(0.0, 0.0); exposurePointFeature.setValue(expectedPoint); @@ -63,45 +58,113 @@ public void getValue_should_echo_the_set_value() { @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 exposurePointFeature = new ExposurePointFeature(mockCameraProperties); exposurePointFeature.setValue(new Point(null, 0.0)); - verify(mockCameraRegions, times(1)).resetAutoExposureMeteringRectangle(); + assertNull(exposurePointFeature.getValue()); } @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 exposurePointFeature = new ExposurePointFeature(mockCameraProperties); exposurePointFeature.setValue(new Point(0.0, null)); - verify(mockCameraRegions, times(1)).resetAutoExposureMeteringRectangle(); + assertNull(exposurePointFeature.getValue()); } @Test - public void setValue_should_reset_point_when_valid_coords_are_supplied() { + public void setValue_should_set_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); Point point = new Point(0.0, 0.0); exposurePointFeature.setValue(point); - verify(mockCameraRegions, times(1)).setAutoExposureMeteringRectangleFromPoint(point.x, point.y); + assertEquals(point, exposurePointFeature.getValue()); + } + + @Test + public void + setValue_should_determine_metering_rectangle_when_valid_boundaries_and_coords_are_supplied() { + CameraProperties mockCameraProperties = mock(CameraProperties.class); + when(mockCameraProperties.getControlMaxRegionsAutoExposure()).thenReturn(1); + ExposurePointFeature exposurePointFeature = new ExposurePointFeature(mockCameraProperties); + Size mockedCameraBoundaries = mock(Size.class); + exposurePointFeature.setCameraBoundaries(mockedCameraBoundaries); + + try (MockedStatic mockedCameraRegionUtils = + Mockito.mockStatic(CameraRegionUtils.class)) { + + exposurePointFeature.setValue(new Point(0.5, 0.5)); + + mockedCameraRegionUtils.verify( + () -> CameraRegionUtils.convertPointToMeteringRectangle(mockedCameraBoundaries, 0.5, 0.5), + times(1)); + } + } + + @Test + public void setValue_should_not_determine_metering_rectangle_when_no_valid_boundaries_are_set() { + CameraProperties mockCameraProperties = mock(CameraProperties.class); + when(mockCameraProperties.getControlMaxRegionsAutoExposure()).thenReturn(1); + ExposurePointFeature exposurePointFeature = new ExposurePointFeature(mockCameraProperties); + + try (MockedStatic mockedCameraRegionUtils = + Mockito.mockStatic(CameraRegionUtils.class)) { + + exposurePointFeature.setValue(new Point(0.5, 0.5)); + + mockedCameraRegionUtils.verifyNoInteractions(); + } + } + + @Test + public void setValue_should_not_determine_metering_rectangle_when_null_coords_are_set() { + CameraProperties mockCameraProperties = mock(CameraProperties.class); + when(mockCameraProperties.getControlMaxRegionsAutoExposure()).thenReturn(1); + ExposurePointFeature exposurePointFeature = new ExposurePointFeature(mockCameraProperties); + Size mockedCameraBoundaries = mock(Size.class); + exposurePointFeature.setCameraBoundaries(mockedCameraBoundaries); + + try (MockedStatic mockedCameraRegionUtils = + Mockito.mockStatic(CameraRegionUtils.class)) { + + exposurePointFeature.setValue(null); + exposurePointFeature.setValue(new Point(null, 0.5)); + exposurePointFeature.setValue(new Point(0.5, null)); + + mockedCameraRegionUtils.verifyNoInteractions(); + } + } + + @Test + public void + setCameraBoundaries_should_determine_metering_rectangle_when_valid_boundaries_and_coords_are_supplied() { + CameraProperties mockCameraProperties = mock(CameraProperties.class); + when(mockCameraProperties.getControlMaxRegionsAutoExposure()).thenReturn(1); + ExposurePointFeature exposurePointFeature = new ExposurePointFeature(mockCameraProperties); + exposurePointFeature.setValue(new Point(0.5, 0.5)); + Size mockedCameraBoundaries = mock(Size.class); + + try (MockedStatic mockedCameraRegionUtils = + Mockito.mockStatic(CameraRegionUtils.class)) { + + exposurePointFeature.setCameraBoundaries(mockedCameraBoundaries); + + mockedCameraRegionUtils.verify( + () -> CameraRegionUtils.convertPointToMeteringRectangle(mockedCameraBoundaries, 0.5, 0.5), + times(1)); + } } @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); + exposurePointFeature.setCameraBoundaries(new Size(100, 100)); when(mockCameraProperties.getControlMaxRegionsAutoExposure()).thenReturn(null); @@ -111,8 +174,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); + exposurePointFeature.setCameraBoundaries(new Size(100, 100)); when(mockCameraProperties.getControlMaxRegionsAutoExposure()).thenReturn(0); @@ -122,8 +185,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); + exposurePointFeature.setCameraBoundaries(new Size(100, 100)); when(mockCameraProperties.getControlMaxRegionsAutoExposure()).thenReturn(1); @@ -133,64 +196,75 @@ public void checkIsSupported_should_return_true_when_max_regions_is_bigger_then_ @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); + CaptureRequest.Builder mockCaptureRequestBuilder = mock(CaptureRequest.Builder.class); + ExposurePointFeature exposurePointFeature = new ExposurePointFeature(mockCameraProperties); when(mockCameraProperties.getControlMaxRegionsAutoExposure()).thenReturn(0); - exposurePointFeature.updateBuilder(null); + exposurePointFeature.updateBuilder(mockCaptureRequestBuilder); - verify(mockCameraRegions, never()).getAEMeteringRectangle(); + verify(mockCaptureRequestBuilder, never()).set(any(), any()); } @Test - public void updateBuilder_should_set_ae_regions_to_null_when_ae_metering_rectangle_is_null() { + public void + updateBuilder_should_set_metering_rectangle_when_valid_boundaries_and_coords_are_supplied() { 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); + CaptureRequest.Builder mockCaptureRequestBuilder = mock(CaptureRequest.Builder.class); + ExposurePointFeature exposurePointFeature = new ExposurePointFeature(mockCameraProperties); + Size mockedCameraBoundaries = mock(Size.class); + MeteringRectangle mockedMeteringRectangle = mock(MeteringRectangle.class); + + try (MockedStatic mockedCameraRegionUtils = + Mockito.mockStatic(CameraRegionUtils.class)) { + mockedCameraRegionUtils + .when( + () -> + CameraRegionUtils.convertPointToMeteringRectangle( + mockedCameraBoundaries, 0.5, 0.5)) + .thenReturn(mockedMeteringRectangle); + exposurePointFeature.setCameraBoundaries(mockedCameraBoundaries); + exposurePointFeature.setValue(new Point(0.5, 0.5)); + + exposurePointFeature.updateBuilder(mockCaptureRequestBuilder); + } + + verify(mockCaptureRequestBuilder, times(1)) + .set(CaptureRequest.CONTROL_AE_REGIONS, new MeteringRectangle[] {mockedMeteringRectangle}); } @Test - public void updateBuilder_should_set_ae_regions_with_metering_rectangle() { + public void + updateBuilder_should_not_set_metering_rectangle_when_no_valid_boundaries_are_supplied() { 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); + CaptureRequest.Builder mockCaptureRequestBuilder = mock(CaptureRequest.Builder.class); + ExposurePointFeature exposurePointFeature = new ExposurePointFeature(mockCameraProperties); + MeteringRectangle mockedMeteringRectangle = mock(MeteringRectangle.class); + + exposurePointFeature.setValue(new Point(0.5, 0.5)); - exposurePointFeature.updateBuilder(mockBuilder); + exposurePointFeature.updateBuilder(mockCaptureRequestBuilder); - verify(mockBuilder, times(1)) - .set(eq(CaptureRequest.CONTROL_AE_REGIONS), any(MeteringRectangle[].class)); + verify(mockCaptureRequestBuilder, never()).set(any(), any()); } @Test - public void updateBuilder_should_silently_fail_when_exception_occurs() { + public void dateBuilder_should_not_set_metering_rectangle_when_no_valid_coords_are_supplied() { 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()).thenThrow(new IllegalArgumentException()); - - exposurePointFeature.updateBuilder(mockBuilder); - - verify(mockBuilder, times(1)).set(CaptureRequest.CONTROL_AE_REGIONS, null); + CaptureRequest.Builder mockCaptureRequestBuilder = mock(CaptureRequest.Builder.class); + ExposurePointFeature exposurePointFeature = new ExposurePointFeature(mockCameraProperties); + + exposurePointFeature.setValue(null); + exposurePointFeature.updateBuilder(mockCaptureRequestBuilder); + verify(mockCaptureRequestBuilder, never()).set(any(), any()); + exposurePointFeature.setValue(new Point(0d, null)); + exposurePointFeature.updateBuilder(mockCaptureRequestBuilder); + verify(mockCaptureRequestBuilder, never()).set(any(), any()); + exposurePointFeature.setValue(new Point(null, 0d)); + exposurePointFeature.updateBuilder(mockCaptureRequestBuilder); + verify(mockCaptureRequestBuilder, never()).set(any(), any()); } } 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 new file mode 100644 index 000000000000..ec733e64aaba --- /dev/null +++ b/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/features/focuspoint/FocusPointFeatureTest.java @@ -0,0 +1,270 @@ +// Copyright 2013 The Flutter 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; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNull; +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.hardware.camera2.params.MeteringRectangle; +import android.util.Size; +import io.flutter.plugins.camera.CameraProperties; +import io.flutter.plugins.camera.CameraRegionUtils; +import io.flutter.plugins.camera.features.Point; +import org.junit.Test; +import org.mockito.MockedStatic; +import org.mockito.Mockito; + +public class FocusPointFeatureTest { + @Test + public void getDebugName_should_return_the_name_of_the_feature() { + CameraProperties mockCameraProperties = mock(CameraProperties.class); + CameraRegionUtils mockCameraRegions = mock(CameraRegionUtils.class); + FocusPointFeature focusPointFeature = new FocusPointFeature(mockCameraProperties); + + assertEquals("FocusPointFeature", focusPointFeature.getDebugName()); + } + + @Test + public void getValue_should_return_null_if_not_set() { + CameraProperties mockCameraProperties = mock(CameraProperties.class); + FocusPointFeature focusPointFeature = new FocusPointFeature(mockCameraProperties); + Point actualPoint = focusPointFeature.getValue(); + assertNull(focusPointFeature.getValue()); + } + + @Test + public void getValue_should_echo_the_set_value() { + CameraProperties mockCameraProperties = mock(CameraProperties.class); + FocusPointFeature focusPointFeature = new FocusPointFeature(mockCameraProperties); + 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); + FocusPointFeature focusPointFeature = new FocusPointFeature(mockCameraProperties); + + focusPointFeature.setValue(new Point(null, 0.0)); + + assertNull(focusPointFeature.getValue()); + } + + @Test + public void setValue_should_reset_point_when_y_coord_is_null() { + CameraProperties mockCameraProperties = mock(CameraProperties.class); + FocusPointFeature focusPointFeature = new FocusPointFeature(mockCameraProperties); + + focusPointFeature.setValue(new Point(0.0, null)); + + assertNull(focusPointFeature.getValue()); + } + + @Test + public void setValue_should_set_point_when_valid_coords_are_supplied() { + CameraProperties mockCameraProperties = mock(CameraProperties.class); + FocusPointFeature focusPointFeature = new FocusPointFeature(mockCameraProperties); + Point point = new Point(0.0, 0.0); + + focusPointFeature.setValue(point); + + assertEquals(point, focusPointFeature.getValue()); + } + + @Test + public void + setValue_should_determine_metering_rectangle_when_valid_boundaries_and_coords_are_supplied() { + CameraProperties mockCameraProperties = mock(CameraProperties.class); + when(mockCameraProperties.getControlMaxRegionsAutoFocus()).thenReturn(1); + FocusPointFeature focusPointFeature = new FocusPointFeature(mockCameraProperties); + Size mockedCameraBoundaries = mock(Size.class); + focusPointFeature.setCameraBoundaries(mockedCameraBoundaries); + + try (MockedStatic mockedCameraRegionUtils = + Mockito.mockStatic(CameraRegionUtils.class)) { + + focusPointFeature.setValue(new Point(0.5, 0.5)); + + mockedCameraRegionUtils.verify( + () -> CameraRegionUtils.convertPointToMeteringRectangle(mockedCameraBoundaries, 0.5, 0.5), + times(1)); + } + } + + @Test + public void setValue_should_not_determine_metering_rectangle_when_no_valid_boundaries_are_set() { + CameraProperties mockCameraProperties = mock(CameraProperties.class); + when(mockCameraProperties.getControlMaxRegionsAutoFocus()).thenReturn(1); + FocusPointFeature focusPointFeature = new FocusPointFeature(mockCameraProperties); + + try (MockedStatic mockedCameraRegionUtils = + Mockito.mockStatic(CameraRegionUtils.class)) { + + focusPointFeature.setValue(new Point(0.5, 0.5)); + + mockedCameraRegionUtils.verifyNoInteractions(); + } + } + + @Test + public void setValue_should_not_determine_metering_rectangle_when_null_coords_are_set() { + CameraProperties mockCameraProperties = mock(CameraProperties.class); + when(mockCameraProperties.getControlMaxRegionsAutoFocus()).thenReturn(1); + FocusPointFeature focusPointFeature = new FocusPointFeature(mockCameraProperties); + Size mockedCameraBoundaries = mock(Size.class); + focusPointFeature.setCameraBoundaries(mockedCameraBoundaries); + + try (MockedStatic mockedCameraRegionUtils = + Mockito.mockStatic(CameraRegionUtils.class)) { + + focusPointFeature.setValue(null); + focusPointFeature.setValue(new Point(null, 0.5)); + focusPointFeature.setValue(new Point(0.5, null)); + + mockedCameraRegionUtils.verifyNoInteractions(); + } + } + + @Test + public void + setCameraBoundaries_should_determine_metering_rectangle_when_valid_boundaries_and_coords_are_supplied() { + CameraProperties mockCameraProperties = mock(CameraProperties.class); + when(mockCameraProperties.getControlMaxRegionsAutoFocus()).thenReturn(1); + FocusPointFeature focusPointFeature = new FocusPointFeature(mockCameraProperties); + focusPointFeature.setValue(new Point(0.5, 0.5)); + Size mockedCameraBoundaries = mock(Size.class); + + try (MockedStatic mockedCameraRegionUtils = + Mockito.mockStatic(CameraRegionUtils.class)) { + + focusPointFeature.setCameraBoundaries(mockedCameraBoundaries); + + mockedCameraRegionUtils.verify( + () -> CameraRegionUtils.convertPointToMeteringRectangle(mockedCameraBoundaries, 0.5, 0.5), + times(1)); + } + } + + @Test + public void checkIsSupported_should_return_false_when_max_regions_is_null() { + CameraProperties mockCameraProperties = mock(CameraProperties.class); + FocusPointFeature focusPointFeature = new FocusPointFeature(mockCameraProperties); + focusPointFeature.setCameraBoundaries(new Size(100, 100)); + + 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); + focusPointFeature.setCameraBoundaries(new Size(100, 100)); + + 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); + focusPointFeature.setCameraBoundaries(new Size(100, 100)); + + when(mockCameraProperties.getControlMaxRegionsAutoFocus()).thenReturn(1); + + assertTrue(focusPointFeature.checkIsSupported()); + } + + @Test + public void updateBuilder_should_return_when_checkIsSupported_is_false() { + CameraProperties mockCameraProperties = mock(CameraProperties.class); + CaptureRequest.Builder mockCaptureRequestBuilder = mock(CaptureRequest.Builder.class); + FocusPointFeature focusPointFeature = new FocusPointFeature(mockCameraProperties); + + when(mockCameraProperties.getControlMaxRegionsAutoFocus()).thenReturn(0); + + focusPointFeature.updateBuilder(mockCaptureRequestBuilder); + + verify(mockCaptureRequestBuilder, never()).set(any(), any()); + } + + @Test + public void + updateBuilder_should_set_metering_rectangle_when_valid_boundaries_and_coords_are_supplied() { + CameraProperties mockCameraProperties = mock(CameraProperties.class); + when(mockCameraProperties.getControlMaxRegionsAutoFocus()).thenReturn(1); + CaptureRequest.Builder mockCaptureRequestBuilder = mock(CaptureRequest.Builder.class); + FocusPointFeature focusPointFeature = new FocusPointFeature(mockCameraProperties); + Size mockedCameraBoundaries = mock(Size.class); + MeteringRectangle mockedMeteringRectangle = mock(MeteringRectangle.class); + + try (MockedStatic mockedCameraRegionUtils = + Mockito.mockStatic(CameraRegionUtils.class)) { + mockedCameraRegionUtils + .when( + () -> + CameraRegionUtils.convertPointToMeteringRectangle( + mockedCameraBoundaries, 0.5, 0.5)) + .thenReturn(mockedMeteringRectangle); + focusPointFeature.setCameraBoundaries(mockedCameraBoundaries); + focusPointFeature.setValue(new Point(0.5, 0.5)); + + focusPointFeature.updateBuilder(mockCaptureRequestBuilder); + } + + verify(mockCaptureRequestBuilder, times(1)) + .set(CaptureRequest.CONTROL_AE_REGIONS, new MeteringRectangle[] {mockedMeteringRectangle}); + } + + @Test + public void + updateBuilder_should_not_set_metering_rectangle_when_no_valid_boundaries_are_supplied() { + CameraProperties mockCameraProperties = mock(CameraProperties.class); + when(mockCameraProperties.getControlMaxRegionsAutoFocus()).thenReturn(1); + CaptureRequest.Builder mockCaptureRequestBuilder = mock(CaptureRequest.Builder.class); + FocusPointFeature focusPointFeature = new FocusPointFeature(mockCameraProperties); + MeteringRectangle mockedMeteringRectangle = mock(MeteringRectangle.class); + + focusPointFeature.setValue(new Point(0.5, 0.5)); + + focusPointFeature.updateBuilder(mockCaptureRequestBuilder); + + verify(mockCaptureRequestBuilder, never()).set(any(), any()); + } + + @Test + public void dateBuilder_should_not_set_metering_rectangle_when_no_valid_coords_are_supplied() { + CameraProperties mockCameraProperties = mock(CameraProperties.class); + when(mockCameraProperties.getControlMaxRegionsAutoFocus()).thenReturn(1); + CaptureRequest.Builder mockCaptureRequestBuilder = mock(CaptureRequest.Builder.class); + FocusPointFeature focusPointFeature = new FocusPointFeature(mockCameraProperties); + + focusPointFeature.setValue(null); + focusPointFeature.updateBuilder(mockCaptureRequestBuilder); + verify(mockCaptureRequestBuilder, never()).set(any(), any()); + focusPointFeature.setValue(new Point(0d, null)); + focusPointFeature.updateBuilder(mockCaptureRequestBuilder); + verify(mockCaptureRequestBuilder, never()).set(any(), any()); + focusPointFeature.setValue(new Point(null, 0d)); + focusPointFeature.updateBuilder(mockCaptureRequestBuilder); + verify(mockCaptureRequestBuilder, never()).set(any(), any()); + } +} diff --git a/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/types/CameraRegionsFactoryTest.java b/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/types/CameraRegionsFactoryTest.java deleted file mode 100644 index 5fa0c2c4a2a4..000000000000 --- a/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/types/CameraRegionsFactoryTest.java +++ /dev/null @@ -1,201 +0,0 @@ -// Copyright 2013 The Flutter 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.types; - -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertNull; -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.Before; -import org.junit.Test; - -public class CameraRegionsFactoryTest { - private Size mockSize; - - @Before - public void before() { - mockSize = mock(Size.class); - - when(mockSize.getHeight()).thenReturn(640); - when(mockSize.getWidth()).thenReturn(480); - } - - @Test - public void - create_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); - - CameraRegions cameraRegions = CameraRegions.Factory.create(mockCameraProperties, mockBuilder); - - assertEquals(mockSize, cameraRegions.getBoundaries()); - verify(mockCameraProperties, never()).getSensorInfoPreCorrectionActiveArraySize(); - verify(mockCameraProperties, never()).getSensorInfoActiveArraySize(); - } finally { - updateSdkVersion(0); - } - } - - @Test - public void - create_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); - - CameraRegions cameraRegions = CameraRegions.Factory.create(mockCameraProperties, mockBuilder); - - assertEquals(mockSize, cameraRegions.getBoundaries()); - verify(mockCameraProperties, never()).getSensorInfoPreCorrectionActiveArraySize(); - verify(mockCameraProperties, never()).getSensorInfoActiveArraySize(); - } finally { - updateSdkVersion(0); - } - } - - @Test - public void - create_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); - - CameraRegions cameraRegions = CameraRegions.Factory.create(mockCameraProperties, mockBuilder); - - assertEquals(mockSize, cameraRegions.getBoundaries()); - verify(mockCameraProperties, never()).getSensorInfoPreCorrectionActiveArraySize(); - verify(mockCameraProperties, never()).getSensorInfoActiveArraySize(); - } finally { - updateSdkVersion(0); - } - } - - @Test - public void - create_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); - - CameraRegions cameraRegions = CameraRegions.Factory.create(mockCameraProperties, mockBuilder); - - assertNull(cameraRegions.getBoundaries()); - verify(mockCameraProperties, never()).getSensorInfoPixelArraySize(); - verify(mockCameraProperties, never()).getSensorInfoActiveArraySize(); - } finally { - updateSdkVersion(0); - } - } - - @Test - public void - create_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); - - CameraRegions cameraRegions = CameraRegions.Factory.create(mockCameraProperties, mockBuilder); - - assertNull(cameraRegions.getBoundaries()); - 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); - - CameraRegions cameraRegions = CameraRegions.Factory.create(mockCameraProperties, mockBuilder); - - assertNull(cameraRegions.getBoundaries()); - verify(mockCameraProperties, never()).getSensorInfoPixelArraySize(); - verify(mockCameraProperties, never()).getSensorInfoPreCorrectionActiveArraySize(); - } finally { - updateSdkVersion(0); - } - } - - @Test - public void getBoundaries_should_return_null_if_not_set() { - CameraProperties mockCameraProperties = mock(CameraProperties.class); - CaptureRequest.Builder mockBuilder = mock(CaptureRequest.Builder.class); - CameraRegions cameraRegions = CameraRegions.Factory.create(mockCameraProperties, mockBuilder); - - assertNull(cameraRegions.getBoundaries()); - } - - private static void updateSdkVersion(int version) { - TestUtils.setFinalStatic(VERSION.class, "SDK_INT", version); - } -} diff --git a/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/types/CameraRegionsTest.java b/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/types/CameraRegionsTest.java deleted file mode 100644 index b760e1e9ca29..000000000000 --- a/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/types/CameraRegionsTest.java +++ /dev/null @@ -1,114 +0,0 @@ -// Copyright 2013 The Flutter 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.types; - -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 { - io.flutter.plugins.camera.types.CameraRegions cameraRegions; - - @Before - public void setUp() { - this.cameraRegions = new io.flutter.plugins.camera.types.CameraRegions(new Size(100, 100)); - } - - @Test(expected = AssertionError.class) - public void getMeteringRectangleForPoint_should_throw_for_x_upper_bound() { - cameraRegions.convertPointToMeteringRectangle(1.5, 0); - } - - @Test(expected = AssertionError.class) - public void getMeteringRectangleForPoint_should_throw_for_x_lower_bound() { - cameraRegions.convertPointToMeteringRectangle(-0.5, 0); - } - - @Test(expected = AssertionError.class) - public void getMeteringRectangleForPoint_should_throw_for_y_upper_bound() { - cameraRegions.convertPointToMeteringRectangle(0, 1.5); - } - - @Test(expected = AssertionError.class) - public void getMeteringRectangleForPoint_should_throw_for_y_lower_bound() { - cameraRegions.convertPointToMeteringRectangle(0, -0.5); - } - - @Test - public void getMeteringRectangleForPoint_should_return_valid_MeteringRectangle() { - MeteringRectangle r; - // Center - r = cameraRegions.convertPointToMeteringRectangle(0.5, 0.5); - assertEquals(new MeteringRectangle(45, 45, 10, 10, 1), r); - - // Top left - r = cameraRegions.convertPointToMeteringRectangle(0.0, 0.0); - assertEquals(new MeteringRectangle(0, 0, 10, 10, 1), r); - - // Bottom right - r = cameraRegions.convertPointToMeteringRectangle(1.0, 1.0); - assertEquals(new MeteringRectangle(89, 89, 10, 10, 1), r); - - // Top left - r = cameraRegions.convertPointToMeteringRectangle(0.0, 1.0); - assertEquals(new MeteringRectangle(0, 89, 10, 10, 1), r); - - // Top right - r = cameraRegions.convertPointToMeteringRectangle(1.0, 0.0); - assertEquals(new MeteringRectangle(89, 0, 10, 10, 1), r); - } - - @Test(expected = AssertionError.class) - public void constructor_should_throw_for_0_width_boundary() { - new io.flutter.plugins.camera.CameraRegions(new Size(0, 50)); - } - - @Test(expected = AssertionError.class) - public void constructor_should_throw_for_0_height_boundary() { - new io.flutter.plugins.camera.CameraRegions(new Size(100, 0)); - } - - @Test - public void setAutoExposureMeteringRectangleFromPoint_should_set_aeMeteringRectangle_for_point() { - cameraRegions.setAutoExposureMeteringRectangleFromPoint(0, 0); - assertEquals(new MeteringRectangle(0, 0, 10, 10, 1), cameraRegions.getAEMeteringRectangle()); - } - - @Test - public void resetAutoExposureMeteringRectangle_should_reset_aeMeteringRectangle() { - io.flutter.plugins.camera.types.CameraRegions cr = - new io.flutter.plugins.camera.types.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() { - io.flutter.plugins.camera.types.CameraRegions cr = - new io.flutter.plugins.camera.types.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() { - io.flutter.plugins.camera.types.CameraRegions cr = - new io.flutter.plugins.camera.types.CameraRegions(new Size(100, 50)); - cr.setAutoFocusMeteringRectangleFromPoint(0, 0); - assertNotNull(cr.getAFMeteringRectangle()); - cr.resetAutoFocusMeteringRectangle(); - assertNull(cr.getAFMeteringRectangle()); - } -} From 94fed0898430c439a6a45bbb22078b14c97023ce Mon Sep 17 00:00:00 2001 From: BeMacized Date: Mon, 14 Jun 2021 08:43:16 +0200 Subject: [PATCH 20/63] Added noise reduction feature --- .../noisereduction/NoiseReductionFeature.java | 89 +++++++++++ .../noisereduction/NoiseReductionMode.java | 32 ++++ .../NoiseReductionFeatureTest.java | 151 ++++++++++++++++++ 3 files changed, 272 insertions(+) create mode 100644 packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/noisereduction/NoiseReductionFeature.java create mode 100644 packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/noisereduction/NoiseReductionMode.java create mode 100644 packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/features/noisereduction/NoiseReductionFeatureTest.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 new file mode 100644 index 000000000000..b24fda4bdd8d --- /dev/null +++ b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/noisereduction/NoiseReductionFeature.java @@ -0,0 +1,89 @@ +// 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; +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 + * and full support the fast mode. + * https://developer.android.com/reference/android/hardware/camera2/CameraCharacteristics#NOISE_REDUCTION_AVAILABLE_NOISE_REDUCTION_MODES + */ +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); + 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); + } + + @Override + public String getDebugName() { + return "NoiseReductionFeature"; + } + + @Override + public NoiseReductionMode getValue() { + return currentSetting; + } + + @Override + public void setValue(NoiseReductionMode value) { + this.currentSetting = value; + } + + @Override + 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; + * 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) { + if (!checkIsSupported()) { + return; + } + + Log.i("Camera", "updateNoiseReduction | currentSetting: " + currentSetting); + + // Always use fast mode. + requestBuilder.set( + 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 new file mode 100644 index 000000000000..5219fd4c046e --- /dev/null +++ b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/noisereduction/NoiseReductionMode.java @@ -0,0 +1,32 @@ +// 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. */ +public enum NoiseReductionMode { + off("off"), + fast("fast"), + highQuality("highQuality"), + minimal("minimal"), + zeroShutterLag("zeroShutterLag"); + + 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/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..4905c68bce8a --- /dev/null +++ b/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/features/noisereduction/NoiseReductionFeatureTest.java @@ -0,0 +1,151 @@ +// 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.After; +import org.junit.Before; +import org.junit.Test; + +public class NoiseReductionFeatureTest { + @Before + 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); + } + + @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); + } + + @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); + } +} From 4f169aba29756c7509143fcf7599739bd535ddb6 Mon Sep 17 00:00:00 2001 From: BeMacized Date: Tue, 15 Jun 2021 13:56:02 +0200 Subject: [PATCH 21/63] Implemented PR feedback --- packages/camera/camera/android/build.gradle | 2 +- .../exposurepoint/ExposurePointFeature.java | 9 ++--- .../focuspoint/FocusPointFeature.java | 13 +++---- .../plugins/camera/CameraRegionUtilsTest.java | 3 ++ .../ExposurePointFeatureTest.java | 35 ++++++++++++------- .../focuspoint/FocusPointFeatureTest.java | 35 ++++++++++++------- 6 files changed, 62 insertions(+), 35 deletions(-) diff --git a/packages/camera/camera/android/build.gradle b/packages/camera/camera/android/build.gradle index 21b25ad66dea..65c6d26edb49 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-inline:3.11.0' + testImplementation 'org.mockito:mockito-inline:3.11.1' 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/features/exposurepoint/ExposurePointFeature.java b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/exposurepoint/ExposurePointFeature.java index 905c9922f600..b98e2666bec2 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.Size; +import androidx.annotation.NonNull; import io.flutter.plugins.camera.CameraProperties; import io.flutter.plugins.camera.CameraRegionUtils; import io.flutter.plugins.camera.features.CameraFeature; @@ -33,7 +34,7 @@ public ExposurePointFeature(CameraProperties cameraProperties) { * * @param cameraBoundaries - The camera boundaries to set. */ - public void setCameraBoundaries(Size cameraBoundaries) { + public void setCameraBoundaries(@NonNull Size cameraBoundaries) { this.cameraBoundaries = cameraBoundaries; this.buildExposureRectangle(); } @@ -57,7 +58,6 @@ public void setValue(Point value) { // Whether or not this camera can set the exposure point. @Override public boolean checkIsSupported() { - if (cameraBoundaries == null) return false; Integer supportedRegions = cameraProperties.getControlMaxRegionsAutoExposure(); return supportedRegions != null && supportedRegions > 0; } @@ -73,8 +73,9 @@ public void updateBuilder(CaptureRequest.Builder requestBuilder) { } private void buildExposureRectangle() { - if (!checkIsSupported()) { - return; + if (this.cameraBoundaries == null) { + throw new AssertionError( + "The cameraBoundaries should be set (using `ExposurePointFeature.setCameraBoundaries(Size)`) before updating the exposure point."); } if (this.exposurePoint == null) { this.exposureRectangle = null; 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 8f2941646b4a..92fcfa9f1132 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 @@ -1,4 +1,4 @@ -// Copyright 2019 The Chromium Authors. All rights reserved. +// Copyright 2013 The Flutter Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. @@ -7,6 +7,7 @@ import android.hardware.camera2.CaptureRequest; import android.hardware.camera2.params.MeteringRectangle; import android.util.Size; +import androidx.annotation.NonNull; import io.flutter.plugins.camera.CameraProperties; import io.flutter.plugins.camera.CameraRegionUtils; import io.flutter.plugins.camera.features.CameraFeature; @@ -33,7 +34,7 @@ public FocusPointFeature(CameraProperties cameraProperties) { * * @param cameraBoundaries - The camera boundaries to set. */ - public void setCameraBoundaries(Size cameraBoundaries) { + public void setCameraBoundaries(@NonNull Size cameraBoundaries) { this.cameraBoundaries = cameraBoundaries; this.buildFocusRectangle(); } @@ -54,10 +55,9 @@ public void setValue(Point value) { this.buildFocusRectangle(); } - // Whether or not this camera can set the exposure point. + // Whether or not this camera can set the focus point. @Override public boolean checkIsSupported() { - if (cameraBoundaries == null) return false; Integer supportedRegions = cameraProperties.getControlMaxRegionsAutoFocus(); return supportedRegions != null && supportedRegions > 0; } @@ -73,8 +73,9 @@ public void updateBuilder(CaptureRequest.Builder requestBuilder) { } private void buildFocusRectangle() { - if (!checkIsSupported()) { - return; + if (this.cameraBoundaries == null) { + throw new AssertionError( + "The cameraBoundaries should be set (using `FocusPointFeature.setCameraBoundaries(Size)`) before updating the focus point."); } if (this.focusPoint == null) { this.focusRectangle = null; diff --git a/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/CameraRegionUtilsTest.java b/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/CameraRegionUtilsTest.java index e2255f2c665c..e248e8adfeb4 100644 --- a/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/CameraRegionUtilsTest.java +++ b/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/CameraRegionUtilsTest.java @@ -1,3 +1,6 @@ +// Copyright 2013 The Flutter 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; 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 1c38affa28cc..4a515c6fd0ec 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 @@ -9,6 +9,7 @@ import static org.junit.Assert.assertNull; import static org.junit.Assert.assertTrue; import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.isNull; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.never; import static org.mockito.Mockito.times; @@ -21,11 +22,22 @@ import io.flutter.plugins.camera.CameraProperties; import io.flutter.plugins.camera.CameraRegionUtils; import io.flutter.plugins.camera.features.Point; +import org.junit.Before; import org.junit.Test; import org.mockito.MockedStatic; import org.mockito.Mockito; public class ExposurePointFeatureTest { + + Size mockCameraBoundaries; + + @Before + public void setUp() { + this.mockCameraBoundaries = mock(Size.class); + when(this.mockCameraBoundaries.getWidth()).thenReturn(100); + when(this.mockCameraBoundaries.getHeight()).thenReturn(100); + } + @Test public void getDebugName_should_return_the_name_of_the_feature() { CameraProperties mockCameraProperties = mock(CameraProperties.class); @@ -47,6 +59,7 @@ public void getValue_should_return_null_if_not_set() { public void getValue_should_echo_the_set_value() { CameraProperties mockCameraProperties = mock(CameraProperties.class); ExposurePointFeature exposurePointFeature = new ExposurePointFeature(mockCameraProperties); + exposurePointFeature.setCameraBoundaries(this.mockCameraBoundaries); Point expectedPoint = new Point(0.0, 0.0); exposurePointFeature.setValue(expectedPoint); @@ -59,6 +72,7 @@ public void getValue_should_echo_the_set_value() { public void setValue_should_reset_point_when_x_coord_is_null() { CameraProperties mockCameraProperties = mock(CameraProperties.class); ExposurePointFeature exposurePointFeature = new ExposurePointFeature(mockCameraProperties); + exposurePointFeature.setCameraBoundaries(this.mockCameraBoundaries); exposurePointFeature.setValue(new Point(null, 0.0)); @@ -69,6 +83,7 @@ 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); ExposurePointFeature exposurePointFeature = new ExposurePointFeature(mockCameraProperties); + exposurePointFeature.setCameraBoundaries(this.mockCameraBoundaries); exposurePointFeature.setValue(new Point(0.0, null)); @@ -79,6 +94,7 @@ public void setValue_should_reset_point_when_y_coord_is_null() { public void setValue_should_set_point_when_valid_coords_are_supplied() { CameraProperties mockCameraProperties = mock(CameraProperties.class); ExposurePointFeature exposurePointFeature = new ExposurePointFeature(mockCameraProperties); + exposurePointFeature.setCameraBoundaries(this.mockCameraBoundaries); Point point = new Point(0.0, 0.0); exposurePointFeature.setValue(point); @@ -106,18 +122,15 @@ public void setValue_should_set_point_when_valid_coords_are_supplied() { } } - @Test - public void setValue_should_not_determine_metering_rectangle_when_no_valid_boundaries_are_set() { + @Test(expected = AssertionError.class) + public void setValue_should_throw_assertion_error_when_no_valid_boundaries_are_set() { CameraProperties mockCameraProperties = mock(CameraProperties.class); when(mockCameraProperties.getControlMaxRegionsAutoExposure()).thenReturn(1); ExposurePointFeature exposurePointFeature = new ExposurePointFeature(mockCameraProperties); try (MockedStatic mockedCameraRegionUtils = Mockito.mockStatic(CameraRegionUtils.class)) { - exposurePointFeature.setValue(new Point(0.5, 0.5)); - - mockedCameraRegionUtils.verifyNoInteractions(); } } @@ -146,6 +159,7 @@ public void setValue_should_not_determine_metering_rectangle_when_null_coords_ar CameraProperties mockCameraProperties = mock(CameraProperties.class); when(mockCameraProperties.getControlMaxRegionsAutoExposure()).thenReturn(1); ExposurePointFeature exposurePointFeature = new ExposurePointFeature(mockCameraProperties); + exposurePointFeature.setCameraBoundaries(this.mockCameraBoundaries); exposurePointFeature.setValue(new Point(0.5, 0.5)); Size mockedCameraBoundaries = mock(Size.class); @@ -243,28 +257,25 @@ public void updateBuilder_should_return_when_checkIsSupported_is_false() { ExposurePointFeature exposurePointFeature = new ExposurePointFeature(mockCameraProperties); MeteringRectangle mockedMeteringRectangle = mock(MeteringRectangle.class); - exposurePointFeature.setValue(new Point(0.5, 0.5)); - exposurePointFeature.updateBuilder(mockCaptureRequestBuilder); - verify(mockCaptureRequestBuilder, never()).set(any(), any()); + verify(mockCaptureRequestBuilder, times(1)).set(any(), isNull()); } @Test - public void dateBuilder_should_not_set_metering_rectangle_when_no_valid_coords_are_supplied() { + public void updateBuilder_should_not_set_metering_rectangle_when_no_valid_coords_are_supplied() { CameraProperties mockCameraProperties = mock(CameraProperties.class); when(mockCameraProperties.getControlMaxRegionsAutoExposure()).thenReturn(1); CaptureRequest.Builder mockCaptureRequestBuilder = mock(CaptureRequest.Builder.class); ExposurePointFeature exposurePointFeature = new ExposurePointFeature(mockCameraProperties); + exposurePointFeature.setCameraBoundaries(this.mockCameraBoundaries); exposurePointFeature.setValue(null); exposurePointFeature.updateBuilder(mockCaptureRequestBuilder); - verify(mockCaptureRequestBuilder, never()).set(any(), any()); exposurePointFeature.setValue(new Point(0d, null)); exposurePointFeature.updateBuilder(mockCaptureRequestBuilder); - verify(mockCaptureRequestBuilder, never()).set(any(), any()); exposurePointFeature.setValue(new Point(null, 0d)); exposurePointFeature.updateBuilder(mockCaptureRequestBuilder); - verify(mockCaptureRequestBuilder, never()).set(any(), any()); + verify(mockCaptureRequestBuilder, times(3)).set(any(), isNull()); } } 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 ec733e64aaba..d158336ef235 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 @@ -9,6 +9,7 @@ import static org.junit.Assert.assertNull; import static org.junit.Assert.assertTrue; import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.isNull; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.never; import static org.mockito.Mockito.times; @@ -21,11 +22,22 @@ import io.flutter.plugins.camera.CameraProperties; import io.flutter.plugins.camera.CameraRegionUtils; import io.flutter.plugins.camera.features.Point; +import org.junit.Before; import org.junit.Test; import org.mockito.MockedStatic; import org.mockito.Mockito; public class FocusPointFeatureTest { + + Size mockCameraBoundaries; + + @Before + public void setUp() { + this.mockCameraBoundaries = mock(Size.class); + when(this.mockCameraBoundaries.getWidth()).thenReturn(100); + when(this.mockCameraBoundaries.getHeight()).thenReturn(100); + } + @Test public void getDebugName_should_return_the_name_of_the_feature() { CameraProperties mockCameraProperties = mock(CameraProperties.class); @@ -47,6 +59,7 @@ public void getValue_should_return_null_if_not_set() { public void getValue_should_echo_the_set_value() { CameraProperties mockCameraProperties = mock(CameraProperties.class); FocusPointFeature focusPointFeature = new FocusPointFeature(mockCameraProperties); + focusPointFeature.setCameraBoundaries(this.mockCameraBoundaries); Point expectedPoint = new Point(0.0, 0.0); focusPointFeature.setValue(expectedPoint); @@ -59,6 +72,7 @@ public void getValue_should_echo_the_set_value() { public void setValue_should_reset_point_when_x_coord_is_null() { CameraProperties mockCameraProperties = mock(CameraProperties.class); FocusPointFeature focusPointFeature = new FocusPointFeature(mockCameraProperties); + focusPointFeature.setCameraBoundaries(this.mockCameraBoundaries); focusPointFeature.setValue(new Point(null, 0.0)); @@ -69,6 +83,7 @@ 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); FocusPointFeature focusPointFeature = new FocusPointFeature(mockCameraProperties); + focusPointFeature.setCameraBoundaries(this.mockCameraBoundaries); focusPointFeature.setValue(new Point(0.0, null)); @@ -79,6 +94,7 @@ public void setValue_should_reset_point_when_y_coord_is_null() { public void setValue_should_set_point_when_valid_coords_are_supplied() { CameraProperties mockCameraProperties = mock(CameraProperties.class); FocusPointFeature focusPointFeature = new FocusPointFeature(mockCameraProperties); + focusPointFeature.setCameraBoundaries(this.mockCameraBoundaries); Point point = new Point(0.0, 0.0); focusPointFeature.setValue(point); @@ -106,18 +122,15 @@ public void setValue_should_set_point_when_valid_coords_are_supplied() { } } - @Test - public void setValue_should_not_determine_metering_rectangle_when_no_valid_boundaries_are_set() { + @Test(expected = AssertionError.class) + public void setValue_should_throw_assertion_error_when_no_valid_boundaries_are_set() { CameraProperties mockCameraProperties = mock(CameraProperties.class); when(mockCameraProperties.getControlMaxRegionsAutoFocus()).thenReturn(1); FocusPointFeature focusPointFeature = new FocusPointFeature(mockCameraProperties); try (MockedStatic mockedCameraRegionUtils = Mockito.mockStatic(CameraRegionUtils.class)) { - focusPointFeature.setValue(new Point(0.5, 0.5)); - - mockedCameraRegionUtils.verifyNoInteractions(); } } @@ -146,6 +159,7 @@ public void setValue_should_not_determine_metering_rectangle_when_null_coords_ar CameraProperties mockCameraProperties = mock(CameraProperties.class); when(mockCameraProperties.getControlMaxRegionsAutoFocus()).thenReturn(1); FocusPointFeature focusPointFeature = new FocusPointFeature(mockCameraProperties); + focusPointFeature.setCameraBoundaries(this.mockCameraBoundaries); focusPointFeature.setValue(new Point(0.5, 0.5)); Size mockedCameraBoundaries = mock(Size.class); @@ -243,28 +257,25 @@ public void updateBuilder_should_return_when_checkIsSupported_is_false() { FocusPointFeature focusPointFeature = new FocusPointFeature(mockCameraProperties); MeteringRectangle mockedMeteringRectangle = mock(MeteringRectangle.class); - focusPointFeature.setValue(new Point(0.5, 0.5)); - focusPointFeature.updateBuilder(mockCaptureRequestBuilder); - verify(mockCaptureRequestBuilder, never()).set(any(), any()); + verify(mockCaptureRequestBuilder, times(1)).set(any(), isNull()); } @Test - public void dateBuilder_should_not_set_metering_rectangle_when_no_valid_coords_are_supplied() { + public void updateBuilder_should_not_set_metering_rectangle_when_no_valid_coords_are_supplied() { CameraProperties mockCameraProperties = mock(CameraProperties.class); when(mockCameraProperties.getControlMaxRegionsAutoFocus()).thenReturn(1); CaptureRequest.Builder mockCaptureRequestBuilder = mock(CaptureRequest.Builder.class); FocusPointFeature focusPointFeature = new FocusPointFeature(mockCameraProperties); + focusPointFeature.setCameraBoundaries(this.mockCameraBoundaries); focusPointFeature.setValue(null); focusPointFeature.updateBuilder(mockCaptureRequestBuilder); - verify(mockCaptureRequestBuilder, never()).set(any(), any()); focusPointFeature.setValue(new Point(0d, null)); focusPointFeature.updateBuilder(mockCaptureRequestBuilder); - verify(mockCaptureRequestBuilder, never()).set(any(), any()); focusPointFeature.setValue(new Point(null, 0d)); focusPointFeature.updateBuilder(mockCaptureRequestBuilder); - verify(mockCaptureRequestBuilder, never()).set(any(), any()); + verify(mockCaptureRequestBuilder, times(3)).set(any(), isNull()); } } From 3a68294765e326a85af3930694e26eebd986fd95 Mon Sep 17 00:00:00 2001 From: BeMacized Date: Tue, 15 Jun 2021 14:06:11 +0200 Subject: [PATCH 22/63] Implemented PR feedback --- .../plugins/camera/features/flash/FlashMode.java | 9 +++++++++ .../noisereduction/NoiseReductionFeature.java | 7 ++++++- .../features/noisereduction/NoiseReductionMode.java | 11 ++++++++++- .../noisereduction/NoiseReductionFeatureTest.java | 2 +- 4 files changed, 26 insertions(+), 3 deletions(-) diff --git a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/flash/FlashMode.java b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/flash/FlashMode.java index d4a5ee0ab12f..788c768e0b3c 100644 --- a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/flash/FlashMode.java +++ b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/flash/FlashMode.java @@ -17,6 +17,15 @@ public enum FlashMode { this.strValue = strValue; } + /** + * Tries to convert the supplied string into a {@see FlashMode} enum value. + * + *

When the supplied string doesn't match a valid {@see FlashMode} enum value, null is + * returned. + * + * @param modeStr String value to convert into an {@see FlashMode} enum value. + * @return Matching {@see FlashMode} enum value, or null if no match is found. + */ public static FlashMode getValueForString(String modeStr) { for (FlashMode value : values()) { if (value.strValue.equals(modeStr)) return value; 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 b24fda4bdd8d..847a817641ab 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 @@ -1,4 +1,4 @@ -// Copyright 2019 The Chromium Authors. All rights reserved. +// Copyright 2013 The Flutter Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. @@ -35,6 +35,11 @@ public class NoiseReductionFeature extends CameraFeature { } } + /** + * Creates a new instance of the {@link NoiseReductionFeature}. + * + * @param cameraProperties Collection of the characteristics for the current camera device. + */ 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 5219fd4c046e..425a458e2a2b 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,4 +1,4 @@ -// Copyright 2019 The Chromium Authors. All rights reserved. +// Copyright 2013 The Flutter Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. @@ -18,6 +18,15 @@ public enum NoiseReductionMode { this.strValue = strValue; } + /** + * Tries to convert the supplied string into a {@see NoiseReductionMode} enum value. + * + *

When the supplied string doesn't match a valid {@see NoiseReductionMode} enum value, null is + * returned. + * + * @param modeStr String value to convert into an {@see NoiseReductionMode} enum value. + * @return Matching {@see NoiseReductionMode} enum value, or null if no match is found. + */ public static NoiseReductionMode getValueForString(String modeStr) { for (NoiseReductionMode value : values()) { if (value.strValue.equals(modeStr)) return value; 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 4905c68bce8a..eb1a639a2ac3 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 @@ -1,4 +1,4 @@ -// Copyright 2019 The Chromium Authors. All rights reserved. +// Copyright 2013 The Flutter Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. From 70dbbd5ff8c457d436a36c3703537263ab934e33 Mon Sep 17 00:00:00 2001 From: BeMacized Date: Tue, 15 Jun 2021 17:01:00 +0200 Subject: [PATCH 23/63] Add supporting functionality for android refactor --- .../plugins/camera/CameraCaptureCallback.java | 142 +++++++ .../flutter/plugins/camera/CameraState.java | 27 ++ .../io/flutter/plugins/camera/ImageSaver.java | 83 ++++ .../camera/features/CameraFeatureFactory.java | 135 +++++++ .../features/CameraFeatureFactoryImpl.java | 88 +++++ .../camera/features/CameraFeatures.java | 35 ++ .../camera/types/CaptureTimeoutsWrapper.java | 36 ++ .../flutter/plugins/camera/types/Timeout.java | 51 +++ .../CameraCaptureCallbackStatesTest.java | 373 ++++++++++++++++++ .../plugins/camera/ImageSaverTests.java | 101 +++++ 10 files changed, 1071 insertions(+) create mode 100644 packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/CameraCaptureCallback.java create mode 100644 packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/CameraState.java create mode 100644 packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/ImageSaver.java 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 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/types/CaptureTimeoutsWrapper.java create mode 100644 packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/types/Timeout.java 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/ImageSaverTests.java 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..87dd859e5478 --- /dev/null +++ b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/CameraCaptureCallback.java @@ -0,0 +1,142 @@ +// 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; +import android.hardware.camera2.CameraCaptureSession.CaptureCallback; +import android.hardware.camera2.CaptureRequest; +import android.hardware.camera2.CaptureResult; +import android.hardware.camera2.TotalCaptureResult; +import android.util.Log; +import androidx.annotation.NonNull; +import io.flutter.plugins.camera.types.CaptureTimeoutsWrapper; + +class CameraCaptureCallback extends CaptureCallback { + private final CameraCaptureStateListener cameraStateListener; + private CameraState cameraState; + private final CaptureTimeoutsWrapper captureTimeouts; + + private CameraCaptureCallback( + @NonNull CameraCaptureStateListener cameraStateListener, + @NonNull CaptureTimeoutsWrapper captureTimeouts) { + cameraState = CameraState.STATE_PREVIEW; + this.cameraStateListener = cameraStateListener; + this.captureTimeouts = captureTimeouts; + } + + public static CameraCaptureCallback create( + @NonNull CameraCaptureStateListener cameraStateListener, + @NonNull CaptureTimeoutsWrapper captureTimeouts) { + return new CameraCaptureCallback(cameraStateListener, captureTimeouts); + } + + public CameraState getCameraState() { + return cameraState; + } + + public void setCameraState(@NonNull CameraState state) { + cameraState = state; + } + + 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( + "Camera", + "CameraCaptureCallback | 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 == CaptureResult.CONTROL_AF_STATE_FOCUSED_LOCKED + || afState == CaptureResult.CONTROL_AF_STATE_NOT_FOCUSED_LOCKED) { + handleWaitingFocusState(aeState); + } else if (captureTimeouts.getPreCaptureFocusing().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) { + setCameraState(CameraState.STATE_WAITING_PRECAPTURE_DONE); + } else if (captureTimeouts.getPreCaptureMetering().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) { + cameraStateListener.onConverged(); + } else if (captureTimeouts.getPreCaptureMetering().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, + @NonNull CaptureRequest request, + @NonNull CaptureResult partialResult) { + process(partialResult); + } + + @Override + public void onCaptureCompleted( + @NonNull CameraCaptureSession session, + @NonNull CaptureRequest request, + @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/CameraState.java b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/CameraState.java new file mode 100644 index 000000000000..2df2298a58ab --- /dev/null +++ b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/CameraState.java @@ -0,0 +1,27 @@ +// 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; + +/** + * 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_START, + + /** waiting for autoexposure to complete. */ + STATE_WAITING_PRECAPTURE_DONE, + + /** Capturing an image. */ + STATE_CAPTURING, +} 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..332a94c697ef --- /dev/null +++ b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/ImageSaver.java @@ -0,0 +1,83 @@ +// 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; +import androidx.annotation.NonNull; +import androidx.annotation.VisibleForTesting; +import java.io.File; +import java.io.FileNotFoundException; +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 report the status of the save action. */ + private final Callback mCallback; + + ImageSaver(@NonNull Image image, @NonNull File file, @NonNull Callback callback) { + mImage = image; + mFile = file; + mCallback = callback; + } + + @Override + public void run() { + ByteBuffer buffer = mImage.getPlanes()[0].getBuffer(); + byte[] bytes = new byte[buffer.remaining()]; + buffer.get(bytes); + FileOutputStream output = null; + try { + output = FileOutputStreamFactory.create(mFile); + output.write(bytes); + + mCallback.onComplete(mFile.getAbsolutePath()); + + } catch (IOException e) { + mCallback.onError("IOError", "Failed saving image"); + } finally { + mImage.close(); + if (null != output) { + try { + output.close(); + } catch (IOException e) { + mCallback.onError("cameraAccess", e.getMessage()); + } + } + } + } + + public interface Callback { + void onComplete(String absolutePath); + + void onError(String errorCode, String errorMessage); + } + + /** Factory class that assists in creating a {@link FileOutputStream} instance. */ + static class FileOutputStreamFactory { + /** + * Creates a new instance of the {@link FileOutputStream} class. + * + *

This method is visible for testing purposes only and should never be used outside this * + * class. + * + * @param file - The file to create the output stream for + * @return new instance of the {@link FileOutputStream} class. + * @throws FileNotFoundException when the supplied file could not be found. + */ + @VisibleForTesting + public static FileOutputStream create(File file) throws FileNotFoundException { + return new FileOutputStream(file); + } + } +} 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..c8adcd0c3e46 --- /dev/null +++ b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/CameraFeatureFactory.java @@ -0,0 +1,135 @@ +// 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 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.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; + +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 cameraId - the id 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, int cameraId); + + /** + * Creates a new instance of the focus point feature. + * + * @param cameraProperties instance of the CameraProperties class containing information about the + * cameras features. + * @return newly created instance of the FocusPointFeature class. + */ + FocusPointFeature createFocusPointFeature(@NonNull CameraProperties cameraProperties); + + /** + * 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 exposure point feature. + * + * @param cameraProperties instance of the CameraProperties class containing information about the + * cameras features. + * @return newly created instance of the ExposurePointFeature class. + */ + ExposurePointFeature createExposurePointFeature(@NonNull CameraProperties cameraProperties); + + /** + * 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..561ae9c4c7e9 --- /dev/null +++ b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/CameraFeatureFactoryImpl.java @@ -0,0 +1,88 @@ +// 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 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.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; + +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, int cameraId) { + return new ResolutionFeature(cameraProperties, initialSetting, cameraId); + } + + @Override + public FocusPointFeature createFocusPointFeature(@NonNull CameraProperties cameraProperties) { + return new FocusPointFeature(cameraProperties); + } + + @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 ExposurePointFeature createExposurePointFeature( + @NonNull CameraProperties cameraProperties) { + return new ExposurePointFeature(cameraProperties); + } + + @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 new file mode 100644 index 000000000000..d4db06f27b7b --- /dev/null +++ b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/CameraFeatures.java @@ -0,0 +1,35 @@ +// 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; + +/** + * 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"), + resolution("resolution"), + focusPoint("focusPoint"), + fpsRange("fpsRange"), + sensorOrientation("sensorOrientation"), + zoomLevel("zoomLevel"), + regionBoundaries("regionBoundaries"), + exposurePoint("exposurePoint"), + noiseReduction("noiseReduction"); + + private final String strValue; + + CameraFeatures(String strValue) { + this.strValue = strValue; + } + + @Override + public String toString() { + return strValue; + } +} diff --git a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/types/CaptureTimeoutsWrapper.java b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/types/CaptureTimeoutsWrapper.java new file mode 100644 index 000000000000..a74b92e1b59e --- /dev/null +++ b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/types/CaptureTimeoutsWrapper.java @@ -0,0 +1,36 @@ +package io.flutter.plugins.camera.types; + +public class CaptureTimeoutsWrapper { + private final Timeout preCaptureFocusing; + private final Timeout preCaptureMetering; + + /** + * Create a new wrapper instance with the specified timeout values. + * + * @param preCaptureFocusingTimeoutMs focusing timeout milliseconds. + * @param preCaptureMeteringTimeoutMs metering timeout milliseconds. + */ + public CaptureTimeoutsWrapper( + long preCaptureFocusingTimeoutMs, long preCaptureMeteringTimeoutMs) { + this.preCaptureFocusing = Timeout.create(preCaptureFocusingTimeoutMs); + this.preCaptureMetering = Timeout.create(preCaptureMeteringTimeoutMs); + } + + /** + * Returns the timeout instance related to precapture focusing. + * + * @return - The timeout object + */ + public Timeout getPreCaptureFocusing() { + return preCaptureFocusing; + } + + /** + * Returns the timeout instance related to precapture metering. + * + * @return - The timeout object + */ + public Timeout getPreCaptureMetering() { + return preCaptureMetering; + } +} diff --git a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/types/Timeout.java b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/types/Timeout.java new file mode 100644 index 000000000000..70625864e887 --- /dev/null +++ b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/types/Timeout.java @@ -0,0 +1,51 @@ +// 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.types; + +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. + */ +public 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; + + /** + * Factory method to create a new Timeout. + * + * @param timeoutMs timeout to use. + * @return returns a new Timeout. + */ + public 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(); + } + + /** Will return true when the timeout period has lapsed. */ + public boolean getIsExpired() { + return (SystemClock.elapsedRealtime() - timeStarted) > timeoutMs; + } +} 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..0b730595f0ff --- /dev/null +++ b/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/CameraCaptureCallbackStatesTest.java @@ -0,0 +1,373 @@ +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.plugins.camera.CameraCaptureCallback.CameraCaptureStateListener; +import io.flutter.plugins.camera.types.CaptureTimeoutsWrapper; +import io.flutter.plugins.camera.types.Timeout; +import io.flutter.plugins.camera.utils.TestUtils; +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 CaptureTimeoutsWrapper mockCaptureTimeouts; + 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; + } + + 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); + mockCaptureTimeouts = mock(CaptureTimeoutsWrapper.class); + when(mockCaptureTimeouts.getPreCaptureFocusing()).thenReturn(mockTimeout); + when(mockCaptureTimeouts.getPreCaptureMetering()).thenReturn(mockTimeout); + + 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, mockCaptureTimeouts); + } + + @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) { + when(mockTimeout.getIsExpired()).thenReturn(true); + cameraCaptureCallback.onCaptureCompleted( + mockCameraCaptureSession, mockCaptureRequest, mockTotalCaptureResult); + } else { + cameraCaptureCallback.onCaptureProgressed( + mockCameraCaptureSession, mockCaptureRequest, mockPartialCaptureResult); + } + + validate.run(); + } + + 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); + } +} diff --git a/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/ImageSaverTests.java b/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/ImageSaverTests.java new file mode 100644 index 000000000000..1ebff9b816d8 --- /dev/null +++ b/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/ImageSaverTests.java @@ -0,0 +1,101 @@ +package io.flutter.plugins.camera; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.doThrow; +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.media.Image; +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.nio.ByteBuffer; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.mockito.MockedStatic; +import org.mockito.invocation.InvocationOnMock; +import org.mockito.stubbing.Answer; + +public class ImageSaverTests { + + Image mockImage; + File mockFile; + ImageSaver.Callback mockCallback; + ImageSaver imageSaver; + Image.Plane mockPlane; + ByteBuffer mockBuffer; + MockedStatic mockFileOutputStreamFactory; + FileOutputStream mockFileOutputStream; + + @Before + public void setup() { + // Set up mocked file dependency + mockFile = mock(File.class); + when(mockFile.getAbsolutePath()).thenReturn("absolute/path"); + mockPlane = mock(Image.Plane.class); + mockBuffer = mock(ByteBuffer.class); + when(mockBuffer.remaining()).thenReturn(3); + when(mockBuffer.get(any())) + .thenAnswer( + new Answer() { + @Override + public Object answer(InvocationOnMock invocation) throws Throwable { + byte[] bytes = invocation.getArgument(0); + bytes[0] = 0x42; + bytes[1] = 0x00; + bytes[2] = 0x13; + return mockBuffer; + } + }); + + // Set up mocked image dependency + mockImage = mock(Image.class); + when(mockPlane.getBuffer()).thenReturn(mockBuffer); + when(mockImage.getPlanes()).thenReturn(new Image.Plane[] {mockPlane}); + + // Set up mocked FileOutputStream + mockFileOutputStreamFactory = mockStatic(ImageSaver.FileOutputStreamFactory.class); + mockFileOutputStream = mock(FileOutputStream.class); + mockFileOutputStreamFactory + .when(() -> ImageSaver.FileOutputStreamFactory.create(any())) + .thenReturn(mockFileOutputStream); + + // Set up testable ImageSaver instance + mockCallback = mock(ImageSaver.Callback.class); + imageSaver = new ImageSaver(mockImage, mockFile, mockCallback); + } + + @After + public void teardown() { + mockFileOutputStreamFactory.close(); + } + + @Test + public void run_writes_bytes_to_file_and_finishes_with_path() throws IOException { + imageSaver.run(); + + verify(mockFileOutputStream, times(1)).write(new byte[] {0x42, 0x00, 0x13}); + verify(mockCallback, times(1)).onComplete("absolute/path"); + verify(mockCallback, never()).onError(any(), any()); + } + + @Test + public void run_calls_error_on_write_ioexception() throws IOException { + doThrow(new IOException()).when(mockFileOutputStream).write(any()); + imageSaver.run(); + verify(mockCallback, times(1)).onError("IOError", "Failed saving image"); + verify(mockCallback, never()).onComplete(any()); + } + + @Test + public void run_calls_error_on_close_ioexception() throws IOException { + doThrow(new IOException("message")).when(mockFileOutputStream).close(); + imageSaver.run(); + verify(mockCallback, times(1)).onError("cameraAccess", "message"); + } +} From ab332ee2a3a84d6ee5bbf7ce3dbc3afc33f6dd96 Mon Sep 17 00:00:00 2001 From: Bodhi Mulders Date: Wed, 16 Jun 2021 09:41:42 +0200 Subject: [PATCH 24/63] Update packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/CameraCaptureCallback.java Co-authored-by: Maurits van Beusekom --- .../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 87dd859e5478..fba509bf5ce9 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,4 +1,4 @@ -// Copyright 2019 The Chromium Authors. All rights reserved. +// Copyright 2013 The Flutter Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. From bc16dd31a6c0b24b14620e2afcdb3e3c1c5f76ff Mon Sep 17 00:00:00 2001 From: Bodhi Mulders Date: Wed, 16 Jun 2021 09:41:53 +0200 Subject: [PATCH 25/63] Update packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/CameraState.java Co-authored-by: Maurits van Beusekom --- .../src/main/java/io/flutter/plugins/camera/CameraState.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 2df2298a58ab..ac48caf18ac6 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,4 +1,4 @@ -// Copyright 2019 The Chromium Authors. All rights reserved. +// Copyright 2013 The Flutter Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. From a85e31234350ac3cef170b6bc6b9e4e1946eebad Mon Sep 17 00:00:00 2001 From: Bodhi Mulders Date: Wed, 16 Jun 2021 09:42:03 +0200 Subject: [PATCH 26/63] Update packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/ImageSaver.java Co-authored-by: Maurits van Beusekom --- .../src/main/java/io/flutter/plugins/camera/ImageSaver.java | 2 +- 1 file changed, 1 insertion(+), 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 332a94c697ef..2fbb5dae085d 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,4 +1,4 @@ -// Copyright 2019 The Chromium Authors. All rights reserved. +// Copyright 2013 The Flutter Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. From 84d6db2052b0e36dd4448eb67445eeebf1ce22e3 Mon Sep 17 00:00:00 2001 From: Bodhi Mulders Date: Wed, 16 Jun 2021 09:42:50 +0200 Subject: [PATCH 27/63] Update packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/CameraFeatureFactoryImpl.java Co-authored-by: Maurits van Beusekom --- .../plugins/camera/features/CameraFeatureFactoryImpl.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 561ae9c4c7e9..f0e8497db36b 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 @@ -1,4 +1,4 @@ -// Copyright 2019 The Chromium Authors. All rights reserved. +// Copyright 2013 The Flutter Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. From 071e879abf8832d772bf5cc1f9ce64d61de7f334 Mon Sep 17 00:00:00 2001 From: Bodhi Mulders Date: Wed, 16 Jun 2021 09:43:20 +0200 Subject: [PATCH 28/63] Update packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/CameraFeatureFactory.java --- .../flutter/plugins/camera/features/CameraFeatureFactory.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 c8adcd0c3e46..25b6fd5404f7 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 @@ -1,4 +1,4 @@ -// Copyright 2019 The Chromium Authors. All rights reserved. +// Copyright 2013 The Flutter Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. From 273762bff118484c08a3993cc8cafc436f837d21 Mon Sep 17 00:00:00 2001 From: Bodhi Mulders Date: Wed, 16 Jun 2021 09:43:37 +0200 Subject: [PATCH 29/63] Update packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/CameraCaptureCallbackStatesTest.java Co-authored-by: Maurits van Beusekom --- .../plugins/camera/CameraCaptureCallbackStatesTest.java | 4 ++++ 1 file changed, 4 insertions(+) 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 0b730595f0ff..4964aef8b8c9 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 @@ -1,3 +1,7 @@ +// Copyright 2013 The Flutter 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.mockito.Mockito.mock; From e9e9bc515605512072ae22043d7e10e5768daba4 Mon Sep 17 00:00:00 2001 From: Bodhi Mulders Date: Wed, 16 Jun 2021 09:43:48 +0200 Subject: [PATCH 30/63] Update packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/CameraRegionUtilsTest.java Co-authored-by: Maurits van Beusekom --- .../java/io/flutter/plugins/camera/CameraRegionUtilsTest.java | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/CameraRegionUtilsTest.java b/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/CameraRegionUtilsTest.java index e248e8adfeb4..156f55c73995 100644 --- a/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/CameraRegionUtilsTest.java +++ b/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/CameraRegionUtilsTest.java @@ -1,6 +1,10 @@ // Copyright 2013 The Flutter Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. +// Copyright 2013 The Flutter 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; From 1af54371b98ec0fc0ce9b71919ffb6c26c1cbad8 Mon Sep 17 00:00:00 2001 From: Bodhi Mulders Date: Wed, 16 Jun 2021 09:43:56 +0200 Subject: [PATCH 31/63] Update packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/ImageSaverTests.java Co-authored-by: Maurits van Beusekom --- .../test/java/io/flutter/plugins/camera/ImageSaverTests.java | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/ImageSaverTests.java b/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/ImageSaverTests.java index 1ebff9b816d8..d2c9f4498332 100644 --- a/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/ImageSaverTests.java +++ b/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/ImageSaverTests.java @@ -1,3 +1,7 @@ +// Copyright 2013 The Flutter 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.mockito.ArgumentMatchers.any; From b49b743667823f10a78ed53d2e99ec38621d252b Mon Sep 17 00:00:00 2001 From: Maurits van Beusekom Date: Wed, 16 Jun 2021 10:22:48 +0200 Subject: [PATCH 32/63] Added documentation to DartMessenger --- .../flutter/plugins/camera/DartMessenger.java | 51 +++++++++++++++++-- 1 file changed, 47 insertions(+), 4 deletions(-) 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 37bfbf294663..93b963e65821 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 @@ -16,12 +16,15 @@ import java.util.HashMap; import java.util.Map; +/** Utility class that facilitates communication to the Flutter client */ public class DartMessenger { @NonNull private final Handler handler; @Nullable private MethodChannel cameraChannel; @Nullable private MethodChannel deviceChannel; + /** Specifies the different device related message types. */ enum DeviceEventType { + /** Indicates the device's orientation has changed. */ ORIENTATION_CHANGED("orientation_changed"); private final String method; @@ -30,24 +33,47 @@ enum DeviceEventType { } } + /** Specifies the different camera related message types. */ enum CameraEventType { + /** Indicates that an error occurred while interacting with the camera. */ ERROR("error"), + /** Indicates that the camera is closing. */ CLOSING("camera_closing"), + /** Indicates that the camera is initialized. */ INITIALIZED("initialized"); private final String method; + /** + * Converts the supplied method name to the matching {@link CameraEventType}. + * + * @param method name to be converted into a {@link CameraEventType}. + */ CameraEventType(String method) { this.method = method; } } + /** + * Creates a new instance of the {@link DartMessenger} class. + * + * @param messenger is the {@link BinaryMessenger} that is used to communicate with Flutter. + * @param cameraId identifies the camera which is the source of the communication. + * @param handler the handler used to manage the thread's message queue. This should always be a + * handler managing the main thread since communication with Flutter should always happen on + * the main thread. The handler is mainly supplied so it will be easier test this class. + */ DartMessenger(BinaryMessenger messenger, long cameraId, @NonNull Handler handler) { cameraChannel = new MethodChannel(messenger, "flutter.io/cameraPlugin/camera" + cameraId); deviceChannel = new MethodChannel(messenger, "flutter.io/cameraPlugin/device"); this.handler = handler; } + /** + * Sends a message to the Flutter client informing the orientation of the device has been changed. + * + * @param orientation specifies the new orientation of the device. + */ public void sendDeviceOrientationChangeEvent(PlatformChannel.DeviceOrientation orientation) { assert (orientation != null); this.send( @@ -59,6 +85,16 @@ public void sendDeviceOrientationChangeEvent(PlatformChannel.DeviceOrientation o }); } + /** + * Sends a message to the Flutter client informing that the camera has been initialized. + * + * @param previewWidth describes the preview width that is supported by the camera. + * @param previewHeight describes the preview height that is supported by the camera. + * @param exposureMode describes the current exposure mode that is set on the camera. + * @param focusMode describes the current focus mode that is set on the camera. + * @param exposurePointSupported indicates if the camera supports setting an exposure point. + * @param focusPointSupported indicates if the camera supports setting a focus point. + */ void sendCameraInitializedEvent( Integer previewWidth, Integer previewHeight, @@ -86,10 +122,17 @@ void sendCameraInitializedEvent( }); } + /** Sends a message to the Flutter client informing that the camera is closing. */ void sendCameraClosingEvent() { send(CameraEventType.CLOSING); } + /** + * Sends a message to the Flutter client informing that an error occurred while interacting with + * the camera. + * + * @param description contains details regarding the error that occurred. + */ void sendCameraErrorEvent(@Nullable String description) { this.send( CameraEventType.ERROR, @@ -100,11 +143,11 @@ void sendCameraErrorEvent(@Nullable String description) { }); } - void send(CameraEventType eventType) { + private void send(CameraEventType eventType) { send(eventType, new HashMap<>()); } - void send(CameraEventType eventType, Map args) { + private void send(CameraEventType eventType, Map args) { if (cameraChannel == null) { return; } @@ -118,11 +161,11 @@ public void run() { }); } - void send(DeviceEventType eventType) { + private void send(DeviceEventType eventType) { send(eventType, new HashMap<>()); } - void send(DeviceEventType eventType, Map args) { + private void send(DeviceEventType eventType, Map args) { if (deviceChannel == null) { return; } From 5b8c0d8115e05b626aa3c845e8eef26889f863be Mon Sep 17 00:00:00 2001 From: Bodhi Mulders Date: Wed, 16 Jun 2021 10:36:53 +0200 Subject: [PATCH 33/63] Update packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/types/Timeout.java Co-authored-by: Maurits van Beusekom --- .../src/main/java/io/flutter/plugins/camera/types/Timeout.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/types/Timeout.java b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/types/Timeout.java index 70625864e887..67e05499d47a 100644 --- a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/types/Timeout.java +++ b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/types/Timeout.java @@ -1,4 +1,4 @@ -// Copyright 2019 The Chromium Authors. All rights reserved. +// Copyright 2013 The Flutter Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. From 62a2f211a1b9c4d6f6886021857f0b8b2677c6c5 Mon Sep 17 00:00:00 2001 From: BeMacized Date: Wed, 16 Jun 2021 10:39:46 +0200 Subject: [PATCH 34/63] Add missing CaptureTimeoutsWrapper documentation --- .../flutter/plugins/camera/types/CaptureTimeoutsWrapper.java | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/types/CaptureTimeoutsWrapper.java b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/types/CaptureTimeoutsWrapper.java index a74b92e1b59e..8304cd539145 100644 --- a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/types/CaptureTimeoutsWrapper.java +++ b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/types/CaptureTimeoutsWrapper.java @@ -1,5 +1,9 @@ package io.flutter.plugins.camera.types; +/** + * Wrapper class that provides a container for all {@link Timeout} instances + * that are required for the capture flow. + */ public class CaptureTimeoutsWrapper { private final Timeout preCaptureFocusing; private final Timeout preCaptureMetering; From 43c07160fe557c23210d321609868e6a21ca005c Mon Sep 17 00:00:00 2001 From: Bodhi Mulders Date: Wed, 16 Jun 2021 10:41:07 +0200 Subject: [PATCH 35/63] Update packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/types/CaptureTimeoutsWrapper.java Co-authored-by: Maurits van Beusekom --- .../flutter/plugins/camera/types/CaptureTimeoutsWrapper.java | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/types/CaptureTimeoutsWrapper.java b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/types/CaptureTimeoutsWrapper.java index 8304cd539145..f162dd2759fa 100644 --- a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/types/CaptureTimeoutsWrapper.java +++ b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/types/CaptureTimeoutsWrapper.java @@ -1,3 +1,7 @@ +// Copyright 2013 The Flutter 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.types; /** From 24c6c9adbee88f7216584865b3937d8a6cd3a95a Mon Sep 17 00:00:00 2001 From: Maurits van Beusekom Date: Wed, 16 Jun 2021 10:57:57 +0200 Subject: [PATCH 36/63] Added documentation to the CameraCaptureCallback --- .../plugins/camera/CameraCaptureCallback.java | 32 +++++++++++++++++++ 1 file changed, 32 insertions(+) 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 fba509bf5ce9..df5cb1bb938f 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,6 +13,10 @@ import androidx.annotation.NonNull; import io.flutter.plugins.camera.types.CaptureTimeoutsWrapper; +/** + * A callback object for tracking the progress of a {@link android.hardware.camera2.CaptureRequest} + * submitted to the camera device. + */ class CameraCaptureCallback extends CaptureCallback { private final CameraCaptureStateListener cameraStateListener; private CameraState cameraState; @@ -26,16 +30,34 @@ private CameraCaptureCallback( this.captureTimeouts = captureTimeouts; } + /** + * Creates a new instance of the {@link CameraCaptureCallback} class. + * + * @param cameraStateListener instance which will be called when the camera state changes. + * @param captureTimeouts specifying the different timeout counters that should be taken into + * account. + * @return a configured instance of the {@link CameraCaptureCallback} class. + */ public static CameraCaptureCallback create( @NonNull CameraCaptureStateListener cameraStateListener, @NonNull CaptureTimeoutsWrapper captureTimeouts) { return new CameraCaptureCallback(cameraStateListener, captureTimeouts); } + /** + * Gets the current {@link CameraState}. + * + * @return the current {@link CameraState}. + */ public CameraState getCameraState() { return cameraState; } + /** + * Sets the {@link CameraState}. + * + * @param state the camera is currently in. + */ public void setCameraState(@NonNull CameraState state) { cameraState = state; } @@ -134,9 +156,19 @@ public void onCaptureCompleted( process(result); } + /** + * An interface that describes the different state changes implementers can be informed about. + */ interface CameraCaptureStateListener { + + /** + * Called when the {@link android.hardware.camera2.CaptureRequest} has been converged. + */ void onConverged(); + /** + * Called when the {@link android.hardware.camera2.CaptureRequest} enters the pre-capture state. + */ void onPrecapture(); } } From b2536d4d427c453ba140988ea5cdc8a9b4a211e0 Mon Sep 17 00:00:00 2001 From: BeMacized Date: Wed, 16 Jun 2021 11:00:22 +0200 Subject: [PATCH 37/63] Add missing documentation for ImageSaver runnable --- .../io/flutter/plugins/camera/ImageSaver.java | 43 +++++++++++++------ 1 file changed, 31 insertions(+), 12 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 2fbb5dae085d..163a51763517 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 @@ -17,49 +17,68 @@ public class ImageSaver implements Runnable { /** The JPEG image */ - private final Image mImage; + private final Image image; /** The file we save the image into. */ - private final File mFile; + private final File file; /** Used to report the status of the save action. */ - private final Callback mCallback; + private final Callback callback; + /** + * Creates an instance of the ImageSaver runnable + * @param image - The image to save + * @param file - The file to save the image to + * @param callback - The callback that is run on completion, or when an error is encountered. + */ ImageSaver(@NonNull Image image, @NonNull File file, @NonNull Callback callback) { - mImage = image; - mFile = file; - mCallback = callback; + this.image = image; + this.file = file; + this.callback = callback; } @Override public void run() { - ByteBuffer buffer = mImage.getPlanes()[0].getBuffer(); + ByteBuffer buffer = image.getPlanes()[0].getBuffer(); byte[] bytes = new byte[buffer.remaining()]; buffer.get(bytes); FileOutputStream output = null; try { - output = FileOutputStreamFactory.create(mFile); + output = FileOutputStreamFactory.create(file); output.write(bytes); - mCallback.onComplete(mFile.getAbsolutePath()); + callback.onComplete(file.getAbsolutePath()); } catch (IOException e) { - mCallback.onError("IOError", "Failed saving image"); + callback.onError("IOError", "Failed saving image"); } finally { - mImage.close(); + image.close(); if (null != output) { try { output.close(); } catch (IOException e) { - mCallback.onError("cameraAccess", e.getMessage()); + callback.onError("cameraAccess", e.getMessage()); } } } } + /** + * The interface for the callback that is passed to ImageSaver, + * for detecting completion or failure of the image saving task. + */ public interface Callback { + /** + * Called when the image file has been saved successfully. + * @param absolutePath - The absolute path of the file that was saved. + */ void onComplete(String absolutePath); + /** + * Called when an error is encountered while saving the image file. + * @param errorCode - The error code. + * @param errorMessage - The human readable error message. + */ void onError(String errorCode, String errorMessage); } From 311868010d3ff501ab74fdb257387d9d08204420 Mon Sep 17 00:00:00 2001 From: Bodhi Mulders Date: Wed, 16 Jun 2021 11:01:26 +0200 Subject: [PATCH 38/63] Update packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/CameraFeatures.java Co-authored-by: Maurits van Beusekom --- .../java/io/flutter/plugins/camera/features/CameraFeatures.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 d4db06f27b7b..d96f486e5b38 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,4 +1,4 @@ -// Copyright 2019 The Chromium Authors. All rights reserved. +// Copyright 2013 The Flutter Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. From 7dbbdce265f0fe831ea2480788dfa556173a9638 Mon Sep 17 00:00:00 2001 From: Maurits van Beusekom Date: Wed, 16 Jun 2021 11:07:29 +0200 Subject: [PATCH 39/63] Added documentation to the CameraFeatureFactory --- .../flutter/plugins/camera/features/CameraFeatureFactory.java | 4 ++++ .../plugins/camera/features/CameraFeatureFactoryImpl.java | 4 ++++ 2 files changed, 8 insertions(+) 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 25b6fd5404f7..2fa47255ac56 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 @@ -21,6 +21,10 @@ import io.flutter.plugins.camera.features.sensororientation.SensorOrientationFeature; import io.flutter.plugins.camera.features.zoomlevel.ZoomLevelFeature; +/** + * Factory for creating the supported feature implementation controlling different aspects + * of the {@link android.hardware.camera2.CaptureRequest}. + */ public interface CameraFeatureFactory { /** 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 f0e8497db36b..d9caafed3c3f 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 @@ -21,6 +21,10 @@ import io.flutter.plugins.camera.features.sensororientation.SensorOrientationFeature; import io.flutter.plugins.camera.features.zoomlevel.ZoomLevelFeature; +/** + * Implementation of the {@link CameraFeatureFactory} interface creating the supported feature + * implementation controlling different aspects of the {@link android.hardware.camera2.CaptureRequest}. + */ public class CameraFeatureFactoryImpl implements CameraFeatureFactory { @Override From 2b2bea3bdb4baf1d2b92cf3b30c5245c5aff1ec6 Mon Sep 17 00:00:00 2001 From: Maurits van Beusekom Date: Wed, 16 Jun 2021 11:33:29 +0200 Subject: [PATCH 40/63] Configure log tag in one place and optimised loggin --- .../flutter/plugins/camera/CameraCaptureCallback.java | 11 ++++++----- 1 file changed, 6 insertions(+), 5 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 df5cb1bb938f..f614725d2f64 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 @@ -18,6 +18,7 @@ * submitted to the camera device. */ class CameraCaptureCallback extends CaptureCallback { + private static final String TAG = "CameraCaptureCallback"; private final CameraCaptureStateListener cameraStateListener; private CameraState cameraState; private final CaptureTimeoutsWrapper captureTimeouts; @@ -67,8 +68,8 @@ private void process(CaptureResult result) { Integer afState = result.get(CaptureResult.CONTROL_AF_STATE); if (cameraState != CameraState.STATE_PREVIEW) { - Log.i( - "Camera", + Log.d( + TAG, "CameraCaptureCallback | state: " + cameraState + " | afState: " @@ -91,7 +92,7 @@ private void process(CaptureResult result) { || afState == CaptureResult.CONTROL_AF_STATE_NOT_FOCUSED_LOCKED) { handleWaitingFocusState(aeState); } else if (captureTimeouts.getPreCaptureFocusing().getIsExpired()) { - Log.w("Camera", "Focus timeout, moving on with capture"); + Log.w(TAG, "Focus timeout, moving on with capture"); handleWaitingFocusState(aeState); } @@ -107,7 +108,7 @@ private void process(CaptureResult result) { setCameraState(CameraState.STATE_WAITING_PRECAPTURE_DONE); } else if (captureTimeouts.getPreCaptureMetering().getIsExpired()) { Log.w( - "Camera", + TAG, "Metering timeout waiting for pre-capture to start, moving on with capture"); setCameraState(CameraState.STATE_WAITING_PRECAPTURE_DONE); @@ -121,7 +122,7 @@ private void process(CaptureResult result) { cameraStateListener.onConverged(); } else if (captureTimeouts.getPreCaptureMetering().getIsExpired()) { Log.w( - "Camera", + TAG, "Metering timeout waiting for pre-capture to finish, moving on with capture"); cameraStateListener.onConverged(); } From 96d285b045aebced174dd8e8d18b5c3715bd13a7 Mon Sep 17 00:00:00 2001 From: BeMacized Date: Wed, 16 Jun 2021 16:10:43 +0200 Subject: [PATCH 41/63] Android rework finalization (wip) --- .../io/flutter/plugins/camera/Camera.java | 2072 ++++++++--------- .../plugins/camera/CameraProperties.java | 3 +- .../flutter/plugins/camera/CameraUtils.java | 70 +- .../flutter/plugins/camera/DartMessenger.java | 345 +-- .../plugins/camera/MethodCallHandlerImpl.java | 670 +++--- .../camera/features/CameraFeatureFactory.java | 4 +- .../features/CameraFeatureFactoryImpl.java | 4 +- .../camera/features/CameraFeatures.java | 271 ++- .../resolution/ResolutionFeature.java | 282 +-- .../camera/types/CaptureTimeoutsWrapper.java | 68 +- 10 files changed, 1986 insertions(+), 1803 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 4c1370f2f3cb..35e2306ff275 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,27 +4,20 @@ 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; 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; -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; -import android.hardware.camera2.params.MeteringRectangle; import android.hardware.camera2.params.OutputConfiguration; import android.hardware.camera2.params.SessionConfiguration; import android.media.CamcorderProfile; @@ -35,27 +28,44 @@ import android.os.Build.VERSION; import android.os.Build.VERSION_CODES; import android.os.Handler; +import android.os.HandlerThread; import android.os.Looper; -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.Display; 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; import io.flutter.plugin.common.MethodChannel.Result; -import io.flutter.plugins.camera.PictureCaptureRequest.State; +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.AutoFocusFeature; +import io.flutter.plugins.camera.features.autofocus.FocusMode; +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.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.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.ZoomLevelFeature; 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.plugins.camera.types.CaptureTimeoutsWrapper; 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; @@ -68,1136 +78,1086 @@ @FunctionalInterface interface ErrorCallback { - void onError(String errorCode, String errorMessage); + void onError(String errorCode, String errorMessage); } -public class Camera { - private static final String TAG = "Camera"; - - /** Timeout for the pre-capture sequence. */ - private static final long PRECAPTURE_TIMEOUT_MS = 1000; - - 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 CameraDevice cameraDevice; - private CameraCaptureSession cameraCaptureSession; - private ImageReader pictureImageReader; - private ImageReader imageStreamReader; - private CaptureRequest.Builder captureRequestBuilder; - private MediaRecorder mediaRecorder; - private boolean recordingVideo; - private File videoRecordingFile; - private FlashMode flashMode; - private ExposureMode exposureMode; - private FocusMode focusMode; - private PictureCaptureRequest pictureCaptureRequest; - private CameraRegions cameraRegions; - private int exposureOffset; - private boolean useAutoFocus = true; - private Range fpsRange; - 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 { - if (activity == null) { - throw new IllegalStateException("No activity available!"); - } - 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.exposureMode = ExposureMode.auto; - this.focusMode = FocusMode.auto; - this.exposureOffset = 0; - - cameraCharacteristics = cameraManager.getCameraCharacteristics(cameraName); - initFps(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(); - } - - private void initFps(CameraCharacteristics cameraCharacteristics) { - 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) { - e.printStackTrace(); +/** + * 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. + */ +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); } - Log.i("Camera", "[FPS Range] is:" + fpsRange); - } - private void prepareMediaRecorder(String outputFilePath) throws IOException { - if (mediaRecorder != null) { - mediaRecorder.release(); + // Current supported outputs + static { + supportedImageFormats = new HashMap<>(); + supportedImageFormats.put("yuv420", ImageFormat.YUV_420_888); + supportedImageFormats.put("jpeg", ImageFormat.JPEG); } - 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; + + /** + * Holds all of the camera features/settings and will be used to update the request builder when + * one changes. + */ + private final CameraFeatures cameraFeatures; + + private final SurfaceTextureEntry flutterTexture; + private final boolean enableAudio; + 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; + /** + * A {@link Handler} for running tasks in the background. + */ + 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 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. + backgroundHandler.post( + new ImageSaver( + reader.acquireNextImage(), captureFile, new ImageSaver.Callback() { + @Override + public void onComplete(String absolutePath) { + dartMessenger.finish(flutterResult, absolutePath); + } + + @Override + public void onError(String errorCode, String errorMessage) { + dartMessenger.error(flutterResult, errorCode, errorMessage, null); + } + })); + cameraCaptureCallback.setCameraState(CameraState.STATE_PREVIEW); + } + }; + /** + * An additional thread for running tasks that shouldn't block the UI. + */ + 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 previewRequestBuilder; + + private MediaRecorder mediaRecorder; + /** + * True when recording video. + */ + private boolean recordingVideo; + + private File captureFile; + + /** + * Holds the current capture timeouts + */ + private CaptureTimeoutsWrapper captureTimeouts; + + private MethodChannel.Result flutterResult; + + public Camera( + final Activity activity, + final SurfaceTextureEntry flutterTexture, + final CameraFeatureFactory cameraFeatureFactory, + 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; + this.cameraFeatureFactory = cameraFeatureFactory; + + // Setup camera features + this.cameraFeatures = new CameraFeatures(); + this.cameraFeatures.setAutoFocus( + cameraFeatureFactory.createAutoFocusFeature(cameraProperties, false) + ); + this.cameraFeatures.setExposureLock(cameraFeatureFactory.createExposureLockFeature(cameraProperties)); + this.cameraFeatures.setExposureOffset(cameraFeatureFactory.createExposureOffsetFeature(cameraProperties)); + this.cameraFeatures.setExposurePoint( + cameraFeatureFactory.createExposurePointFeature(cameraProperties) + ); + this.cameraFeatures.setFlash( + cameraFeatureFactory.createFlashFeature(cameraProperties) + ); + this.cameraFeatures.setFocusPoint( + cameraFeatureFactory.createFocusPointFeature(cameraProperties) + ); + this.cameraFeatures.setFpsRange( + cameraFeatureFactory.createFpsRangeFeature(cameraProperties) + ); + this.cameraFeatures.setNoiseReduction( + cameraFeatureFactory.createNoiseReductionFeature(cameraProperties) + ); + this.cameraFeatures.setResolution( + cameraFeatureFactory.createResolutionFeature( + cameraProperties, resolutionPreset, cameraProperties.getCameraName()) + ); + this.cameraFeatures.setSensorOrientation( + cameraFeatureFactory.createSensorOrientationFeature( + cameraProperties, activity, dartMessenger) + ); + this.cameraFeatures.setZoomLevel( + cameraFeatureFactory.createZoomLevelFeature(cameraProperties) + ); + + // Create capture callback + captureTimeouts = new CaptureTimeoutsWrapper(3000, 3000); + cameraCaptureCallback = CameraCaptureCallback.create(this, captureTimeouts); + + // Start background thread. + startBackgroundThread(); } - // Used to steam image byte data to dart side. - imageStreamReader = - ImageReader.newInstance(previewSize.getWidth(), previewSize.getHeight(), imageFormat, 2); + @Override + public void onConverged() { + takePictureAfterPrecapture(); + } - 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(); - } - } - - @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 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 { - createCaptureSession(templateType, null, surfaces); - } - - private void createCaptureSession( - int templateType, Runnable onSuccessCallback, Surface... surfaces) - throws CameraAccessException { - // Close any existing capture session. - closeCaptureSession(); - - // Create a new capture builder. - captureRequestBuilder = 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); - - 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); - } + @Override + public void onPrecapture() { + runPrecaptureSequence(); } - cameraRegions = new CameraRegions(getRegionBoundaries()); + /** + * Update the builder settings with all of our available features. + * + * @param requestBuilder request builder to update. + */ + private void updateBuilderSettings(CaptureRequest.Builder requestBuilder) { + for (CameraFeature feature : cameraFeatures.getAllFeatures()) { + Log.d(TAG, "Updating builder with feature: " + feature.getDebugName()); + feature.updateBuilder(requestBuilder); + } + } - // 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; + private void prepareMediaRecorder(String outputFilePath) throws IOException { + Log.i(TAG, "prepareMediaRecorder"); - updateFpsRange(); - updateFocus(focusMode); - updateFlash(flashMode); - updateExposure(exposureMode); + if (mediaRecorder != null) { + mediaRecorder.release(); + } - 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); - } - } - - @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) { - if (cameraCaptureSession == null) { - return; + final PlatformChannel.DeviceOrientation lockedOrientation = + ((SensorOrientationFeature) cameraFeatures.getSensorOrientation()) + .getLockedCaptureOrientation(); + + mediaRecorder = + new MediaRecorderBuilder(getRecordingProfile(), outputFilePath) + .setEnableAudio(enableAudio) + .setMediaOrientation( + lockedOrientation == null + ? getDeviceOrientationManager().getMediaOrientation() + : getDeviceOrientationManager().getMediaOrientation(lockedOrientation)) + .build(); } - try { - cameraCaptureSession.setRepeatingRequest( - captureRequestBuilder.build(), - pictureCaptureCallback, - new Handler(Looper.getMainLooper())); - - if (onSuccessCallback != null) { - onSuccessCallback.run(); - } - } catch (CameraAccessException | IllegalStateException | IllegalArgumentException e) { - onErrorCallback.onError("cameraAccess", e.getMessage()); + @SuppressLint("MissingPermission") + public void open(String imageFormatGroup) throws CameraAccessException { + // We always capture using JPEG format. + final ResolutionFeature resolutionFeature = cameraFeatures.getResolution(); + pictureImageReader = + ImageReader.newInstance( + resolutionFeature.getCaptureSize().getWidth(), resolutionFeature.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; + } + imageStreamReader = + ImageReader.newInstance( + resolutionFeature.getPreviewSize().getWidth(), resolutionFeature.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) { + cameraDevice = device; + try { + startPreview(); + dartMessenger.sendCameraInitializedEvent( + resolutionFeature.getPreviewSize().getWidth(), + resolutionFeature.getPreviewSize().getHeight(), + cameraFeatures.getExposureLock().getValue(), + cameraFeatures.getAutoFocus().getValue(), + cameraFeatures.getExposurePoint().checkIsSupported(), + cameraFeatures.getFocusPoint().checkIsSupported()); + } 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); + } + }, + backgroundHandler); } - } - private void writeToFile(ByteBuffer buffer, File file) throws IOException { - try (FileOutputStream outputStream = new FileOutputStream(file)) { - while (0 < buffer.remaining()) { - outputStream.getChannel().write(buffer); - } + private void createCaptureSession(int templateType, Surface... surfaces) + throws CameraAccessException { + createCaptureSession(templateType, null, surfaces); } - } - public void takePicture(@NonNull final Result result) { - // Only take 1 picture at a time - if (pictureCaptureRequest != null && !pictureCaptureRequest.isFinished()) { - result.error("captureAlreadyActive", "Picture is currently already being captured", null); - return; + private void createCaptureSession( + int templateType, Runnable onSuccessCallback, Surface... surfaces) + throws CameraAccessException { + // Close any existing capture session. + closeCaptureSession(); + + // Create a new capture builder. + previewRequestBuilder = cameraDevice.createCaptureRequest(templateType); + + // Build Flutter surface to render to + final ResolutionFeature resolutionFeature = cameraFeatures.getResolution(); + SurfaceTexture surfaceTexture = flutterTexture.surfaceTexture(); + surfaceTexture.setDefaultBufferSize(resolutionFeature.getPreviewSize().getWidth(), resolutionFeature.getPreviewSize().getHeight()); + Surface flutterSurface = new Surface(surfaceTexture); + 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) { + previewRequestBuilder.addTarget(surface); + } + } + + // Update camera regions + Size cameraBoundaries = CameraRegionUtils.getCameraBoundaries(cameraProperties, previewRequestBuilder); + cameraFeatures.getExposurePoint().setCameraBoundaries(cameraBoundaries); + cameraFeatures.getFocusPoint().setCameraBoundaries(cameraBoundaries); + + // 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; + + Log.i(TAG, "Updating builder settings"); + updateBuilderSettings(previewRequestBuilder); + + 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); + } } - // Store the result - this.pictureCaptureRequest = new PictureCaptureRequest(result); - - // Create temporary file - final File outputDir = applicationContext.getCacheDir(); - final File file; - try { - file = File.createTempFile("CAP", ".jpg", outputDir); - } catch (IOException | SecurityException e) { - pictureCaptureRequest.error("cannotCreateFile", e.getMessage(), null); - return; + + @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)); } - // 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); - - if (useAutoFocus) { - runPictureAutoFocus(); - } else { - runPicturePreCapture(); + @TargetApi(VERSION_CODES.LOLLIPOP) + @SuppressWarnings("deprecation") + private void createCaptureSession( + List surfaces, CameraCaptureSession.StateCallback callback) + throws CameraAccessException { + cameraDevice.createCaptureSession(surfaces, callback, backgroundHandler); } - } - - private final CameraCaptureSession.CaptureCallback pictureCaptureCallback = - new CameraCaptureSession.CaptureCallback() { - @Override - public void onCaptureCompleted( - @NonNull CameraCaptureSession session, - @NonNull CaptureRequest request, - @NonNull TotalCaptureResult result) { - processCapture(result); + + // 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; } - @Override - public void onCaptureProgressed( - @NonNull CameraCaptureSession session, - @NonNull CaptureRequest request, - @NonNull CaptureResult partialResult) { - processCapture(partialResult); + try { + captureSession.setRepeatingRequest( + previewRequestBuilder.build(), cameraCaptureCallback, backgroundHandler); + + if (onSuccessCallback != null) { + onSuccessCallback.run(); + } + + } catch (CameraAccessException | IllegalStateException | IllegalArgumentException e) { + onErrorCallback.onError("cameraAccess", e.getMessage()); } + } - @Override - public void onCaptureFailed( - @NonNull CameraCaptureSession session, - @NonNull CaptureRequest request, - @NonNull CaptureFailure failure) { - if (pictureCaptureRequest == null || pictureCaptureRequest.isFinished()) { + public void takePicture(@NonNull final Result result) { + // Only take one 1 picture at a time. + if (cameraCaptureCallback.getCameraState() != CameraState.STATE_PREVIEW) { + result.error("captureAlreadyActive", "Picture is currently already being captured", null); 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); } - private void processCapture(CaptureResult result) { - if (pictureCaptureRequest == null) { + flutterResult = result; + + // Create temporary file + final File outputDir = applicationContext.getCacheDir(); + try { + captureFile = File.createTempFile("CAP", ".jpg", outputDir); + captureTimeouts.reset(); + } catch (IOException | SecurityException e) { + dartMessenger.error(flutterResult, "cannotCreateFile", e.getMessage(), null); return; - } + } - 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(); - } - } - } + // Listen for picture being taken + pictureImageReader.setOnImageAvailableListener(onImageAvailableListener, backgroundHandler); + + final AutoFocusFeature autoFocusFeature = cameraFeatures.getAutoFocus(); + final boolean isAutoFocusSupported = autoFocusFeature.checkIsSupported(); + if (isAutoFocusSupported && autoFocusFeature.getValue() == FocusMode.auto) { + runPictureAutoFocus(); + } else { + runPrecaptureSequence(); } - }; - - private void runPictureAutoFocus() { - assert (pictureCaptureRequest != null); - - pictureCaptureRequest.setState(PictureCaptureRequest.State.focusing); - lockAutoFocus(pictureCaptureCallback); - } - - private void runPicturePreCapture() { - assert (pictureCaptureRequest != null); - pictureCaptureRequest.setState(PictureCaptureRequest.State.preCapture); - - 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)); - } - - 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); - break; - case auto: - captureBuilder.set( - CaptureRequest.CONTROL_AE_MODE, CaptureRequest.CONTROL_AE_MODE_ON_AUTO_FLASH); - break; - case always: - default: - captureBuilder.set( - CaptureRequest.CONTROL_AE_MODE, CaptureRequest.CONTROL_AE_MODE_ON_ALWAYS_FLASH); - break; - } - cameraCaptureSession.stopRepeating(); - cameraCaptureSession.capture( - captureBuilder.build(), - new CameraCaptureSession.CaptureCallback() { - @Override - public void onCaptureCompleted( - @NonNull CameraCaptureSession session, - @NonNull CaptureRequest request, - @NonNull TotalCaptureResult result) { - unlockAutoFocus(); - } - }, - null); - } catch (CameraAccessException e) { - pictureCaptureRequest.error("cameraAccess", e.getMessage(), null); } - } - - private void lockAutoFocus(CaptureCallback callback) { - captureRequestBuilder.set( - CaptureRequest.CONTROL_AF_TRIGGER, CaptureRequest.CONTROL_AF_TRIGGER_START); - - refreshPreviewCaptureSession( - null, (code, message) -> pictureCaptureRequest.error(code, message, null)); - } - - private void unlockAutoFocus() { - captureRequestBuilder.set( - CaptureRequest.CONTROL_AF_TRIGGER, CameraMetadata.CONTROL_AF_TRIGGER_CANCEL); - updateFocus(focusMode); - try { - cameraCaptureSession.capture(captureRequestBuilder.build(), null, null); - } catch (CameraAccessException ignored) { + + /** + * Run the precapture sequence for capturing a still image. This method should be called when we + * 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 + previewRequestBuilder.set( + CaptureRequest.CONTROL_AE_PRECAPTURE_TRIGGER, + CaptureRequest.CONTROL_AE_PRECAPTURE_TRIGGER_IDLE); + captureSession.capture( + previewRequestBuilder.build(), cameraCaptureCallback, backgroundHandler); + + // Repeating request to refresh preview session + refreshPreviewCaptureSession( + null, (code, message) -> dartMessenger.error(flutterResult, "cameraAccess", message, null)); + + // Start precapture now + cameraCaptureCallback.setCameraState(CameraState.STATE_WAITING_PRECAPTURE_START); + + previewRequestBuilder.set( + CaptureRequest.CONTROL_AE_PRECAPTURE_TRIGGER, + CaptureRequest.CONTROL_AE_PRECAPTURE_TRIGGER_START); + + // Trigger one capture to start AE sequence + captureSession.capture( + previewRequestBuilder.build(), cameraCaptureCallback, backgroundHandler); + + } catch (CameraAccessException e) { + e.printStackTrace(); + } } - captureRequestBuilder.set( - CaptureRequest.CONTROL_AF_TRIGGER, CaptureRequest.CONTROL_AF_TRIGGER_IDLE); - - 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; + + /** + * Capture a still picture. This method should be called when we get a response in {@link + * #cameraCaptureCallback} from both lockFocus(). + */ + private void takePictureAfterPrecapture() { + Log.i(TAG, "captureStillPicture"); + cameraCaptureCallback.setCameraState(CameraState.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, + previewRequestBuilder.get(CaptureRequest.SCALER_CROP_REGION)); + + // Update builder settings + updateBuilderSettings(stillBuilder); + + // Orientation + int rotation = 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, backgroundHandler); + } catch (CameraAccessException e) { + dartMessenger.error(flutterResult, "cameraAccess", e.getMessage(), 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); + @SuppressWarnings("deprecation") + private Display getDefaultDisplay() { + return activity.getWindowManager().getDefaultDisplay(); } - } - public void stopVideoRecording(@NonNull final Result result) { - if (!recordingVideo) { - result.success(null); - return; + /** + * Starts a background thread and its {@link Handler}. TODO: call when activity resumed + */ + private void startBackgroundThread() { + backgroundHandlerThread = new HandlerThread("CameraBackground"); + backgroundHandlerThread.start(); + backgroundHandler = new Handler(backgroundHandlerThread.getLooper()); } - try { - recordingVideo = false; - - try { - cameraCaptureSession.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); + /** + * Stops the background thread and its {@link Handler}. TODO: call when activity paused + */ + private void stopBackgroundThread() { + try { + if (backgroundHandlerThread != null) { + backgroundHandlerThread.quitSafely(); + backgroundHandlerThread.join(); + backgroundHandlerThread = null; + } + + backgroundHandler = null; + } catch (InterruptedException e) { + dartMessenger.error(flutterResult, "cameraAccess", e.getMessage(), null); + } } - } - public void pauseVideoRecording(@NonNull final Result result) { - if (!recordingVideo) { - result.success(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. + final Integer sensorOrientation = cameraFeatures.getSensorOrientation().getValue(); + return (ORIENTATIONS.get(rotation) + sensorOrientation + 270) % 360; } - 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; + /** + * Start capturing a picture, doing autofocus first. + */ + private void runPictureAutoFocus() { + Log.i(TAG, "runPictureAutoFocus"); + + cameraCaptureCallback.setCameraState(CameraState.STATE_WAITING_FOCUS); + lockAutoFocus(); + } + + /** + * 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 + previewRequestBuilder.set( + CaptureRequest.CONTROL_AF_TRIGGER, CaptureRequest.CONTROL_AF_TRIGGER_START); + + try { + captureSession.capture(previewRequestBuilder.build(), null, backgroundHandler); + } catch (CameraAccessException e) { + Log.i(TAG, "Error unlocking focus: " + e.getMessage()); + dartMessenger.sendCameraErrorEvent(e.getMessage()); + return; + } } - result.success(null); - } + /** + * Cancel and reset auto focus state and refresh the preview session. + */ + 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); + captureSession.capture(previewRequestBuilder.build(), null, backgroundHandler); + + // Set AF state to idle again + previewRequestBuilder.set( + CaptureRequest.CONTROL_AF_TRIGGER, CameraMetadata.CONTROL_AF_TRIGGER_IDLE); + + captureSession.capture(previewRequestBuilder.build(), null, backgroundHandler); + } catch (CameraAccessException e) { + Log.i(TAG, "Error unlocking focus: " + e.getMessage()); + dartMessenger.sendCameraErrorEvent(e.getMessage()); + return; + } - public void resumeVideoRecording(@NonNull final Result result) { - if (!recordingVideo) { - result.success(null); - return; + refreshPreviewCaptureSession( + null, + (errorCode, errorMessage) -> dartMessenger.error(flutterResult, errorCode, errorMessage, 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 startVideoRecording(Result result) { + final File outputDir = applicationContext.getCacheDir(); + try { + captureFile = File.createTempFile("REC", ".mp4", outputDir); + } catch (IOException | SecurityException e) { + result.error("cannotCreateFile", e.getMessage(), null); + return; + } + + try { + prepareMediaRecorder(captureFile.getAbsolutePath()); + + // Re-create autofocus feature so it's using video focus mode now + cameraFeatures.setAutoFocus(cameraFeatureFactory.createAutoFocusFeature(cameraProperties, true)); + recordingVideo = true; + + createCaptureSession( + CameraDevice.TEMPLATE_RECORD, () -> mediaRecorder.start(), mediaRecorder.getSurface()); + result.success(null); + } catch (CameraAccessException | IOException e) { + recordingVideo = false; + captureFile = null; + result.error("videoRecordingFailed", e.getMessage(), null); + } } - result.success(null); - } - - public void setFlashMode(@NonNull final Result result, FlashMode mode) - throws CameraAccessException { - // Get the flash availability - 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 stopVideoRecording(@NonNull final Result result) { + if (!recordingVideo) { + result.success(null); + return; + } + + try { + // Re-create autofocus feature so it's using continuous capture focus mode now + cameraFeatures.setAutoFocus(cameraFeatureFactory.createAutoFocusFeature(cameraProperties, false)); + 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(captureFile.getAbsolutePath()); + captureFile = null; + } catch (CameraAccessException | IllegalStateException e) { + result.error("videoRecordingFailed", e.getMessage(), null); + } } - // 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) { + 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; - } - - updateFlash(mode); - refreshPreviewCaptureSession( - () -> { - result.success(null); - isFinished = true; - }, - (code, message) -> - result.error("setFlashModeFailed", "Could not set flash mode.", null)); } + } catch (IllegalStateException e) { + result.error("videoRecordingFailed", e.getMessage(), null); + return; + } - @Override - public void onCaptureFailed( - @NonNull CameraCaptureSession session, - @NonNull CaptureRequest request, - @NonNull CaptureFailure failure) { - if (isFinished) { - return; - } + result.success(null); + } - result.error("setFlashModeFailed", "Could not set flash mode.", null); - isFinished = true; + 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; } - }, - null); - } else { - updateFlash(mode); - - refreshPreviewCaptureSession( - () -> 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 { - updateExposure(mode); - cameraCaptureSession.setRepeatingRequest(captureRequestBuilder.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; - } - // Check if the current region boundaries are known - if (cameraRegions.getMaxBoundaries() == null) { - result.error("setExposurePointFailed", "Could not determine max region boundaries", null); - return; + } catch (IllegalStateException e) { + result.error("videoRecordingFailed", e.getMessage(), null); + return; + } + + result.success(null); } - // 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)); - } - - public void setFocusMode(@NonNull final Result result, FocusMode mode) - throws CameraAccessException { - this.focusMode = mode; - - updateFocus(mode); - - switch (mode) { - case auto: + + /** + * 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 Flutter result. + * @param newMode new mode. + */ + public void setFlashMode(@NonNull final Result result, FlashMode newMode) { + // Save the new flash mode setting + final FlashFeature flashFeature = cameraFeatures.getFlash(); + flashFeature.setValue(newMode); + flashFeature.updateBuilder(previewRequestBuilder); + refreshPreviewCaptureSession( - 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(); - } - }); - break; + () -> result.success(null), + (code, message) -> result.error("setFlashModeFailed", "Could not set flash mode.", null)); } - 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; + + /** + * Dart handler for setting new exposure mode setting. + * + * @param result Flutter result. + * @param newMode new mode. + */ + public void setExposureMode(@NonNull final Result result, ExposureMode newMode) { + final ExposureLockFeature exposureLockFeature = cameraFeatures.getExposureLock(); + exposureLockFeature.setValue(newMode); + exposureLockFeature.updateBuilder(previewRequestBuilder); + + refreshPreviewCaptureSession( + () -> result.success(null), + (code, message) -> + result.error("setExposureModeFailed", "Could not set exposure 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; + /** + * Set new exposure point from dart. + * + * @param result Flutter result. + * @param point The exposure point. + */ + public void setExposurePoint(@NonNull final Result result, Point point) { + final ExposurePointFeature exposurePointFeature = cameraFeatures.getExposurePoint(); + exposurePointFeature.setValue(point); + exposurePointFeature.updateBuilder(previewRequestBuilder); + + refreshPreviewCaptureSession( + () -> result.success(null), + (code, message) -> + result.error("setExposurePointFailed", "Could not set exposure point.", null)); } - // Set the metering rectangle - if (x == null || y == null) { - cameraRegions.resetAutoFocusMeteringRectangle(); - } else { - cameraRegions.setAutoFocusMeteringRectangleFromPoint(y, 1 - x); + /** + * Return the max exposure offset value supported by the camera to dart. + */ + public double getMaxExposureOffset() { + return cameraFeatures.getExposureOffset().getMaxExposureOffset(); } - // Apply the new metering rectangle - setFocusMode(result, focusMode); - } - - @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); + /** + * Return the min exposure offset value supported by the camera to dart. + */ + public double getMinExposureOffset() { + return cameraFeatures.getExposureOffset().getMinExposureOffset(); } - // Get the current distortion correction mode - Integer distortionCorrectionMode = - captureRequestBuilder.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 the exposure offset step size to dart. + */ + public double getExposureOffsetStepSize() { + return cameraFeatures.getExposureOffset().getExposureOffsetStepSize(); } - 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 - updateExposure(exposureMode); - this.cameraCaptureSession.setRepeatingRequest(captureRequestBuilder.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; + + /** + * Set new focus mode from dart. + * + * @param result Flutter result. + * @param newMode New mode. + */ + public void setFocusMode(@NonNull final Result result, FocusMode newMode) { + final AutoFocusFeature autoFocusFeature = cameraFeatures.getAutoFocus(); + autoFocusFeature.setValue(newMode); + autoFocusFeature.updateBuilder(previewRequestBuilder); + + /* + * 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 + previewRequestBuilder.set( + CaptureRequest.CONTROL_AF_TRIGGER, CameraMetadata.CONTROL_AF_TRIGGER_IDLE); + + try { + captureSession.setRepeatingRequest( + previewRequestBuilder.build(), null, backgroundHandler); + } 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); } - //Zoom area is calculated relative to sensor area (activeRect) - if (captureRequestBuilder != null) { - final Rect computedZoom = cameraZoom.computeZoom(zoom); - captureRequestBuilder.set(CaptureRequest.SCALER_CROP_REGION, computedZoom); - cameraCaptureSession.setRepeatingRequest(captureRequestBuilder.build(), null, null); + /** + * Sets new focus point from dart. + * + * @param result Flutter result. + * @param point the new coordinates. + */ + public void setFocusPoint(@NonNull final Result result, Point point) { + final FocusPointFeature focusPointFeature = cameraFeatures.getFocusPoint(); + focusPointFeature.setValue(point); + focusPointFeature.updateBuilder(previewRequestBuilder); + + refreshPreviewCaptureSession( + () -> result.success(null), + (code, message) -> result.error("setFocusPointFailed", "Could not set focus point.", null)); } - result.success(null); - } + /** + * Set a new exposure offset from dart. From dart the offset comes as a double, like +1.3 or -1.3. + * + * @param result flutter result. + * @param offset new value. + */ + public void setExposureOffset(@NonNull final Result result, double offset) { + final ExposureOffsetFeature exposureOffsetFeature = cameraFeatures.getExposureOffset(); + exposureOffsetFeature.setValue(offset); + exposureOffsetFeature.updateBuilder(previewRequestBuilder); - public void lockCaptureOrientation(PlatformChannel.DeviceOrientation orientation) { - this.lockedCaptureOrientation = orientation; - } + refreshPreviewCaptureSession( + () -> result.success(null), + (code, message) -> result.error("setFocusModeFailed", "Could not set focus mode.", null)); + } - public void unlockCaptureOrientation() { - this.lockedCaptureOrientation = null; - } + public float getMaxZoomLevel() { + return cameraFeatures.getZoomLevel().getMaximumZoomLevel(); + } - private void updateFpsRange() { - if (fpsRange == null) { - return; + public float getMinZoomLevel() { + return cameraFeatures.getZoomLevel().getMinimumZoomLevel(); } - captureRequestBuilder.set(CaptureRequest.CONTROL_AE_TARGET_FPS_RANGE, fpsRange); - } - - private void updateFocus(FocusMode mode) { - 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; - captureRequestBuilder.set( - CaptureRequest.CONTROL_AF_MODE, CaptureRequest.CONTROL_AF_MODE_OFF); - } else { - // Applying auto focus - switch (mode) { - case locked: - captureRequestBuilder.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); - default: - break; - } - MeteringRectangle afRect = cameraRegions.getAFMeteringRectangle(); - captureRequestBuilder.set( - CaptureRequest.CONTROL_AF_REGIONS, - afRect == null ? null : new MeteringRectangle[] {afRect}); - } - } else { - captureRequestBuilder.set(CaptureRequest.CONTROL_AF_MODE, CaptureRequest.CONTROL_AF_MODE_OFF); + /** + * Shortcut to get current recording profile. + */ + CamcorderProfile getRecordingProfile() { + return cameraFeatures.getResolution().getRecordingProfile(); } - } - - private void updateExposure(ExposureMode mode) { - exposureMode = mode; - - // Applying auto exposure - MeteringRectangle aeRect = cameraRegions.getAEMeteringRectangle(); - captureRequestBuilder.set( - CaptureRequest.CONTROL_AE_REGIONS, - aeRect == null ? null : new MeteringRectangle[] {cameraRegions.getAEMeteringRectangle()}); - - switch (mode) { - case locked: - captureRequestBuilder.set(CaptureRequest.CONTROL_AE_LOCK, true); - break; - case auto: - default: - captureRequestBuilder.set(CaptureRequest.CONTROL_AE_LOCK, false); - break; + + /** + * Shortut to get deviceOrientationListener. + */ + DeviceOrientationManager getDeviceOrientationManager() { + return cameraFeatures.getSensorOrientation().getDeviceOrientationManager(); } - 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; + /** + * Set zoom level from dart. + * + * @param result Flutter result. + * @param zoom new value. + */ + public void setZoomLevel(@NonNull final Result result, float zoom) throws CameraAccessException { + final ZoomLevelFeature zoomLevel = cameraFeatures.getZoomLevel(); + float maxZoom = zoomLevel.getMaximumZoomLevel(); + float minZoom = zoomLevel.getMinimumZoomLevel(); + + 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; + } + + zoomLevel.setValue(zoom); + zoomLevel.updateBuilder(previewRequestBuilder); + + refreshPreviewCaptureSession( + () -> result.success(null), + (code, message) -> result.error("setZoomLevelFailed", "Could not set zoom level.", null)); } - } - - public void startPreview() throws CameraAccessException { - if (pictureImageReader == null || pictureImageReader.getSurface() == null) return; - - createCaptureSession(CameraDevice.TEMPLATE_PREVIEW, pictureImageReader.getSurface()); - } - - public void startPreviewWithImageStream(EventChannel imageStreamChannel) - throws CameraAccessException { - createCaptureSession(CameraDevice.TEMPLATE_RECORD, imageStreamReader.getSurface()); - - 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); + + /** + * Lock capture orientation from dart. + * + * @param orientation new orientation. + */ + public void lockCaptureOrientation(PlatformChannel.DeviceOrientation orientation) { + cameraFeatures.getSensorOrientation().lockCaptureOrientation(orientation); } - 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 (cameraCaptureSession != null) { - cameraCaptureSession.close(); - cameraCaptureSession = null; + + /** + * Unlock capture orientation from dart. + */ + public void unlockCaptureOrientation() { + cameraFeatures.getSensorOrientation().unlockCaptureOrientation(); } - } - public void 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, backgroundHandler); + } + }); } - 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(); + }, + backgroundHandler); } - if (mediaRecorder != null) { - mediaRecorder.reset(); - mediaRecorder.release(); - mediaRecorder = null; + + private void closeCaptureSession() { + if (captureSession != null) { + Log.i(TAG, "closeCaptureSession"); + + captureSession.close(); + captureSession = null; + } } - } - public void dispose() { - close(); - flutterTexture.release(); - deviceOrientationListener.stop(); - } -} + 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(); + } + + public void dispose() { + Log.i(TAG, "dispose"); + + close(); + flutterTexture.release(); + getDeviceOrientationManager().stop(); + } +} \ No newline at end of file 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 a69ddd0410d4..5b05dbb62806 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 @@ -253,7 +253,8 @@ public CameraPropertiesImpl(String cameraName, CameraManager cameraManager) @Override public String getCameraName() { - return cameraName; + return "WOOPS"; +// return cameraName; } @Override 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 b4d4689f2b4e..a5a195a6bd7c 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 @@ -1,7 +1,6 @@ // Copyright 2013 The Flutter 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.app.Activity; @@ -12,10 +11,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; @@ -29,6 +26,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; @@ -60,7 +61,7 @@ static String serializeDeviceOrientation(PlatformChannel.DeviceOrientation orien return "landscapeRight"; default: throw new UnsupportedOperationException( - "Could not serialize device orientation: " + orientation.toString()); + "Could not serialize device orientation: " + orientation.toString()); } } @@ -78,29 +79,19 @@ static PlatformChannel.DeviceOrientation deserializeDeviceOrientation(String ori return PlatformChannel.DeviceOrientation.LANDSCAPE_RIGHT; default: throw new UnsupportedOperationException( - "Could not deserialize device orientation: " + orientation); + "Could not deserialize device orientation: " + orientation); } } - 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( - Arrays.asList(streamConfigurationMap.getOutputSizes(ImageFormat.JPEG)), - new CompareSizesByArea()); + Arrays.asList(streamConfigurationMap.getOutputSizes(ImageFormat.JPEG)), + new CompareSizesByArea()); } public static List> getAvailableCameras(Activity activity) - throws CameraAccessException { + throws CameraAccessException { CameraManager cameraManager = (CameraManager) activity.getSystemService(Context.CAMERA_SERVICE); String[] cameraNames = cameraManager.getCameraIdList(); List> cameras = new ArrayList<>(); @@ -128,51 +119,12 @@ 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) { // We cast here to ensure the multiplications won't overflow. return Long.signum( - (long) lhs.getWidth() * lhs.getHeight() - (long) rhs.getWidth() * rhs.getHeight()); + (long) lhs.getWidth() * lhs.getHeight() - (long) rhs.getWidth() * rhs.getHeight()); } } -} +} \ No newline at end of file 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 93b963e65821..df65eecdb062 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 @@ -6,176 +6,219 @@ import android.os.Handler; import android.text.TextUtils; + import androidx.annotation.NonNull; import androidx.annotation.Nullable; + 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.exposurelock.ExposureMode; +import io.flutter.plugins.camera.features.autofocus.FocusMode; + import java.util.HashMap; import java.util.Map; -/** Utility class that facilitates communication to the Flutter client */ +/** + * Utility class that facilitates communication to the Flutter client + */ public class DartMessenger { - @NonNull private final Handler handler; - @Nullable private MethodChannel cameraChannel; - @Nullable private MethodChannel deviceChannel; - - /** Specifies the different device related message types. */ - enum DeviceEventType { - /** Indicates the device's orientation has changed. */ - ORIENTATION_CHANGED("orientation_changed"); - private final String method; - - DeviceEventType(String method) { - this.method = method; + @NonNull + private final Handler handler; + @Nullable + private MethodChannel cameraChannel; + @Nullable + private MethodChannel deviceChannel; + + /** + * Specifies the different device related message types. + */ + enum DeviceEventType { + /** + * Indicates the device's orientation has changed. + */ + ORIENTATION_CHANGED("orientation_changed"); + private final String method; + + DeviceEventType(String method) { + this.method = method; + } } - } - /** Specifies the different camera related message types. */ - enum CameraEventType { - /** Indicates that an error occurred while interacting with the camera. */ - ERROR("error"), - /** Indicates that the camera is closing. */ - CLOSING("camera_closing"), - /** Indicates that the camera is initialized. */ - INITIALIZED("initialized"); + /** + * Specifies the different camera related message types. + */ + enum CameraEventType { + /** + * Indicates that an error occurred while interacting with the camera. + */ + ERROR("error"), + /** + * Indicates that the camera is closing. + */ + CLOSING("camera_closing"), + /** + * Indicates that the camera is initialized. + */ + INITIALIZED("initialized"); + + private final String method; + + /** + * Converts the supplied method name to the matching {@link CameraEventType}. + * + * @param method name to be converted into a {@link CameraEventType}. + */ + CameraEventType(String method) { + this.method = method; + } + } - private final String method; + /** + * Creates a new instance of the {@link DartMessenger} class. + * + * @param messenger is the {@link BinaryMessenger} that is used to communicate with Flutter. + * @param cameraId identifies the camera which is the source of the communication. + * @param handler the handler used to manage the thread's message queue. This should always be a + * handler managing the main thread since communication with Flutter should always happen on + * the main thread. The handler is mainly supplied so it will be easier test this class. + */ + DartMessenger(BinaryMessenger messenger, long cameraId, @NonNull Handler handler) { + cameraChannel = new MethodChannel(messenger, "flutter.io/cameraPlugin/camera" + cameraId); + deviceChannel = new MethodChannel(messenger, "flutter.io/cameraPlugin/device"); + this.handler = handler; + } /** - * Converts the supplied method name to the matching {@link CameraEventType}. + * Sends a message to the Flutter client informing the orientation of the device has been changed. * - * @param method name to be converted into a {@link CameraEventType}. + * @param orientation specifies the new orientation of the device. */ - CameraEventType(String method) { - this.method = method; + public void sendDeviceOrientationChangeEvent(PlatformChannel.DeviceOrientation orientation) { + assert (orientation != null); + this.send( + DeviceEventType.ORIENTATION_CHANGED, + new HashMap() { + { + put("orientation", CameraUtils.serializeDeviceOrientation(orientation)); + } + }); } - } - - /** - * Creates a new instance of the {@link DartMessenger} class. - * - * @param messenger is the {@link BinaryMessenger} that is used to communicate with Flutter. - * @param cameraId identifies the camera which is the source of the communication. - * @param handler the handler used to manage the thread's message queue. This should always be a - * handler managing the main thread since communication with Flutter should always happen on - * the main thread. The handler is mainly supplied so it will be easier test this class. - */ - DartMessenger(BinaryMessenger messenger, long cameraId, @NonNull Handler handler) { - cameraChannel = new MethodChannel(messenger, "flutter.io/cameraPlugin/camera" + cameraId); - deviceChannel = new MethodChannel(messenger, "flutter.io/cameraPlugin/device"); - this.handler = handler; - } - - /** - * Sends a message to the Flutter client informing the orientation of the device has been changed. - * - * @param orientation specifies the new orientation of the device. - */ - public void sendDeviceOrientationChangeEvent(PlatformChannel.DeviceOrientation orientation) { - assert (orientation != null); - this.send( - DeviceEventType.ORIENTATION_CHANGED, - new HashMap() { - { - put("orientation", CameraUtils.serializeDeviceOrientation(orientation)); - } - }); - } - - /** - * Sends a message to the Flutter client informing that the camera has been initialized. - * - * @param previewWidth describes the preview width that is supported by the camera. - * @param previewHeight describes the preview height that is supported by the camera. - * @param exposureMode describes the current exposure mode that is set on the camera. - * @param focusMode describes the current focus mode that is set on the camera. - * @param exposurePointSupported indicates if the camera supports setting an exposure point. - * @param focusPointSupported indicates if the camera supports setting a focus point. - */ - void sendCameraInitializedEvent( - Integer previewWidth, - Integer previewHeight, - ExposureMode exposureMode, - FocusMode focusMode, - Boolean exposurePointSupported, - Boolean focusPointSupported) { - assert (previewWidth != null); - assert (previewHeight != null); - assert (exposureMode != null); - assert (focusMode != null); - assert (exposurePointSupported != null); - assert (focusPointSupported != null); - this.send( - CameraEventType.INITIALIZED, - new HashMap() { - { - put("previewWidth", previewWidth.doubleValue()); - put("previewHeight", previewHeight.doubleValue()); - put("exposureMode", exposureMode.toString()); - put("focusMode", focusMode.toString()); - put("exposurePointSupported", exposurePointSupported); - put("focusPointSupported", focusPointSupported); - } - }); - } - - /** Sends a message to the Flutter client informing that the camera is closing. */ - void sendCameraClosingEvent() { - send(CameraEventType.CLOSING); - } - - /** - * Sends a message to the Flutter client informing that an error occurred while interacting with - * the camera. - * - * @param description contains details regarding the error that occurred. - */ - void sendCameraErrorEvent(@Nullable String description) { - this.send( - CameraEventType.ERROR, - new HashMap() { - { - if (!TextUtils.isEmpty(description)) put("description", description); - } - }); - } - - private void send(CameraEventType eventType) { - send(eventType, new HashMap<>()); - } - - private void send(CameraEventType eventType, Map args) { - if (cameraChannel == null) { - return; + + /** + * Sends a message to the Flutter client informing that the camera has been initialized. + * + * @param previewWidth describes the preview width that is supported by the camera. + * @param previewHeight describes the preview height that is supported by the camera. + * @param exposureMode describes the current exposure mode that is set on the camera. + * @param focusMode describes the current focus mode that is set on the camera. + * @param exposurePointSupported indicates if the camera supports setting an exposure point. + * @param focusPointSupported indicates if the camera supports setting a focus point. + */ + void sendCameraInitializedEvent( + Integer previewWidth, + Integer previewHeight, + ExposureMode exposureMode, + FocusMode focusMode, + Boolean exposurePointSupported, + Boolean focusPointSupported) { + assert (previewWidth != null); + assert (previewHeight != null); + assert (exposureMode != null); + assert (focusMode != null); + assert (exposurePointSupported != null); + assert (focusPointSupported != null); + this.send( + CameraEventType.INITIALIZED, + new HashMap() { + { + put("previewWidth", previewWidth.doubleValue()); + put("previewHeight", previewHeight.doubleValue()); + put("exposureMode", exposureMode.toString()); + put("focusMode", focusMode.toString()); + put("exposurePointSupported", exposurePointSupported); + put("focusPointSupported", focusPointSupported); + } + }); } - handler.post( - new Runnable() { - @Override - public void run() { - cameraChannel.invokeMethod(eventType.method, args); - } - }); - } - - private void send(DeviceEventType eventType) { - send(eventType, new HashMap<>()); - } - - private void send(DeviceEventType eventType, Map args) { - if (deviceChannel == null) { - return; + /** + * Sends a message to the Flutter client informing that the camera is closing. + */ + void sendCameraClosingEvent() { + send(CameraEventType.CLOSING); } - handler.post( - new Runnable() { - @Override - public void run() { - deviceChannel.invokeMethod(eventType.method, args); - } - }); - } + /** + * Sends a message to the Flutter client informing that an error occurred while interacting with + * the camera. + * + * @param description contains details regarding the error that occurred. + */ + void sendCameraErrorEvent(@Nullable String description) { + this.send( + CameraEventType.ERROR, + new HashMap() { + { + if (!TextUtils.isEmpty(description)) put("description", description); + } + }); + } + + private void send(CameraEventType eventType) { + send(eventType, new HashMap<>()); + } + + private void send(CameraEventType eventType, Map args) { + if (cameraChannel == null) { + return; + } + + handler.post( + new Runnable() { + @Override + public void run() { + cameraChannel.invokeMethod(eventType.method, args); + } + }); + } + + private void send(DeviceEventType eventType) { + send(eventType, new HashMap<>()); + } + + private void send(DeviceEventType eventType, Map args) { + if (deviceChannel == null) { + return; + } + + handler.post( + new Runnable() { + @Override + public void run() { + deviceChannel.invokeMethod(eventType.method, args); + } + }); + } + + /** + * Send a success payload to a {@link MethodChannel.Result} on the main thread. + * + * @param payload The payload to send. + */ + public void finish(MethodChannel.Result result, Object payload) { + handler.post(() -> result.success(payload)); + } + + /** + * Send an error payload to a {@link MethodChannel.Result} on the main thread. + * + * @param errorCode error code. + * @param errorMessage error message. + * @param errorDetails error details. + */ + public void error(MethodChannel.Result result, + String errorCode, @Nullable String errorMessage, @Nullable Object errorDetails) { + handler.post(() -> result.error(errorCode, errorMessage, errorDetails)); + } } 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 50bca6349217..e51e4361b034 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 @@ -8,8 +8,10 @@ import android.hardware.camera2.CameraAccessException; import android.os.Handler; import android.os.Looper; + import androidx.annotation.NonNull; import androidx.annotation.Nullable; + import io.flutter.embedding.engine.systemchannels.PlatformChannel; import io.flutter.plugin.common.BinaryMessenger; import io.flutter.plugin.common.EventChannel; @@ -17,372 +19,356 @@ 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.features.CameraFeatureFactoryImpl; +import io.flutter.plugins.camera.features.Point; +import io.flutter.plugins.camera.features.exposurelock.ExposureMode; +import io.flutter.plugins.camera.features.flash.FlashMode; +import io.flutter.plugins.camera.features.autofocus.FocusMode; +import io.flutter.plugins.camera.features.resolution.ResolutionPreset; import io.flutter.view.TextureRegistry; + import java.util.HashMap; import java.util.Map; final class MethodCallHandlerImpl implements MethodChannel.MethodCallHandler { - private final Activity activity; - private final BinaryMessenger messenger; - private final CameraPermissions cameraPermissions; - private final PermissionsRegistry permissionsRegistry; - private final TextureRegistry textureRegistry; - private final MethodChannel methodChannel; - private final EventChannel imageStreamChannel; - private @Nullable Camera camera; - - MethodCallHandlerImpl( - Activity activity, - BinaryMessenger messenger, - CameraPermissions cameraPermissions, - PermissionsRegistry permissionsAdder, - TextureRegistry textureRegistry) { - this.activity = activity; - this.messenger = messenger; - this.cameraPermissions = cameraPermissions; - this.permissionsRegistry = permissionsAdder; - this.textureRegistry = textureRegistry; + private final Activity activity; + private final BinaryMessenger messenger; + private final CameraPermissions cameraPermissions; + private final PermissionsRegistry permissionsRegistry; + private final TextureRegistry textureRegistry; + private final MethodChannel methodChannel; + private final EventChannel imageStreamChannel; + private @Nullable + Camera camera; - methodChannel = new MethodChannel(messenger, "plugins.flutter.io/camera"); - imageStreamChannel = new EventChannel(messenger, "plugins.flutter.io/camera/imageStream"); - methodChannel.setMethodCallHandler(this); - } + MethodCallHandlerImpl( + Activity activity, + BinaryMessenger messenger, + CameraPermissions cameraPermissions, + PermissionsRegistry permissionsAdder, + TextureRegistry textureRegistry) { + this.activity = activity; + this.messenger = messenger; + this.cameraPermissions = cameraPermissions; + this.permissionsRegistry = permissionsAdder; + this.textureRegistry = textureRegistry; - @Override - public void onMethodCall(@NonNull MethodCall call, @NonNull final Result result) { - switch (call.method) { - case "availableCameras": - try { - result.success(CameraUtils.getAvailableCameras(activity)); - } catch (Exception e) { - handleException(e, result); - } - break; - case "create": - { - if (camera != null) { - camera.close(); - } + methodChannel = new MethodChannel(messenger, "plugins.flutter.io/camera"); + imageStreamChannel = new EventChannel(messenger, "plugins.flutter.io/camera/imageStream"); + methodChannel.setMethodCallHandler(this); + } - cameraPermissions.requestPermissions( - activity, - permissionsRegistry, - call.argument("enableAudio"), - (String errCode, String errDesc) -> { - if (errCode == null) { - try { - instantiateCamera(call, result); - } catch (Exception e) { + @Override + public void onMethodCall(@NonNull MethodCall call, @NonNull final Result result) { + switch (call.method) { + case "availableCameras": + try { + result.success(CameraUtils.getAvailableCameras(activity)); + } catch (Exception e) { handleException(e, result); - } + } + break; + case "create": { + if (camera != null) { + camera.close(); + } + + cameraPermissions.requestPermissions( + activity, + permissionsRegistry, + call.argument("enableAudio"), + (String errCode, String errDesc) -> { + if (errCode == null) { + try { + instantiateCamera(call, result); + } catch (Exception e) { + handleException(e, result); + } + } else { + result.error(errCode, errDesc, null); + } + }); + break; + } + case "initialize": { + if (camera != null) { + try { + camera.open(call.argument("imageFormatGroup")); + result.success(null); + } catch (Exception e) { + handleException(e, result); + } } else { - result.error(errCode, errDesc, null); + result.error( + "cameraNotFound", + "Camera not found. Please call the 'create' method before calling 'initialize'.", + null); } - }); - break; - } - case "initialize": - { - if (camera != null) { - try { - camera.open(call.argument("imageFormatGroup")); - result.success(null); - } catch (Exception e) { - handleException(e, result); + break; } - } else { - result.error( - "cameraNotFound", - "Camera not found. Please call the 'create' method before calling 'initialize'.", - null); - } - break; - } - case "takePicture": - { - camera.takePicture(result); - break; - } - case "prepareForVideoRecording": - { - // This optimization is not required for Android. - result.success(null); - break; - } - case "startVideoRecording": - { - camera.startVideoRecording(result); - break; - } - case "stopVideoRecording": - { - camera.stopVideoRecording(result); - break; - } - case "pauseVideoRecording": - { - camera.pauseVideoRecording(result); - break; - } - case "resumeVideoRecording": - { - camera.resumeVideoRecording(result); - break; - } - case "setFlashMode": - { - String modeStr = call.argument("mode"); - FlashMode mode = FlashMode.getValueForString(modeStr); - if (mode == null) { - result.error("setFlashModeFailed", "Unknown flash mode " + modeStr, null); - return; - } - try { - camera.setFlashMode(result, mode); - } catch (Exception e) { - handleException(e, result); - } - break; - } - case "setExposureMode": - { - String modeStr = call.argument("mode"); - ExposureMode mode = ExposureMode.getValueForString(modeStr); - if (mode == null) { - result.error("setExposureModeFailed", "Unknown exposure mode " + modeStr, null); - return; - } - try { - camera.setExposureMode(result, mode); - } catch (Exception e) { - handleException(e, result); - } - break; - } - case "setExposurePoint": - { - Boolean reset = call.argument("reset"); - Double x = null; - Double y = null; - if (reset == null || !reset) { - x = call.argument("x"); - y = call.argument("y"); - } - try { - camera.setExposurePoint(result, x, y); - } catch (Exception e) { - handleException(e, result); - } - break; - } - case "getMinExposureOffset": - { - try { - result.success(camera.getMinExposureOffset()); - } catch (Exception e) { - handleException(e, result); - } - break; - } - case "getMaxExposureOffset": - { - try { - result.success(camera.getMaxExposureOffset()); - } catch (Exception e) { - handleException(e, result); - } - break; - } - case "getExposureOffsetStepSize": - { - try { - result.success(camera.getExposureOffsetStepSize()); - } catch (Exception e) { - handleException(e, result); - } - break; - } - case "setExposureOffset": - { - try { - camera.setExposureOffset(result, call.argument("offset")); - } catch (Exception e) { - handleException(e, result); - } - break; - } - case "setFocusMode": - { - String modeStr = call.argument("mode"); - FocusMode mode = FocusMode.getValueForString(modeStr); - if (mode == null) { - result.error("setFocusModeFailed", "Unknown focus mode " + modeStr, null); - return; - } - try { - camera.setFocusMode(result, mode); - } catch (Exception e) { - handleException(e, result); - } - break; - } - case "setFocusPoint": - { - Boolean reset = call.argument("reset"); - Double x = null; - Double y = null; - if (reset == null || !reset) { - x = call.argument("x"); - y = call.argument("y"); - } - try { - camera.setFocusPoint(result, x, y); - } catch (Exception e) { - handleException(e, result); - } - break; - } - case "startImageStream": - { - try { - camera.startPreviewWithImageStream(imageStreamChannel); - result.success(null); - } catch (Exception e) { - handleException(e, result); - } - break; - } - case "stopImageStream": - { - try { - camera.startPreview(); - result.success(null); - } catch (Exception e) { - handleException(e, result); - } - break; - } - case "getMaxZoomLevel": - { - assert camera != null; + case "takePicture": { + camera.takePicture(result); + break; + } + case "prepareForVideoRecording": { + // This optimization is not required for Android. + result.success(null); + break; + } + case "startVideoRecording": { + camera.startVideoRecording(result); + break; + } + case "stopVideoRecording": { + camera.stopVideoRecording(result); + break; + } + case "pauseVideoRecording": { + camera.pauseVideoRecording(result); + break; + } + case "resumeVideoRecording": { + camera.resumeVideoRecording(result); + break; + } + case "setFlashMode": { + String modeStr = call.argument("mode"); + FlashMode mode = FlashMode.getValueForString(modeStr); + if (mode == null) { + result.error("setFlashModeFailed", "Unknown flash mode " + modeStr, null); + return; + } + try { + camera.setFlashMode(result, mode); + } catch (Exception e) { + handleException(e, result); + } + break; + } + case "setExposureMode": { + String modeStr = call.argument("mode"); + ExposureMode mode = ExposureMode.getValueForString(modeStr); + if (mode == null) { + result.error("setExposureModeFailed", "Unknown exposure mode " + modeStr, null); + return; + } + try { + camera.setExposureMode(result, mode); + } catch (Exception e) { + handleException(e, result); + } + break; + } + case "setExposurePoint": { + Boolean reset = call.argument("reset"); + Double x = null; + Double y = null; + if (reset == null || !reset) { + x = call.argument("x"); + y = call.argument("y"); + } + try { + camera.setExposurePoint(result, new Point(x, y)); + } catch (Exception e) { + handleException(e, result); + } + break; + } + case "getMinExposureOffset": { + try { + result.success(camera.getMinExposureOffset()); + } catch (Exception e) { + handleException(e, result); + } + break; + } + case "getMaxExposureOffset": { + try { + result.success(camera.getMaxExposureOffset()); + } catch (Exception e) { + handleException(e, result); + } + break; + } + case "getExposureOffsetStepSize": { + try { + result.success(camera.getExposureOffsetStepSize()); + } catch (Exception e) { + handleException(e, result); + } + break; + } + case "setExposureOffset": { + try { + camera.setExposureOffset(result, call.argument("offset")); + } catch (Exception e) { + handleException(e, result); + } + break; + } + case "setFocusMode": { + String modeStr = call.argument("mode"); + FocusMode mode = FocusMode.getValueForString(modeStr); + if (mode == null) { + result.error("setFocusModeFailed", "Unknown focus mode " + modeStr, null); + return; + } + try { + camera.setFocusMode(result, mode); + } catch (Exception e) { + handleException(e, result); + } + break; + } + case "setFocusPoint": { + Boolean reset = call.argument("reset"); + Double x = null; + Double y = null; + if (reset == null || !reset) { + x = call.argument("x"); + y = call.argument("y"); + } + try { + camera.setFocusPoint(result, new Point(x, y)); + } catch (Exception e) { + handleException(e, result); + } + break; + } + case "startImageStream": { + try { + camera.startPreviewWithImageStream(imageStreamChannel); + result.success(null); + } catch (Exception e) { + handleException(e, result); + } + break; + } + case "stopImageStream": { + try { + camera.startPreview(); + result.success(null); + } catch (Exception e) { + handleException(e, result); + } + break; + } + case "getMaxZoomLevel": { + assert camera != null; - try { - float maxZoomLevel = camera.getMaxZoomLevel(); - result.success(maxZoomLevel); - } catch (Exception e) { - handleException(e, result); - } - break; - } - case "getMinZoomLevel": - { - assert camera != null; + try { + float maxZoomLevel = camera.getMaxZoomLevel(); + result.success(maxZoomLevel); + } catch (Exception e) { + handleException(e, result); + } + break; + } + case "getMinZoomLevel": { + assert camera != null; - try { - float minZoomLevel = camera.getMinZoomLevel(); - result.success(minZoomLevel); - } catch (Exception e) { - handleException(e, result); - } - break; - } - case "setZoomLevel": - { - assert camera != null; + try { + float minZoomLevel = camera.getMinZoomLevel(); + result.success(minZoomLevel); + } catch (Exception e) { + handleException(e, result); + } + break; + } + case "setZoomLevel": { + assert camera != null; - Double zoom = call.argument("zoom"); + Double zoom = call.argument("zoom"); - if (zoom == null) { - result.error( - "ZOOM_ERROR", "setZoomLevel is called without specifying a zoom level.", null); - return; - } + if (zoom == null) { + result.error( + "ZOOM_ERROR", "setZoomLevel is called without specifying a zoom level.", null); + return; + } - try { - camera.setZoomLevel(result, zoom.floatValue()); - } catch (Exception e) { - handleException(e, result); - } - break; - } - case "lockCaptureOrientation": - { - PlatformChannel.DeviceOrientation orientation = - CameraUtils.deserializeDeviceOrientation(call.argument("orientation")); + try { + camera.setZoomLevel(result, zoom.floatValue()); + } catch (Exception e) { + handleException(e, result); + } + break; + } + case "lockCaptureOrientation": { + PlatformChannel.DeviceOrientation orientation = + CameraUtils.deserializeDeviceOrientation(call.argument("orientation")); - try { - camera.lockCaptureOrientation(orientation); - result.success(null); - } catch (Exception e) { - handleException(e, result); - } - break; - } - case "unlockCaptureOrientation": - { - try { - camera.unlockCaptureOrientation(); - result.success(null); - } catch (Exception e) { - handleException(e, result); - } - break; - } - case "dispose": - { - if (camera != null) { - camera.dispose(); - } - result.success(null); - break; + try { + camera.lockCaptureOrientation(orientation); + result.success(null); + } catch (Exception e) { + handleException(e, result); + } + break; + } + case "unlockCaptureOrientation": { + try { + camera.unlockCaptureOrientation(); + result.success(null); + } catch (Exception e) { + handleException(e, result); + } + break; + } + case "dispose": { + if (camera != null) { + camera.dispose(); + } + result.success(null); + break; + } + default: + result.notImplemented(); + break; } - default: - result.notImplemented(); - break; } - } - void stopListening() { - methodChannel.setMethodCallHandler(null); - } + void stopListening() { + methodChannel.setMethodCallHandler(null); + } + + private void instantiateCamera(MethodCall call, Result result) throws CameraAccessException { + String cameraName = call.argument("cameraName"); + String preset = call.argument("resolutionPreset"); + boolean enableAudio = call.argument("enableAudio"); - private void instantiateCamera(MethodCall call, Result result) throws CameraAccessException { - String cameraName = call.argument("cameraName"); - String resolutionPreset = call.argument("resolutionPreset"); - boolean enableAudio = call.argument("enableAudio"); - TextureRegistry.SurfaceTextureEntry flutterSurfaceTexture = - textureRegistry.createSurfaceTexture(); - DartMessenger dartMessenger = - new DartMessenger( - messenger, flutterSurfaceTexture.id(), new Handler(Looper.getMainLooper())); - camera = - new Camera( - activity, - flutterSurfaceTexture, - dartMessenger, - cameraName, - resolutionPreset, - enableAudio); + TextureRegistry.SurfaceTextureEntry flutterSurfaceTexture = + textureRegistry.createSurfaceTexture(); + DartMessenger dartMessenger = new DartMessenger(messenger, flutterSurfaceTexture.id(), new Handler(Looper.getMainLooper())); + CameraProperties cameraProperties = + new CameraPropertiesImpl(cameraName, CameraUtils.getCameraManager(activity)); + ResolutionPreset resolutionPreset = ResolutionPreset.valueOf(preset); - Map reply = new HashMap<>(); - reply.put("cameraId", flutterSurfaceTexture.id()); - result.success(reply); - } + camera = + new Camera( + activity, + flutterSurfaceTexture, + new CameraFeatureFactoryImpl(), + dartMessenger, + cameraProperties, + resolutionPreset, + enableAudio); - // We move catching CameraAccessException out of onMethodCall because it causes a crash - // on plugin registration for sdks incompatible with Camera2 (< 21). We want this plugin to - // to be able to compile with <21 sdks for apps that want the camera and support earlier version. - @SuppressWarnings("ConstantConditions") - private void handleException(Exception exception, Result result) { - if (exception instanceof CameraAccessException) { - result.error("CameraAccess", exception.getMessage(), null); - return; + Map reply = new HashMap<>(); + reply.put("cameraId", flutterSurfaceTexture.id()); + result.success(reply); } - // CameraAccessException can not be cast to a RuntimeException. - throw (RuntimeException) exception; - } + // We move catching CameraAccessException out of onMethodCall because it causes a crash + // on plugin registration for sdks incompatible with Camera2 (< 21). We want this plugin to + // to be able to compile with <21 sdks for apps that want the camera and support earlier version. + @SuppressWarnings("ConstantConditions") + private void handleException(Exception exception, Result result) { + if (exception instanceof CameraAccessException) { + result.error("CameraAccess", exception.getMessage(), null); + return; + } + + // CameraAccessException can not be cast to a RuntimeException. + throw (RuntimeException) exception; + } } 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 2fa47255ac56..c35c02e137c9 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 @@ -71,11 +71,11 @@ AutoFocusFeature createAutoFocusFeature( * @param cameraProperties instance of the CameraProperties class containing information about the * cameras features. * @param initialSetting initial resolution preset. - * @param cameraId - the id of the camera which can be used to identify the camera device. + * @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, int cameraId); + @NonNull CameraProperties cameraProperties, ResolutionPreset initialSetting, String cameraName); /** * Creates a new instance of the focus point feature. 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 d9caafed3c3f..4c4ceaeb40dd 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 @@ -51,8 +51,8 @@ public FlashFeature createFlashFeature(@NonNull CameraProperties cameraPropertie @Override public ResolutionFeature createResolutionFeature( - @NonNull CameraProperties cameraProperties, ResolutionPreset initialSetting, int cameraId) { - return new ResolutionFeature(cameraProperties, initialSetting, cameraId); + @NonNull CameraProperties cameraProperties, ResolutionPreset initialSetting, String cameraName) { + return new ResolutionFeature(cameraProperties, initialSetting, cameraName); } @Override 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 d96f486e5b38..b6f3cb327763 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 @@ -4,32 +4,251 @@ package io.flutter.plugins.camera.features; +import java.util.Collection; +import java.util.HashMap; +import java.util.Map; + +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.resolution.ResolutionFeature; +import io.flutter.plugins.camera.features.sensororientation.SensorOrientationFeature; +import io.flutter.plugins.camera.features.zoomlevel.ZoomLevelFeature; + /** - * 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. + * These are all of our available features in the camera. Used in the Camera to access + * all features in a simpler way. */ -public enum CameraFeatures { - 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; - - CameraFeatures(String strValue) { - this.strValue = strValue; - } - - @Override - public String toString() { - return strValue; - } +public class CameraFeatures { + private static final String AUTO_FOCUS = "AUTO_FOCUS"; + private static final String EXPOSURE_LOCK = "EXPOSURE_LOCK"; + private static final String EXPOSURE_OFFSET = "EXPOSURE_OFFSET"; + private static final String EXPOSURE_POINT = "EXPOSURE_POINT"; + private static final String FLASH = "FLASH"; + private static final String FOCUS_POINT = "FOCUS_POINT"; + private static final String FPS_RANGE = "FPS_RANGE"; + private static final String NOISE_REDUCTION = "NOISE_REDUCTION"; + private static final String REGION_BOUNDARIES = "REGION_BOUNDARIES"; + private static final String RESOLUTION = "RESOLUTION"; + private static final String SENSOR_ORIENTATION = "SENSOR_ORIENTATION"; + private static final String ZOOM_LEVEL = "ZOOM_LEVEL"; + + private Map featureMap = new HashMap<>(); + + + private T getFeature(String key) { + return (T) featureMap.get(key); + } + + /** + * Gets a collection of all features that have been set. + * + * @return A collection of all features that have been set. + */ + public Collection getAllFeatures() { + return this.featureMap.values(); + } + + /** + * Gets the auto focus feature if it has been set. + * + * @return the auto focus feature. + */ + public AutoFocusFeature getAutoFocus() { + return getFeature(AUTO_FOCUS); + } + + /** + * Sets the instance of the auto focus feature. + * + * @param autoFocus the {@link AutoFocusFeature} instance to set. + */ + public void setAutoFocus(AutoFocusFeature autoFocus) { + this.featureMap.put(AUTO_FOCUS, autoFocus); + } + + /** + * Gets the exposure lock feature if it has been set. + * + * @return the exposure lock feature. + */ + public ExposureLockFeature getExposureLock() { + return getFeature(EXPOSURE_LOCK); + } + + /** + * Sets the instance of the exposure lock feature. + * + * @param exposureLock the {@link ExposureLockFeature} instance to set. + */ + public void setExposureLock(ExposureLockFeature exposureLock) { + this.featureMap.put(EXPOSURE_LOCK, exposureLock); + } + + /** + * Gets the exposure offset feature if it has been set. + * + * @return the exposure offset feature. + */ + public ExposureOffsetFeature getExposureOffset() { + return getFeature(EXPOSURE_OFFSET); + } + + /** + * Sets the instance of the exposure offset feature. + * + * @param exposureOffset the {@link ExposureOffsetFeature} instance to set. + */ + public void setExposureOffset(ExposureOffsetFeature exposureOffset) { + this.featureMap.put(EXPOSURE_OFFSET, exposureOffset); + } + + /** + * Gets the exposure point feature if it has been set. + * + * @return the exposure point feature. + */ + public ExposurePointFeature getExposurePoint() { + return getFeature(EXPOSURE_POINT); + } + + /** + * Sets the instance of the exposure point feature. + * + * @param exposurePoint the {@link ExposurePointFeature} instance to set. + */ + public void setExposurePoint(ExposurePointFeature exposurePoint) { + this.featureMap.put(EXPOSURE_POINT, exposurePoint); + } + + /** + * Gets the flash feature if it has been set. + * + * @return the flash feature. + */ + public FlashFeature getFlash() { + return getFeature(FLASH); + } + + /** + * Sets the instance of the flash feature. + * + * @param flash the {@link FlashFeature} instance to set. + */ + public void setFlash(FlashFeature flash) { + this.featureMap.put(FLASH, flash); + } + + /** + * Gets the focus point feature if it has been set. + * + * @return the focus point feature. + */ + public FocusPointFeature getFocusPoint() { + return getFeature(FOCUS_POINT); + } + + /** + * Sets the instance of the focus point feature. + * + * @param focusPoint the {@link FocusPointFeature} instance to set. + */ + public void setFocusPoint(FocusPointFeature focusPoint) { + this.featureMap.put(FOCUS_POINT, focusPoint); + } + + /** + * Gets the fps range feature if it has been set. + * + * @return the fps range feature. + */ + public FpsRangeFeature getFpsRange() { + return getFeature(FPS_RANGE); + } + + /** + * Sets the instance of the fps range feature. + * + * @param fpsRange the {@link FpsRangeFeature} instance to set. + */ + public void setFpsRange(FpsRangeFeature fpsRange) { + this.featureMap.put(FPS_RANGE, fpsRange); + } + + /** + * Gets the noise reduction feature if it has been set. + * + * @return the noise reduction feature. + */ + public NoiseReductionFeature getNoiseReduction() { + return getFeature(NOISE_REDUCTION); + } + + /** + * Sets the instance of the noise reduction feature. + * + * @param noiseReduction the {@link NoiseReductionFeature} instance to set. + */ + public void setNoiseReduction(NoiseReductionFeature noiseReduction) { + this.featureMap.put(NOISE_REDUCTION, noiseReduction); + } + + /** + * Gets the resolution feature if it has been set. + * + * @return the resolution feature. + */ + public ResolutionFeature getResolution() { + return getFeature(RESOLUTION); + } + + /** + * Sets the instance of the resolution feature. + * + * @param resolution the {@link ResolutionFeature} instance to set. + */ + public void setResolution(ResolutionFeature resolution) { + this.featureMap.put(RESOLUTION, resolution); + } + + /** + * Gets the sensor orientation feature if it has been set. + * + * @return the sensor orientation feature. + */ + public SensorOrientationFeature getSensorOrientation() { + return getFeature(SENSOR_ORIENTATION); + } + + /** + * Sets the instance of the sensor orientation feature. + * + * @param sensorOrientation the {@link SensorOrientationFeature} instance to set. + */ + public void setSensorOrientation(SensorOrientationFeature sensorOrientation) { + this.featureMap.put(SENSOR_ORIENTATION, sensorOrientation); + } + + /** + * Gets the zoom level feature if it has been set. + * + * @return the zoom level feature. + */ + public ZoomLevelFeature getZoomLevel() { + return getFeature(ZOOM_LEVEL); + } + + /** + * Sets the instance of the zoom level feature. + * + * @param zoomLevel the {@link ZoomLevelFeature} instance to set. + */ + public void setZoomLevel(ZoomLevelFeature zoomLevel) { + this.featureMap.put(ZOOM_LEVEL, zoomLevel); + } } 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 a5469c63359b..eb2123be93f2 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 @@ -7,7 +7,9 @@ import android.hardware.camera2.CaptureRequest; import android.media.CamcorderProfile; import android.util.Size; + import androidx.annotation.VisibleForTesting; + import io.flutter.plugins.camera.CameraProperties; import io.flutter.plugins.camera.features.CameraFeature; @@ -19,148 +21,158 @@ * required to configure the resolution using the {@link android.hardware.camera2} API. */ public class ResolutionFeature extends CameraFeature { - private Size captureSize; - private Size previewSize; - private CamcorderProfile recordingProfile; - private ResolutionPreset currentSetting; - private int cameraId; - - /** - * Creates a new instance of the {@link ResolutionFeature}. - * - * @param cameraProperties Collection of characteristics for the current camera device. - * @param resolutionPreset Platform agnostic enum containing resolution information. - * @param cameraId Camera identifier of the camera for which to configure the resolution. - */ - public ResolutionFeature( - CameraProperties cameraProperties, ResolutionPreset resolutionPreset, int cameraId) { - super(cameraProperties); - this.currentSetting = resolutionPreset; - this.cameraId = cameraId; - - configureResolution(resolutionPreset, cameraId); - } - - /** - * Gets the best possible {@link android.media.CamcorderProfile} for the supplied {@link - * ResolutionPreset}. - * - * @param cameraId Camera identifier which indicates the device's camera for which to select a - * {@link android.media.CamcorderProfile}. - * @param preset The {@link ResolutionPreset} for which is to be translated to a {@link - * android.media.CamcorderProfile}. - * @return The best possible {@link android.media.CamcorderProfile} that matches the supplied - * {@link ResolutionPreset}. - */ - public static CamcorderProfile getBestAvailableCamcorderProfileForResolutionPreset( - int cameraId, ResolutionPreset preset) { - - 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); + private Size captureSize; + private Size previewSize; + private CamcorderProfile recordingProfile; + private ResolutionPreset currentSetting; + private int cameraId; + + /** + * Creates a new instance of the {@link ResolutionFeature}. + * + * @param cameraProperties Collection of characteristics for the current camera device. + * @param resolutionPreset Platform agnostic enum containing resolution information. + * @param cameraName Camera identifier of the camera for which to configure the resolution. + */ + public ResolutionFeature( + CameraProperties cameraProperties, ResolutionPreset resolutionPreset, String cameraName) { + super(cameraProperties); + this.currentSetting = resolutionPreset; + try { + this.cameraId = Integer.parseInt(cameraName, 10); + } catch (NumberFormatException e) { + this.cameraId = -1; + return; } - case high: - if (CamcorderProfile.hasProfile(cameraId, CamcorderProfile.QUALITY_720P)) { - return CamcorderProfile.get(cameraId, CamcorderProfile.QUALITY_720P); + configureResolution(resolutionPreset, cameraId); + } + + /** + * Gets the best possible {@link android.media.CamcorderProfile} for the supplied {@link + * ResolutionPreset}. + * + * @param cameraId Camera identifier which indicates the device's camera for which to select a + * {@link android.media.CamcorderProfile}. + * @param preset The {@link ResolutionPreset} for which is to be translated to a {@link + * android.media.CamcorderProfile}. + * @return The best possible {@link android.media.CamcorderProfile} that matches the supplied + * {@link ResolutionPreset}. + */ + public static CamcorderProfile getBestAvailableCamcorderProfileForResolutionPreset( + int cameraId, ResolutionPreset preset) { + if (cameraId < 0) { + throw new AssertionError("getBestAvailableCamcorderProfileForResolutionPreset can only be used with valid (>=0) camera identifiers."); } - case medium: - if (CamcorderProfile.hasProfile(cameraId, CamcorderProfile.QUALITY_480P)) { - return CamcorderProfile.get(cameraId, CamcorderProfile.QUALITY_480P); + + 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."); + } } - case low: - if (CamcorderProfile.hasProfile(cameraId, CamcorderProfile.QUALITY_QVGA)) { - return CamcorderProfile.get(cameraId, CamcorderProfile.QUALITY_QVGA); + } + + @VisibleForTesting + static Size computeBestPreviewSize(int cameraId, ResolutionPreset preset) { + if (preset.ordinal() > ResolutionPreset.high.ordinal()) { + preset = ResolutionPreset.high; } - 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."); + + CamcorderProfile profile = + getBestAvailableCamcorderProfileForResolutionPreset(cameraId, preset); + return new Size(profile.videoFrameWidth, profile.videoFrameHeight); + } + + private void configureResolution(ResolutionPreset resolutionPreset, int cameraId) { + if (!checkIsSupported()) { + return; } + recordingProfile = + getBestAvailableCamcorderProfileForResolutionPreset(cameraId, resolutionPreset); + captureSize = new Size(recordingProfile.videoFrameWidth, recordingProfile.videoFrameHeight); + previewSize = computeBestPreviewSize(cameraId, resolutionPreset); } - } - @VisibleForTesting - static Size computeBestPreviewSize(int cameraId, ResolutionPreset preset) { - if (preset.ordinal() > ResolutionPreset.high.ordinal()) { - preset = ResolutionPreset.high; + @Override + public String getDebugName() { + return "ResolutionFeature"; } - CamcorderProfile profile = - getBestAvailableCamcorderProfileForResolutionPreset(cameraId, preset); - return new Size(profile.videoFrameWidth, profile.videoFrameHeight); - } - - private void configureResolution(ResolutionPreset resolutionPreset, int cameraId) { - recordingProfile = - getBestAvailableCamcorderProfileForResolutionPreset(cameraId, resolutionPreset); - captureSize = new Size(recordingProfile.videoFrameWidth, recordingProfile.videoFrameHeight); - previewSize = computeBestPreviewSize(cameraId, resolutionPreset); - } - - @Override - public String getDebugName() { - return "ResolutionFeature"; - } - - @Override - public ResolutionPreset getValue() { - return currentSetting; - } - - @Override - public void setValue(ResolutionPreset value) { - this.currentSetting = value; - configureResolution(currentSetting, cameraId); - } - - // Always supported - @Override - public boolean checkIsSupported() { - return true; - } - - @Override - public void updateBuilder(CaptureRequest.Builder requestBuilder) { - // No-op: when setting a resolution there is no need to update the request builder. - } - - /** - * Gets the {@link android.media.CamcorderProfile} containing the information to configure the - * resolution using the {@link android.hardware.camera2} API. - * - * @return Resolution information to configure the {@link android.hardware.camera2} API. - */ - public CamcorderProfile getRecordingProfile() { - return this.recordingProfile; - } - - /** - * Gets the optimal preview size based on the configured resolution. - * - * @return The optimal preview size. - */ - public Size getPreviewSize() { - return this.previewSize; - } - - /** - * Gets the optimal capture size based on the configured resolution. - * - * @return The optimal capture size. - */ - public Size getCaptureSize() { - return this.captureSize; - } + @Override + public ResolutionPreset getValue() { + return currentSetting; + } + + @Override + public void setValue(ResolutionPreset value) { + this.currentSetting = value; + configureResolution(currentSetting, cameraId); + } + + // Always supported + @Override + public boolean checkIsSupported() { + return cameraId >= 0; + } + + @Override + public void updateBuilder(CaptureRequest.Builder requestBuilder) { + // No-op: when setting a resolution there is no need to update the request builder. + } + + /** + * Gets the {@link android.media.CamcorderProfile} containing the information to configure the + * resolution using the {@link android.hardware.camera2} API. + * + * @return Resolution information to configure the {@link android.hardware.camera2} API. + */ + public CamcorderProfile getRecordingProfile() { + return this.recordingProfile; + } + + /** + * Gets the optimal preview size based on the configured resolution. + * + * @return The optimal preview size. + */ + public Size getPreviewSize() { + return this.previewSize; + } + + /** + * Gets the optimal capture size based on the configured resolution. + * + * @return The optimal capture size. + */ + public Size getCaptureSize() { + return this.captureSize; + } } diff --git a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/types/CaptureTimeoutsWrapper.java b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/types/CaptureTimeoutsWrapper.java index f162dd2759fa..223b33038584 100644 --- a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/types/CaptureTimeoutsWrapper.java +++ b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/types/CaptureTimeoutsWrapper.java @@ -9,36 +9,46 @@ * that are required for the capture flow. */ public class CaptureTimeoutsWrapper { - private final Timeout preCaptureFocusing; - private final Timeout preCaptureMetering; + private Timeout preCaptureFocusing; + private Timeout preCaptureMetering; + private final long preCaptureFocusingTimeoutMs; + private final long preCaptureMeteringTimeoutMs; - /** - * Create a new wrapper instance with the specified timeout values. - * - * @param preCaptureFocusingTimeoutMs focusing timeout milliseconds. - * @param preCaptureMeteringTimeoutMs metering timeout milliseconds. - */ - public CaptureTimeoutsWrapper( - long preCaptureFocusingTimeoutMs, long preCaptureMeteringTimeoutMs) { - this.preCaptureFocusing = Timeout.create(preCaptureFocusingTimeoutMs); - this.preCaptureMetering = Timeout.create(preCaptureMeteringTimeoutMs); - } + /** + * Create a new wrapper instance with the specified timeout values. + * + * @param preCaptureFocusingTimeoutMs focusing timeout milliseconds. + * @param preCaptureMeteringTimeoutMs metering timeout milliseconds. + */ + public CaptureTimeoutsWrapper( + long preCaptureFocusingTimeoutMs, long preCaptureMeteringTimeoutMs) { + this.preCaptureFocusingTimeoutMs = preCaptureFocusingTimeoutMs; + this.preCaptureMeteringTimeoutMs = preCaptureMeteringTimeoutMs; + } - /** - * Returns the timeout instance related to precapture focusing. - * - * @return - The timeout object - */ - public Timeout getPreCaptureFocusing() { - return preCaptureFocusing; - } + /** + * Reset all timeouts to the current timestamp. + */ + public void reset() { + this.preCaptureFocusing = Timeout.create(preCaptureFocusingTimeoutMs); + this.preCaptureMetering = Timeout.create(preCaptureMeteringTimeoutMs); + } - /** - * Returns the timeout instance related to precapture metering. - * - * @return - The timeout object - */ - public Timeout getPreCaptureMetering() { - return preCaptureMetering; - } + /** + * Returns the timeout instance related to precapture focusing. + * + * @return - The timeout object + */ + public Timeout getPreCaptureFocusing() { + return preCaptureFocusing; + } + + /** + * Returns the timeout instance related to precapture metering. + * + * @return - The timeout object + */ + public Timeout getPreCaptureMetering() { + return preCaptureMetering; + } } From 81920e0c7cefe78751c3697f888fa2637e28543b Mon Sep 17 00:00:00 2001 From: BeMacized Date: Wed, 16 Jun 2021 16:26:04 +0200 Subject: [PATCH 42/63] Progress on final implementation of android rework --- .../io/flutter/plugins/camera/Camera.java | 1947 ++++++++--------- .../plugins/camera/CameraCaptureCallback.java | 17 +- .../plugins/camera/CameraProperties.java | 4 +- .../flutter/plugins/camera/CameraUtils.java | 78 +- .../flutter/plugins/camera/DartMessenger.java | 367 ++-- .../io/flutter/plugins/camera/ImageSaver.java | 7 +- .../plugins/camera/MethodCallHandlerImpl.java | 667 +++--- .../camera/features/CameraFeatureFactory.java | 8 +- .../features/CameraFeatureFactoryImpl.java | 7 +- .../camera/features/CameraFeatures.java | 462 ++-- .../resolution/ResolutionFeature.java | 287 ++- .../camera/types/CaptureTimeoutsWrapper.java | 78 +- 12 files changed, 1950 insertions(+), 1979 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 35e2306ff275..8365743f2094 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 @@ -35,10 +35,8 @@ import android.util.SparseIntArray; import android.view.Display; 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; @@ -64,7 +62,6 @@ import io.flutter.plugins.camera.media.MediaRecorderBuilder; import io.flutter.plugins.camera.types.CaptureTimeoutsWrapper; import io.flutter.view.TextureRegistry.SurfaceTextureEntry; - import java.io.File; import java.io.IOException; import java.nio.ByteBuffer; @@ -78,7 +75,7 @@ @FunctionalInterface interface ErrorCallback { - void onError(String errorCode, String errorMessage); + void onError(String errorCode, String errorMessage); } /** @@ -96,1068 +93,1050 @@ interface ErrorCallback { * 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 CameraFeatures cameraFeatures; - - private final SurfaceTextureEntry flutterTexture; - private final boolean enableAudio; - 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; - /** - * A {@link Handler} for running tasks in the background. - */ - 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 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. - backgroundHandler.post( - new ImageSaver( - reader.acquireNextImage(), captureFile, new ImageSaver.Callback() { - @Override - public void onComplete(String absolutePath) { - dartMessenger.finish(flutterResult, absolutePath); - } - - @Override - public void onError(String errorCode, String errorMessage) { - dartMessenger.error(flutterResult, errorCode, errorMessage, null); - } - })); - cameraCaptureCallback.setCameraState(CameraState.STATE_PREVIEW); - } - }; - /** - * An additional thread for running tasks that shouldn't block the UI. - */ - 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 previewRequestBuilder; - - private MediaRecorder mediaRecorder; - /** - * True when recording video. - */ - private boolean recordingVideo; - - private File captureFile; - - /** - * Holds the current capture timeouts - */ - private CaptureTimeoutsWrapper captureTimeouts; - - private MethodChannel.Result flutterResult; - - public Camera( - final Activity activity, - final SurfaceTextureEntry flutterTexture, - final CameraFeatureFactory cameraFeatureFactory, - 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; - this.cameraFeatureFactory = cameraFeatureFactory; - - // Setup camera features - this.cameraFeatures = new CameraFeatures(); - this.cameraFeatures.setAutoFocus( - cameraFeatureFactory.createAutoFocusFeature(cameraProperties, false) - ); - this.cameraFeatures.setExposureLock(cameraFeatureFactory.createExposureLockFeature(cameraProperties)); - this.cameraFeatures.setExposureOffset(cameraFeatureFactory.createExposureOffsetFeature(cameraProperties)); - this.cameraFeatures.setExposurePoint( - cameraFeatureFactory.createExposurePointFeature(cameraProperties) - ); - this.cameraFeatures.setFlash( - cameraFeatureFactory.createFlashFeature(cameraProperties) - ); - this.cameraFeatures.setFocusPoint( - cameraFeatureFactory.createFocusPointFeature(cameraProperties) - ); - this.cameraFeatures.setFpsRange( - cameraFeatureFactory.createFpsRangeFeature(cameraProperties) - ); - this.cameraFeatures.setNoiseReduction( - cameraFeatureFactory.createNoiseReductionFeature(cameraProperties) - ); - this.cameraFeatures.setResolution( - cameraFeatureFactory.createResolutionFeature( - cameraProperties, resolutionPreset, cameraProperties.getCameraName()) - ); - this.cameraFeatures.setSensorOrientation( - cameraFeatureFactory.createSensorOrientationFeature( - cameraProperties, activity, dartMessenger) - ); - this.cameraFeatures.setZoomLevel( - cameraFeatureFactory.createZoomLevelFeature(cameraProperties) - ); - - // Create capture callback - captureTimeouts = new CaptureTimeoutsWrapper(3000, 3000); - cameraCaptureCallback = CameraCaptureCallback.create(this, captureTimeouts); - - // Start background thread. - startBackgroundThread(); - } - - @Override - public void onConverged() { - takePictureAfterPrecapture(); - } - - @Override - public void onPrecapture() { - runPrecaptureSequence(); - } - - /** - * Update the builder settings with all of our available features. - * - * @param requestBuilder request builder to update. - */ - private void updateBuilderSettings(CaptureRequest.Builder requestBuilder) { - for (CameraFeature feature : cameraFeatures.getAllFeatures()) { - Log.d(TAG, "Updating builder with feature: " + feature.getDebugName()); - feature.updateBuilder(requestBuilder); - } - } - - private void prepareMediaRecorder(String outputFilePath) throws IOException { - Log.i(TAG, "prepareMediaRecorder"); - - if (mediaRecorder != null) { - mediaRecorder.release(); - } - - final PlatformChannel.DeviceOrientation lockedOrientation = - ((SensorOrientationFeature) cameraFeatures.getSensorOrientation()) - .getLockedCaptureOrientation(); - - mediaRecorder = - new MediaRecorderBuilder(getRecordingProfile(), outputFilePath) - .setEnableAudio(enableAudio) - .setMediaOrientation( - lockedOrientation == null - ? getDeviceOrientationManager().getMediaOrientation() - : getDeviceOrientationManager().getMediaOrientation(lockedOrientation)) - .build(); - } - - @SuppressLint("MissingPermission") - public void open(String imageFormatGroup) throws CameraAccessException { - // We always capture using JPEG format. - final ResolutionFeature resolutionFeature = cameraFeatures.getResolution(); - pictureImageReader = - ImageReader.newInstance( - resolutionFeature.getCaptureSize().getWidth(), resolutionFeature.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; - } - imageStreamReader = - ImageReader.newInstance( - resolutionFeature.getPreviewSize().getWidth(), resolutionFeature.getPreviewSize().getHeight(), imageFormat, 1); - - // Open the camera now - CameraManager cameraManager = CameraUtils.getCameraManager(activity); - cameraManager.openCamera( - cameraProperties.getCameraName(), - new CameraDevice.StateCallback() { + 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 CameraFeatures cameraFeatures; + + private final SurfaceTextureEntry flutterTexture; + private final boolean enableAudio; + 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; + /** A {@link Handler} for running tasks in the background. */ + 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 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. + backgroundHandler.post( + new ImageSaver( + reader.acquireNextImage(), + captureFile, + new ImageSaver.Callback() { @Override - public void onOpened(@NonNull CameraDevice device) { - cameraDevice = device; - try { - startPreview(); - dartMessenger.sendCameraInitializedEvent( - resolutionFeature.getPreviewSize().getWidth(), - resolutionFeature.getPreviewSize().getHeight(), - cameraFeatures.getExposureLock().getValue(), - cameraFeatures.getAutoFocus().getValue(), - cameraFeatures.getExposurePoint().checkIsSupported(), - cameraFeatures.getFocusPoint().checkIsSupported()); - } catch (CameraAccessException e) { - dartMessenger.sendCameraErrorEvent(e.getMessage()); - close(); - } + public void onComplete(String absolutePath) { + dartMessenger.finish(flutterResult, absolutePath); } @Override - public void onClosed(@NonNull CameraDevice camera) { - Log.i(TAG, "open | onClosed"); - - dartMessenger.sendCameraClosingEvent(); - super.onClosed(camera); + public void onError(String errorCode, String errorMessage) { + dartMessenger.error(flutterResult, errorCode, errorMessage, null); } - - @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); - } - }, - backgroundHandler); + })); + cameraCaptureCallback.setCameraState(CameraState.STATE_PREVIEW); + } + }; + /** An additional thread for running tasks that shouldn't block the UI. */ + 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 previewRequestBuilder; + + private MediaRecorder mediaRecorder; + /** True when recording video. */ + private boolean recordingVideo; + + private File captureFile; + + /** Holds the current capture timeouts */ + private CaptureTimeoutsWrapper captureTimeouts; + + private MethodChannel.Result flutterResult; + + public Camera( + final Activity activity, + final SurfaceTextureEntry flutterTexture, + final CameraFeatureFactory cameraFeatureFactory, + final DartMessenger dartMessenger, + final CameraProperties cameraProperties, + final ResolutionPreset resolutionPreset, + final boolean enableAudio) { + + if (activity == null) { + throw new IllegalStateException("No activity available!"); } - - private void createCaptureSession(int templateType, Surface... surfaces) - throws CameraAccessException { - createCaptureSession(templateType, null, surfaces); + this.activity = activity; + this.enableAudio = enableAudio; + this.flutterTexture = flutterTexture; + this.dartMessenger = dartMessenger; + this.applicationContext = activity.getApplicationContext(); + this.cameraProperties = cameraProperties; + this.cameraFeatureFactory = cameraFeatureFactory; + + // Setup camera features + this.cameraFeatures = new CameraFeatures(); + this.cameraFeatures.setAutoFocus( + cameraFeatureFactory.createAutoFocusFeature(cameraProperties, false)); + this.cameraFeatures.setExposureLock( + cameraFeatureFactory.createExposureLockFeature(cameraProperties)); + this.cameraFeatures.setExposureOffset( + cameraFeatureFactory.createExposureOffsetFeature(cameraProperties)); + this.cameraFeatures.setExposurePoint( + cameraFeatureFactory.createExposurePointFeature(cameraProperties)); + this.cameraFeatures.setFlash(cameraFeatureFactory.createFlashFeature(cameraProperties)); + this.cameraFeatures.setFocusPoint( + cameraFeatureFactory.createFocusPointFeature(cameraProperties)); + this.cameraFeatures.setFpsRange(cameraFeatureFactory.createFpsRangeFeature(cameraProperties)); + this.cameraFeatures.setNoiseReduction( + cameraFeatureFactory.createNoiseReductionFeature(cameraProperties)); + this.cameraFeatures.setResolution( + cameraFeatureFactory.createResolutionFeature( + cameraProperties, resolutionPreset, cameraProperties.getCameraName())); + this.cameraFeatures.setSensorOrientation( + cameraFeatureFactory.createSensorOrientationFeature( + cameraProperties, activity, dartMessenger)); + this.cameraFeatures.setZoomLevel(cameraFeatureFactory.createZoomLevelFeature(cameraProperties)); + + // Create capture callback + captureTimeouts = new CaptureTimeoutsWrapper(3000, 3000); + cameraCaptureCallback = CameraCaptureCallback.create(this, captureTimeouts); + + // Start background thread. + startBackgroundThread(); + } + + @Override + public void onConverged() { + takePictureAfterPrecapture(); + } + + @Override + public void onPrecapture() { + runPrecaptureSequence(); + } + + /** + * Update the builder settings with all of our available features. + * + * @param requestBuilder request builder to update. + */ + private void updateBuilderSettings(CaptureRequest.Builder requestBuilder) { + for (CameraFeature feature : cameraFeatures.getAllFeatures()) { + Log.d(TAG, "Updating builder with feature: " + feature.getDebugName()); + feature.updateBuilder(requestBuilder); } + } - private void createCaptureSession( - int templateType, Runnable onSuccessCallback, Surface... surfaces) - throws CameraAccessException { - // Close any existing capture session. - closeCaptureSession(); - - // Create a new capture builder. - previewRequestBuilder = cameraDevice.createCaptureRequest(templateType); - - // Build Flutter surface to render to - final ResolutionFeature resolutionFeature = cameraFeatures.getResolution(); - SurfaceTexture surfaceTexture = flutterTexture.surfaceTexture(); - surfaceTexture.setDefaultBufferSize(resolutionFeature.getPreviewSize().getWidth(), resolutionFeature.getPreviewSize().getHeight()); - Surface flutterSurface = new Surface(surfaceTexture); - 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) { - previewRequestBuilder.addTarget(surface); - } - } - - // Update camera regions - Size cameraBoundaries = CameraRegionUtils.getCameraBoundaries(cameraProperties, previewRequestBuilder); - cameraFeatures.getExposurePoint().setCameraBoundaries(cameraBoundaries); - cameraFeatures.getFocusPoint().setCameraBoundaries(cameraBoundaries); - - // 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; - - Log.i(TAG, "Updating builder settings"); - updateBuilderSettings(previewRequestBuilder); - - refreshPreviewCaptureSession( - onSuccessCallback, (code, message) -> dartMessenger.sendCameraErrorEvent(message)); - } + private void prepareMediaRecorder(String outputFilePath) throws IOException { + Log.i(TAG, "prepareMediaRecorder"); - @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(); } - @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)); + final PlatformChannel.DeviceOrientation lockedOrientation = + ((SensorOrientationFeature) cameraFeatures.getSensorOrientation()) + .getLockedCaptureOrientation(); + + mediaRecorder = + new MediaRecorderBuilder(getRecordingProfile(), outputFilePath) + .setEnableAudio(enableAudio) + .setMediaOrientation( + lockedOrientation == null + ? getDeviceOrientationManager().getMediaOrientation() + : getDeviceOrientationManager().getMediaOrientation(lockedOrientation)) + .build(); + } + + @SuppressLint("MissingPermission") + public void open(String imageFormatGroup) throws CameraAccessException { + final ResolutionFeature resolutionFeature = cameraFeatures.getResolution(); + + if (!resolutionFeature.checkIsSupported()) { + // Tell the user that the camera they are trying to open is not supported, + // as its {@link android.media.CamcorderProfile} cannot be fetched due to the name + // not being a valid parsable integer. + dartMessenger.sendCameraErrorEvent( + "Camera with name \"" + + cameraProperties.getCameraName() + + "\" is not supported by this plugin."); + return; } - @TargetApi(VERSION_CODES.LOLLIPOP) - @SuppressWarnings("deprecation") - private void createCaptureSession( - List surfaces, CameraCaptureSession.StateCallback callback) - throws CameraAccessException { - cameraDevice.createCaptureSession(surfaces, callback, backgroundHandler); + // We always capture using JPEG format. + pictureImageReader = + ImageReader.newInstance( + resolutionFeature.getCaptureSize().getWidth(), + resolutionFeature.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; } - - // 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( - previewRequestBuilder.build(), cameraCaptureCallback, backgroundHandler); - - if (onSuccessCallback != null) { - onSuccessCallback.run(); + imageStreamReader = + ImageReader.newInstance( + resolutionFeature.getPreviewSize().getWidth(), + resolutionFeature.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) { + cameraDevice = device; + try { + startPreview(); + dartMessenger.sendCameraInitializedEvent( + resolutionFeature.getPreviewSize().getWidth(), + resolutionFeature.getPreviewSize().getHeight(), + cameraFeatures.getExposureLock().getValue(), + cameraFeatures.getAutoFocus().getValue(), + cameraFeatures.getExposurePoint().checkIsSupported(), + cameraFeatures.getFocusPoint().checkIsSupported()); + } catch (CameraAccessException e) { + dartMessenger.sendCameraErrorEvent(e.getMessage()); + close(); } + } - } catch (CameraAccessException | IllegalStateException | IllegalArgumentException e) { - onErrorCallback.onError("cameraAccess", e.getMessage()); - } - } + @Override + public void onClosed(@NonNull CameraDevice camera) { + Log.i(TAG, "open | onClosed"); - public void takePicture(@NonNull final Result result) { - // Only take one 1 picture at a time. - if (cameraCaptureCallback.getCameraState() != CameraState.STATE_PREVIEW) { - result.error("captureAlreadyActive", "Picture is currently already being captured", null); - return; - } + dartMessenger.sendCameraClosingEvent(); + super.onClosed(camera); + } - flutterResult = result; + @Override + public void onDisconnected(@NonNull CameraDevice cameraDevice) { + Log.i(TAG, "open | onDisconnected"); - // Create temporary file - final File outputDir = applicationContext.getCacheDir(); - try { - captureFile = File.createTempFile("CAP", ".jpg", outputDir); - captureTimeouts.reset(); - } catch (IOException | SecurityException e) { - dartMessenger.error(flutterResult, "cannotCreateFile", e.getMessage(), null); - return; - } - - // Listen for picture being taken - pictureImageReader.setOnImageAvailableListener(onImageAvailableListener, backgroundHandler); + close(); + dartMessenger.sendCameraErrorEvent("The camera was disconnected."); + } - final AutoFocusFeature autoFocusFeature = cameraFeatures.getAutoFocus(); - final boolean isAutoFocusSupported = autoFocusFeature.checkIsSupported(); - if (isAutoFocusSupported && autoFocusFeature.getValue() == FocusMode.auto) { - runPictureAutoFocus(); - } else { - runPrecaptureSequence(); - } - } + @Override + public void onError(@NonNull CameraDevice cameraDevice, int errorCode) { + Log.i(TAG, "open | onError"); - /** - * Run the precapture sequence for capturing a still image. This method should be called when we - * 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 - previewRequestBuilder.set( - CaptureRequest.CONTROL_AE_PRECAPTURE_TRIGGER, - CaptureRequest.CONTROL_AE_PRECAPTURE_TRIGGER_IDLE); - captureSession.capture( - previewRequestBuilder.build(), cameraCaptureCallback, backgroundHandler); - - // Repeating request to refresh preview session - refreshPreviewCaptureSession( - null, (code, message) -> dartMessenger.error(flutterResult, "cameraAccess", message, null)); - - // Start precapture now - cameraCaptureCallback.setCameraState(CameraState.STATE_WAITING_PRECAPTURE_START); - - previewRequestBuilder.set( - CaptureRequest.CONTROL_AE_PRECAPTURE_TRIGGER, - CaptureRequest.CONTROL_AE_PRECAPTURE_TRIGGER_START); - - // Trigger one capture to start AE sequence - captureSession.capture( - previewRequestBuilder.build(), cameraCaptureCallback, backgroundHandler); - - } catch (CameraAccessException e) { - e.printStackTrace(); - } - } - - /** - * Capture a still picture. This method should be called when we get a response in {@link - * #cameraCaptureCallback} from both lockFocus(). - */ - private void takePictureAfterPrecapture() { - Log.i(TAG, "captureStillPicture"); - cameraCaptureCallback.setCameraState(CameraState.STATE_CAPTURING); - - try { - if (null == cameraDevice) { - return; + 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"; } - // 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, - previewRequestBuilder.get(CaptureRequest.SCALER_CROP_REGION)); - - // Update builder settings - updateBuilderSettings(stillBuilder); - - // Orientation - int rotation = 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, backgroundHandler); - } catch (CameraAccessException e) { - dartMessenger.error(flutterResult, "cameraAccess", e.getMessage(), null); - } - } - - @SuppressWarnings("deprecation") - private Display getDefaultDisplay() { - return activity.getWindowManager().getDefaultDisplay(); - } - - /** - * Starts a background thread and its {@link Handler}. TODO: call when activity resumed - */ - private void startBackgroundThread() { - backgroundHandlerThread = new HandlerThread("CameraBackground"); - backgroundHandlerThread.start(); - backgroundHandler = new Handler(backgroundHandlerThread.getLooper()); + dartMessenger.sendCameraErrorEvent(errorDescription); + } + }, + backgroundHandler); + } + + private void createCaptureSession(int templateType, Surface... surfaces) + throws CameraAccessException { + createCaptureSession(templateType, null, surfaces); + } + + private void createCaptureSession( + int templateType, Runnable onSuccessCallback, Surface... surfaces) + throws CameraAccessException { + // Close any existing capture session. + closeCaptureSession(); + + // Create a new capture builder. + previewRequestBuilder = cameraDevice.createCaptureRequest(templateType); + + // Build Flutter surface to render to + final ResolutionFeature resolutionFeature = cameraFeatures.getResolution(); + SurfaceTexture surfaceTexture = flutterTexture.surfaceTexture(); + surfaceTexture.setDefaultBufferSize( + resolutionFeature.getPreviewSize().getWidth(), + resolutionFeature.getPreviewSize().getHeight()); + Surface flutterSurface = new Surface(surfaceTexture); + 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) { + previewRequestBuilder.addTarget(surface); + } } - /** - * Stops the background thread and its {@link Handler}. TODO: call when activity paused - */ - private void stopBackgroundThread() { - try { - if (backgroundHandlerThread != null) { - backgroundHandlerThread.quitSafely(); - backgroundHandlerThread.join(); - backgroundHandlerThread = null; + // Update camera regions + Size cameraBoundaries = + CameraRegionUtils.getCameraBoundaries(cameraProperties, previewRequestBuilder); + cameraFeatures.getExposurePoint().setCameraBoundaries(cameraBoundaries); + cameraFeatures.getFocusPoint().setCameraBoundaries(cameraBoundaries); + + // 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; - backgroundHandler = null; - } catch (InterruptedException e) { - dartMessenger.error(flutterResult, "cameraAccess", e.getMessage(), null); - } - } + Log.i(TAG, "Updating builder settings"); + updateBuilderSettings(previewRequestBuilder); - /** - * 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 = cameraFeatures.getSensorOrientation().getValue(); - return (ORIENTATIONS.get(rotation) + sensorOrientation + 270) % 360; + 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 capturing a picture, doing autofocus first. - */ - private void runPictureAutoFocus() { - Log.i(TAG, "runPictureAutoFocus"); - - cameraCaptureCallback.setCameraState(CameraState.STATE_WAITING_FOCUS); - lockAutoFocus(); + } + + @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, backgroundHandler); + } + + // 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; } - /** - * Start the autofocus routine on the current capture request and calls the onCompleted callback. - */ - private void lockAutoFocus() { - Log.i(TAG, "lockAutoFocus"); + try { + captureSession.setRepeatingRequest( + previewRequestBuilder.build(), cameraCaptureCallback, backgroundHandler); - // Trigger AF to start - previewRequestBuilder.set( - CaptureRequest.CONTROL_AF_TRIGGER, CaptureRequest.CONTROL_AF_TRIGGER_START); + if (onSuccessCallback != null) { + onSuccessCallback.run(); + } - try { - captureSession.capture(previewRequestBuilder.build(), null, backgroundHandler); - } catch (CameraAccessException e) { - Log.i(TAG, "Error unlocking focus: " + e.getMessage()); - dartMessenger.sendCameraErrorEvent(e.getMessage()); - return; - } + } catch (CameraAccessException | IllegalStateException | IllegalArgumentException e) { + onErrorCallback.onError("cameraAccess", e.getMessage()); } + } - /** - * Cancel and reset auto focus state and refresh the preview session. - */ - 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); - captureSession.capture(previewRequestBuilder.build(), null, backgroundHandler); - - // Set AF state to idle again - previewRequestBuilder.set( - CaptureRequest.CONTROL_AF_TRIGGER, CameraMetadata.CONTROL_AF_TRIGGER_IDLE); - - captureSession.capture(previewRequestBuilder.build(), null, backgroundHandler); - } catch (CameraAccessException e) { - Log.i(TAG, "Error unlocking focus: " + e.getMessage()); - dartMessenger.sendCameraErrorEvent(e.getMessage()); - return; - } - - refreshPreviewCaptureSession( - null, - (errorCode, errorMessage) -> dartMessenger.error(flutterResult, errorCode, errorMessage, null)); + public void takePicture(@NonNull final Result result) { + // Only take one 1 picture at a time. + if (cameraCaptureCallback.getCameraState() != CameraState.STATE_PREVIEW) { + result.error("captureAlreadyActive", "Picture is currently already being captured", null); + return; } - public void startVideoRecording(Result result) { - final File outputDir = applicationContext.getCacheDir(); - try { - captureFile = File.createTempFile("REC", ".mp4", outputDir); - } catch (IOException | SecurityException e) { - result.error("cannotCreateFile", e.getMessage(), null); - return; - } + flutterResult = result; - try { - prepareMediaRecorder(captureFile.getAbsolutePath()); - - // Re-create autofocus feature so it's using video focus mode now - cameraFeatures.setAutoFocus(cameraFeatureFactory.createAutoFocusFeature(cameraProperties, true)); - recordingVideo = true; - - createCaptureSession( - CameraDevice.TEMPLATE_RECORD, () -> mediaRecorder.start(), mediaRecorder.getSurface()); - result.success(null); - } catch (CameraAccessException | IOException e) { - recordingVideo = false; - captureFile = null; - result.error("videoRecordingFailed", e.getMessage(), null); - } + // Create temporary file + final File outputDir = applicationContext.getCacheDir(); + try { + captureFile = File.createTempFile("CAP", ".jpg", outputDir); + captureTimeouts.reset(); + } catch (IOException | SecurityException e) { + dartMessenger.error(flutterResult, "cannotCreateFile", e.getMessage(), null); + return; } - public void stopVideoRecording(@NonNull final Result result) { - if (!recordingVideo) { - result.success(null); - return; - } - - try { - // Re-create autofocus feature so it's using continuous capture focus mode now - cameraFeatures.setAutoFocus(cameraFeatureFactory.createAutoFocusFeature(cameraProperties, false)); - recordingVideo = false; - - try { - captureSession.abortCaptures(); - mediaRecorder.stop(); - } catch (CameraAccessException | IllegalStateException e) { - // Ignore exceptions and try to continue (changes are camera session already aborted capture) - } + // Listen for picture being taken + pictureImageReader.setOnImageAvailableListener(onImageAvailableListener, backgroundHandler); - mediaRecorder.reset(); - startPreview(); - result.success(captureFile.getAbsolutePath()); - captureFile = null; - } catch (CameraAccessException | IllegalStateException e) { - result.error("videoRecordingFailed", e.getMessage(), null); - } + final AutoFocusFeature autoFocusFeature = cameraFeatures.getAutoFocus(); + final boolean isAutoFocusSupported = autoFocusFeature.checkIsSupported(); + if (isAutoFocusSupported && autoFocusFeature.getValue() == FocusMode.auto) { + runPictureAutoFocus(); + } else { + runPrecaptureSequence(); } - - 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); + } + + /** + * Run the precapture sequence for capturing a still image. This method should be called when we + * 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 + previewRequestBuilder.set( + CaptureRequest.CONTROL_AE_PRECAPTURE_TRIGGER, + CaptureRequest.CONTROL_AE_PRECAPTURE_TRIGGER_IDLE); + captureSession.capture( + previewRequestBuilder.build(), cameraCaptureCallback, backgroundHandler); + + // Repeating request to refresh preview session + refreshPreviewCaptureSession( + null, + (code, message) -> dartMessenger.error(flutterResult, "cameraAccess", message, null)); + + // Start precapture now + cameraCaptureCallback.setCameraState(CameraState.STATE_WAITING_PRECAPTURE_START); + + previewRequestBuilder.set( + CaptureRequest.CONTROL_AE_PRECAPTURE_TRIGGER, + CaptureRequest.CONTROL_AE_PRECAPTURE_TRIGGER_START); + + // Trigger one capture to start AE sequence + captureSession.capture( + previewRequestBuilder.build(), cameraCaptureCallback, backgroundHandler); + + } catch (CameraAccessException e) { + e.printStackTrace(); } + } + + /** + * Capture a still picture. This method should be called when we get a response in {@link + * #cameraCaptureCallback} from both lockFocus(). + */ + private void takePictureAfterPrecapture() { + Log.i(TAG, "captureStillPicture"); + cameraCaptureCallback.setCameraState(CameraState.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, + previewRequestBuilder.get(CaptureRequest.SCALER_CROP_REGION)); + + // Update builder settings + updateBuilderSettings(stillBuilder); + + // Orientation + int rotation = 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"); + } - 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 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); + @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, backgroundHandler); + } catch (CameraAccessException e) { + dartMessenger.error(flutterResult, "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 Flutter result. - * @param newMode new mode. - */ - public void setFlashMode(@NonNull final Result result, FlashMode newMode) { - // Save the new flash mode setting - final FlashFeature flashFeature = cameraFeatures.getFlash(); - flashFeature.setValue(newMode); - flashFeature.updateBuilder(previewRequestBuilder); - - refreshPreviewCaptureSession( - () -> result.success(null), - (code, message) -> result.error("setFlashModeFailed", "Could not set flash mode.", null)); + } + + @SuppressWarnings("deprecation") + private Display getDefaultDisplay() { + return activity.getWindowManager().getDefaultDisplay(); + } + + /** Starts a background thread and its {@link Handler}. TODO: call when activity resumed */ + private void startBackgroundThread() { + 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 (backgroundHandlerThread != null) { + backgroundHandlerThread.quitSafely(); + backgroundHandlerThread.join(); + backgroundHandlerThread = null; + } + + backgroundHandler = null; + } catch (InterruptedException e) { + dartMessenger.error(flutterResult, "cameraAccess", e.getMessage(), null); } - - /** - * Dart handler for setting new exposure mode setting. - * - * @param result Flutter result. - * @param newMode new mode. - */ - public void setExposureMode(@NonNull final Result result, ExposureMode newMode) { - final ExposureLockFeature exposureLockFeature = cameraFeatures.getExposureLock(); - exposureLockFeature.setValue(newMode); - exposureLockFeature.updateBuilder(previewRequestBuilder); - - 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 = cameraFeatures.getSensorOrientation().getValue(); + return (ORIENTATIONS.get(rotation) + sensorOrientation + 270) % 360; + } + + /** Start capturing a picture, doing autofocus first. */ + private void runPictureAutoFocus() { + Log.i(TAG, "runPictureAutoFocus"); + + cameraCaptureCallback.setCameraState(CameraState.STATE_WAITING_FOCUS); + lockAutoFocus(); + } + + /** + * 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 + previewRequestBuilder.set( + CaptureRequest.CONTROL_AF_TRIGGER, CaptureRequest.CONTROL_AF_TRIGGER_START); + + try { + captureSession.capture(previewRequestBuilder.build(), null, backgroundHandler); + } catch (CameraAccessException e) { + Log.i(TAG, "Error unlocking focus: " + e.getMessage()); + dartMessenger.sendCameraErrorEvent(e.getMessage()); + return; } - - /** - * Set new exposure point from dart. - * - * @param result Flutter result. - * @param point The exposure point. - */ - public void setExposurePoint(@NonNull final Result result, Point point) { - final ExposurePointFeature exposurePointFeature = cameraFeatures.getExposurePoint(); - exposurePointFeature.setValue(point); - exposurePointFeature.updateBuilder(previewRequestBuilder); - - refreshPreviewCaptureSession( - () -> result.success(null), - (code, message) -> - result.error("setExposurePointFailed", "Could not set exposure point.", null)); + } + + /** Cancel and reset auto focus state and refresh the preview session. */ + 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); + captureSession.capture(previewRequestBuilder.build(), null, backgroundHandler); + + // Set AF state to idle again + previewRequestBuilder.set( + CaptureRequest.CONTROL_AF_TRIGGER, CameraMetadata.CONTROL_AF_TRIGGER_IDLE); + + captureSession.capture(previewRequestBuilder.build(), null, backgroundHandler); + } catch (CameraAccessException e) { + Log.i(TAG, "Error unlocking focus: " + e.getMessage()); + dartMessenger.sendCameraErrorEvent(e.getMessage()); + return; } - /** - * Return the max exposure offset value supported by the camera to dart. - */ - public double getMaxExposureOffset() { - return cameraFeatures.getExposureOffset().getMaxExposureOffset(); + refreshPreviewCaptureSession( + null, + (errorCode, errorMessage) -> + dartMessenger.error(flutterResult, errorCode, errorMessage, null)); + } + + public void startVideoRecording(Result result) { + final File outputDir = applicationContext.getCacheDir(); + try { + captureFile = File.createTempFile("REC", ".mp4", outputDir); + } catch (IOException | SecurityException e) { + result.error("cannotCreateFile", e.getMessage(), null); + return; } - /** - * Return the min exposure offset value supported by the camera to dart. - */ - public double getMinExposureOffset() { - return cameraFeatures.getExposureOffset().getMinExposureOffset(); + try { + prepareMediaRecorder(captureFile.getAbsolutePath()); + + // Re-create autofocus feature so it's using video focus mode now + cameraFeatures.setAutoFocus( + cameraFeatureFactory.createAutoFocusFeature(cameraProperties, true)); + recordingVideo = true; + + createCaptureSession( + CameraDevice.TEMPLATE_RECORD, () -> mediaRecorder.start(), mediaRecorder.getSurface()); + result.success(null); + } catch (CameraAccessException | IOException e) { + recordingVideo = false; + captureFile = null; + result.error("videoRecordingFailed", e.getMessage(), null); } + } - /** - * Return the exposure offset step size to dart. - */ - public double getExposureOffsetStepSize() { - return cameraFeatures.getExposureOffset().getExposureOffsetStepSize(); + public void stopVideoRecording(@NonNull final Result result) { + if (!recordingVideo) { + result.success(null); + return; } - /** - * Set new focus mode from dart. - * - * @param result Flutter result. - * @param newMode New mode. - */ - public void setFocusMode(@NonNull final Result result, FocusMode newMode) { - final AutoFocusFeature autoFocusFeature = cameraFeatures.getAutoFocus(); - autoFocusFeature.setValue(newMode); - autoFocusFeature.updateBuilder(previewRequestBuilder); - - /* - * 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 - previewRequestBuilder.set( - CaptureRequest.CONTROL_AF_TRIGGER, CameraMetadata.CONTROL_AF_TRIGGER_IDLE); - - try { - captureSession.setRepeatingRequest( - previewRequestBuilder.build(), null, backgroundHandler); - } 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); + try { + // Re-create autofocus feature so it's using continuous capture focus mode now + cameraFeatures.setAutoFocus( + cameraFeatureFactory.createAutoFocusFeature(cameraProperties, false)); + 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(captureFile.getAbsolutePath()); + captureFile = null; + } catch (CameraAccessException | IllegalStateException e) { + result.error("videoRecordingFailed", e.getMessage(), null); } + } - /** - * Sets new focus point from dart. - * - * @param result Flutter result. - * @param point the new coordinates. - */ - public void setFocusPoint(@NonNull final Result result, Point point) { - final FocusPointFeature focusPointFeature = cameraFeatures.getFocusPoint(); - focusPointFeature.setValue(point); - focusPointFeature.updateBuilder(previewRequestBuilder); - - refreshPreviewCaptureSession( - () -> result.success(null), - (code, message) -> result.error("setFocusPointFailed", "Could not set focus point.", null)); + public void pauseVideoRecording(@NonNull final Result result) { + if (!recordingVideo) { + result.success(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 flutter result. - * @param offset new value. - */ - public void setExposureOffset(@NonNull final Result result, double offset) { - final ExposureOffsetFeature exposureOffsetFeature = cameraFeatures.getExposureOffset(); - exposureOffsetFeature.setValue(offset); - exposureOffsetFeature.updateBuilder(previewRequestBuilder); - - refreshPreviewCaptureSession( - () -> result.success(null), - (code, message) -> result.error("setFocusModeFailed", "Could not set focus mode.", 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 float getMaxZoomLevel() { - return cameraFeatures.getZoomLevel().getMaximumZoomLevel(); - } + result.success(null); + } - public float getMinZoomLevel() { - return cameraFeatures.getZoomLevel().getMinimumZoomLevel(); + public void resumeVideoRecording(@NonNull final Result result) { + if (!recordingVideo) { + result.success(null); + return; } - /** - * Shortcut to get current recording profile. - */ - CamcorderProfile getRecordingProfile() { - return cameraFeatures.getResolution().getRecordingProfile(); + 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; } - /** - * Shortut to get deviceOrientationListener. + 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 Flutter result. + * @param newMode new mode. + */ + public void setFlashMode(@NonNull final Result result, FlashMode newMode) { + // Save the new flash mode setting + final FlashFeature flashFeature = cameraFeatures.getFlash(); + flashFeature.setValue(newMode); + flashFeature.updateBuilder(previewRequestBuilder); + + 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 Flutter result. + * @param newMode new mode. + */ + public void setExposureMode(@NonNull final Result result, ExposureMode newMode) { + final ExposureLockFeature exposureLockFeature = cameraFeatures.getExposureLock(); + exposureLockFeature.setValue(newMode); + exposureLockFeature.updateBuilder(previewRequestBuilder); + + refreshPreviewCaptureSession( + () -> result.success(null), + (code, message) -> + result.error("setExposureModeFailed", "Could not set exposure mode.", null)); + } + + /** + * Set new exposure point from dart. + * + * @param result Flutter result. + * @param point The exposure point. + */ + public void setExposurePoint(@NonNull final Result result, Point point) { + final ExposurePointFeature exposurePointFeature = cameraFeatures.getExposurePoint(); + exposurePointFeature.setValue(point); + exposurePointFeature.updateBuilder(previewRequestBuilder); + + 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() { + return cameraFeatures.getExposureOffset().getMaxExposureOffset(); + } + + /** Return the min exposure offset value supported by the camera to dart. */ + public double getMinExposureOffset() { + return cameraFeatures.getExposureOffset().getMinExposureOffset(); + } + + /** Return the exposure offset step size to dart. */ + public double getExposureOffsetStepSize() { + return cameraFeatures.getExposureOffset().getExposureOffsetStepSize(); + } + + /** + * Set new focus mode from dart. + * + * @param result Flutter result. + * @param newMode New mode. + */ + public void setFocusMode(@NonNull final Result result, FocusMode newMode) { + final AutoFocusFeature autoFocusFeature = cameraFeatures.getAutoFocus(); + autoFocusFeature.setValue(newMode); + autoFocusFeature.updateBuilder(previewRequestBuilder); + + /* + * 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. */ - DeviceOrientationManager getDeviceOrientationManager() { - return cameraFeatures.getSensorOrientation().getDeviceOrientationManager(); - } + switch (newMode) { + case locked: + // Perform a single focus trigger + lockAutoFocus(); - /** - * Set zoom level from dart. - * - * @param result Flutter result. - * @param zoom new value. - */ - public void setZoomLevel(@NonNull final Result result, float zoom) throws CameraAccessException { - final ZoomLevelFeature zoomLevel = cameraFeatures.getZoomLevel(); - float maxZoom = zoomLevel.getMaximumZoomLevel(); - float minZoom = zoomLevel.getMinimumZoomLevel(); - - 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 AF state to idle again + previewRequestBuilder.set( + CaptureRequest.CONTROL_AF_TRIGGER, CameraMetadata.CONTROL_AF_TRIGGER_IDLE); - zoomLevel.setValue(zoom); - zoomLevel.updateBuilder(previewRequestBuilder); + try { + captureSession.setRepeatingRequest( + previewRequestBuilder.build(), null, backgroundHandler); + } catch (CameraAccessException e) { + result.error("setFocusModeFailed", "Error setting focus mode: " + e.getMessage(), null); + } + break; - refreshPreviewCaptureSession( - () -> result.success(null), - (code, message) -> result.error("setZoomLevelFailed", "Could not set zoom level.", null)); + case auto: + // Cancel current AF trigger and set AF to idle again + unlockAutoFocus(); + break; } - /** - * Lock capture orientation from dart. - * - * @param orientation new orientation. - */ - public void lockCaptureOrientation(PlatformChannel.DeviceOrientation orientation) { - cameraFeatures.getSensorOrientation().lockCaptureOrientation(orientation); + result.success(null); + } + + /** + * Sets new focus point from dart. + * + * @param result Flutter result. + * @param point the new coordinates. + */ + public void setFocusPoint(@NonNull final Result result, Point point) { + final FocusPointFeature focusPointFeature = cameraFeatures.getFocusPoint(); + focusPointFeature.setValue(point); + focusPointFeature.updateBuilder(previewRequestBuilder); + + 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 flutter result. + * @param offset new value. + */ + public void setExposureOffset(@NonNull final Result result, double offset) { + final ExposureOffsetFeature exposureOffsetFeature = cameraFeatures.getExposureOffset(); + exposureOffsetFeature.setValue(offset); + exposureOffsetFeature.updateBuilder(previewRequestBuilder); + + refreshPreviewCaptureSession( + () -> result.success(null), + (code, message) -> result.error("setFocusModeFailed", "Could not set focus mode.", null)); + } + + public float getMaxZoomLevel() { + return cameraFeatures.getZoomLevel().getMaximumZoomLevel(); + } + + public float getMinZoomLevel() { + return cameraFeatures.getZoomLevel().getMinimumZoomLevel(); + } + + /** Shortcut to get current recording profile. */ + CamcorderProfile getRecordingProfile() { + return cameraFeatures.getResolution().getRecordingProfile(); + } + + /** Shortut to get deviceOrientationListener. */ + DeviceOrientationManager getDeviceOrientationManager() { + return cameraFeatures.getSensorOrientation().getDeviceOrientationManager(); + } + + /** + * Set zoom level from dart. + * + * @param result Flutter result. + * @param zoom new value. + */ + public void setZoomLevel(@NonNull final Result result, float zoom) throws CameraAccessException { + final ZoomLevelFeature zoomLevel = cameraFeatures.getZoomLevel(); + float maxZoom = zoomLevel.getMaximumZoomLevel(); + float minZoom = zoomLevel.getMinimumZoomLevel(); + + 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; } - /** - * Unlock capture orientation from dart. - */ - public void unlockCaptureOrientation() { - cameraFeatures.getSensorOrientation().unlockCaptureOrientation(); + zoomLevel.setValue(zoom); + zoomLevel.updateBuilder(previewRequestBuilder); + + refreshPreviewCaptureSession( + () -> result.success(null), + (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) { + cameraFeatures.getSensorOrientation().lockCaptureOrientation(orientation); + } + + /** Unlock capture orientation from dart. */ + public void unlockCaptureOrientation() { + cameraFeatures.getSensorOrientation().unlockCaptureOrientation(); + } + + 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, backgroundHandler); + } + }); + } + + 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(); + }, + backgroundHandler); + } + + private void closeCaptureSession() { + if (captureSession != null) { + Log.i(TAG, "closeCaptureSession"); + + captureSession.close(); + captureSession = null; } + } - public void startPreview() throws CameraAccessException { - if (pictureImageReader == null || pictureImageReader.getSurface() == null) return; - Log.i(TAG, "startPreview"); + public void close() { + Log.i(TAG, "close"); + closeCaptureSession(); - createCaptureSession(CameraDevice.TEMPLATE_PREVIEW, pictureImageReader.getSurface()); + if (cameraDevice != null) { + cameraDevice.close(); + cameraDevice = 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, backgroundHandler); - } - }); + if (pictureImageReader != null) { + pictureImageReader.close(); + pictureImageReader = 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(); - }, - backgroundHandler); + if (imageStreamReader != null) { + imageStreamReader.close(); + imageStreamReader = null; } - - 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(); - - 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(); - getDeviceOrientationManager().stop(); - } -} \ No newline at end of file + 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 f614725d2f64..21dcb602655d 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 @@ -36,7 +36,7 @@ private CameraCaptureCallback( * * @param cameraStateListener instance which will be called when the camera state changes. * @param captureTimeouts specifying the different timeout counters that should be taken into - * account. + * account. * @return a configured instance of the {@link CameraCaptureCallback} class. */ public static CameraCaptureCallback create( @@ -107,9 +107,7 @@ private void process(CaptureResult result) { || aeState == CaptureResult.CONTROL_AE_STATE_FLASH_REQUIRED) { setCameraState(CameraState.STATE_WAITING_PRECAPTURE_DONE); } else if (captureTimeouts.getPreCaptureMetering().getIsExpired()) { - Log.w( - TAG, - "Metering timeout waiting for pre-capture to start, moving on with capture"); + Log.w(TAG, "Metering timeout waiting for pre-capture to start, moving on with capture"); setCameraState(CameraState.STATE_WAITING_PRECAPTURE_DONE); } @@ -122,8 +120,7 @@ private void process(CaptureResult result) { cameraStateListener.onConverged(); } else if (captureTimeouts.getPreCaptureMetering().getIsExpired()) { Log.w( - TAG, - "Metering timeout waiting for pre-capture to finish, moving on with capture"); + TAG, "Metering timeout waiting for pre-capture to finish, moving on with capture"); cameraStateListener.onConverged(); } @@ -157,14 +154,10 @@ public void onCaptureCompleted( process(result); } - /** - * An interface that describes the different state changes implementers can be informed about. - */ + /** An interface that describes the different state changes implementers can be informed about. */ interface CameraCaptureStateListener { - /** - * Called when the {@link android.hardware.camera2.CaptureRequest} has been converged. - */ + /** Called when the {@link android.hardware.camera2.CaptureRequest} has been converged. */ void onConverged(); /** 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 5b05dbb62806..6e2fdab06d91 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 @@ -253,8 +253,8 @@ public CameraPropertiesImpl(String cameraName, CameraManager cameraManager) @Override public String getCameraName() { - return "WOOPS"; -// return cameraName; + return "WOOPS"; + // return cameraName; } @Override 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 a5a195a6bd7c..003d80a6c241 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 @@ -1,22 +1,17 @@ // Copyright 2013 The Flutter 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.app.Activity; import android.content.Context; -import android.graphics.ImageFormat; import android.hardware.camera2.CameraAccessException; import android.hardware.camera2.CameraCharacteristics; import android.hardware.camera2.CameraManager; import android.hardware.camera2.CameraMetadata; -import android.hardware.camera2.params.StreamConfigurationMap; -import android.util.Size; import io.flutter.embedding.engine.systemchannels.PlatformChannel; import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collections; -import java.util.Comparator; import java.util.HashMap; import java.util.List; import java.util.Map; @@ -26,27 +21,24 @@ public final class CameraUtils { private CameraUtils() {} + /** + * Gets the {@link CameraManager} singleton. + * + * @param context The context to get the {@link CameraManager} singleton from. + * @return The {@link CameraManager} singleton. + */ 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; - // Determine the corresponding device orientation. - switch (degrees) { - case 90: - return PlatformChannel.DeviceOrientation.LANDSCAPE_LEFT; - case 180: - return PlatformChannel.DeviceOrientation.PORTRAIT_DOWN; - case 270: - return PlatformChannel.DeviceOrientation.LANDSCAPE_RIGHT; - case 0: - default: - return PlatformChannel.DeviceOrientation.PORTRAIT_UP; - } - } - + /** + * Serializes the {@link PlatformChannel.DeviceOrientation} to a string value. + * + * @param orientation The orientation to serialize. + * @return The serialized orientation. + * @throws UnsupportedOperationException when the provided orientation not have a corresponding + * string value. + */ static String serializeDeviceOrientation(PlatformChannel.DeviceOrientation orientation) { if (orientation == null) throw new UnsupportedOperationException("Could not serialize null device orientation."); @@ -61,10 +53,19 @@ static String serializeDeviceOrientation(PlatformChannel.DeviceOrientation orien return "landscapeRight"; default: throw new UnsupportedOperationException( - "Could not serialize device orientation: " + orientation.toString()); + "Could not serialize device orientation: " + orientation.toString()); } } + /** + * Deserializes a string value to its corresponding {@link PlatformChannel.DeviceOrientation} + * value. + * + * @param orientation The string value to deserialize. + * @return The deserialized orientation. + * @throws UnsupportedOperationException when the provided string value does not have a + * corresponding {@link PlatformChannel.DeviceOrientation}. + */ static PlatformChannel.DeviceOrientation deserializeDeviceOrientation(String orientation) { if (orientation == null) throw new UnsupportedOperationException("Could not deserialize null device orientation."); @@ -79,19 +80,19 @@ static PlatformChannel.DeviceOrientation deserializeDeviceOrientation(String ori return PlatformChannel.DeviceOrientation.LANDSCAPE_RIGHT; default: throw new UnsupportedOperationException( - "Could not deserialize device orientation: " + orientation); + "Could not deserialize device orientation: " + orientation); } } - static Size computeBestCaptureSize(StreamConfigurationMap streamConfigurationMap) { - // For still image captures, we use the largest available size. - return Collections.max( - Arrays.asList(streamConfigurationMap.getOutputSizes(ImageFormat.JPEG)), - new CompareSizesByArea()); - } - + /** + * Gets all the available cameras for the device. + * + * @param activity The current Android activity. + * @return A map of all the available cameras, with their name as their key. + * @throws CameraAccessException when the camera could not be accessed. + */ public static List> getAvailableCameras(Activity activity) - throws CameraAccessException { + throws CameraAccessException { CameraManager cameraManager = (CameraManager) activity.getSystemService(Context.CAMERA_SERVICE); String[] cameraNames = cameraManager.getCameraIdList(); List> cameras = new ArrayList<>(); @@ -118,13 +119,4 @@ public static List> getAvailableCameras(Activity activity) } return cameras; } - - private static class CompareSizesByArea implements Comparator { - @Override - public int compare(Size lhs, Size rhs) { - // We cast here to ensure the multiplications won't overflow. - return Long.signum( - (long) lhs.getWidth() * lhs.getHeight() - (long) rhs.getWidth() * rhs.getHeight()); - } - } -} \ No newline at end of file +} 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 df65eecdb062..dc62fce524d3 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 @@ -6,219 +6,200 @@ import android.os.Handler; import android.text.TextUtils; - import androidx.annotation.NonNull; import androidx.annotation.Nullable; - import io.flutter.embedding.engine.systemchannels.PlatformChannel; import io.flutter.plugin.common.BinaryMessenger; import io.flutter.plugin.common.MethodChannel; -import io.flutter.plugins.camera.features.exposurelock.ExposureMode; import io.flutter.plugins.camera.features.autofocus.FocusMode; - +import io.flutter.plugins.camera.features.exposurelock.ExposureMode; import java.util.HashMap; import java.util.Map; -/** - * Utility class that facilitates communication to the Flutter client - */ +/** Utility class that facilitates communication to the Flutter client */ public class DartMessenger { - @NonNull - private final Handler handler; - @Nullable - private MethodChannel cameraChannel; - @Nullable - private MethodChannel deviceChannel; - - /** - * Specifies the different device related message types. - */ - enum DeviceEventType { - /** - * Indicates the device's orientation has changed. - */ - ORIENTATION_CHANGED("orientation_changed"); - private final String method; - - DeviceEventType(String method) { - this.method = method; - } + @NonNull private final Handler handler; + @Nullable private MethodChannel cameraChannel; + @Nullable private MethodChannel deviceChannel; + + /** Specifies the different device related message types. */ + enum DeviceEventType { + /** Indicates the device's orientation has changed. */ + ORIENTATION_CHANGED("orientation_changed"); + private final String method; + + DeviceEventType(String method) { + this.method = method; } + } - /** - * Specifies the different camera related message types. - */ - enum CameraEventType { - /** - * Indicates that an error occurred while interacting with the camera. - */ - ERROR("error"), - /** - * Indicates that the camera is closing. - */ - CLOSING("camera_closing"), - /** - * Indicates that the camera is initialized. - */ - INITIALIZED("initialized"); - - private final String method; - - /** - * Converts the supplied method name to the matching {@link CameraEventType}. - * - * @param method name to be converted into a {@link CameraEventType}. - */ - CameraEventType(String method) { - this.method = method; - } - } + /** Specifies the different camera related message types. */ + enum CameraEventType { + /** Indicates that an error occurred while interacting with the camera. */ + ERROR("error"), + /** Indicates that the camera is closing. */ + CLOSING("camera_closing"), + /** Indicates that the camera is initialized. */ + INITIALIZED("initialized"); - /** - * Creates a new instance of the {@link DartMessenger} class. - * - * @param messenger is the {@link BinaryMessenger} that is used to communicate with Flutter. - * @param cameraId identifies the camera which is the source of the communication. - * @param handler the handler used to manage the thread's message queue. This should always be a - * handler managing the main thread since communication with Flutter should always happen on - * the main thread. The handler is mainly supplied so it will be easier test this class. - */ - DartMessenger(BinaryMessenger messenger, long cameraId, @NonNull Handler handler) { - cameraChannel = new MethodChannel(messenger, "flutter.io/cameraPlugin/camera" + cameraId); - deviceChannel = new MethodChannel(messenger, "flutter.io/cameraPlugin/device"); - this.handler = handler; - } + private final String method; /** - * Sends a message to the Flutter client informing the orientation of the device has been changed. + * Converts the supplied method name to the matching {@link CameraEventType}. * - * @param orientation specifies the new orientation of the device. + * @param method name to be converted into a {@link CameraEventType}. */ - public void sendDeviceOrientationChangeEvent(PlatformChannel.DeviceOrientation orientation) { - assert (orientation != null); - this.send( - DeviceEventType.ORIENTATION_CHANGED, - new HashMap() { - { - put("orientation", CameraUtils.serializeDeviceOrientation(orientation)); - } - }); + CameraEventType(String method) { + this.method = method; } - - /** - * Sends a message to the Flutter client informing that the camera has been initialized. - * - * @param previewWidth describes the preview width that is supported by the camera. - * @param previewHeight describes the preview height that is supported by the camera. - * @param exposureMode describes the current exposure mode that is set on the camera. - * @param focusMode describes the current focus mode that is set on the camera. - * @param exposurePointSupported indicates if the camera supports setting an exposure point. - * @param focusPointSupported indicates if the camera supports setting a focus point. - */ - void sendCameraInitializedEvent( - Integer previewWidth, - Integer previewHeight, - ExposureMode exposureMode, - FocusMode focusMode, - Boolean exposurePointSupported, - Boolean focusPointSupported) { - assert (previewWidth != null); - assert (previewHeight != null); - assert (exposureMode != null); - assert (focusMode != null); - assert (exposurePointSupported != null); - assert (focusPointSupported != null); - this.send( - CameraEventType.INITIALIZED, - new HashMap() { - { - put("previewWidth", previewWidth.doubleValue()); - put("previewHeight", previewHeight.doubleValue()); - put("exposureMode", exposureMode.toString()); - put("focusMode", focusMode.toString()); - put("exposurePointSupported", exposurePointSupported); - put("focusPointSupported", focusPointSupported); - } - }); + } + + /** + * Creates a new instance of the {@link DartMessenger} class. + * + * @param messenger is the {@link BinaryMessenger} that is used to communicate with Flutter. + * @param cameraId identifies the camera which is the source of the communication. + * @param handler the handler used to manage the thread's message queue. This should always be a + * handler managing the main thread since communication with Flutter should always happen on + * the main thread. The handler is mainly supplied so it will be easier test this class. + */ + DartMessenger(BinaryMessenger messenger, long cameraId, @NonNull Handler handler) { + cameraChannel = new MethodChannel(messenger, "flutter.io/cameraPlugin/camera" + cameraId); + deviceChannel = new MethodChannel(messenger, "flutter.io/cameraPlugin/device"); + this.handler = handler; + } + + /** + * Sends a message to the Flutter client informing the orientation of the device has been changed. + * + * @param orientation specifies the new orientation of the device. + */ + public void sendDeviceOrientationChangeEvent(PlatformChannel.DeviceOrientation orientation) { + assert (orientation != null); + this.send( + DeviceEventType.ORIENTATION_CHANGED, + new HashMap() { + { + put("orientation", CameraUtils.serializeDeviceOrientation(orientation)); + } + }); + } + + /** + * Sends a message to the Flutter client informing that the camera has been initialized. + * + * @param previewWidth describes the preview width that is supported by the camera. + * @param previewHeight describes the preview height that is supported by the camera. + * @param exposureMode describes the current exposure mode that is set on the camera. + * @param focusMode describes the current focus mode that is set on the camera. + * @param exposurePointSupported indicates if the camera supports setting an exposure point. + * @param focusPointSupported indicates if the camera supports setting a focus point. + */ + void sendCameraInitializedEvent( + Integer previewWidth, + Integer previewHeight, + ExposureMode exposureMode, + FocusMode focusMode, + Boolean exposurePointSupported, + Boolean focusPointSupported) { + assert (previewWidth != null); + assert (previewHeight != null); + assert (exposureMode != null); + assert (focusMode != null); + assert (exposurePointSupported != null); + assert (focusPointSupported != null); + this.send( + CameraEventType.INITIALIZED, + new HashMap() { + { + put("previewWidth", previewWidth.doubleValue()); + put("previewHeight", previewHeight.doubleValue()); + put("exposureMode", exposureMode.toString()); + put("focusMode", focusMode.toString()); + put("exposurePointSupported", exposurePointSupported); + put("focusPointSupported", focusPointSupported); + } + }); + } + + /** Sends a message to the Flutter client informing that the camera is closing. */ + void sendCameraClosingEvent() { + send(CameraEventType.CLOSING); + } + + /** + * Sends a message to the Flutter client informing that an error occurred while interacting with + * the camera. + * + * @param description contains details regarding the error that occurred. + */ + void sendCameraErrorEvent(@Nullable String description) { + this.send( + CameraEventType.ERROR, + new HashMap() { + { + if (!TextUtils.isEmpty(description)) put("description", description); + } + }); + } + + private void send(CameraEventType eventType) { + send(eventType, new HashMap<>()); + } + + private void send(CameraEventType eventType, Map args) { + if (cameraChannel == null) { + return; } - /** - * Sends a message to the Flutter client informing that the camera is closing. - */ - void sendCameraClosingEvent() { - send(CameraEventType.CLOSING); + handler.post( + new Runnable() { + @Override + public void run() { + cameraChannel.invokeMethod(eventType.method, args); + } + }); + } + + private void send(DeviceEventType eventType) { + send(eventType, new HashMap<>()); + } + + private void send(DeviceEventType eventType, Map args) { + if (deviceChannel == null) { + return; } - /** - * Sends a message to the Flutter client informing that an error occurred while interacting with - * the camera. - * - * @param description contains details regarding the error that occurred. - */ - void sendCameraErrorEvent(@Nullable String description) { - this.send( - CameraEventType.ERROR, - new HashMap() { - { - if (!TextUtils.isEmpty(description)) put("description", description); - } - }); - } - - private void send(CameraEventType eventType) { - send(eventType, new HashMap<>()); - } - - private void send(CameraEventType eventType, Map args) { - if (cameraChannel == null) { - return; - } - - handler.post( - new Runnable() { - @Override - public void run() { - cameraChannel.invokeMethod(eventType.method, args); - } - }); - } - - private void send(DeviceEventType eventType) { - send(eventType, new HashMap<>()); - } - - private void send(DeviceEventType eventType, Map args) { - if (deviceChannel == null) { - return; - } - - handler.post( - new Runnable() { - @Override - public void run() { - deviceChannel.invokeMethod(eventType.method, args); - } - }); - } - - /** - * Send a success payload to a {@link MethodChannel.Result} on the main thread. - * - * @param payload The payload to send. - */ - public void finish(MethodChannel.Result result, Object payload) { - handler.post(() -> result.success(payload)); - } - - /** - * Send an error payload to a {@link MethodChannel.Result} on the main thread. - * - * @param errorCode error code. - * @param errorMessage error message. - * @param errorDetails error details. - */ - public void error(MethodChannel.Result result, - String errorCode, @Nullable String errorMessage, @Nullable Object errorDetails) { - handler.post(() -> result.error(errorCode, errorMessage, errorDetails)); - } + handler.post( + new Runnable() { + @Override + public void run() { + deviceChannel.invokeMethod(eventType.method, args); + } + }); + } + + /** + * Send a success payload to a {@link MethodChannel.Result} on the main thread. + * + * @param payload The payload to send. + */ + public void finish(MethodChannel.Result result, Object payload) { + handler.post(() -> result.success(payload)); + } + + /** + * Send an error payload to a {@link MethodChannel.Result} on the main thread. + * + * @param errorCode error code. + * @param errorMessage error message. + * @param errorDetails error details. + */ + public void error( + MethodChannel.Result result, + String errorCode, + @Nullable String errorMessage, + @Nullable Object errorDetails) { + handler.post(() -> result.error(errorCode, errorMessage, errorDetails)); + } } 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 163a51763517..821c9a50c13f 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 @@ -27,6 +27,7 @@ public class ImageSaver implements Runnable { /** * Creates an instance of the ImageSaver runnable + * * @param image - The image to save * @param file - The file to save the image to * @param callback - The callback that is run on completion, or when an error is encountered. @@ -64,18 +65,20 @@ public void run() { } /** - * The interface for the callback that is passed to ImageSaver, - * for detecting completion or failure of the image saving task. + * The interface for the callback that is passed to ImageSaver, for detecting completion or + * failure of the image saving task. */ public interface Callback { /** * Called when the image file has been saved successfully. + * * @param absolutePath - The absolute path of the file that was saved. */ void onComplete(String absolutePath); /** * Called when an error is encountered while saving the image file. + * * @param errorCode - The error code. * @param errorMessage - The human readable error message. */ 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 e51e4361b034..ac6af3ded19c 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 @@ -8,10 +8,8 @@ import android.hardware.camera2.CameraAccessException; import android.os.Handler; import android.os.Looper; - import androidx.annotation.NonNull; import androidx.annotation.Nullable; - import io.flutter.embedding.engine.systemchannels.PlatformChannel; import io.flutter.plugin.common.BinaryMessenger; import io.flutter.plugin.common.EventChannel; @@ -21,354 +19,379 @@ 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; -import io.flutter.plugins.camera.features.autofocus.FocusMode; import io.flutter.plugins.camera.features.resolution.ResolutionPreset; import io.flutter.view.TextureRegistry; - import java.util.HashMap; import java.util.Map; final class MethodCallHandlerImpl implements MethodChannel.MethodCallHandler { - private final Activity activity; - private final BinaryMessenger messenger; - private final CameraPermissions cameraPermissions; - private final PermissionsRegistry permissionsRegistry; - private final TextureRegistry textureRegistry; - private final MethodChannel methodChannel; - private final EventChannel imageStreamChannel; - private @Nullable - Camera camera; - - MethodCallHandlerImpl( - Activity activity, - BinaryMessenger messenger, - CameraPermissions cameraPermissions, - PermissionsRegistry permissionsAdder, - TextureRegistry textureRegistry) { - this.activity = activity; - this.messenger = messenger; - this.cameraPermissions = cameraPermissions; - this.permissionsRegistry = permissionsAdder; - this.textureRegistry = textureRegistry; + private final Activity activity; + private final BinaryMessenger messenger; + private final CameraPermissions cameraPermissions; + private final PermissionsRegistry permissionsRegistry; + private final TextureRegistry textureRegistry; + private final MethodChannel methodChannel; + private final EventChannel imageStreamChannel; + private @Nullable Camera camera; - methodChannel = new MethodChannel(messenger, "plugins.flutter.io/camera"); - imageStreamChannel = new EventChannel(messenger, "plugins.flutter.io/camera/imageStream"); - methodChannel.setMethodCallHandler(this); - } + MethodCallHandlerImpl( + Activity activity, + BinaryMessenger messenger, + CameraPermissions cameraPermissions, + PermissionsRegistry permissionsAdder, + TextureRegistry textureRegistry) { + this.activity = activity; + this.messenger = messenger; + this.cameraPermissions = cameraPermissions; + this.permissionsRegistry = permissionsAdder; + this.textureRegistry = textureRegistry; - @Override - public void onMethodCall(@NonNull MethodCall call, @NonNull final Result result) { - switch (call.method) { - case "availableCameras": - try { - result.success(CameraUtils.getAvailableCameras(activity)); - } catch (Exception e) { - handleException(e, result); - } - break; - case "create": { - if (camera != null) { - camera.close(); - } + methodChannel = new MethodChannel(messenger, "plugins.flutter.io/camera"); + imageStreamChannel = new EventChannel(messenger, "plugins.flutter.io/camera/imageStream"); + methodChannel.setMethodCallHandler(this); + } - cameraPermissions.requestPermissions( - activity, - permissionsRegistry, - call.argument("enableAudio"), - (String errCode, String errDesc) -> { - if (errCode == null) { - try { - instantiateCamera(call, result); - } catch (Exception e) { - handleException(e, result); - } - } else { - result.error(errCode, errDesc, null); - } - }); - break; - } - case "initialize": { - if (camera != null) { - try { - camera.open(call.argument("imageFormatGroup")); - result.success(null); - } catch (Exception e) { - handleException(e, result); - } - } else { - result.error( - "cameraNotFound", - "Camera not found. Please call the 'create' method before calling 'initialize'.", - null); - } - break; - } - case "takePicture": { - camera.takePicture(result); - break; - } - case "prepareForVideoRecording": { - // This optimization is not required for Android. - result.success(null); - break; - } - case "startVideoRecording": { - camera.startVideoRecording(result); - break; - } - case "stopVideoRecording": { - camera.stopVideoRecording(result); - break; - } - case "pauseVideoRecording": { - camera.pauseVideoRecording(result); - break; - } - case "resumeVideoRecording": { - camera.resumeVideoRecording(result); - break; - } - case "setFlashMode": { - String modeStr = call.argument("mode"); - FlashMode mode = FlashMode.getValueForString(modeStr); - if (mode == null) { - result.error("setFlashModeFailed", "Unknown flash mode " + modeStr, null); - return; - } - try { - camera.setFlashMode(result, mode); - } catch (Exception e) { - handleException(e, result); - } - break; - } - case "setExposureMode": { - String modeStr = call.argument("mode"); - ExposureMode mode = ExposureMode.getValueForString(modeStr); - if (mode == null) { - result.error("setExposureModeFailed", "Unknown exposure mode " + modeStr, null); - return; - } - try { - camera.setExposureMode(result, mode); - } catch (Exception e) { - handleException(e, result); - } - break; - } - case "setExposurePoint": { - Boolean reset = call.argument("reset"); - Double x = null; - Double y = null; - if (reset == null || !reset) { - x = call.argument("x"); - y = call.argument("y"); - } - try { - camera.setExposurePoint(result, new Point(x, y)); - } catch (Exception e) { - handleException(e, result); - } - break; - } - case "getMinExposureOffset": { - try { - result.success(camera.getMinExposureOffset()); - } catch (Exception e) { - handleException(e, result); - } - break; - } - case "getMaxExposureOffset": { - try { - result.success(camera.getMaxExposureOffset()); - } catch (Exception e) { - handleException(e, result); - } - break; - } - case "getExposureOffsetStepSize": { - try { - result.success(camera.getExposureOffsetStepSize()); - } catch (Exception e) { - handleException(e, result); - } - break; - } - case "setExposureOffset": { - try { - camera.setExposureOffset(result, call.argument("offset")); - } catch (Exception e) { - handleException(e, result); - } - break; - } - case "setFocusMode": { - String modeStr = call.argument("mode"); - FocusMode mode = FocusMode.getValueForString(modeStr); - if (mode == null) { - result.error("setFocusModeFailed", "Unknown focus mode " + modeStr, null); - return; - } - try { - camera.setFocusMode(result, mode); - } catch (Exception e) { - handleException(e, result); - } - break; - } - case "setFocusPoint": { - Boolean reset = call.argument("reset"); - Double x = null; - Double y = null; - if (reset == null || !reset) { - x = call.argument("x"); - y = call.argument("y"); - } - try { - camera.setFocusPoint(result, new Point(x, y)); - } catch (Exception e) { - handleException(e, result); - } - break; - } - case "startImageStream": { - try { - camera.startPreviewWithImageStream(imageStreamChannel); - result.success(null); - } catch (Exception e) { - handleException(e, result); - } - break; - } - case "stopImageStream": { - try { - camera.startPreview(); - result.success(null); - } catch (Exception e) { - handleException(e, result); - } - break; - } - case "getMaxZoomLevel": { - assert camera != null; + @Override + public void onMethodCall(@NonNull MethodCall call, @NonNull final Result result) { + switch (call.method) { + case "availableCameras": + try { + result.success(CameraUtils.getAvailableCameras(activity)); + } catch (Exception e) { + handleException(e, result); + } + break; + case "create": + { + if (camera != null) { + camera.close(); + } - try { - float maxZoomLevel = camera.getMaxZoomLevel(); - result.success(maxZoomLevel); - } catch (Exception e) { + cameraPermissions.requestPermissions( + activity, + permissionsRegistry, + call.argument("enableAudio"), + (String errCode, String errDesc) -> { + if (errCode == null) { + try { + instantiateCamera(call, result); + } catch (Exception e) { handleException(e, result); + } + } else { + result.error(errCode, errDesc, null); } - break; + }); + break; + } + case "initialize": + { + if (camera != null) { + try { + camera.open(call.argument("imageFormatGroup")); + result.success(null); + } catch (Exception e) { + handleException(e, result); } - case "getMinZoomLevel": { - assert camera != null; + } else { + result.error( + "cameraNotFound", + "Camera not found. Please call the 'create' method before calling 'initialize'.", + null); + } + break; + } + case "takePicture": + { + camera.takePicture(result); + break; + } + case "prepareForVideoRecording": + { + // This optimization is not required for Android. + result.success(null); + break; + } + case "startVideoRecording": + { + camera.startVideoRecording(result); + break; + } + case "stopVideoRecording": + { + camera.stopVideoRecording(result); + break; + } + case "pauseVideoRecording": + { + camera.pauseVideoRecording(result); + break; + } + case "resumeVideoRecording": + { + camera.resumeVideoRecording(result); + break; + } + case "setFlashMode": + { + String modeStr = call.argument("mode"); + FlashMode mode = FlashMode.getValueForString(modeStr); + if (mode == null) { + result.error("setFlashModeFailed", "Unknown flash mode " + modeStr, null); + return; + } + try { + camera.setFlashMode(result, mode); + } catch (Exception e) { + handleException(e, result); + } + break; + } + case "setExposureMode": + { + String modeStr = call.argument("mode"); + ExposureMode mode = ExposureMode.getValueForString(modeStr); + if (mode == null) { + result.error("setExposureModeFailed", "Unknown exposure mode " + modeStr, null); + return; + } + try { + camera.setExposureMode(result, mode); + } catch (Exception e) { + handleException(e, result); + } + break; + } + case "setExposurePoint": + { + Boolean reset = call.argument("reset"); + Double x = null; + Double y = null; + if (reset == null || !reset) { + x = call.argument("x"); + y = call.argument("y"); + } + try { + camera.setExposurePoint(result, new Point(x, y)); + } catch (Exception e) { + handleException(e, result); + } + break; + } + case "getMinExposureOffset": + { + try { + result.success(camera.getMinExposureOffset()); + } catch (Exception e) { + handleException(e, result); + } + break; + } + case "getMaxExposureOffset": + { + try { + result.success(camera.getMaxExposureOffset()); + } catch (Exception e) { + handleException(e, result); + } + break; + } + case "getExposureOffsetStepSize": + { + try { + result.success(camera.getExposureOffsetStepSize()); + } catch (Exception e) { + handleException(e, result); + } + break; + } + case "setExposureOffset": + { + try { + camera.setExposureOffset(result, call.argument("offset")); + } catch (Exception e) { + handleException(e, result); + } + break; + } + case "setFocusMode": + { + String modeStr = call.argument("mode"); + FocusMode mode = FocusMode.getValueForString(modeStr); + if (mode == null) { + result.error("setFocusModeFailed", "Unknown focus mode " + modeStr, null); + return; + } + try { + camera.setFocusMode(result, mode); + } catch (Exception e) { + handleException(e, result); + } + break; + } + case "setFocusPoint": + { + Boolean reset = call.argument("reset"); + Double x = null; + Double y = null; + if (reset == null || !reset) { + x = call.argument("x"); + y = call.argument("y"); + } + try { + camera.setFocusPoint(result, new Point(x, y)); + } catch (Exception e) { + handleException(e, result); + } + break; + } + case "startImageStream": + { + try { + camera.startPreviewWithImageStream(imageStreamChannel); + result.success(null); + } catch (Exception e) { + handleException(e, result); + } + break; + } + case "stopImageStream": + { + try { + camera.startPreview(); + result.success(null); + } catch (Exception e) { + handleException(e, result); + } + break; + } + case "getMaxZoomLevel": + { + assert camera != null; - try { - float minZoomLevel = camera.getMinZoomLevel(); - result.success(minZoomLevel); - } catch (Exception e) { - handleException(e, result); - } - break; - } - case "setZoomLevel": { - assert camera != null; + try { + float maxZoomLevel = camera.getMaxZoomLevel(); + result.success(maxZoomLevel); + } catch (Exception e) { + handleException(e, result); + } + break; + } + case "getMinZoomLevel": + { + assert camera != null; - Double zoom = call.argument("zoom"); + try { + float minZoomLevel = camera.getMinZoomLevel(); + result.success(minZoomLevel); + } catch (Exception e) { + handleException(e, result); + } + break; + } + case "setZoomLevel": + { + assert camera != null; - if (zoom == null) { - result.error( - "ZOOM_ERROR", "setZoomLevel is called without specifying a zoom level.", null); - return; - } + Double zoom = call.argument("zoom"); - try { - camera.setZoomLevel(result, zoom.floatValue()); - } catch (Exception e) { - handleException(e, result); - } - break; - } - case "lockCaptureOrientation": { - PlatformChannel.DeviceOrientation orientation = - CameraUtils.deserializeDeviceOrientation(call.argument("orientation")); + if (zoom == null) { + result.error( + "ZOOM_ERROR", "setZoomLevel is called without specifying a zoom level.", null); + return; + } - try { - camera.lockCaptureOrientation(orientation); - result.success(null); - } catch (Exception e) { - handleException(e, result); - } - break; - } - case "unlockCaptureOrientation": { - try { - camera.unlockCaptureOrientation(); - result.success(null); - } catch (Exception e) { - handleException(e, result); - } - break; - } - case "dispose": { - if (camera != null) { - camera.dispose(); - } - result.success(null); - break; - } - default: - result.notImplemented(); - break; + try { + camera.setZoomLevel(result, zoom.floatValue()); + } catch (Exception e) { + handleException(e, result); + } + break; } - } + case "lockCaptureOrientation": + { + PlatformChannel.DeviceOrientation orientation = + CameraUtils.deserializeDeviceOrientation(call.argument("orientation")); - void stopListening() { - methodChannel.setMethodCallHandler(null); + try { + camera.lockCaptureOrientation(orientation); + result.success(null); + } catch (Exception e) { + handleException(e, result); + } + break; + } + case "unlockCaptureOrientation": + { + try { + camera.unlockCaptureOrientation(); + result.success(null); + } catch (Exception e) { + handleException(e, result); + } + break; + } + case "dispose": + { + if (camera != null) { + camera.dispose(); + } + result.success(null); + break; + } + default: + result.notImplemented(); + break; } + } - private void instantiateCamera(MethodCall call, Result result) throws CameraAccessException { - String cameraName = call.argument("cameraName"); - String preset = call.argument("resolutionPreset"); - boolean enableAudio = call.argument("enableAudio"); + void stopListening() { + methodChannel.setMethodCallHandler(null); + } - TextureRegistry.SurfaceTextureEntry flutterSurfaceTexture = - textureRegistry.createSurfaceTexture(); - DartMessenger dartMessenger = new DartMessenger(messenger, flutterSurfaceTexture.id(), new Handler(Looper.getMainLooper())); - CameraProperties cameraProperties = - new CameraPropertiesImpl(cameraName, CameraUtils.getCameraManager(activity)); - ResolutionPreset resolutionPreset = ResolutionPreset.valueOf(preset); + private void instantiateCamera(MethodCall call, Result result) throws CameraAccessException { + String cameraName = call.argument("cameraName"); + String preset = call.argument("resolutionPreset"); + boolean enableAudio = call.argument("enableAudio"); - camera = - new Camera( - activity, - flutterSurfaceTexture, - new CameraFeatureFactoryImpl(), - dartMessenger, - cameraProperties, - resolutionPreset, - enableAudio); + TextureRegistry.SurfaceTextureEntry flutterSurfaceTexture = + textureRegistry.createSurfaceTexture(); + DartMessenger dartMessenger = + new DartMessenger( + messenger, flutterSurfaceTexture.id(), new Handler(Looper.getMainLooper())); + CameraProperties cameraProperties = + new CameraPropertiesImpl(cameraName, CameraUtils.getCameraManager(activity)); + ResolutionPreset resolutionPreset = ResolutionPreset.valueOf(preset); - Map reply = new HashMap<>(); - reply.put("cameraId", flutterSurfaceTexture.id()); - result.success(reply); - } + camera = + new Camera( + activity, + flutterSurfaceTexture, + new CameraFeatureFactoryImpl(), + dartMessenger, + cameraProperties, + resolutionPreset, + enableAudio); - // We move catching CameraAccessException out of onMethodCall because it causes a crash - // on plugin registration for sdks incompatible with Camera2 (< 21). We want this plugin to - // to be able to compile with <21 sdks for apps that want the camera and support earlier version. - @SuppressWarnings("ConstantConditions") - private void handleException(Exception exception, Result result) { - if (exception instanceof CameraAccessException) { - result.error("CameraAccess", exception.getMessage(), null); - return; - } + Map reply = new HashMap<>(); + reply.put("cameraId", flutterSurfaceTexture.id()); + result.success(reply); + } - // CameraAccessException can not be cast to a RuntimeException. - throw (RuntimeException) exception; + // We move catching CameraAccessException out of onMethodCall because it causes a crash + // on plugin registration for sdks incompatible with Camera2 (< 21). We want this plugin to + // to be able to compile with <21 sdks for apps that want the camera and support earlier version. + @SuppressWarnings("ConstantConditions") + private void handleException(Exception exception, Result result) { + if (exception instanceof CameraAccessException) { + result.error("CameraAccess", exception.getMessage(), null); + return; } + + // CameraAccessException can not be cast to a RuntimeException. + throw (RuntimeException) exception; + } } 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 c35c02e137c9..8d10c445788c 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 @@ -22,8 +22,8 @@ import io.flutter.plugins.camera.features.zoomlevel.ZoomLevelFeature; /** - * Factory for creating the supported feature implementation controlling different aspects - * of the {@link android.hardware.camera2.CaptureRequest}. + * Factory for creating the supported feature implementation controlling different aspects of the + * {@link android.hardware.camera2.CaptureRequest}. */ public interface CameraFeatureFactory { @@ -75,7 +75,9 @@ AutoFocusFeature createAutoFocusFeature( * @return newly created instance of the ResolutionFeature class. */ ResolutionFeature createResolutionFeature( - @NonNull CameraProperties cameraProperties, ResolutionPreset initialSetting, String cameraName); + @NonNull CameraProperties cameraProperties, + ResolutionPreset initialSetting, + String cameraName); /** * Creates a new instance of the focus point feature. 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 4c4ceaeb40dd..b12ad3626226 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 @@ -23,7 +23,8 @@ /** * Implementation of the {@link CameraFeatureFactory} interface creating the supported feature - * implementation controlling different aspects of the {@link android.hardware.camera2.CaptureRequest}. + * implementation controlling different aspects of the {@link + * android.hardware.camera2.CaptureRequest}. */ public class CameraFeatureFactoryImpl implements CameraFeatureFactory { @@ -51,7 +52,9 @@ public FlashFeature createFlashFeature(@NonNull CameraProperties cameraPropertie @Override public ResolutionFeature createResolutionFeature( - @NonNull CameraProperties cameraProperties, ResolutionPreset initialSetting, String cameraName) { + @NonNull CameraProperties cameraProperties, + ResolutionPreset initialSetting, + String cameraName) { return new ResolutionFeature(cameraProperties, initialSetting, cameraName); } 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 b6f3cb327763..d744b77e4bd3 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 @@ -4,10 +4,6 @@ package io.flutter.plugins.camera.features; -import java.util.Collection; -import java.util.HashMap; -import java.util.Map; - import io.flutter.plugins.camera.features.autofocus.AutoFocusFeature; import io.flutter.plugins.camera.features.exposurelock.ExposureLockFeature; import io.flutter.plugins.camera.features.exposureoffset.ExposureOffsetFeature; @@ -19,236 +15,238 @@ import io.flutter.plugins.camera.features.resolution.ResolutionFeature; import io.flutter.plugins.camera.features.sensororientation.SensorOrientationFeature; import io.flutter.plugins.camera.features.zoomlevel.ZoomLevelFeature; +import java.util.Collection; +import java.util.HashMap; +import java.util.Map; /** - * These are all of our available features in the camera. Used in the Camera to access - * all features in a simpler way. + * These are all of our available features in the camera. Used in the Camera to access all features + * in a simpler way. */ public class CameraFeatures { - private static final String AUTO_FOCUS = "AUTO_FOCUS"; - private static final String EXPOSURE_LOCK = "EXPOSURE_LOCK"; - private static final String EXPOSURE_OFFSET = "EXPOSURE_OFFSET"; - private static final String EXPOSURE_POINT = "EXPOSURE_POINT"; - private static final String FLASH = "FLASH"; - private static final String FOCUS_POINT = "FOCUS_POINT"; - private static final String FPS_RANGE = "FPS_RANGE"; - private static final String NOISE_REDUCTION = "NOISE_REDUCTION"; - private static final String REGION_BOUNDARIES = "REGION_BOUNDARIES"; - private static final String RESOLUTION = "RESOLUTION"; - private static final String SENSOR_ORIENTATION = "SENSOR_ORIENTATION"; - private static final String ZOOM_LEVEL = "ZOOM_LEVEL"; - - private Map featureMap = new HashMap<>(); - - - private T getFeature(String key) { - return (T) featureMap.get(key); - } - - /** - * Gets a collection of all features that have been set. - * - * @return A collection of all features that have been set. - */ - public Collection getAllFeatures() { - return this.featureMap.values(); - } - - /** - * Gets the auto focus feature if it has been set. - * - * @return the auto focus feature. - */ - public AutoFocusFeature getAutoFocus() { - return getFeature(AUTO_FOCUS); - } - - /** - * Sets the instance of the auto focus feature. - * - * @param autoFocus the {@link AutoFocusFeature} instance to set. - */ - public void setAutoFocus(AutoFocusFeature autoFocus) { - this.featureMap.put(AUTO_FOCUS, autoFocus); - } - - /** - * Gets the exposure lock feature if it has been set. - * - * @return the exposure lock feature. - */ - public ExposureLockFeature getExposureLock() { - return getFeature(EXPOSURE_LOCK); - } - - /** - * Sets the instance of the exposure lock feature. - * - * @param exposureLock the {@link ExposureLockFeature} instance to set. - */ - public void setExposureLock(ExposureLockFeature exposureLock) { - this.featureMap.put(EXPOSURE_LOCK, exposureLock); - } - - /** - * Gets the exposure offset feature if it has been set. - * - * @return the exposure offset feature. - */ - public ExposureOffsetFeature getExposureOffset() { - return getFeature(EXPOSURE_OFFSET); - } - - /** - * Sets the instance of the exposure offset feature. - * - * @param exposureOffset the {@link ExposureOffsetFeature} instance to set. - */ - public void setExposureOffset(ExposureOffsetFeature exposureOffset) { - this.featureMap.put(EXPOSURE_OFFSET, exposureOffset); - } - - /** - * Gets the exposure point feature if it has been set. - * - * @return the exposure point feature. - */ - public ExposurePointFeature getExposurePoint() { - return getFeature(EXPOSURE_POINT); - } - - /** - * Sets the instance of the exposure point feature. - * - * @param exposurePoint the {@link ExposurePointFeature} instance to set. - */ - public void setExposurePoint(ExposurePointFeature exposurePoint) { - this.featureMap.put(EXPOSURE_POINT, exposurePoint); - } - - /** - * Gets the flash feature if it has been set. - * - * @return the flash feature. - */ - public FlashFeature getFlash() { - return getFeature(FLASH); - } - - /** - * Sets the instance of the flash feature. - * - * @param flash the {@link FlashFeature} instance to set. - */ - public void setFlash(FlashFeature flash) { - this.featureMap.put(FLASH, flash); - } - - /** - * Gets the focus point feature if it has been set. - * - * @return the focus point feature. - */ - public FocusPointFeature getFocusPoint() { - return getFeature(FOCUS_POINT); - } - - /** - * Sets the instance of the focus point feature. - * - * @param focusPoint the {@link FocusPointFeature} instance to set. - */ - public void setFocusPoint(FocusPointFeature focusPoint) { - this.featureMap.put(FOCUS_POINT, focusPoint); - } - - /** - * Gets the fps range feature if it has been set. - * - * @return the fps range feature. - */ - public FpsRangeFeature getFpsRange() { - return getFeature(FPS_RANGE); - } - - /** - * Sets the instance of the fps range feature. - * - * @param fpsRange the {@link FpsRangeFeature} instance to set. - */ - public void setFpsRange(FpsRangeFeature fpsRange) { - this.featureMap.put(FPS_RANGE, fpsRange); - } - - /** - * Gets the noise reduction feature if it has been set. - * - * @return the noise reduction feature. - */ - public NoiseReductionFeature getNoiseReduction() { - return getFeature(NOISE_REDUCTION); - } - - /** - * Sets the instance of the noise reduction feature. - * - * @param noiseReduction the {@link NoiseReductionFeature} instance to set. - */ - public void setNoiseReduction(NoiseReductionFeature noiseReduction) { - this.featureMap.put(NOISE_REDUCTION, noiseReduction); - } - - /** - * Gets the resolution feature if it has been set. - * - * @return the resolution feature. - */ - public ResolutionFeature getResolution() { - return getFeature(RESOLUTION); - } - - /** - * Sets the instance of the resolution feature. - * - * @param resolution the {@link ResolutionFeature} instance to set. - */ - public void setResolution(ResolutionFeature resolution) { - this.featureMap.put(RESOLUTION, resolution); - } - - /** - * Gets the sensor orientation feature if it has been set. - * - * @return the sensor orientation feature. - */ - public SensorOrientationFeature getSensorOrientation() { - return getFeature(SENSOR_ORIENTATION); - } - - /** - * Sets the instance of the sensor orientation feature. - * - * @param sensorOrientation the {@link SensorOrientationFeature} instance to set. - */ - public void setSensorOrientation(SensorOrientationFeature sensorOrientation) { - this.featureMap.put(SENSOR_ORIENTATION, sensorOrientation); - } - - /** - * Gets the zoom level feature if it has been set. - * - * @return the zoom level feature. - */ - public ZoomLevelFeature getZoomLevel() { - return getFeature(ZOOM_LEVEL); - } - - /** - * Sets the instance of the zoom level feature. - * - * @param zoomLevel the {@link ZoomLevelFeature} instance to set. - */ - public void setZoomLevel(ZoomLevelFeature zoomLevel) { - this.featureMap.put(ZOOM_LEVEL, zoomLevel); - } + private static final String AUTO_FOCUS = "AUTO_FOCUS"; + private static final String EXPOSURE_LOCK = "EXPOSURE_LOCK"; + private static final String EXPOSURE_OFFSET = "EXPOSURE_OFFSET"; + private static final String EXPOSURE_POINT = "EXPOSURE_POINT"; + private static final String FLASH = "FLASH"; + private static final String FOCUS_POINT = "FOCUS_POINT"; + private static final String FPS_RANGE = "FPS_RANGE"; + private static final String NOISE_REDUCTION = "NOISE_REDUCTION"; + private static final String REGION_BOUNDARIES = "REGION_BOUNDARIES"; + private static final String RESOLUTION = "RESOLUTION"; + private static final String SENSOR_ORIENTATION = "SENSOR_ORIENTATION"; + private static final String ZOOM_LEVEL = "ZOOM_LEVEL"; + + private Map featureMap = new HashMap<>(); + + private T getFeature(String key) { + return (T) featureMap.get(key); + } + + /** + * Gets a collection of all features that have been set. + * + * @return A collection of all features that have been set. + */ + public Collection getAllFeatures() { + return this.featureMap.values(); + } + + /** + * Gets the auto focus feature if it has been set. + * + * @return the auto focus feature. + */ + public AutoFocusFeature getAutoFocus() { + return getFeature(AUTO_FOCUS); + } + + /** + * Sets the instance of the auto focus feature. + * + * @param autoFocus the {@link AutoFocusFeature} instance to set. + */ + public void setAutoFocus(AutoFocusFeature autoFocus) { + this.featureMap.put(AUTO_FOCUS, autoFocus); + } + + /** + * Gets the exposure lock feature if it has been set. + * + * @return the exposure lock feature. + */ + public ExposureLockFeature getExposureLock() { + return getFeature(EXPOSURE_LOCK); + } + + /** + * Sets the instance of the exposure lock feature. + * + * @param exposureLock the {@link ExposureLockFeature} instance to set. + */ + public void setExposureLock(ExposureLockFeature exposureLock) { + this.featureMap.put(EXPOSURE_LOCK, exposureLock); + } + + /** + * Gets the exposure offset feature if it has been set. + * + * @return the exposure offset feature. + */ + public ExposureOffsetFeature getExposureOffset() { + return getFeature(EXPOSURE_OFFSET); + } + + /** + * Sets the instance of the exposure offset feature. + * + * @param exposureOffset the {@link ExposureOffsetFeature} instance to set. + */ + public void setExposureOffset(ExposureOffsetFeature exposureOffset) { + this.featureMap.put(EXPOSURE_OFFSET, exposureOffset); + } + + /** + * Gets the exposure point feature if it has been set. + * + * @return the exposure point feature. + */ + public ExposurePointFeature getExposurePoint() { + return getFeature(EXPOSURE_POINT); + } + + /** + * Sets the instance of the exposure point feature. + * + * @param exposurePoint the {@link ExposurePointFeature} instance to set. + */ + public void setExposurePoint(ExposurePointFeature exposurePoint) { + this.featureMap.put(EXPOSURE_POINT, exposurePoint); + } + + /** + * Gets the flash feature if it has been set. + * + * @return the flash feature. + */ + public FlashFeature getFlash() { + return getFeature(FLASH); + } + + /** + * Sets the instance of the flash feature. + * + * @param flash the {@link FlashFeature} instance to set. + */ + public void setFlash(FlashFeature flash) { + this.featureMap.put(FLASH, flash); + } + + /** + * Gets the focus point feature if it has been set. + * + * @return the focus point feature. + */ + public FocusPointFeature getFocusPoint() { + return getFeature(FOCUS_POINT); + } + + /** + * Sets the instance of the focus point feature. + * + * @param focusPoint the {@link FocusPointFeature} instance to set. + */ + public void setFocusPoint(FocusPointFeature focusPoint) { + this.featureMap.put(FOCUS_POINT, focusPoint); + } + + /** + * Gets the fps range feature if it has been set. + * + * @return the fps range feature. + */ + public FpsRangeFeature getFpsRange() { + return getFeature(FPS_RANGE); + } + + /** + * Sets the instance of the fps range feature. + * + * @param fpsRange the {@link FpsRangeFeature} instance to set. + */ + public void setFpsRange(FpsRangeFeature fpsRange) { + this.featureMap.put(FPS_RANGE, fpsRange); + } + + /** + * Gets the noise reduction feature if it has been set. + * + * @return the noise reduction feature. + */ + public NoiseReductionFeature getNoiseReduction() { + return getFeature(NOISE_REDUCTION); + } + + /** + * Sets the instance of the noise reduction feature. + * + * @param noiseReduction the {@link NoiseReductionFeature} instance to set. + */ + public void setNoiseReduction(NoiseReductionFeature noiseReduction) { + this.featureMap.put(NOISE_REDUCTION, noiseReduction); + } + + /** + * Gets the resolution feature if it has been set. + * + * @return the resolution feature. + */ + public ResolutionFeature getResolution() { + return getFeature(RESOLUTION); + } + + /** + * Sets the instance of the resolution feature. + * + * @param resolution the {@link ResolutionFeature} instance to set. + */ + public void setResolution(ResolutionFeature resolution) { + this.featureMap.put(RESOLUTION, resolution); + } + + /** + * Gets the sensor orientation feature if it has been set. + * + * @return the sensor orientation feature. + */ + public SensorOrientationFeature getSensorOrientation() { + return getFeature(SENSOR_ORIENTATION); + } + + /** + * Sets the instance of the sensor orientation feature. + * + * @param sensorOrientation the {@link SensorOrientationFeature} instance to set. + */ + public void setSensorOrientation(SensorOrientationFeature sensorOrientation) { + this.featureMap.put(SENSOR_ORIENTATION, sensorOrientation); + } + + /** + * Gets the zoom level feature if it has been set. + * + * @return the zoom level feature. + */ + public ZoomLevelFeature getZoomLevel() { + return getFeature(ZOOM_LEVEL); + } + + /** + * Sets the instance of the zoom level feature. + * + * @param zoomLevel the {@link ZoomLevelFeature} instance to set. + */ + public void setZoomLevel(ZoomLevelFeature zoomLevel) { + this.featureMap.put(ZOOM_LEVEL, zoomLevel); + } } 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 eb2123be93f2..d5432da210da 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 @@ -7,9 +7,7 @@ import android.hardware.camera2.CaptureRequest; import android.media.CamcorderProfile; import android.util.Size; - import androidx.annotation.VisibleForTesting; - import io.flutter.plugins.camera.CameraProperties; import io.flutter.plugins.camera.features.CameraFeature; @@ -21,158 +19,159 @@ * required to configure the resolution using the {@link android.hardware.camera2} API. */ public class ResolutionFeature extends CameraFeature { - private Size captureSize; - private Size previewSize; - private CamcorderProfile recordingProfile; - private ResolutionPreset currentSetting; - private int cameraId; - - /** - * Creates a new instance of the {@link ResolutionFeature}. - * - * @param cameraProperties Collection of characteristics for the current camera device. - * @param resolutionPreset Platform agnostic enum containing resolution information. - * @param cameraName Camera identifier of the camera for which to configure the resolution. - */ - public ResolutionFeature( - CameraProperties cameraProperties, ResolutionPreset resolutionPreset, String cameraName) { - super(cameraProperties); - this.currentSetting = resolutionPreset; - try { - this.cameraId = Integer.parseInt(cameraName, 10); - } catch (NumberFormatException e) { - this.cameraId = -1; - return; - } - configureResolution(resolutionPreset, cameraId); + private Size captureSize; + private Size previewSize; + private CamcorderProfile recordingProfile; + private ResolutionPreset currentSetting; + private int cameraId; + + /** + * Creates a new instance of the {@link ResolutionFeature}. + * + * @param cameraProperties Collection of characteristics for the current camera device. + * @param resolutionPreset Platform agnostic enum containing resolution information. + * @param cameraName Camera identifier of the camera for which to configure the resolution. + */ + public ResolutionFeature( + CameraProperties cameraProperties, ResolutionPreset resolutionPreset, String cameraName) { + super(cameraProperties); + this.currentSetting = resolutionPreset; + try { + this.cameraId = Integer.parseInt(cameraName, 10); + } catch (NumberFormatException e) { + this.cameraId = -1; + return; + } + configureResolution(resolutionPreset, cameraId); + } + + /** + * Gets the best possible {@link android.media.CamcorderProfile} for the supplied {@link + * ResolutionPreset}. + * + * @param cameraId Camera identifier which indicates the device's camera for which to select a + * {@link android.media.CamcorderProfile}. + * @param preset The {@link ResolutionPreset} for which is to be translated to a {@link + * android.media.CamcorderProfile}. + * @return The best possible {@link android.media.CamcorderProfile} that matches the supplied + * {@link ResolutionPreset}. + */ + public static CamcorderProfile getBestAvailableCamcorderProfileForResolutionPreset( + int cameraId, ResolutionPreset preset) { + if (cameraId < 0) { + throw new AssertionError( + "getBestAvailableCamcorderProfileForResolutionPreset can only be used with valid (>=0) camera identifiers."); } - /** - * Gets the best possible {@link android.media.CamcorderProfile} for the supplied {@link - * ResolutionPreset}. - * - * @param cameraId Camera identifier which indicates the device's camera for which to select a - * {@link android.media.CamcorderProfile}. - * @param preset The {@link ResolutionPreset} for which is to be translated to a {@link - * android.media.CamcorderProfile}. - * @return The best possible {@link android.media.CamcorderProfile} that matches the supplied - * {@link ResolutionPreset}. - */ - public static CamcorderProfile getBestAvailableCamcorderProfileForResolutionPreset( - int cameraId, ResolutionPreset preset) { - if (cameraId < 0) { - throw new AssertionError("getBestAvailableCamcorderProfileForResolutionPreset can only be used with valid (>=0) camera identifiers."); + 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); } - - 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."); - } + case ultraHigh: + if (CamcorderProfile.hasProfile(cameraId, CamcorderProfile.QUALITY_2160P)) { + return CamcorderProfile.get(cameraId, CamcorderProfile.QUALITY_2160P); } - } - - @VisibleForTesting - static Size computeBestPreviewSize(int cameraId, ResolutionPreset preset) { - if (preset.ordinal() > ResolutionPreset.high.ordinal()) { - preset = ResolutionPreset.high; + case veryHigh: + if (CamcorderProfile.hasProfile(cameraId, CamcorderProfile.QUALITY_1080P)) { + return CamcorderProfile.get(cameraId, CamcorderProfile.QUALITY_1080P); } - - CamcorderProfile profile = - getBestAvailableCamcorderProfileForResolutionPreset(cameraId, preset); - return new Size(profile.videoFrameWidth, profile.videoFrameHeight); - } - - private void configureResolution(ResolutionPreset resolutionPreset, int cameraId) { - if (!checkIsSupported()) { - return; + 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."); } - recordingProfile = - getBestAvailableCamcorderProfileForResolutionPreset(cameraId, resolutionPreset); - captureSize = new Size(recordingProfile.videoFrameWidth, recordingProfile.videoFrameHeight); - previewSize = computeBestPreviewSize(cameraId, resolutionPreset); - } - - @Override - public String getDebugName() { - return "ResolutionFeature"; - } - - @Override - public ResolutionPreset getValue() { - return currentSetting; - } - - @Override - public void setValue(ResolutionPreset value) { - this.currentSetting = value; - configureResolution(currentSetting, cameraId); - } - - // Always supported - @Override - public boolean checkIsSupported() { - return cameraId >= 0; - } - - @Override - public void updateBuilder(CaptureRequest.Builder requestBuilder) { - // No-op: when setting a resolution there is no need to update the request builder. } + } - /** - * Gets the {@link android.media.CamcorderProfile} containing the information to configure the - * resolution using the {@link android.hardware.camera2} API. - * - * @return Resolution information to configure the {@link android.hardware.camera2} API. - */ - public CamcorderProfile getRecordingProfile() { - return this.recordingProfile; + @VisibleForTesting + static Size computeBestPreviewSize(int cameraId, ResolutionPreset preset) { + if (preset.ordinal() > ResolutionPreset.high.ordinal()) { + preset = ResolutionPreset.high; } - /** - * Gets the optimal preview size based on the configured resolution. - * - * @return The optimal preview size. - */ - public Size getPreviewSize() { - return this.previewSize; - } + CamcorderProfile profile = + getBestAvailableCamcorderProfileForResolutionPreset(cameraId, preset); + return new Size(profile.videoFrameWidth, profile.videoFrameHeight); + } - /** - * Gets the optimal capture size based on the configured resolution. - * - * @return The optimal capture size. - */ - public Size getCaptureSize() { - return this.captureSize; + private void configureResolution(ResolutionPreset resolutionPreset, int cameraId) { + if (!checkIsSupported()) { + return; } + recordingProfile = + getBestAvailableCamcorderProfileForResolutionPreset(cameraId, resolutionPreset); + captureSize = new Size(recordingProfile.videoFrameWidth, recordingProfile.videoFrameHeight); + previewSize = computeBestPreviewSize(cameraId, resolutionPreset); + } + + @Override + public String getDebugName() { + return "ResolutionFeature"; + } + + @Override + public ResolutionPreset getValue() { + return currentSetting; + } + + @Override + public void setValue(ResolutionPreset value) { + this.currentSetting = value; + configureResolution(currentSetting, cameraId); + } + + // Always supported + @Override + public boolean checkIsSupported() { + return cameraId >= 0; + } + + @Override + public void updateBuilder(CaptureRequest.Builder requestBuilder) { + // No-op: when setting a resolution there is no need to update the request builder. + } + + /** + * Gets the {@link android.media.CamcorderProfile} containing the information to configure the + * resolution using the {@link android.hardware.camera2} API. + * + * @return Resolution information to configure the {@link android.hardware.camera2} API. + */ + public CamcorderProfile getRecordingProfile() { + return this.recordingProfile; + } + + /** + * Gets the optimal preview size based on the configured resolution. + * + * @return The optimal preview size. + */ + public Size getPreviewSize() { + return this.previewSize; + } + + /** + * Gets the optimal capture size based on the configured resolution. + * + * @return The optimal capture size. + */ + public Size getCaptureSize() { + return this.captureSize; + } } diff --git a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/types/CaptureTimeoutsWrapper.java b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/types/CaptureTimeoutsWrapper.java index 223b33038584..ad59bd09c754 100644 --- a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/types/CaptureTimeoutsWrapper.java +++ b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/types/CaptureTimeoutsWrapper.java @@ -5,50 +5,48 @@ package io.flutter.plugins.camera.types; /** - * Wrapper class that provides a container for all {@link Timeout} instances - * that are required for the capture flow. + * Wrapper class that provides a container for all {@link Timeout} instances that are required for + * the capture flow. */ public class CaptureTimeoutsWrapper { - private Timeout preCaptureFocusing; - private Timeout preCaptureMetering; - private final long preCaptureFocusingTimeoutMs; - private final long preCaptureMeteringTimeoutMs; + private Timeout preCaptureFocusing; + private Timeout preCaptureMetering; + private final long preCaptureFocusingTimeoutMs; + private final long preCaptureMeteringTimeoutMs; - /** - * Create a new wrapper instance with the specified timeout values. - * - * @param preCaptureFocusingTimeoutMs focusing timeout milliseconds. - * @param preCaptureMeteringTimeoutMs metering timeout milliseconds. - */ - public CaptureTimeoutsWrapper( - long preCaptureFocusingTimeoutMs, long preCaptureMeteringTimeoutMs) { - this.preCaptureFocusingTimeoutMs = preCaptureFocusingTimeoutMs; - this.preCaptureMeteringTimeoutMs = preCaptureMeteringTimeoutMs; - } + /** + * Create a new wrapper instance with the specified timeout values. + * + * @param preCaptureFocusingTimeoutMs focusing timeout milliseconds. + * @param preCaptureMeteringTimeoutMs metering timeout milliseconds. + */ + public CaptureTimeoutsWrapper( + long preCaptureFocusingTimeoutMs, long preCaptureMeteringTimeoutMs) { + this.preCaptureFocusingTimeoutMs = preCaptureFocusingTimeoutMs; + this.preCaptureMeteringTimeoutMs = preCaptureMeteringTimeoutMs; + } - /** - * Reset all timeouts to the current timestamp. - */ - public void reset() { - this.preCaptureFocusing = Timeout.create(preCaptureFocusingTimeoutMs); - this.preCaptureMetering = Timeout.create(preCaptureMeteringTimeoutMs); - } + /** Reset all timeouts to the current timestamp. */ + public void reset() { + this.preCaptureFocusing = Timeout.create(preCaptureFocusingTimeoutMs); + this.preCaptureMetering = Timeout.create(preCaptureMeteringTimeoutMs); + } - /** - * Returns the timeout instance related to precapture focusing. - * - * @return - The timeout object - */ - public Timeout getPreCaptureFocusing() { - return preCaptureFocusing; - } + /** + * Returns the timeout instance related to precapture focusing. + * + * @return - The timeout object + */ + public Timeout getPreCaptureFocusing() { + return preCaptureFocusing; + } - /** - * Returns the timeout instance related to precapture metering. - * - * @return - The timeout object - */ - public Timeout getPreCaptureMetering() { - return preCaptureMetering; - } + /** + * Returns the timeout instance related to precapture metering. + * + * @return - The timeout object + */ + public Timeout getPreCaptureMetering() { + return preCaptureMetering; + } } From 6fe67da886ff63847cfc65e9d5632a35c5631b4b Mon Sep 17 00:00:00 2001 From: BeMacized Date: Wed, 16 Jun 2021 16:55:24 +0200 Subject: [PATCH 43/63] Update tests --- .../camera/features/CameraFeatures.java | 447 +++++++++--------- .../io/flutter/plugins/camera/CameraTest.java | 380 +++++++++++++++ .../plugins/camera/CameraUtilsTest.java | 47 -- .../plugins/camera/DartMessengerTest.java | 4 +- .../resolution/ResolutionFeatureTest.java | 10 +- 5 files changed, 609 insertions(+), 279 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/features/CameraFeatures.java b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/CameraFeatures.java index d744b77e4bd3..86a93fe79fd9 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 @@ -15,6 +15,7 @@ import io.flutter.plugins.camera.features.resolution.ResolutionFeature; import io.flutter.plugins.camera.features.sensororientation.SensorOrientationFeature; import io.flutter.plugins.camera.features.zoomlevel.ZoomLevelFeature; + import java.util.Collection; import java.util.HashMap; import java.util.Map; @@ -24,229 +25,225 @@ * in a simpler way. */ public class CameraFeatures { - private static final String AUTO_FOCUS = "AUTO_FOCUS"; - private static final String EXPOSURE_LOCK = "EXPOSURE_LOCK"; - private static final String EXPOSURE_OFFSET = "EXPOSURE_OFFSET"; - private static final String EXPOSURE_POINT = "EXPOSURE_POINT"; - private static final String FLASH = "FLASH"; - private static final String FOCUS_POINT = "FOCUS_POINT"; - private static final String FPS_RANGE = "FPS_RANGE"; - private static final String NOISE_REDUCTION = "NOISE_REDUCTION"; - private static final String REGION_BOUNDARIES = "REGION_BOUNDARIES"; - private static final String RESOLUTION = "RESOLUTION"; - private static final String SENSOR_ORIENTATION = "SENSOR_ORIENTATION"; - private static final String ZOOM_LEVEL = "ZOOM_LEVEL"; - - private Map featureMap = new HashMap<>(); - - private T getFeature(String key) { - return (T) featureMap.get(key); - } - - /** - * Gets a collection of all features that have been set. - * - * @return A collection of all features that have been set. - */ - public Collection getAllFeatures() { - return this.featureMap.values(); - } - - /** - * Gets the auto focus feature if it has been set. - * - * @return the auto focus feature. - */ - public AutoFocusFeature getAutoFocus() { - return getFeature(AUTO_FOCUS); - } - - /** - * Sets the instance of the auto focus feature. - * - * @param autoFocus the {@link AutoFocusFeature} instance to set. - */ - public void setAutoFocus(AutoFocusFeature autoFocus) { - this.featureMap.put(AUTO_FOCUS, autoFocus); - } - - /** - * Gets the exposure lock feature if it has been set. - * - * @return the exposure lock feature. - */ - public ExposureLockFeature getExposureLock() { - return getFeature(EXPOSURE_LOCK); - } - - /** - * Sets the instance of the exposure lock feature. - * - * @param exposureLock the {@link ExposureLockFeature} instance to set. - */ - public void setExposureLock(ExposureLockFeature exposureLock) { - this.featureMap.put(EXPOSURE_LOCK, exposureLock); - } - - /** - * Gets the exposure offset feature if it has been set. - * - * @return the exposure offset feature. - */ - public ExposureOffsetFeature getExposureOffset() { - return getFeature(EXPOSURE_OFFSET); - } - - /** - * Sets the instance of the exposure offset feature. - * - * @param exposureOffset the {@link ExposureOffsetFeature} instance to set. - */ - public void setExposureOffset(ExposureOffsetFeature exposureOffset) { - this.featureMap.put(EXPOSURE_OFFSET, exposureOffset); - } - - /** - * Gets the exposure point feature if it has been set. - * - * @return the exposure point feature. - */ - public ExposurePointFeature getExposurePoint() { - return getFeature(EXPOSURE_POINT); - } - - /** - * Sets the instance of the exposure point feature. - * - * @param exposurePoint the {@link ExposurePointFeature} instance to set. - */ - public void setExposurePoint(ExposurePointFeature exposurePoint) { - this.featureMap.put(EXPOSURE_POINT, exposurePoint); - } - - /** - * Gets the flash feature if it has been set. - * - * @return the flash feature. - */ - public FlashFeature getFlash() { - return getFeature(FLASH); - } - - /** - * Sets the instance of the flash feature. - * - * @param flash the {@link FlashFeature} instance to set. - */ - public void setFlash(FlashFeature flash) { - this.featureMap.put(FLASH, flash); - } - - /** - * Gets the focus point feature if it has been set. - * - * @return the focus point feature. - */ - public FocusPointFeature getFocusPoint() { - return getFeature(FOCUS_POINT); - } - - /** - * Sets the instance of the focus point feature. - * - * @param focusPoint the {@link FocusPointFeature} instance to set. - */ - public void setFocusPoint(FocusPointFeature focusPoint) { - this.featureMap.put(FOCUS_POINT, focusPoint); - } - - /** - * Gets the fps range feature if it has been set. - * - * @return the fps range feature. - */ - public FpsRangeFeature getFpsRange() { - return getFeature(FPS_RANGE); - } - - /** - * Sets the instance of the fps range feature. - * - * @param fpsRange the {@link FpsRangeFeature} instance to set. - */ - public void setFpsRange(FpsRangeFeature fpsRange) { - this.featureMap.put(FPS_RANGE, fpsRange); - } - - /** - * Gets the noise reduction feature if it has been set. - * - * @return the noise reduction feature. - */ - public NoiseReductionFeature getNoiseReduction() { - return getFeature(NOISE_REDUCTION); - } - - /** - * Sets the instance of the noise reduction feature. - * - * @param noiseReduction the {@link NoiseReductionFeature} instance to set. - */ - public void setNoiseReduction(NoiseReductionFeature noiseReduction) { - this.featureMap.put(NOISE_REDUCTION, noiseReduction); - } - - /** - * Gets the resolution feature if it has been set. - * - * @return the resolution feature. - */ - public ResolutionFeature getResolution() { - return getFeature(RESOLUTION); - } - - /** - * Sets the instance of the resolution feature. - * - * @param resolution the {@link ResolutionFeature} instance to set. - */ - public void setResolution(ResolutionFeature resolution) { - this.featureMap.put(RESOLUTION, resolution); - } - - /** - * Gets the sensor orientation feature if it has been set. - * - * @return the sensor orientation feature. - */ - public SensorOrientationFeature getSensorOrientation() { - return getFeature(SENSOR_ORIENTATION); - } - - /** - * Sets the instance of the sensor orientation feature. - * - * @param sensorOrientation the {@link SensorOrientationFeature} instance to set. - */ - public void setSensorOrientation(SensorOrientationFeature sensorOrientation) { - this.featureMap.put(SENSOR_ORIENTATION, sensorOrientation); - } - - /** - * Gets the zoom level feature if it has been set. - * - * @return the zoom level feature. - */ - public ZoomLevelFeature getZoomLevel() { - return getFeature(ZOOM_LEVEL); - } - - /** - * Sets the instance of the zoom level feature. - * - * @param zoomLevel the {@link ZoomLevelFeature} instance to set. - */ - public void setZoomLevel(ZoomLevelFeature zoomLevel) { - this.featureMap.put(ZOOM_LEVEL, zoomLevel); - } + private static final String AUTO_FOCUS = "AUTO_FOCUS"; + private static final String EXPOSURE_LOCK = "EXPOSURE_LOCK"; + private static final String EXPOSURE_OFFSET = "EXPOSURE_OFFSET"; + private static final String EXPOSURE_POINT = "EXPOSURE_POINT"; + private static final String FLASH = "FLASH"; + private static final String FOCUS_POINT = "FOCUS_POINT"; + private static final String FPS_RANGE = "FPS_RANGE"; + private static final String NOISE_REDUCTION = "NOISE_REDUCTION"; + private static final String REGION_BOUNDARIES = "REGION_BOUNDARIES"; + private static final String RESOLUTION = "RESOLUTION"; + private static final String SENSOR_ORIENTATION = "SENSOR_ORIENTATION"; + private static final String ZOOM_LEVEL = "ZOOM_LEVEL"; + + private Map featureMap = new HashMap<>(); + + /** + * Gets a collection of all features that have been set. + * + * @return A collection of all features that have been set. + */ + public Collection getAllFeatures() { + return this.featureMap.values(); + } + + /** + * Gets the auto focus feature if it has been set. + * + * @return the auto focus feature. + */ + public AutoFocusFeature getAutoFocus() { + return (AutoFocusFeature) featureMap.get(AUTO_FOCUS); + } + + /** + * Sets the instance of the auto focus feature. + * + * @param autoFocus the {@link AutoFocusFeature} instance to set. + */ + public void setAutoFocus(AutoFocusFeature autoFocus) { + this.featureMap.put(AUTO_FOCUS, autoFocus); + } + + /** + * Gets the exposure lock feature if it has been set. + * + * @return the exposure lock feature. + */ + public ExposureLockFeature getExposureLock() { + return (ExposureLockFeature) featureMap.get(EXPOSURE_LOCK); + } + + /** + * Sets the instance of the exposure lock feature. + * + * @param exposureLock the {@link ExposureLockFeature} instance to set. + */ + public void setExposureLock(ExposureLockFeature exposureLock) { + this.featureMap.put(EXPOSURE_LOCK, exposureLock); + } + + /** + * Gets the exposure offset feature if it has been set. + * + * @return the exposure offset feature. + */ + public ExposureOffsetFeature getExposureOffset() { + return (ExposureOffsetFeature) featureMap.get(EXPOSURE_OFFSET); + } + + /** + * Sets the instance of the exposure offset feature. + * + * @param exposureOffset the {@link ExposureOffsetFeature} instance to set. + */ + public void setExposureOffset(ExposureOffsetFeature exposureOffset) { + this.featureMap.put(EXPOSURE_OFFSET, exposureOffset); + } + + /** + * Gets the exposure point feature if it has been set. + * + * @return the exposure point feature. + */ + public ExposurePointFeature getExposurePoint() { + return (ExposurePointFeature) featureMap.get(EXPOSURE_POINT); + } + + /** + * Sets the instance of the exposure point feature. + * + * @param exposurePoint the {@link ExposurePointFeature} instance to set. + */ + public void setExposurePoint(ExposurePointFeature exposurePoint) { + this.featureMap.put(EXPOSURE_POINT, exposurePoint); + } + + /** + * Gets the flash feature if it has been set. + * + * @return the flash feature. + */ + public FlashFeature getFlash() { + return (FlashFeature) featureMap.get(FLASH); + } + + /** + * Sets the instance of the flash feature. + * + * @param flash the {@link FlashFeature} instance to set. + */ + public void setFlash(FlashFeature flash) { + this.featureMap.put(FLASH, flash); + } + + /** + * Gets the focus point feature if it has been set. + * + * @return the focus point feature. + */ + public FocusPointFeature getFocusPoint() { + return (FocusPointFeature) featureMap.get(FOCUS_POINT); + } + + /** + * Sets the instance of the focus point feature. + * + * @param focusPoint the {@link FocusPointFeature} instance to set. + */ + public void setFocusPoint(FocusPointFeature focusPoint) { + this.featureMap.put(FOCUS_POINT, focusPoint); + } + + /** + * Gets the fps range feature if it has been set. + * + * @return the fps range feature. + */ + public FpsRangeFeature getFpsRange() { + return (FpsRangeFeature) featureMap.get(FPS_RANGE); + } + + /** + * Sets the instance of the fps range feature. + * + * @param fpsRange the {@link FpsRangeFeature} instance to set. + */ + public void setFpsRange(FpsRangeFeature fpsRange) { + this.featureMap.put(FPS_RANGE, fpsRange); + } + + /** + * Gets the noise reduction feature if it has been set. + * + * @return the noise reduction feature. + */ + public NoiseReductionFeature getNoiseReduction() { + return (NoiseReductionFeature) featureMap.get(NOISE_REDUCTION); + } + + /** + * Sets the instance of the noise reduction feature. + * + * @param noiseReduction the {@link NoiseReductionFeature} instance to set. + */ + public void setNoiseReduction(NoiseReductionFeature noiseReduction) { + this.featureMap.put(NOISE_REDUCTION, noiseReduction); + } + + /** + * Gets the resolution feature if it has been set. + * + * @return the resolution feature. + */ + public ResolutionFeature getResolution() { + return (ResolutionFeature) featureMap.get(RESOLUTION); + } + + /** + * Sets the instance of the resolution feature. + * + * @param resolution the {@link ResolutionFeature} instance to set. + */ + public void setResolution(ResolutionFeature resolution) { + this.featureMap.put(RESOLUTION, resolution); + } + + /** + * Gets the sensor orientation feature if it has been set. + * + * @return the sensor orientation feature. + */ + public SensorOrientationFeature getSensorOrientation() { + return (SensorOrientationFeature) featureMap.get(SENSOR_ORIENTATION); + } + + /** + * Sets the instance of the sensor orientation feature. + * + * @param sensorOrientation the {@link SensorOrientationFeature} instance to set. + */ + public void setSensorOrientation(SensorOrientationFeature sensorOrientation) { + this.featureMap.put(SENSOR_ORIENTATION, sensorOrientation); + } + + /** + * Gets the zoom level feature if it has been set. + * + * @return the zoom level feature. + */ + public ZoomLevelFeature getZoomLevel() { + return (ZoomLevelFeature) featureMap.get(ZOOM_LEVEL); + } + + /** + * Sets the instance of the zoom level feature. + * + * @param zoomLevel the {@link ZoomLevelFeature} instance to set. + */ + public void setZoomLevel(ZoomLevelFeature zoomLevel) { + this.featureMap.put(ZOOM_LEVEL, zoomLevel); + } } 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..590d8720ebe8 --- /dev/null +++ b/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/CameraTest.java @@ -0,0 +1,380 @@ +// Copyright 2013 The Flutter 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.eq; +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.app.Activity; +import android.hardware.camera2.CameraAccessException; +import android.media.CamcorderProfile; + +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.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.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.ZoomLevelFeature; +import io.flutter.view.TextureRegistry; + +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_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 String cameraName = "1"; + final ResolutionPreset resolutionPreset = ResolutionPreset.high; + final boolean enableAudio = false; + + when(mockCameraProperties.getCameraName()).thenReturn(cameraName); + + Camera camera = + 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)) + .createExposurePointFeature(eq(mockCameraProperties)); + verify(mockCameraFeatureFactory, times(1)).createExposureOffsetFeature(mockCameraProperties); + verify(mockCameraFeatureFactory, times(1)).createFlashFeature(mockCameraProperties); + verify(mockCameraFeatureFactory, times(1)) + .createFocusPointFeature(eq(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); + assertNotNull("should create a camera", camera); + } + + @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; + + when(mockExposureOffsetFeature.getMaxExposureOffset()).thenReturn(expectedMaxOffset); + + double actualMaxOffset = camera.getMaxExposureOffset(); + + verify(mockExposureOffsetFeature, times(1)).getMaxExposureOffset(); + assertEquals(expectedMaxOffset, actualMaxOffset, 0); + } + + @Test + public void getMinExposureOffset() { + ExposureOffsetFeature mockExposureOffsetFeature = + mockCameraFeatureFactory.createExposureOffsetFeature(mockCameraProperties); + double expectedMinOffset = 21.5; + + when(mockExposureOffsetFeature.getMinExposureOffset()).thenReturn(21.5); + + double actualMinOffset = camera.getMinExposureOffset(); + + verify(mockExposureOffsetFeature, times(1)).getMinExposureOffset(); + assertEquals(expectedMinOffset, actualMinOffset, 0); + } + + @Test + public void getMaxZoomLevel() { + ZoomLevelFeature mockZoomLevelFeature = + mockCameraFeatureFactory.createZoomLevelFeature(mockCameraProperties); + float expectedMaxZoomLevel = 4.2f; + + when(mockZoomLevelFeature.getMaximumZoomLevel()).thenReturn(expectedMaxZoomLevel); + + float actualMaxZoomLevel = camera.getMaxZoomLevel(); + + verify(mockZoomLevelFeature, times(1)).getMaximumZoomLevel(); + assertEquals(expectedMaxZoomLevel, actualMaxZoomLevel, 0); + } + + @Test + public void getMinZoomLevel() { + ZoomLevelFeature mockZoomLevelFeature = + mockCameraFeatureFactory.createZoomLevelFeature(mockCameraProperties); + float expectedMinZoomLevel = 4.2f; + + when(mockZoomLevelFeature.getMinimumZoomLevel()).thenReturn(expectedMinZoomLevel); + + float actualMinZoomLevel = camera.getMinZoomLevel(); + + verify(mockZoomLevelFeature, times(1)).getMinimumZoomLevel(); + 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); + 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); + 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); + float zoomLevel = 1.0f; + + when(mockZoomLevelFeature.getValue()).thenReturn(zoomLevel); + when(mockZoomLevelFeature.getMinimumZoomLevel()).thenReturn(0f); + when(mockZoomLevelFeature.getMaximumZoomLevel()).thenReturn(2f); + + 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 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.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) { + 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 ExposurePointFeature createExposurePointFeature( + @NonNull CameraProperties cameraProperties + ) { + return mockExposurePointFeature; + } + + @Override + public NoiseReductionFeature createNoiseReductionFeature( + @NonNull CameraProperties cameraProperties) { + return mockNoiseReductionFeature; + } + } +} \ No newline at end of file diff --git a/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/CameraUtilsTest.java b/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/CameraUtilsTest.java index b97192b889cf..04199c1f767b 100644 --- a/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/CameraUtilsTest.java +++ b/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/CameraUtilsTest.java @@ -52,51 +52,4 @@ public void deserializeDeviceOrientation_deserializes_correctly() { public void deserializeDeviceOrientation_throws_for_null() { CameraUtils.deserializeDeviceOrientation(null); } - - @Test - public void getDeviceOrientationFromDegrees_converts_correctly() { - // Portrait UP - assertEquals( - PlatformChannel.DeviceOrientation.PORTRAIT_UP, - CameraUtils.getDeviceOrientationFromDegrees(0)); - assertEquals( - PlatformChannel.DeviceOrientation.PORTRAIT_UP, - CameraUtils.getDeviceOrientationFromDegrees(315)); - assertEquals( - PlatformChannel.DeviceOrientation.PORTRAIT_UP, - CameraUtils.getDeviceOrientationFromDegrees(44)); - assertEquals( - PlatformChannel.DeviceOrientation.PORTRAIT_UP, - CameraUtils.getDeviceOrientationFromDegrees(-45)); - // Portrait DOWN - assertEquals( - PlatformChannel.DeviceOrientation.PORTRAIT_DOWN, - CameraUtils.getDeviceOrientationFromDegrees(180)); - assertEquals( - PlatformChannel.DeviceOrientation.PORTRAIT_DOWN, - CameraUtils.getDeviceOrientationFromDegrees(135)); - assertEquals( - PlatformChannel.DeviceOrientation.PORTRAIT_DOWN, - CameraUtils.getDeviceOrientationFromDegrees(224)); - // Landscape LEFT - assertEquals( - PlatformChannel.DeviceOrientation.LANDSCAPE_LEFT, - CameraUtils.getDeviceOrientationFromDegrees(90)); - assertEquals( - PlatformChannel.DeviceOrientation.LANDSCAPE_LEFT, - CameraUtils.getDeviceOrientationFromDegrees(45)); - assertEquals( - PlatformChannel.DeviceOrientation.LANDSCAPE_LEFT, - CameraUtils.getDeviceOrientationFromDegrees(134)); - // Landscape RIGHT - assertEquals( - PlatformChannel.DeviceOrientation.LANDSCAPE_RIGHT, - CameraUtils.getDeviceOrientationFromDegrees(270)); - assertEquals( - PlatformChannel.DeviceOrientation.LANDSCAPE_RIGHT, - CameraUtils.getDeviceOrientationFromDegrees(225)); - assertEquals( - PlatformChannel.DeviceOrientation.LANDSCAPE_RIGHT, - CameraUtils.getDeviceOrientationFromDegrees(314)); - } } 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 25f5df9e9db9..5ba6952d4cd7 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 @@ -16,8 +16,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.exposurelock.ExposureMode; +import io.flutter.plugins.camera.features.autofocus.FocusMode; 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/features/resolution/ResolutionFeatureTest.java b/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/features/resolution/ResolutionFeatureTest.java index 716219246a27..bb9cb61e1508 100644 --- 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 @@ -17,7 +17,7 @@ import org.mockito.MockedStatic; public class ResolutionFeatureTest { - private static final int cameraId = 1; + private static final String cameraName = "1"; private CamcorderProfile mockProfileLow; private MockedStatic mockedStaticProfile; @@ -82,7 +82,7 @@ public void after() { public void getDebugName_should_return_the_name_of_the_feature() { CameraProperties mockCameraProperties = mock(CameraProperties.class); ResolutionFeature resolutionFeature = - new ResolutionFeature(mockCameraProperties, ResolutionPreset.max, cameraId); + new ResolutionFeature(mockCameraProperties, ResolutionPreset.max, cameraName); assertEquals("ResolutionFeature", resolutionFeature.getDebugName()); } @@ -91,7 +91,7 @@ public void getDebugName_should_return_the_name_of_the_feature() { public void getValue_should_return_initial_value_when_not_set() { CameraProperties mockCameraProperties = mock(CameraProperties.class); ResolutionFeature resolutionFeature = - new ResolutionFeature(mockCameraProperties, ResolutionPreset.max, cameraId); + new ResolutionFeature(mockCameraProperties, ResolutionPreset.max, cameraName); assertEquals(ResolutionPreset.max, resolutionFeature.getValue()); } @@ -100,7 +100,7 @@ public void getValue_should_return_initial_value_when_not_set() { public void getValue_should_echo_setValue() { CameraProperties mockCameraProperties = mock(CameraProperties.class); ResolutionFeature resolutionFeature = - new ResolutionFeature(mockCameraProperties, ResolutionPreset.max, cameraId); + new ResolutionFeature(mockCameraProperties, ResolutionPreset.max, cameraName); resolutionFeature.setValue(ResolutionPreset.high); @@ -111,7 +111,7 @@ public void getValue_should_echo_setValue() { public void checkIsSupport_returns_true() { CameraProperties mockCameraProperties = mock(CameraProperties.class); ResolutionFeature resolutionFeature = - new ResolutionFeature(mockCameraProperties, ResolutionPreset.max, cameraId); + new ResolutionFeature(mockCameraProperties, ResolutionPreset.max, cameraName); assertTrue(resolutionFeature.checkIsSupported()); } From 7f0180e7a389340a0a857df63d589115eb5d5461 Mon Sep 17 00:00:00 2001 From: Maurits van Beusekom Date: Wed, 16 Jun 2021 17:06:09 +0200 Subject: [PATCH 44/63] Accept cameraName as String --- .../resolution/ResolutionFeature.java | 25 +++++++++++++------ .../resolution/ResolutionFeatureTest.java | 12 ++++----- 2 files changed, 24 insertions(+), 13 deletions(-) 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 a5469c63359b..46239f372de7 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 @@ -30,14 +30,18 @@ public class ResolutionFeature extends CameraFeature { * * @param cameraProperties Collection of characteristics for the current camera device. * @param resolutionPreset Platform agnostic enum containing resolution information. - * @param cameraId Camera identifier of the camera for which to configure the resolution. + * @param cameraName Camera identifier of the camera for which to configure the resolution. */ public ResolutionFeature( - CameraProperties cameraProperties, ResolutionPreset resolutionPreset, int cameraId) { + CameraProperties cameraProperties, ResolutionPreset resolutionPreset, String cameraName) { super(cameraProperties); this.currentSetting = resolutionPreset; - this.cameraId = cameraId; - + try { + this.cameraId = Integer.parseInt(cameraName, 10); + } catch (NumberFormatException e) { + this.cameraId = -1; + return; + } configureResolution(resolutionPreset, cameraId); } @@ -54,9 +58,13 @@ public ResolutionFeature( */ public static CamcorderProfile getBestAvailableCamcorderProfileForResolutionPreset( int cameraId, ResolutionPreset preset) { + if (cameraId < 0) { + throw new AssertionError( + "getBestAvailableCamcorderProfileForResolutionPreset can only be used with valid (>=0) camera identifiers."); + } switch (preset) { - // All of these cases deliberately fall through to get the best available profile. + // 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); @@ -103,6 +111,9 @@ static Size computeBestPreviewSize(int cameraId, ResolutionPreset preset) { } private void configureResolution(ResolutionPreset resolutionPreset, int cameraId) { + if (!checkIsSupported()) { + return; + } recordingProfile = getBestAvailableCamcorderProfileForResolutionPreset(cameraId, resolutionPreset); captureSize = new Size(recordingProfile.videoFrameWidth, recordingProfile.videoFrameHeight); @@ -128,7 +139,7 @@ public void setValue(ResolutionPreset value) { // Always supported @Override public boolean checkIsSupported() { - return true; + return cameraId >= 0; } @Override @@ -163,4 +174,4 @@ public Size getPreviewSize() { public Size getCaptureSize() { return this.captureSize; } -} +} \ No newline at end of file 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 index 716219246a27..a18b3b4a1f9b 100644 --- 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 @@ -17,7 +17,7 @@ import org.mockito.MockedStatic; public class ResolutionFeatureTest { - private static final int cameraId = 1; + private static final String cameraName = "1"; private CamcorderProfile mockProfileLow; private MockedStatic mockedStaticProfile; @@ -82,7 +82,7 @@ public void after() { public void getDebugName_should_return_the_name_of_the_feature() { CameraProperties mockCameraProperties = mock(CameraProperties.class); ResolutionFeature resolutionFeature = - new ResolutionFeature(mockCameraProperties, ResolutionPreset.max, cameraId); + new ResolutionFeature(mockCameraProperties, ResolutionPreset.max, cameraName); assertEquals("ResolutionFeature", resolutionFeature.getDebugName()); } @@ -91,7 +91,7 @@ public void getDebugName_should_return_the_name_of_the_feature() { public void getValue_should_return_initial_value_when_not_set() { CameraProperties mockCameraProperties = mock(CameraProperties.class); ResolutionFeature resolutionFeature = - new ResolutionFeature(mockCameraProperties, ResolutionPreset.max, cameraId); + new ResolutionFeature(mockCameraProperties, ResolutionPreset.max, cameraName); assertEquals(ResolutionPreset.max, resolutionFeature.getValue()); } @@ -100,7 +100,7 @@ public void getValue_should_return_initial_value_when_not_set() { public void getValue_should_echo_setValue() { CameraProperties mockCameraProperties = mock(CameraProperties.class); ResolutionFeature resolutionFeature = - new ResolutionFeature(mockCameraProperties, ResolutionPreset.max, cameraId); + new ResolutionFeature(mockCameraProperties, ResolutionPreset.max, cameraName); resolutionFeature.setValue(ResolutionPreset.high); @@ -111,7 +111,7 @@ public void getValue_should_echo_setValue() { public void checkIsSupport_returns_true() { CameraProperties mockCameraProperties = mock(CameraProperties.class); ResolutionFeature resolutionFeature = - new ResolutionFeature(mockCameraProperties, ResolutionPreset.max, cameraId); + new ResolutionFeature(mockCameraProperties, ResolutionPreset.max, cameraName); assertTrue(resolutionFeature.checkIsSupported()); } @@ -187,4 +187,4 @@ public void computeBestPreviewSize_should_use_QVGA_when_resolution_preset_low() mockedStaticProfile.verify(() -> CamcorderProfile.get(1, CamcorderProfile.QUALITY_QVGA)); } -} +} \ No newline at end of file From 24af3676d7ff62f61fde5d382a268e976e43b3c4 Mon Sep 17 00:00:00 2001 From: Maurits van Beusekom Date: Wed, 16 Jun 2021 17:06:52 +0200 Subject: [PATCH 45/63] Format --- .../plugins/camera/features/resolution/ResolutionFeature.java | 4 ++-- .../camera/features/resolution/ResolutionFeatureTest.java | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) 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 46239f372de7..d5432da210da 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 @@ -64,7 +64,7 @@ public static CamcorderProfile getBestAvailableCamcorderProfileForResolutionPres } switch (preset) { - // All of these cases deliberately fall through to get the best available profile. + // 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); @@ -174,4 +174,4 @@ public Size getPreviewSize() { public Size getCaptureSize() { return this.captureSize; } -} \ No newline at end of file +} 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 index a18b3b4a1f9b..bb9cb61e1508 100644 --- 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 @@ -187,4 +187,4 @@ public void computeBestPreviewSize_should_use_QVGA_when_resolution_preset_low() mockedStaticProfile.verify(() -> CamcorderProfile.get(1, CamcorderProfile.QUALITY_QVGA)); } -} \ No newline at end of file +} From a98771106f201d3b152138b5e17d836bcde4bcc8 Mon Sep 17 00:00:00 2001 From: BeMacized Date: Wed, 16 Jun 2021 17:09:23 +0200 Subject: [PATCH 46/63] Changes required for integrating main camera class --- .../plugins/camera/CameraCaptureCallback.java | 129 ++++----- .../plugins/camera/CameraProperties.java | 13 +- .../flutter/plugins/camera/CameraUtils.java | 126 +++----- .../io/flutter/plugins/camera/ImageSaver.java | 9 +- .../camera/features/CameraFeatureFactory.java | 20 +- .../features/CameraFeatureFactoryImpl.java | 133 ++++----- .../camera/features/CameraFeatures.java | 268 ++++++++++++++++-- .../camera/types/CaptureTimeoutsWrapper.java | 20 +- .../plugins/camera/CameraUtilsTest.java | 81 ++---- 9 files changed, 461 insertions(+), 338 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 f614725d2f64..576c8f250587 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 @@ -24,8 +24,8 @@ class CameraCaptureCallback extends CaptureCallback { private final CaptureTimeoutsWrapper captureTimeouts; private CameraCaptureCallback( - @NonNull CameraCaptureStateListener cameraStateListener, - @NonNull CaptureTimeoutsWrapper captureTimeouts) { + @NonNull CameraCaptureStateListener cameraStateListener, + @NonNull CaptureTimeoutsWrapper captureTimeouts) { cameraState = CameraState.STATE_PREVIEW; this.cameraStateListener = cameraStateListener; this.captureTimeouts = captureTimeouts; @@ -36,12 +36,12 @@ private CameraCaptureCallback( * * @param cameraStateListener instance which will be called when the camera state changes. * @param captureTimeouts specifying the different timeout counters that should be taken into - * account. + * account. * @return a configured instance of the {@link CameraCaptureCallback} class. */ public static CameraCaptureCallback create( - @NonNull CameraCaptureStateListener cameraStateListener, - @NonNull CaptureTimeoutsWrapper captureTimeouts) { + @NonNull CameraCaptureStateListener cameraStateListener, + @NonNull CaptureTimeoutsWrapper captureTimeouts) { return new CameraCaptureCallback(cameraStateListener, captureTimeouts); } @@ -69,66 +69,63 @@ private void process(CaptureResult result) { if (cameraState != CameraState.STATE_PREVIEW) { Log.d( - TAG, - "CameraCaptureCallback | state: " - + cameraState - + " | afState: " - + afState - + " | aeState: " - + aeState); + TAG, + "CameraCaptureCallback | state: " + + cameraState + + " | afState: " + + afState + + " | aeState: " + + aeState); } 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 == CaptureResult.CONTROL_AF_STATE_FOCUSED_LOCKED - || afState == CaptureResult.CONTROL_AF_STATE_NOT_FOCUSED_LOCKED) { - handleWaitingFocusState(aeState); - } else if (captureTimeouts.getPreCaptureFocusing().getIsExpired()) { - Log.w(TAG, "Focus timeout, moving on with capture"); - handleWaitingFocusState(aeState); - } - - break; + { + if (afState == null) { + return; + } else if (afState == CaptureResult.CONTROL_AF_STATE_FOCUSED_LOCKED + || afState == CaptureResult.CONTROL_AF_STATE_NOT_FOCUSED_LOCKED) { + handleWaitingFocusState(aeState); + } else if (captureTimeouts.getPreCaptureFocusing().getIsExpired()) { + Log.w(TAG, "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) { - setCameraState(CameraState.STATE_WAITING_PRECAPTURE_DONE); - } else if (captureTimeouts.getPreCaptureMetering().getIsExpired()) { - Log.w( - TAG, - "Metering timeout waiting for pre-capture to start, moving on with capture"); - - setCameraState(CameraState.STATE_WAITING_PRECAPTURE_DONE); - } - break; + { + // 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); + } else if (captureTimeouts.getPreCaptureMetering().getIsExpired()) { + Log.w(TAG, "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) { - cameraStateListener.onConverged(); - } else if (captureTimeouts.getPreCaptureMetering().getIsExpired()) { - Log.w( - TAG, - "Metering timeout waiting for pre-capture to finish, moving on with capture"); - cameraStateListener.onConverged(); - } - - break; + { + // CONTROL_AE_STATE can be null on some devices + if (aeState == null || aeState != CaptureResult.CONTROL_AE_STATE_PRECAPTURE) { + cameraStateListener.onConverged(); + } else if (captureTimeouts.getPreCaptureMetering().getIsExpired()) { + Log.w( + TAG, "Metering timeout waiting for pre-capture to finish, moving on with capture"); + cameraStateListener.onConverged(); } + + break; + } } } @@ -143,28 +140,24 @@ private void handleWaitingFocusState(Integer aeState) { @Override public void onCaptureProgressed( - @NonNull CameraCaptureSession session, - @NonNull CaptureRequest request, - @NonNull CaptureResult partialResult) { + @NonNull CameraCaptureSession session, + @NonNull CaptureRequest request, + @NonNull CaptureResult partialResult) { process(partialResult); } @Override public void onCaptureCompleted( - @NonNull CameraCaptureSession session, - @NonNull CaptureRequest request, - @NonNull TotalCaptureResult result) { + @NonNull CameraCaptureSession session, + @NonNull CaptureRequest request, + @NonNull TotalCaptureResult result) { process(result); } - /** - * An interface that describes the different state changes implementers can be informed about. - */ + /** An interface that describes the different state changes implementers can be informed about. */ interface CameraCaptureStateListener { - /** - * Called when the {@link android.hardware.camera2.CaptureRequest} has been converged. - */ + /** Called when the {@link android.hardware.camera2.CaptureRequest} has been converged. */ void onConverged(); /** @@ -172,4 +165,4 @@ interface CameraCaptureStateListener { */ void onPrecapture(); } -} +} \ No newline at end of file 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 a69ddd0410d4..25d94e0be852 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 @@ -246,14 +246,15 @@ class CameraPropertiesImpl implements CameraProperties { private final String cameraName; public CameraPropertiesImpl(String cameraName, CameraManager cameraManager) - throws CameraAccessException { + throws CameraAccessException { this.cameraName = cameraName; this.cameraCharacteristics = cameraManager.getCameraCharacteristics(cameraName); } @Override public String getCameraName() { - return cameraName; + return "WOOPS"; + // return cameraName; } @Override @@ -269,7 +270,7 @@ public Range getControlAutoExposureCompensationRange() { @Override public double getControlAutoExposureCompensationStep() { Rational rational = - cameraCharacteristics.get(CameraCharacteristics.CONTROL_AE_COMPENSATION_STEP); + cameraCharacteristics.get(CameraCharacteristics.CONTROL_AE_COMPENSATION_STEP); return rational == null ? 0.0 : rational.doubleValue(); } @@ -329,7 +330,7 @@ public Size getSensorInfoPixelArraySize() { @Override public Rect getSensorInfoPreCorrectionActiveArraySize() { return cameraCharacteristics.get( - CameraCharacteristics.SENSOR_INFO_PRE_CORRECTION_ACTIVE_ARRAY_SIZE); + CameraCharacteristics.SENSOR_INFO_PRE_CORRECTION_ACTIVE_ARRAY_SIZE); } @Override @@ -345,6 +346,6 @@ public int getHardwareLevel() { @Override public int[] getAvailableNoiseReductionModes() { return cameraCharacteristics.get( - CameraCharacteristics.NOISE_REDUCTION_AVAILABLE_NOISE_REDUCTION_MODES); + CameraCharacteristics.NOISE_REDUCTION_AVAILABLE_NOISE_REDUCTION_MODES); } -} +} \ No newline at end of file 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 b4d4689f2b4e..0cd20ca3082b 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 @@ -6,20 +6,12 @@ import android.app.Activity; import android.content.Context; -import android.graphics.ImageFormat; import android.hardware.camera2.CameraAccessException; import android.hardware.camera2.CameraCharacteristics; 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; -import java.util.Comparator; import java.util.HashMap; import java.util.List; import java.util.Map; @@ -29,23 +21,24 @@ public final class CameraUtils { private CameraUtils() {} - static PlatformChannel.DeviceOrientation getDeviceOrientationFromDegrees(int degrees) { - // Round to the nearest 90 degrees. - degrees = (int) (Math.round(degrees / 90.0) * 90) % 360; - // Determine the corresponding device orientation. - switch (degrees) { - case 90: - return PlatformChannel.DeviceOrientation.LANDSCAPE_LEFT; - case 180: - return PlatformChannel.DeviceOrientation.PORTRAIT_DOWN; - case 270: - return PlatformChannel.DeviceOrientation.LANDSCAPE_RIGHT; - case 0: - default: - return PlatformChannel.DeviceOrientation.PORTRAIT_UP; - } + /** + * Gets the {@link CameraManager} singleton. + * + * @param context The context to get the {@link CameraManager} singleton from. + * @return The {@link CameraManager} singleton. + */ + static CameraManager getCameraManager(Context context) { + return (CameraManager) context.getSystemService(Context.CAMERA_SERVICE); } + /** + * Serializes the {@link PlatformChannel.DeviceOrientation} to a string value. + * + * @param orientation The orientation to serialize. + * @return The serialized orientation. + * @throws UnsupportedOperationException when the provided orientation not have a corresponding + * string value. + */ static String serializeDeviceOrientation(PlatformChannel.DeviceOrientation orientation) { if (orientation == null) throw new UnsupportedOperationException("Could not serialize null device orientation."); @@ -60,10 +53,19 @@ static String serializeDeviceOrientation(PlatformChannel.DeviceOrientation orien return "landscapeRight"; default: throw new UnsupportedOperationException( - "Could not serialize device orientation: " + orientation.toString()); + "Could not serialize device orientation: " + orientation.toString()); } } + /** + * Deserializes a string value to its corresponding {@link PlatformChannel.DeviceOrientation} + * value. + * + * @param orientation The string value to deserialize. + * @return The deserialized orientation. + * @throws UnsupportedOperationException when the provided string value does not have a + * corresponding {@link PlatformChannel.DeviceOrientation}. + */ static PlatformChannel.DeviceOrientation deserializeDeviceOrientation(String orientation) { if (orientation == null) throw new UnsupportedOperationException("Could not deserialize null device orientation."); @@ -78,29 +80,19 @@ static PlatformChannel.DeviceOrientation deserializeDeviceOrientation(String ori return PlatformChannel.DeviceOrientation.LANDSCAPE_RIGHT; default: throw new UnsupportedOperationException( - "Could not deserialize device orientation: " + orientation); + "Could not deserialize device orientation: " + orientation); } } - 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( - Arrays.asList(streamConfigurationMap.getOutputSizes(ImageFormat.JPEG)), - new CompareSizesByArea()); - } - + /** + * Gets all the available cameras for the device. + * + * @param activity The current Android activity. + * @return A map of all the available cameras, with their name as their key. + * @throws CameraAccessException when the camera could not be accessed. + */ public static List> getAvailableCameras(Activity activity) - throws CameraAccessException { + throws CameraAccessException { CameraManager cameraManager = (CameraManager) activity.getSystemService(Context.CAMERA_SERVICE); String[] cameraNames = cameraManager.getCameraIdList(); List> cameras = new ArrayList<>(); @@ -127,52 +119,4 @@ 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) { - // We cast here to ensure the multiplications won't overflow. - return Long.signum( - (long) lhs.getWidth() * lhs.getHeight() - (long) rhs.getWidth() * rhs.getHeight()); - } - } } 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 163a51763517..d538ce85b7c3 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 @@ -27,6 +27,7 @@ public class ImageSaver implements Runnable { /** * Creates an instance of the ImageSaver runnable + * * @param image - The image to save * @param file - The file to save the image to * @param callback - The callback that is run on completion, or when an error is encountered. @@ -64,18 +65,20 @@ public void run() { } /** - * The interface for the callback that is passed to ImageSaver, - * for detecting completion or failure of the image saving task. + * The interface for the callback that is passed to ImageSaver, for detecting completion or + * failure of the image saving task. */ public interface Callback { /** * Called when the image file has been saved successfully. + * * @param absolutePath - The absolute path of the file that was saved. */ void onComplete(String absolutePath); /** * Called when an error is encountered while saving the image file. + * * @param errorCode - The error code. * @param errorMessage - The human readable error message. */ @@ -99,4 +102,4 @@ public static FileOutputStream create(File file) throws FileNotFoundException { return new FileOutputStream(file); } } -} +} \ No newline at end of file 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 2fa47255ac56..46786fb0fdbf 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 @@ -22,8 +22,8 @@ import io.flutter.plugins.camera.features.zoomlevel.ZoomLevelFeature; /** - * Factory for creating the supported feature implementation controlling different aspects - * of the {@link android.hardware.camera2.CaptureRequest}. + * Factory for creating the supported feature implementation controlling different aspects of the + * {@link android.hardware.camera2.CaptureRequest}. */ public interface CameraFeatureFactory { @@ -36,7 +36,7 @@ public interface CameraFeatureFactory { * @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. @@ -71,11 +71,13 @@ AutoFocusFeature createAutoFocusFeature( * @param cameraProperties instance of the CameraProperties class containing information about the * cameras features. * @param initialSetting initial resolution preset. - * @param cameraId - the id of the camera which can be used to identify the camera device. + * @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, int cameraId); + @NonNull CameraProperties cameraProperties, + ResolutionPreset initialSetting, + String cameraName); /** * Creates a new instance of the focus point feature. @@ -106,9 +108,9 @@ ResolutionFeature createResolutionFeature( * @return newly created instance of the SensorOrientationFeature class. */ SensorOrientationFeature createSensorOrientationFeature( - @NonNull CameraProperties cameraProperties, - @NonNull Activity activity, - @NonNull DartMessenger dartMessenger); + @NonNull CameraProperties cameraProperties, + @NonNull Activity activity, + @NonNull DartMessenger dartMessenger); /** * Creates a new instance of the zoom level feature. @@ -136,4 +138,4 @@ SensorOrientationFeature createSensorOrientationFeature( * @return newly created instance of the NoiseReductionFeature class. */ NoiseReductionFeature createNoiseReductionFeature(@NonNull CameraProperties cameraProperties); -} +} \ No newline at end of file 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 d9caafed3c3f..c59d71ef4fa3 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 @@ -5,7 +5,9 @@ package io.flutter.plugins.camera.features; import android.app.Activity; + import androidx.annotation.NonNull; + import io.flutter.plugins.camera.CameraProperties; import io.flutter.plugins.camera.DartMessenger; import io.flutter.plugins.camera.features.autofocus.AutoFocusFeature; @@ -23,70 +25,73 @@ /** * Implementation of the {@link CameraFeatureFactory} interface creating the supported feature - * implementation controlling different aspects of the {@link android.hardware.camera2.CaptureRequest}. + * implementation controlling different aspects of the {@link + * android.hardware.camera2.CaptureRequest}. */ 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, int cameraId) { - return new ResolutionFeature(cameraProperties, initialSetting, cameraId); - } - - @Override - public FocusPointFeature createFocusPointFeature(@NonNull CameraProperties cameraProperties) { - return new FocusPointFeature(cameraProperties); - } - - @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 ExposurePointFeature createExposurePointFeature( - @NonNull CameraProperties cameraProperties) { - return new ExposurePointFeature(cameraProperties); - } - - @Override - public NoiseReductionFeature createNoiseReductionFeature( - @NonNull CameraProperties cameraProperties) { - return new NoiseReductionFeature(cameraProperties); - } -} + @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) { + return new FocusPointFeature(cameraProperties); + } + + @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 ExposurePointFeature createExposurePointFeature( + @NonNull CameraProperties cameraProperties) { + return new ExposurePointFeature(cameraProperties); + } + + @Override + public NoiseReductionFeature createNoiseReductionFeature( + @NonNull CameraProperties cameraProperties) { + return new NoiseReductionFeature(cameraProperties); + } +} \ No newline at end of file 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 d96f486e5b38..268db891b47b 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 @@ -4,32 +4,246 @@ package io.flutter.plugins.camera.features; +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.resolution.ResolutionFeature; +import io.flutter.plugins.camera.features.sensororientation.SensorOrientationFeature; +import io.flutter.plugins.camera.features.zoomlevel.ZoomLevelFeature; + +import java.util.Collection; +import java.util.HashMap; +import java.util.Map; + /** - * 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. + * These are all of our available features in the camera. Used in the Camera to access all features + * in a simpler way. */ -public enum CameraFeatures { - 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; - - CameraFeatures(String strValue) { - this.strValue = strValue; - } - - @Override - public String toString() { - return strValue; - } -} +public class CameraFeatures { + private static final String AUTO_FOCUS = "AUTO_FOCUS"; + private static final String EXPOSURE_LOCK = "EXPOSURE_LOCK"; + private static final String EXPOSURE_OFFSET = "EXPOSURE_OFFSET"; + private static final String EXPOSURE_POINT = "EXPOSURE_POINT"; + private static final String FLASH = "FLASH"; + private static final String FOCUS_POINT = "FOCUS_POINT"; + private static final String FPS_RANGE = "FPS_RANGE"; + private static final String NOISE_REDUCTION = "NOISE_REDUCTION"; + private static final String REGION_BOUNDARIES = "REGION_BOUNDARIES"; + private static final String RESOLUTION = "RESOLUTION"; + private static final String SENSOR_ORIENTATION = "SENSOR_ORIENTATION"; + private static final String ZOOM_LEVEL = "ZOOM_LEVEL"; + + private Map featureMap = new HashMap<>(); + + /** + * Gets a collection of all features that have been set. + * + * @return A collection of all features that have been set. + */ + public Collection getAllFeatures() { + return this.featureMap.values(); + } + + /** + * Gets the auto focus feature if it has been set. + * + * @return the auto focus feature. + */ + public AutoFocusFeature getAutoFocus() { + return (AutoFocusFeature) featureMap.get(AUTO_FOCUS); + } + + /** + * Sets the instance of the auto focus feature. + * + * @param autoFocus the {@link AutoFocusFeature} instance to set. + */ + public void setAutoFocus(AutoFocusFeature autoFocus) { + this.featureMap.put(AUTO_FOCUS, autoFocus); + } + + /** + * Gets the exposure lock feature if it has been set. + * + * @return the exposure lock feature. + */ + public ExposureLockFeature getExposureLock() { + return (ExposureLockFeature) featureMap.get(EXPOSURE_LOCK); + } + + /** + * Sets the instance of the exposure lock feature. + * + * @param exposureLock the {@link ExposureLockFeature} instance to set. + */ + public void setExposureLock(ExposureLockFeature exposureLock) { + this.featureMap.put(EXPOSURE_LOCK, exposureLock); + } + + /** + * Gets the exposure offset feature if it has been set. + * + * @return the exposure offset feature. + */ + public ExposureOffsetFeature getExposureOffset() { + return (ExposureOffsetFeature) featureMap.get(EXPOSURE_OFFSET); + } + + /** + * Sets the instance of the exposure offset feature. + * + * @param exposureOffset the {@link ExposureOffsetFeature} instance to set. + */ + public void setExposureOffset(ExposureOffsetFeature exposureOffset) { + this.featureMap.put(EXPOSURE_OFFSET, exposureOffset); + } + + /** + * Gets the exposure point feature if it has been set. + * + * @return the exposure point feature. + */ + public ExposurePointFeature getExposurePoint() { + return (ExposurePointFeature) featureMap.get(EXPOSURE_POINT); + } + + /** + * Sets the instance of the exposure point feature. + * + * @param exposurePoint the {@link ExposurePointFeature} instance to set. + */ + public void setExposurePoint(ExposurePointFeature exposurePoint) { + this.featureMap.put(EXPOSURE_POINT, exposurePoint); + } + + /** + * Gets the flash feature if it has been set. + * + * @return the flash feature. + */ + public FlashFeature getFlash() { + return (FlashFeature) featureMap.get(FLASH); + } + + /** + * Sets the instance of the flash feature. + * + * @param flash the {@link FlashFeature} instance to set. + */ + public void setFlash(FlashFeature flash) { + this.featureMap.put(FLASH, flash); + } + + /** + * Gets the focus point feature if it has been set. + * + * @return the focus point feature. + */ + public FocusPointFeature getFocusPoint() { + return (FocusPointFeature) featureMap.get(FOCUS_POINT); + } + + /** + * Sets the instance of the focus point feature. + * + * @param focusPoint the {@link FocusPointFeature} instance to set. + */ + public void setFocusPoint(FocusPointFeature focusPoint) { + this.featureMap.put(FOCUS_POINT, focusPoint); + } + + /** + * Gets the fps range feature if it has been set. + * + * @return the fps range feature. + */ + public FpsRangeFeature getFpsRange() { + return (FpsRangeFeature) featureMap.get(FPS_RANGE); + } + + /** + * Sets the instance of the fps range feature. + * + * @param fpsRange the {@link FpsRangeFeature} instance to set. + */ + public void setFpsRange(FpsRangeFeature fpsRange) { + this.featureMap.put(FPS_RANGE, fpsRange); + } + + /** + * Gets the noise reduction feature if it has been set. + * + * @return the noise reduction feature. + */ + public NoiseReductionFeature getNoiseReduction() { + return (NoiseReductionFeature) featureMap.get(NOISE_REDUCTION); + } + + /** + * Sets the instance of the noise reduction feature. + * + * @param noiseReduction the {@link NoiseReductionFeature} instance to set. + */ + public void setNoiseReduction(NoiseReductionFeature noiseReduction) { + this.featureMap.put(NOISE_REDUCTION, noiseReduction); + } + + /** + * Gets the resolution feature if it has been set. + * + * @return the resolution feature. + */ + public ResolutionFeature getResolution() { + return (ResolutionFeature) featureMap.get(RESOLUTION); + } + + /** + * Sets the instance of the resolution feature. + * + * @param resolution the {@link ResolutionFeature} instance to set. + */ + public void setResolution(ResolutionFeature resolution) { + this.featureMap.put(RESOLUTION, resolution); + } + + /** + * Gets the sensor orientation feature if it has been set. + * + * @return the sensor orientation feature. + */ + public SensorOrientationFeature getSensorOrientation() { + return (SensorOrientationFeature) featureMap.get(SENSOR_ORIENTATION); + } + + /** + * Sets the instance of the sensor orientation feature. + * + * @param sensorOrientation the {@link SensorOrientationFeature} instance to set. + */ + public void setSensorOrientation(SensorOrientationFeature sensorOrientation) { + this.featureMap.put(SENSOR_ORIENTATION, sensorOrientation); + } + + /** + * Gets the zoom level feature if it has been set. + * + * @return the zoom level feature. + */ + public ZoomLevelFeature getZoomLevel() { + return (ZoomLevelFeature) featureMap.get(ZOOM_LEVEL); + } + + /** + * Sets the instance of the zoom level feature. + * + * @param zoomLevel the {@link ZoomLevelFeature} instance to set. + */ + public void setZoomLevel(ZoomLevelFeature zoomLevel) { + this.featureMap.put(ZOOM_LEVEL, zoomLevel); + } +} \ No newline at end of file diff --git a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/types/CaptureTimeoutsWrapper.java b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/types/CaptureTimeoutsWrapper.java index f162dd2759fa..8128f0e6317b 100644 --- a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/types/CaptureTimeoutsWrapper.java +++ b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/types/CaptureTimeoutsWrapper.java @@ -5,12 +5,14 @@ package io.flutter.plugins.camera.types; /** - * Wrapper class that provides a container for all {@link Timeout} instances - * that are required for the capture flow. + * Wrapper class that provides a container for all {@link Timeout} instances that are required for + * the capture flow. */ public class CaptureTimeoutsWrapper { - private final Timeout preCaptureFocusing; - private final Timeout preCaptureMetering; + private Timeout preCaptureFocusing; + private Timeout preCaptureMetering; + private final long preCaptureFocusingTimeoutMs; + private final long preCaptureMeteringTimeoutMs; /** * Create a new wrapper instance with the specified timeout values. @@ -19,7 +21,13 @@ public class CaptureTimeoutsWrapper { * @param preCaptureMeteringTimeoutMs metering timeout milliseconds. */ public CaptureTimeoutsWrapper( - long preCaptureFocusingTimeoutMs, long preCaptureMeteringTimeoutMs) { + long preCaptureFocusingTimeoutMs, long preCaptureMeteringTimeoutMs) { + this.preCaptureFocusingTimeoutMs = preCaptureFocusingTimeoutMs; + this.preCaptureMeteringTimeoutMs = preCaptureMeteringTimeoutMs; + } + + /** Reset all timeouts to the current timestamp. */ + public void reset() { this.preCaptureFocusing = Timeout.create(preCaptureFocusingTimeoutMs); this.preCaptureMetering = Timeout.create(preCaptureMeteringTimeoutMs); } @@ -41,4 +49,4 @@ public Timeout getPreCaptureFocusing() { public Timeout getPreCaptureMetering() { return preCaptureMetering; } -} +} \ No newline at end of file diff --git a/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/CameraUtilsTest.java b/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/CameraUtilsTest.java index b97192b889cf..f61fdab6efde 100644 --- a/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/CameraUtilsTest.java +++ b/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/CameraUtilsTest.java @@ -14,17 +14,17 @@ public class CameraUtilsTest { @Test public void serializeDeviceOrientation_serializes_correctly() { assertEquals( - "portraitUp", - CameraUtils.serializeDeviceOrientation(PlatformChannel.DeviceOrientation.PORTRAIT_UP)); + "portraitUp", + CameraUtils.serializeDeviceOrientation(PlatformChannel.DeviceOrientation.PORTRAIT_UP)); assertEquals( - "portraitDown", - CameraUtils.serializeDeviceOrientation(PlatformChannel.DeviceOrientation.PORTRAIT_DOWN)); + "portraitDown", + CameraUtils.serializeDeviceOrientation(PlatformChannel.DeviceOrientation.PORTRAIT_DOWN)); assertEquals( - "landscapeLeft", - CameraUtils.serializeDeviceOrientation(PlatformChannel.DeviceOrientation.LANDSCAPE_LEFT)); + "landscapeLeft", + CameraUtils.serializeDeviceOrientation(PlatformChannel.DeviceOrientation.LANDSCAPE_LEFT)); assertEquals( - "landscapeRight", - CameraUtils.serializeDeviceOrientation(PlatformChannel.DeviceOrientation.LANDSCAPE_RIGHT)); + "landscapeRight", + CameraUtils.serializeDeviceOrientation(PlatformChannel.DeviceOrientation.LANDSCAPE_RIGHT)); } @Test(expected = UnsupportedOperationException.class) @@ -35,68 +35,21 @@ public void serializeDeviceOrientation_throws_for_null() { @Test public void deserializeDeviceOrientation_deserializes_correctly() { assertEquals( - PlatformChannel.DeviceOrientation.PORTRAIT_UP, - CameraUtils.deserializeDeviceOrientation("portraitUp")); + PlatformChannel.DeviceOrientation.PORTRAIT_UP, + CameraUtils.deserializeDeviceOrientation("portraitUp")); assertEquals( - PlatformChannel.DeviceOrientation.PORTRAIT_DOWN, - CameraUtils.deserializeDeviceOrientation("portraitDown")); + PlatformChannel.DeviceOrientation.PORTRAIT_DOWN, + CameraUtils.deserializeDeviceOrientation("portraitDown")); assertEquals( - PlatformChannel.DeviceOrientation.LANDSCAPE_LEFT, - CameraUtils.deserializeDeviceOrientation("landscapeLeft")); + PlatformChannel.DeviceOrientation.LANDSCAPE_LEFT, + CameraUtils.deserializeDeviceOrientation("landscapeLeft")); assertEquals( - PlatformChannel.DeviceOrientation.LANDSCAPE_RIGHT, - CameraUtils.deserializeDeviceOrientation("landscapeRight")); + PlatformChannel.DeviceOrientation.LANDSCAPE_RIGHT, + CameraUtils.deserializeDeviceOrientation("landscapeRight")); } @Test(expected = UnsupportedOperationException.class) public void deserializeDeviceOrientation_throws_for_null() { CameraUtils.deserializeDeviceOrientation(null); } - - @Test - public void getDeviceOrientationFromDegrees_converts_correctly() { - // Portrait UP - assertEquals( - PlatformChannel.DeviceOrientation.PORTRAIT_UP, - CameraUtils.getDeviceOrientationFromDegrees(0)); - assertEquals( - PlatformChannel.DeviceOrientation.PORTRAIT_UP, - CameraUtils.getDeviceOrientationFromDegrees(315)); - assertEquals( - PlatformChannel.DeviceOrientation.PORTRAIT_UP, - CameraUtils.getDeviceOrientationFromDegrees(44)); - assertEquals( - PlatformChannel.DeviceOrientation.PORTRAIT_UP, - CameraUtils.getDeviceOrientationFromDegrees(-45)); - // Portrait DOWN - assertEquals( - PlatformChannel.DeviceOrientation.PORTRAIT_DOWN, - CameraUtils.getDeviceOrientationFromDegrees(180)); - assertEquals( - PlatformChannel.DeviceOrientation.PORTRAIT_DOWN, - CameraUtils.getDeviceOrientationFromDegrees(135)); - assertEquals( - PlatformChannel.DeviceOrientation.PORTRAIT_DOWN, - CameraUtils.getDeviceOrientationFromDegrees(224)); - // Landscape LEFT - assertEquals( - PlatformChannel.DeviceOrientation.LANDSCAPE_LEFT, - CameraUtils.getDeviceOrientationFromDegrees(90)); - assertEquals( - PlatformChannel.DeviceOrientation.LANDSCAPE_LEFT, - CameraUtils.getDeviceOrientationFromDegrees(45)); - assertEquals( - PlatformChannel.DeviceOrientation.LANDSCAPE_LEFT, - CameraUtils.getDeviceOrientationFromDegrees(134)); - // Landscape RIGHT - assertEquals( - PlatformChannel.DeviceOrientation.LANDSCAPE_RIGHT, - CameraUtils.getDeviceOrientationFromDegrees(270)); - assertEquals( - PlatformChannel.DeviceOrientation.LANDSCAPE_RIGHT, - CameraUtils.getDeviceOrientationFromDegrees(225)); - assertEquals( - PlatformChannel.DeviceOrientation.LANDSCAPE_RIGHT, - CameraUtils.getDeviceOrientationFromDegrees(314)); - } -} +} \ No newline at end of file From ce9df6c4b70719bf4ca934af7e9e1702b7970869 Mon Sep 17 00:00:00 2001 From: BeMacized Date: Wed, 16 Jun 2021 17:12:49 +0200 Subject: [PATCH 47/63] Changes required for integrating main camera class --- .../plugins/camera/CameraCaptureCallback.java | 116 +++++++-------- .../plugins/camera/CameraProperties.java | 10 +- .../flutter/plugins/camera/CameraUtils.java | 126 ++++++++++++----- .../io/flutter/plugins/camera/ImageSaver.java | 2 +- .../camera/features/CameraFeatureFactory.java | 16 +-- .../features/CameraFeatureFactoryImpl.java | 132 +++++++++--------- .../camera/features/CameraFeatures.java | 3 +- .../camera/types/CaptureTimeoutsWrapper.java | 4 +- .../plugins/camera/CameraUtilsTest.java | 81 ++++++++--- 9 files changed, 295 insertions(+), 195 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 576c8f250587..21dcb602655d 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 @@ -24,8 +24,8 @@ class CameraCaptureCallback extends CaptureCallback { private final CaptureTimeoutsWrapper captureTimeouts; private CameraCaptureCallback( - @NonNull CameraCaptureStateListener cameraStateListener, - @NonNull CaptureTimeoutsWrapper captureTimeouts) { + @NonNull CameraCaptureStateListener cameraStateListener, + @NonNull CaptureTimeoutsWrapper captureTimeouts) { cameraState = CameraState.STATE_PREVIEW; this.cameraStateListener = cameraStateListener; this.captureTimeouts = captureTimeouts; @@ -40,8 +40,8 @@ private CameraCaptureCallback( * @return a configured instance of the {@link CameraCaptureCallback} class. */ public static CameraCaptureCallback create( - @NonNull CameraCaptureStateListener cameraStateListener, - @NonNull CaptureTimeoutsWrapper captureTimeouts) { + @NonNull CameraCaptureStateListener cameraStateListener, + @NonNull CaptureTimeoutsWrapper captureTimeouts) { return new CameraCaptureCallback(cameraStateListener, captureTimeouts); } @@ -69,63 +69,63 @@ private void process(CaptureResult result) { if (cameraState != CameraState.STATE_PREVIEW) { Log.d( - TAG, - "CameraCaptureCallback | state: " - + cameraState - + " | afState: " - + afState - + " | aeState: " - + aeState); + TAG, + "CameraCaptureCallback | state: " + + cameraState + + " | afState: " + + afState + + " | aeState: " + + aeState); } 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 == CaptureResult.CONTROL_AF_STATE_FOCUSED_LOCKED - || afState == CaptureResult.CONTROL_AF_STATE_NOT_FOCUSED_LOCKED) { - handleWaitingFocusState(aeState); - } else if (captureTimeouts.getPreCaptureFocusing().getIsExpired()) { - Log.w(TAG, "Focus timeout, moving on with capture"); - handleWaitingFocusState(aeState); + { + if (afState == null) { + return; + } else if (afState == CaptureResult.CONTROL_AF_STATE_FOCUSED_LOCKED + || afState == CaptureResult.CONTROL_AF_STATE_NOT_FOCUSED_LOCKED) { + handleWaitingFocusState(aeState); + } else if (captureTimeouts.getPreCaptureFocusing().getIsExpired()) { + Log.w(TAG, "Focus timeout, moving on with capture"); + handleWaitingFocusState(aeState); + } + + 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); - } else if (captureTimeouts.getPreCaptureMetering().getIsExpired()) { - Log.w(TAG, "Metering timeout waiting for pre-capture to start, moving on with capture"); - - 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); + } else if (captureTimeouts.getPreCaptureMetering().getIsExpired()) { + Log.w(TAG, "Metering timeout waiting for pre-capture to start, moving on with capture"); + + 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 (captureTimeouts.getPreCaptureMetering().getIsExpired()) { - Log.w( - TAG, "Metering timeout waiting for pre-capture to finish, moving on with capture"); - cameraStateListener.onConverged(); + { + // CONTROL_AE_STATE can be null on some devices + if (aeState == null || aeState != CaptureResult.CONTROL_AE_STATE_PRECAPTURE) { + cameraStateListener.onConverged(); + } else if (captureTimeouts.getPreCaptureMetering().getIsExpired()) { + Log.w( + TAG, "Metering timeout waiting for pre-capture to finish, moving on with capture"); + cameraStateListener.onConverged(); + } + + break; } - - break; - } } } @@ -140,17 +140,17 @@ private void handleWaitingFocusState(Integer aeState) { @Override public void onCaptureProgressed( - @NonNull CameraCaptureSession session, - @NonNull CaptureRequest request, - @NonNull CaptureResult partialResult) { + @NonNull CameraCaptureSession session, + @NonNull CaptureRequest request, + @NonNull CaptureResult partialResult) { process(partialResult); } @Override public void onCaptureCompleted( - @NonNull CameraCaptureSession session, - @NonNull CaptureRequest request, - @NonNull TotalCaptureResult result) { + @NonNull CameraCaptureSession session, + @NonNull CaptureRequest request, + @NonNull TotalCaptureResult result) { process(result); } @@ -165,4 +165,4 @@ interface CameraCaptureStateListener { */ void onPrecapture(); } -} \ No newline at end of file +} 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 25d94e0be852..6e2fdab06d91 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 @@ -246,7 +246,7 @@ class CameraPropertiesImpl implements CameraProperties { private final String cameraName; public CameraPropertiesImpl(String cameraName, CameraManager cameraManager) - throws CameraAccessException { + throws CameraAccessException { this.cameraName = cameraName; this.cameraCharacteristics = cameraManager.getCameraCharacteristics(cameraName); } @@ -270,7 +270,7 @@ public Range getControlAutoExposureCompensationRange() { @Override public double getControlAutoExposureCompensationStep() { Rational rational = - cameraCharacteristics.get(CameraCharacteristics.CONTROL_AE_COMPENSATION_STEP); + cameraCharacteristics.get(CameraCharacteristics.CONTROL_AE_COMPENSATION_STEP); return rational == null ? 0.0 : rational.doubleValue(); } @@ -330,7 +330,7 @@ public Size getSensorInfoPixelArraySize() { @Override public Rect getSensorInfoPreCorrectionActiveArraySize() { return cameraCharacteristics.get( - CameraCharacteristics.SENSOR_INFO_PRE_CORRECTION_ACTIVE_ARRAY_SIZE); + CameraCharacteristics.SENSOR_INFO_PRE_CORRECTION_ACTIVE_ARRAY_SIZE); } @Override @@ -346,6 +346,6 @@ public int getHardwareLevel() { @Override public int[] getAvailableNoiseReductionModes() { return cameraCharacteristics.get( - CameraCharacteristics.NOISE_REDUCTION_AVAILABLE_NOISE_REDUCTION_MODES); + CameraCharacteristics.NOISE_REDUCTION_AVAILABLE_NOISE_REDUCTION_MODES); } -} \ No newline at end of file +} 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 0cd20ca3082b..b4d4689f2b4e 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 @@ -6,12 +6,20 @@ import android.app.Activity; import android.content.Context; +import android.graphics.ImageFormat; import android.hardware.camera2.CameraAccessException; import android.hardware.camera2.CameraCharacteristics; 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; +import java.util.Comparator; import java.util.HashMap; import java.util.List; import java.util.Map; @@ -21,24 +29,23 @@ public final class CameraUtils { private CameraUtils() {} - /** - * Gets the {@link CameraManager} singleton. - * - * @param context The context to get the {@link CameraManager} singleton from. - * @return The {@link CameraManager} singleton. - */ - 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; + // Determine the corresponding device orientation. + switch (degrees) { + case 90: + return PlatformChannel.DeviceOrientation.LANDSCAPE_LEFT; + case 180: + return PlatformChannel.DeviceOrientation.PORTRAIT_DOWN; + case 270: + return PlatformChannel.DeviceOrientation.LANDSCAPE_RIGHT; + case 0: + default: + return PlatformChannel.DeviceOrientation.PORTRAIT_UP; + } } - /** - * Serializes the {@link PlatformChannel.DeviceOrientation} to a string value. - * - * @param orientation The orientation to serialize. - * @return The serialized orientation. - * @throws UnsupportedOperationException when the provided orientation not have a corresponding - * string value. - */ static String serializeDeviceOrientation(PlatformChannel.DeviceOrientation orientation) { if (orientation == null) throw new UnsupportedOperationException("Could not serialize null device orientation."); @@ -53,19 +60,10 @@ static String serializeDeviceOrientation(PlatformChannel.DeviceOrientation orien return "landscapeRight"; default: throw new UnsupportedOperationException( - "Could not serialize device orientation: " + orientation.toString()); + "Could not serialize device orientation: " + orientation.toString()); } } - /** - * Deserializes a string value to its corresponding {@link PlatformChannel.DeviceOrientation} - * value. - * - * @param orientation The string value to deserialize. - * @return The deserialized orientation. - * @throws UnsupportedOperationException when the provided string value does not have a - * corresponding {@link PlatformChannel.DeviceOrientation}. - */ static PlatformChannel.DeviceOrientation deserializeDeviceOrientation(String orientation) { if (orientation == null) throw new UnsupportedOperationException("Could not deserialize null device orientation."); @@ -80,19 +78,29 @@ static PlatformChannel.DeviceOrientation deserializeDeviceOrientation(String ori return PlatformChannel.DeviceOrientation.LANDSCAPE_RIGHT; default: throw new UnsupportedOperationException( - "Could not deserialize device orientation: " + orientation); + "Could not deserialize device orientation: " + orientation); } } - /** - * Gets all the available cameras for the device. - * - * @param activity The current Android activity. - * @return A map of all the available cameras, with their name as their key. - * @throws CameraAccessException when the camera could not be accessed. - */ + 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( + Arrays.asList(streamConfigurationMap.getOutputSizes(ImageFormat.JPEG)), + new CompareSizesByArea()); + } + public static List> getAvailableCameras(Activity activity) - throws CameraAccessException { + throws CameraAccessException { CameraManager cameraManager = (CameraManager) activity.getSystemService(Context.CAMERA_SERVICE); String[] cameraNames = cameraManager.getCameraIdList(); List> cameras = new ArrayList<>(); @@ -119,4 +127,52 @@ 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) { + // We cast here to ensure the multiplications won't overflow. + return Long.signum( + (long) lhs.getWidth() * lhs.getHeight() - (long) rhs.getWidth() * rhs.getHeight()); + } + } } 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 d538ce85b7c3..821c9a50c13f 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 @@ -102,4 +102,4 @@ public static FileOutputStream create(File file) throws FileNotFoundException { return new FileOutputStream(file); } } -} \ No newline at end of file +} 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 46786fb0fdbf..8d10c445788c 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 @@ -36,7 +36,7 @@ public interface CameraFeatureFactory { * @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. @@ -75,9 +75,9 @@ AutoFocusFeature createAutoFocusFeature( * @return newly created instance of the ResolutionFeature class. */ ResolutionFeature createResolutionFeature( - @NonNull CameraProperties cameraProperties, - ResolutionPreset initialSetting, - String cameraName); + @NonNull CameraProperties cameraProperties, + ResolutionPreset initialSetting, + String cameraName); /** * Creates a new instance of the focus point feature. @@ -108,9 +108,9 @@ ResolutionFeature createResolutionFeature( * @return newly created instance of the SensorOrientationFeature class. */ SensorOrientationFeature createSensorOrientationFeature( - @NonNull CameraProperties cameraProperties, - @NonNull Activity activity, - @NonNull DartMessenger dartMessenger); + @NonNull CameraProperties cameraProperties, + @NonNull Activity activity, + @NonNull DartMessenger dartMessenger); /** * Creates a new instance of the zoom level feature. @@ -138,4 +138,4 @@ SensorOrientationFeature createSensorOrientationFeature( * @return newly created instance of the NoiseReductionFeature class. */ NoiseReductionFeature createNoiseReductionFeature(@NonNull CameraProperties cameraProperties); -} \ No newline at end of file +} 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 c59d71ef4fa3..b12ad3626226 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 @@ -5,9 +5,7 @@ package io.flutter.plugins.camera.features; import android.app.Activity; - import androidx.annotation.NonNull; - import io.flutter.plugins.camera.CameraProperties; import io.flutter.plugins.camera.DartMessenger; import io.flutter.plugins.camera.features.autofocus.AutoFocusFeature; @@ -30,68 +28,68 @@ */ 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) { - return new FocusPointFeature(cameraProperties); - } - - @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 ExposurePointFeature createExposurePointFeature( - @NonNull CameraProperties cameraProperties) { - return new ExposurePointFeature(cameraProperties); - } - - @Override - public NoiseReductionFeature createNoiseReductionFeature( - @NonNull CameraProperties cameraProperties) { - return new NoiseReductionFeature(cameraProperties); - } -} \ No newline at end of file + @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) { + return new FocusPointFeature(cameraProperties); + } + + @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 ExposurePointFeature createExposurePointFeature( + @NonNull CameraProperties cameraProperties) { + return new ExposurePointFeature(cameraProperties); + } + + @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 268db891b47b..0ee8969071bc 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 @@ -15,7 +15,6 @@ import io.flutter.plugins.camera.features.resolution.ResolutionFeature; import io.flutter.plugins.camera.features.sensororientation.SensorOrientationFeature; import io.flutter.plugins.camera.features.zoomlevel.ZoomLevelFeature; - import java.util.Collection; import java.util.HashMap; import java.util.Map; @@ -246,4 +245,4 @@ public ZoomLevelFeature getZoomLevel() { public void setZoomLevel(ZoomLevelFeature zoomLevel) { this.featureMap.put(ZOOM_LEVEL, zoomLevel); } -} \ No newline at end of file +} diff --git a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/types/CaptureTimeoutsWrapper.java b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/types/CaptureTimeoutsWrapper.java index 8128f0e6317b..ad59bd09c754 100644 --- a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/types/CaptureTimeoutsWrapper.java +++ b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/types/CaptureTimeoutsWrapper.java @@ -21,7 +21,7 @@ public class CaptureTimeoutsWrapper { * @param preCaptureMeteringTimeoutMs metering timeout milliseconds. */ public CaptureTimeoutsWrapper( - long preCaptureFocusingTimeoutMs, long preCaptureMeteringTimeoutMs) { + long preCaptureFocusingTimeoutMs, long preCaptureMeteringTimeoutMs) { this.preCaptureFocusingTimeoutMs = preCaptureFocusingTimeoutMs; this.preCaptureMeteringTimeoutMs = preCaptureMeteringTimeoutMs; } @@ -49,4 +49,4 @@ public Timeout getPreCaptureFocusing() { public Timeout getPreCaptureMetering() { return preCaptureMetering; } -} \ No newline at end of file +} diff --git a/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/CameraUtilsTest.java b/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/CameraUtilsTest.java index f61fdab6efde..b97192b889cf 100644 --- a/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/CameraUtilsTest.java +++ b/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/CameraUtilsTest.java @@ -14,17 +14,17 @@ public class CameraUtilsTest { @Test public void serializeDeviceOrientation_serializes_correctly() { assertEquals( - "portraitUp", - CameraUtils.serializeDeviceOrientation(PlatformChannel.DeviceOrientation.PORTRAIT_UP)); + "portraitUp", + CameraUtils.serializeDeviceOrientation(PlatformChannel.DeviceOrientation.PORTRAIT_UP)); assertEquals( - "portraitDown", - CameraUtils.serializeDeviceOrientation(PlatformChannel.DeviceOrientation.PORTRAIT_DOWN)); + "portraitDown", + CameraUtils.serializeDeviceOrientation(PlatformChannel.DeviceOrientation.PORTRAIT_DOWN)); assertEquals( - "landscapeLeft", - CameraUtils.serializeDeviceOrientation(PlatformChannel.DeviceOrientation.LANDSCAPE_LEFT)); + "landscapeLeft", + CameraUtils.serializeDeviceOrientation(PlatformChannel.DeviceOrientation.LANDSCAPE_LEFT)); assertEquals( - "landscapeRight", - CameraUtils.serializeDeviceOrientation(PlatformChannel.DeviceOrientation.LANDSCAPE_RIGHT)); + "landscapeRight", + CameraUtils.serializeDeviceOrientation(PlatformChannel.DeviceOrientation.LANDSCAPE_RIGHT)); } @Test(expected = UnsupportedOperationException.class) @@ -35,21 +35,68 @@ public void serializeDeviceOrientation_throws_for_null() { @Test public void deserializeDeviceOrientation_deserializes_correctly() { assertEquals( - PlatformChannel.DeviceOrientation.PORTRAIT_UP, - CameraUtils.deserializeDeviceOrientation("portraitUp")); + PlatformChannel.DeviceOrientation.PORTRAIT_UP, + CameraUtils.deserializeDeviceOrientation("portraitUp")); assertEquals( - PlatformChannel.DeviceOrientation.PORTRAIT_DOWN, - CameraUtils.deserializeDeviceOrientation("portraitDown")); + PlatformChannel.DeviceOrientation.PORTRAIT_DOWN, + CameraUtils.deserializeDeviceOrientation("portraitDown")); assertEquals( - PlatformChannel.DeviceOrientation.LANDSCAPE_LEFT, - CameraUtils.deserializeDeviceOrientation("landscapeLeft")); + PlatformChannel.DeviceOrientation.LANDSCAPE_LEFT, + CameraUtils.deserializeDeviceOrientation("landscapeLeft")); assertEquals( - PlatformChannel.DeviceOrientation.LANDSCAPE_RIGHT, - CameraUtils.deserializeDeviceOrientation("landscapeRight")); + PlatformChannel.DeviceOrientation.LANDSCAPE_RIGHT, + CameraUtils.deserializeDeviceOrientation("landscapeRight")); } @Test(expected = UnsupportedOperationException.class) public void deserializeDeviceOrientation_throws_for_null() { CameraUtils.deserializeDeviceOrientation(null); } -} \ No newline at end of file + + @Test + public void getDeviceOrientationFromDegrees_converts_correctly() { + // Portrait UP + assertEquals( + PlatformChannel.DeviceOrientation.PORTRAIT_UP, + CameraUtils.getDeviceOrientationFromDegrees(0)); + assertEquals( + PlatformChannel.DeviceOrientation.PORTRAIT_UP, + CameraUtils.getDeviceOrientationFromDegrees(315)); + assertEquals( + PlatformChannel.DeviceOrientation.PORTRAIT_UP, + CameraUtils.getDeviceOrientationFromDegrees(44)); + assertEquals( + PlatformChannel.DeviceOrientation.PORTRAIT_UP, + CameraUtils.getDeviceOrientationFromDegrees(-45)); + // Portrait DOWN + assertEquals( + PlatformChannel.DeviceOrientation.PORTRAIT_DOWN, + CameraUtils.getDeviceOrientationFromDegrees(180)); + assertEquals( + PlatformChannel.DeviceOrientation.PORTRAIT_DOWN, + CameraUtils.getDeviceOrientationFromDegrees(135)); + assertEquals( + PlatformChannel.DeviceOrientation.PORTRAIT_DOWN, + CameraUtils.getDeviceOrientationFromDegrees(224)); + // Landscape LEFT + assertEquals( + PlatformChannel.DeviceOrientation.LANDSCAPE_LEFT, + CameraUtils.getDeviceOrientationFromDegrees(90)); + assertEquals( + PlatformChannel.DeviceOrientation.LANDSCAPE_LEFT, + CameraUtils.getDeviceOrientationFromDegrees(45)); + assertEquals( + PlatformChannel.DeviceOrientation.LANDSCAPE_LEFT, + CameraUtils.getDeviceOrientationFromDegrees(134)); + // Landscape RIGHT + assertEquals( + PlatformChannel.DeviceOrientation.LANDSCAPE_RIGHT, + CameraUtils.getDeviceOrientationFromDegrees(270)); + assertEquals( + PlatformChannel.DeviceOrientation.LANDSCAPE_RIGHT, + CameraUtils.getDeviceOrientationFromDegrees(225)); + assertEquals( + PlatformChannel.DeviceOrientation.LANDSCAPE_RIGHT, + CameraUtils.getDeviceOrientationFromDegrees(314)); + } +} From 32cb969a94b878585258bfeca2f256ba31466386 Mon Sep 17 00:00:00 2001 From: BeMacized Date: Wed, 16 Jun 2021 17:15:11 +0200 Subject: [PATCH 48/63] Remove debug statement --- .../java/io/flutter/plugins/camera/CameraProperties.java | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) 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 6e2fdab06d91..95efebbf6488 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 @@ -124,7 +124,7 @@ public interface CameraProperties { *

  • @see android.hardware.camera2.CameraMetadata.LENS_FACING_EXTERNAL * * - * By default maps to the @see android.hardware.camera2.CameraCharacteristics.LENS_FACING key. + *

    By default maps to the @see android.hardware.camera2.CameraCharacteristics.LENS_FACING key. * * @return int Direction the camera faces relative to device screen. */ @@ -216,7 +216,7 @@ public interface CameraProperties { *

  • @see android.hardware.camera2.CameraMetadata.INFO_SUPPORTED_HARDWARE_LEVEL_EXTERNAL * * - * By default maps to the @see + *

    By default maps to the @see * android.hardware.camera2.CameraCharacteristics#INFO_SUPPORTED_HARDWARE_LEVEL key. * * @return int Level which generally classifies the overall set of the camera device @@ -253,8 +253,7 @@ public CameraPropertiesImpl(String cameraName, CameraManager cameraManager) @Override public String getCameraName() { - return "WOOPS"; - // return cameraName; + return cameraName; } @Override From dd3416bdd789bf4ff8bbf15f07bedebf8ea19ed4 Mon Sep 17 00:00:00 2001 From: BeMacized Date: Wed, 16 Jun 2021 17:23:08 +0200 Subject: [PATCH 49/63] Updated pubspec version and changelog --- packages/camera/camera/CHANGELOG.md | 6 ++++++ packages/camera/camera/pubspec.yaml | 2 +- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/packages/camera/camera/CHANGELOG.md b/packages/camera/camera/CHANGELOG.md index 2cab8e123ae6..3f8c17bee155 100644 --- a/packages/camera/camera/CHANGELOG.md +++ b/packages/camera/camera/CHANGELOG.md @@ -1,3 +1,9 @@ +## 0.8.2 + +* 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. + ## 0.8.1+3 * Do not change camera orientation when iOS device is flat. diff --git a/packages/camera/camera/pubspec.yaml b/packages/camera/camera/pubspec.yaml index a7df9e0d51be..78cad5215afb 100644 --- a/packages/camera/camera/pubspec.yaml +++ b/packages/camera/camera/pubspec.yaml @@ -4,7 +4,7 @@ description: A Flutter plugin for getting information about and controlling the and streaming image buffers to dart. repository: https://github.com/flutter/plugins/tree/master/packages/camera/camera issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+camera%22 -version: 0.8.1+3 +version: 0.8.2 environment: sdk: ">=2.12.0 <3.0.0" From 9282d9ac5b6c0bd3d94e513269e2eface749fe90 Mon Sep 17 00:00:00 2001 From: BeMacized Date: Mon, 28 Jun 2021 16:08:15 +0200 Subject: [PATCH 50/63] Remove unused classes after refactor --- .../flutter/plugins/camera/CameraRegions.java | 76 ------- .../camera/DeviceOrientationManager.java | 200 ------------------ .../plugins/camera/PictureCaptureRequest.java | 96 --------- .../plugins/camera/CameraRegionUtilsTest.java | 24 ++- .../camera/PictureCaptureRequestTest.java | 152 ------------- 5 files changed, 15 insertions(+), 533 deletions(-) delete mode 100644 packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/CameraRegions.java delete mode 100644 packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/DeviceOrientationManager.java delete mode 100644 packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/PictureCaptureRequest.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/CameraRegions.java b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/CameraRegions.java deleted file mode 100644 index 60c866cd82d5..000000000000 --- a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/CameraRegions.java +++ /dev/null @@ -1,76 +0,0 @@ -// Copyright 2013 The Flutter 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.params.MeteringRectangle; -import android.util.Size; - -public final class CameraRegions { - private MeteringRectangle aeMeteringRectangle; - private MeteringRectangle afMeteringRectangle; - private Size maxBoundaries; - - public CameraRegions(Size maxBoundaries) { - assert (maxBoundaries == null || maxBoundaries.getWidth() > 0); - assert (maxBoundaries == null || maxBoundaries.getHeight() > 0); - this.maxBoundaries = maxBoundaries; - } - - public MeteringRectangle getAEMeteringRectangle() { - return aeMeteringRectangle; - } - - public MeteringRectangle getAFMeteringRectangle() { - return afMeteringRectangle; - } - - public Size getMaxBoundaries() { - return this.maxBoundaries; - } - - public void resetAutoExposureMeteringRectangle() { - this.aeMeteringRectangle = null; - } - - public void setAutoExposureMeteringRectangleFromPoint(double x, double y) { - this.aeMeteringRectangle = getMeteringRectangleForPoint(maxBoundaries, x, y); - } - - public void resetAutoFocusMeteringRectangle() { - this.afMeteringRectangle = null; - } - - public void setAutoFocusMeteringRectangleFromPoint(double x, double y) { - this.afMeteringRectangle = getMeteringRectangleForPoint(maxBoundaries, x, y); - } - - public MeteringRectangle getMeteringRectangleForPoint(Size maxBoundaries, double x, double y) { - assert (x >= 0 && x <= 1); - assert (y >= 0 && y <= 1); - if (maxBoundaries == null) - throw new IllegalStateException( - "Functionality for managing metering rectangles is unavailable as this CameraRegions instance was initialized with null boundaries."); - - // Interpolate the target coordinate - int targetX = (int) Math.round(x * ((double) (maxBoundaries.getWidth() - 1))); - int targetY = (int) Math.round(y * ((double) (maxBoundaries.getHeight() - 1))); - // Determine the dimensions of the metering triangle (10th of the viewport) - int targetWidth = (int) Math.round(((double) maxBoundaries.getWidth()) / 10d); - int targetHeight = (int) Math.round(((double) maxBoundaries.getHeight()) / 10d); - // Adjust target coordinate to represent top-left corner of metering rectangle - targetX -= targetWidth / 2; - targetY -= targetHeight / 2; - // Adjust target coordinate as to not fall out of bounds - if (targetX < 0) targetX = 0; - if (targetY < 0) targetY = 0; - int maxTargetX = maxBoundaries.getWidth() - 1 - targetWidth; - int maxTargetY = maxBoundaries.getHeight() - 1 - targetHeight; - if (targetX > maxTargetX) targetX = maxTargetX; - if (targetY > maxTargetY) targetY = maxTargetY; - - // Build the metering rectangle - return new MeteringRectangle(targetX, targetY, targetWidth, targetHeight, 1); - } -} 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 deleted file mode 100644 index 634596dde8bb..000000000000 --- a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/DeviceOrientationManager.java +++ /dev/null @@ -1,200 +0,0 @@ -// Copyright 2013 The Flutter 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.app.Activity; -import android.content.BroadcastReceiver; -import android.content.Context; -import android.content.Intent; -import android.content.IntentFilter; -import android.content.res.Configuration; -import android.hardware.SensorManager; -import android.provider.Settings; -import android.view.Display; -import android.view.OrientationEventListener; -import android.view.Surface; -import android.view.WindowManager; -import io.flutter.embedding.engine.systemchannels.PlatformChannel; - -class DeviceOrientationManager { - - private static final IntentFilter orientationIntentFilter = - new IntentFilter(Intent.ACTION_CONFIGURATION_CHANGED); - - private final Activity activity; - private final DartMessenger messenger; - private final boolean isFrontFacing; - private final int sensorOrientation; - private PlatformChannel.DeviceOrientation lastOrientation; - private OrientationEventListener orientationEventListener; - private BroadcastReceiver broadcastReceiver; - - public DeviceOrientationManager( - Activity activity, DartMessenger messenger, boolean isFrontFacing, int sensorOrientation) { - this.activity = activity; - this.messenger = messenger; - this.isFrontFacing = isFrontFacing; - this.sensorOrientation = sensorOrientation; - } - - public void start() { - startSensorListener(); - startUIListener(); - } - - public void stop() { - stopSensorListener(); - stopUIListener(); - } - - public int getMediaOrientation() { - return this.getMediaOrientation(this.lastOrientation); - } - - public int getMediaOrientation(PlatformChannel.DeviceOrientation orientation) { - int angle = 0; - - // Fallback to device orientation when the orientation value is null - if (orientation == null) { - orientation = getUIOrientation(); - } - - switch (orientation) { - case PORTRAIT_UP: - angle = 0; - break; - case PORTRAIT_DOWN: - angle = 180; - break; - case LANDSCAPE_LEFT: - angle = 90; - break; - case LANDSCAPE_RIGHT: - angle = 270; - break; - } - if (isFrontFacing) angle *= -1; - return (angle + sensorOrientation + 360) % 360; - } - - private void startSensorListener() { - if (orientationEventListener != null) return; - orientationEventListener = - new OrientationEventListener(activity, SensorManager.SENSOR_DELAY_NORMAL) { - @Override - public void onOrientationChanged(int angle) { - if (!isSystemAutoRotationLocked()) { - PlatformChannel.DeviceOrientation newOrientation = calculateSensorOrientation(angle); - if (!newOrientation.equals(lastOrientation)) { - lastOrientation = newOrientation; - messenger.sendDeviceOrientationChangeEvent(newOrientation); - } - } - } - }; - if (orientationEventListener.canDetectOrientation()) { - orientationEventListener.enable(); - } - } - - private void startUIListener() { - if (broadcastReceiver != null) return; - broadcastReceiver = - new BroadcastReceiver() { - @Override - public void onReceive(Context context, Intent intent) { - if (isSystemAutoRotationLocked()) { - PlatformChannel.DeviceOrientation orientation = getUIOrientation(); - if (!orientation.equals(lastOrientation)) { - lastOrientation = orientation; - messenger.sendDeviceOrientationChangeEvent(orientation); - } - } - } - }; - activity.registerReceiver(broadcastReceiver, orientationIntentFilter); - broadcastReceiver.onReceive(activity, null); - } - - private void stopSensorListener() { - if (orientationEventListener == null) return; - orientationEventListener.disable(); - orientationEventListener = null; - } - - private void stopUIListener() { - if (broadcastReceiver == null) return; - activity.unregisterReceiver(broadcastReceiver); - broadcastReceiver = null; - } - - private boolean isSystemAutoRotationLocked() { - return android.provider.Settings.System.getInt( - activity.getContentResolver(), Settings.System.ACCELEROMETER_ROTATION, 0) - != 1; - } - - private PlatformChannel.DeviceOrientation getUIOrientation() { - final int rotation = getDisplay().getRotation(); - final int orientation = activity.getResources().getConfiguration().orientation; - - switch (orientation) { - case Configuration.ORIENTATION_PORTRAIT: - if (rotation == Surface.ROTATION_0 || rotation == Surface.ROTATION_90) { - return PlatformChannel.DeviceOrientation.PORTRAIT_UP; - } else { - return PlatformChannel.DeviceOrientation.PORTRAIT_DOWN; - } - case Configuration.ORIENTATION_LANDSCAPE: - if (rotation == Surface.ROTATION_0 || rotation == Surface.ROTATION_90) { - return PlatformChannel.DeviceOrientation.LANDSCAPE_LEFT; - } else { - return PlatformChannel.DeviceOrientation.LANDSCAPE_RIGHT; - } - default: - return PlatformChannel.DeviceOrientation.PORTRAIT_UP; - } - } - - private PlatformChannel.DeviceOrientation calculateSensorOrientation(int angle) { - final int tolerance = 45; - angle += tolerance; - - // Orientation is 0 in the default orientation mode. This is portait-mode for phones - // and landscape for tablets. We have to compensate for this by calculating the default - // orientation, and apply an offset accordingly. - int defaultDeviceOrientation = getDeviceDefaultOrientation(); - if (defaultDeviceOrientation == Configuration.ORIENTATION_LANDSCAPE) { - angle += 90; - } - // Determine the orientation - angle = angle % 360; - return new PlatformChannel.DeviceOrientation[] { - PlatformChannel.DeviceOrientation.PORTRAIT_UP, - PlatformChannel.DeviceOrientation.LANDSCAPE_LEFT, - PlatformChannel.DeviceOrientation.PORTRAIT_DOWN, - PlatformChannel.DeviceOrientation.LANDSCAPE_RIGHT, - } - [angle / 90]; - } - - private int getDeviceDefaultOrientation() { - Configuration config = activity.getResources().getConfiguration(); - int rotation = getDisplay().getRotation(); - if (((rotation == Surface.ROTATION_0 || rotation == Surface.ROTATION_180) - && config.orientation == Configuration.ORIENTATION_LANDSCAPE) - || ((rotation == Surface.ROTATION_90 || rotation == Surface.ROTATION_270) - && config.orientation == Configuration.ORIENTATION_PORTRAIT)) { - return Configuration.ORIENTATION_LANDSCAPE; - } else { - return Configuration.ORIENTATION_PORTRAIT; - } - } - - @SuppressWarnings("deprecation") - private Display getDisplay() { - return ((WindowManager) activity.getSystemService(Context.WINDOW_SERVICE)).getDefaultDisplay(); - } -} 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 deleted file mode 100644 index 4c11e2d40e62..000000000000 --- a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/PictureCaptureRequest.java +++ /dev/null @@ -1,96 +0,0 @@ -// Copyright 2013 The Flutter 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 androidx.annotation.Nullable; -import io.flutter.plugin.common.MethodChannel; - -class PictureCaptureRequest { - - enum State { - idle, - focusing, - preCapture, - waitingPreCaptureReady, - capturing, - finished, - error, - } - - private final Runnable timeoutCallback = - new Runnable() { - @Override - public void run() { - error("captureTimeout", "Picture capture request timed out", state.toString()); - } - }; - - private final MethodChannel.Result result; - private final TimeoutHandler timeoutHandler; - private State state; - - public PictureCaptureRequest(MethodChannel.Result result) { - this(result, new TimeoutHandler()); - } - - public PictureCaptureRequest(MethodChannel.Result result, TimeoutHandler timeoutHandler) { - this.result = result; - this.state = State.idle; - this.timeoutHandler = timeoutHandler; - } - - public void setState(State state) { - if (isFinished()) throw new IllegalStateException("Request has already been finished"); - this.state = state; - if (state != State.idle && state != State.finished && state != State.error) { - this.timeoutHandler.resetTimeout(timeoutCallback); - } else { - this.timeoutHandler.clearTimeout(timeoutCallback); - } - } - - public State getState() { - return state; - } - - public boolean isFinished() { - return state == State.finished || state == State.error; - } - - public void finish(String absolutePath) { - if (isFinished()) throw new IllegalStateException("Request has already been finished"); - this.timeoutHandler.clearTimeout(timeoutCallback); - result.success(absolutePath); - state = State.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); - state = State.error; - } - - 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/test/java/io/flutter/plugins/camera/CameraRegionUtilsTest.java b/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/CameraRegionUtilsTest.java index 2d65c4e0fc05..c0ff47d8f9f1 100644 --- a/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/CameraRegionUtilsTest.java +++ b/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/CameraRegionUtilsTest.java @@ -247,27 +247,27 @@ public void setUp() { } @Test(expected = AssertionError.class) - public void getMeteringRectangleForPoint_should_throw_for_x_upper_bound() { + public void convertPointToMeteringRectangle_should_throw_for_x_upper_bound() { CameraRegionUtils.convertPointToMeteringRectangle(this.mockCameraBoundaries, 1.5, 0); } @Test(expected = AssertionError.class) - public void getMeteringRectangleForPoint_should_throw_for_x_lower_bound() { + public void convertPointToMeteringRectangle_should_throw_for_x_lower_bound() { CameraRegionUtils.convertPointToMeteringRectangle(this.mockCameraBoundaries, -0.5, 0); } @Test(expected = AssertionError.class) - public void getMeteringRectangleForPoint_should_throw_for_y_upper_bound() { + public void convertPointToMeteringRectangle_should_throw_for_y_upper_bound() { CameraRegionUtils.convertPointToMeteringRectangle(this.mockCameraBoundaries, 0, 1.5); } @Test(expected = AssertionError.class) - public void getMeteringRectangleForPoint_should_throw_for_y_lower_bound() { + public void convertPointToMeteringRectangle_should_throw_for_y_lower_bound() { CameraRegionUtils.convertPointToMeteringRectangle(this.mockCameraBoundaries, 0, -0.5); } @Test - public void getMeteringRectangleForPoint_should_return_valid_MeteringRectangle() { + public void convertPointToMeteringRectangle_should_return_valid_MeteringRectangle() { try (MockedStatic mockedMeteringRectangleFactory = mockStatic(CameraRegionUtils.MeteringRectangleFactory.class)) { @@ -338,13 +338,19 @@ public Boolean answer(InvocationOnMock equalsInvocation) } @Test(expected = AssertionError.class) - public void getMeteringRectangleForPoint_should_throw_for_0_width_boundary() { - new io.flutter.plugins.camera.CameraRegions(new Size(0, 50)); + public void convertPointToMeteringRectangle_should_throw_for_0_width_boundary() { + Size mockCameraBoundaries = mock(Size.class); + when(mockCameraBoundaries.getWidth()).thenReturn(0); + when(mockCameraBoundaries.getHeight()).thenReturn(50); + CameraRegionUtils.convertPointToMeteringRectangle(mockCameraBoundaries, 0, -0.5); } @Test(expected = AssertionError.class) - public void getMeteringRectangleForPoint_should_throw_for_0_height_boundary() { - new io.flutter.plugins.camera.CameraRegions(new Size(100, 0)); + public void convertPointToMeteringRectangle_should_throw_for_0_height_boundary() { + Size mockCameraBoundaries = mock(Size.class); + when(mockCameraBoundaries.getWidth()).thenReturn(50); + when(mockCameraBoundaries.getHeight()).thenReturn(0); + CameraRegionUtils.convertPointToMeteringRectangle(this.mockCameraBoundaries, 0, -0.5); } private static void updateSdkVersion(int version) { 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 f257a7f7fd4b..000000000000 --- a/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/PictureCaptureRequestTest.java +++ /dev/null @@ -1,152 +0,0 @@ -// Copyright 2013 The Flutter 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.never; -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); - } -} From cb3a35c0ba30635cba489223bc2e79a070e71ee7 Mon Sep 17 00:00:00 2001 From: "Bodhi Mulders (BeMacized)" Date: Mon, 28 Jun 2021 19:07:11 +0200 Subject: [PATCH 51/63] Fix camera orientation issues --- .../io/flutter/plugins/camera/Camera.java | 40 +--- .../flutter/plugins/camera/DartMessenger.java | 6 +- .../camera/DeviceOrientationManager.java | 200 ------------------ .../DeviceOrientationManager.java | 186 +++++++--------- .../ios/Runner.xcodeproj/project.pbxproj | 3 + .../camera/camera/ios/Classes/CameraPlugin.m | 2 +- .../camera/lib/src/camera_controller.dart | 31 +-- .../camera/camera/lib/src/camera_preview.dart | 6 +- packages/camera/camera/pubspec.yaml | 4 +- .../camera/test/camera_preview_test.dart | 6 +- packages/camera/camera/test/camera_test.dart | 4 +- .../camera/camera/test/camera_value_test.dart | 12 +- .../lib/src/events/device_event.dart | 15 +- .../method_channel/method_channel_camera.dart | 8 +- .../platform_interface/camera_platform.dart | 7 +- .../test/camera_platform_interface_test.dart | 2 +- .../test/events/device_event_test.dart | 19 +- .../method_channel_camera_test.dart | 2 +- 18 files changed, 158 insertions(+), 395 deletions(-) delete mode 100644 packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/DeviceOrientationManager.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 8365743f2094..b663891331bf 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,7 +32,6 @@ import android.os.Looper; import android.util.Log; import android.util.Size; -import android.util.SparseIntArray; import android.view.Display; import android.view.Surface; import androidx.annotation.NonNull; @@ -95,18 +94,8 @@ interface ErrorCallback { 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<>(); @@ -271,8 +260,8 @@ private void prepareMediaRecorder(String outputFilePath) throws IOException { .setEnableAudio(enableAudio) .setMediaOrientation( lockedOrientation == null - ? getDeviceOrientationManager().getMediaOrientation() - : getDeviceOrientationManager().getMediaOrientation(lockedOrientation)) + ? getDeviceOrientationManager().getVideoOrientation() + : getDeviceOrientationManager().getVideoOrientation(lockedOrientation)) .build(); } @@ -596,8 +585,14 @@ private void takePictureAfterPrecapture() { updateBuilderSettings(stillBuilder); // Orientation - int rotation = getDefaultDisplay().getRotation(); - stillBuilder.set(CaptureRequest.JPEG_ORIENTATION, getOrientation(rotation)); + final PlatformChannel.DeviceOrientation lockedOrientation = + ((SensorOrientationFeature) cameraFeatures.getSensorOrientation()) + .getLockedCaptureOrientation(); + stillBuilder.set( + CaptureRequest.JPEG_ORIENTATION, + lockedOrientation == null + ? getDeviceOrientationManager().getPhotoOrientation() + : getDeviceOrientationManager().getPhotoOrientation(lockedOrientation)); CameraCaptureSession.CaptureCallback captureCallback = new CameraCaptureSession.CaptureCallback() { @@ -665,21 +660,6 @@ private void stopBackgroundThread() { } } - /** - * 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 = cameraFeatures.getSensorOrientation().getValue(); - return (ORIENTATIONS.get(rotation) + sensorOrientation + 270) % 360; - } - /** Start capturing a picture, doing autofocus first. */ private void runPictureAutoFocus() { Log.i(TAG, "runPictureAutoFocus"); 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 dc62fce524d3..12eae0b9901b 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 @@ -25,7 +25,7 @@ public class DartMessenger { /** Specifies the different device related message types. */ enum DeviceEventType { /** Indicates the device's orientation has changed. */ - ORIENTATION_CHANGED("orientation_changed"); + UI_ORIENTATION_CHANGED("ui_orientation_changed"); private final String method; DeviceEventType(String method) { @@ -74,10 +74,10 @@ enum CameraEventType { * * @param orientation specifies the new orientation of the device. */ - public void sendDeviceOrientationChangeEvent(PlatformChannel.DeviceOrientation orientation) { + public void sendDeviceUIOrientationChangeEvent(PlatformChannel.DeviceOrientation orientation) { assert (orientation != null); this.send( - DeviceEventType.ORIENTATION_CHANGED, + DeviceEventType.UI_ORIENTATION_CHANGED, new HashMap() { { put("orientation", CameraUtils.serializeDeviceOrientation(orientation)); 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 deleted file mode 100644 index 634596dde8bb..000000000000 --- a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/DeviceOrientationManager.java +++ /dev/null @@ -1,200 +0,0 @@ -// Copyright 2013 The Flutter 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.app.Activity; -import android.content.BroadcastReceiver; -import android.content.Context; -import android.content.Intent; -import android.content.IntentFilter; -import android.content.res.Configuration; -import android.hardware.SensorManager; -import android.provider.Settings; -import android.view.Display; -import android.view.OrientationEventListener; -import android.view.Surface; -import android.view.WindowManager; -import io.flutter.embedding.engine.systemchannels.PlatformChannel; - -class DeviceOrientationManager { - - private static final IntentFilter orientationIntentFilter = - new IntentFilter(Intent.ACTION_CONFIGURATION_CHANGED); - - private final Activity activity; - private final DartMessenger messenger; - private final boolean isFrontFacing; - private final int sensorOrientation; - private PlatformChannel.DeviceOrientation lastOrientation; - private OrientationEventListener orientationEventListener; - private BroadcastReceiver broadcastReceiver; - - public DeviceOrientationManager( - Activity activity, DartMessenger messenger, boolean isFrontFacing, int sensorOrientation) { - this.activity = activity; - this.messenger = messenger; - this.isFrontFacing = isFrontFacing; - this.sensorOrientation = sensorOrientation; - } - - public void start() { - startSensorListener(); - startUIListener(); - } - - public void stop() { - stopSensorListener(); - stopUIListener(); - } - - public int getMediaOrientation() { - return this.getMediaOrientation(this.lastOrientation); - } - - public int getMediaOrientation(PlatformChannel.DeviceOrientation orientation) { - int angle = 0; - - // Fallback to device orientation when the orientation value is null - if (orientation == null) { - orientation = getUIOrientation(); - } - - switch (orientation) { - case PORTRAIT_UP: - angle = 0; - break; - case PORTRAIT_DOWN: - angle = 180; - break; - case LANDSCAPE_LEFT: - angle = 90; - break; - case LANDSCAPE_RIGHT: - angle = 270; - break; - } - if (isFrontFacing) angle *= -1; - return (angle + sensorOrientation + 360) % 360; - } - - private void startSensorListener() { - if (orientationEventListener != null) return; - orientationEventListener = - new OrientationEventListener(activity, SensorManager.SENSOR_DELAY_NORMAL) { - @Override - public void onOrientationChanged(int angle) { - if (!isSystemAutoRotationLocked()) { - PlatformChannel.DeviceOrientation newOrientation = calculateSensorOrientation(angle); - if (!newOrientation.equals(lastOrientation)) { - lastOrientation = newOrientation; - messenger.sendDeviceOrientationChangeEvent(newOrientation); - } - } - } - }; - if (orientationEventListener.canDetectOrientation()) { - orientationEventListener.enable(); - } - } - - private void startUIListener() { - if (broadcastReceiver != null) return; - broadcastReceiver = - new BroadcastReceiver() { - @Override - public void onReceive(Context context, Intent intent) { - if (isSystemAutoRotationLocked()) { - PlatformChannel.DeviceOrientation orientation = getUIOrientation(); - if (!orientation.equals(lastOrientation)) { - lastOrientation = orientation; - messenger.sendDeviceOrientationChangeEvent(orientation); - } - } - } - }; - activity.registerReceiver(broadcastReceiver, orientationIntentFilter); - broadcastReceiver.onReceive(activity, null); - } - - private void stopSensorListener() { - if (orientationEventListener == null) return; - orientationEventListener.disable(); - orientationEventListener = null; - } - - private void stopUIListener() { - if (broadcastReceiver == null) return; - activity.unregisterReceiver(broadcastReceiver); - broadcastReceiver = null; - } - - private boolean isSystemAutoRotationLocked() { - return android.provider.Settings.System.getInt( - activity.getContentResolver(), Settings.System.ACCELEROMETER_ROTATION, 0) - != 1; - } - - private PlatformChannel.DeviceOrientation getUIOrientation() { - final int rotation = getDisplay().getRotation(); - final int orientation = activity.getResources().getConfiguration().orientation; - - switch (orientation) { - case Configuration.ORIENTATION_PORTRAIT: - if (rotation == Surface.ROTATION_0 || rotation == Surface.ROTATION_90) { - return PlatformChannel.DeviceOrientation.PORTRAIT_UP; - } else { - return PlatformChannel.DeviceOrientation.PORTRAIT_DOWN; - } - case Configuration.ORIENTATION_LANDSCAPE: - if (rotation == Surface.ROTATION_0 || rotation == Surface.ROTATION_90) { - return PlatformChannel.DeviceOrientation.LANDSCAPE_LEFT; - } else { - return PlatformChannel.DeviceOrientation.LANDSCAPE_RIGHT; - } - default: - return PlatformChannel.DeviceOrientation.PORTRAIT_UP; - } - } - - private PlatformChannel.DeviceOrientation calculateSensorOrientation(int angle) { - final int tolerance = 45; - angle += tolerance; - - // Orientation is 0 in the default orientation mode. This is portait-mode for phones - // and landscape for tablets. We have to compensate for this by calculating the default - // orientation, and apply an offset accordingly. - int defaultDeviceOrientation = getDeviceDefaultOrientation(); - if (defaultDeviceOrientation == Configuration.ORIENTATION_LANDSCAPE) { - angle += 90; - } - // Determine the orientation - angle = angle % 360; - return new PlatformChannel.DeviceOrientation[] { - PlatformChannel.DeviceOrientation.PORTRAIT_UP, - PlatformChannel.DeviceOrientation.LANDSCAPE_LEFT, - PlatformChannel.DeviceOrientation.PORTRAIT_DOWN, - PlatformChannel.DeviceOrientation.LANDSCAPE_RIGHT, - } - [angle / 90]; - } - - private int getDeviceDefaultOrientation() { - Configuration config = activity.getResources().getConfiguration(); - int rotation = getDisplay().getRotation(); - if (((rotation == Surface.ROTATION_0 || rotation == Surface.ROTATION_180) - && config.orientation == Configuration.ORIENTATION_LANDSCAPE) - || ((rotation == Surface.ROTATION_90 || rotation == Surface.ROTATION_270) - && config.orientation == Configuration.ORIENTATION_PORTRAIT)) { - return Configuration.ORIENTATION_LANDSCAPE; - } else { - return Configuration.ORIENTATION_PORTRAIT; - } - } - - @SuppressWarnings("deprecation") - private Display getDisplay() { - return ((WindowManager) activity.getSystemService(Context.WINDOW_SERVICE)).getDefaultDisplay(); - } -} 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 2a04caad743a..9223b89e7905 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 @@ -10,10 +10,7 @@ import android.content.Intent; import android.content.IntentFilter; import android.content.res.Configuration; -import android.hardware.SensorManager; -import android.provider.Settings; import android.view.Display; -import android.view.OrientationEventListener; import android.view.Surface; import android.view.WindowManager; import androidx.annotation.NonNull; @@ -35,7 +32,6 @@ public class DeviceOrientationManager { private final boolean isFrontFacing; private final int sensorOrientation; private PlatformChannel.DeviceOrientation lastOrientation; - private OrientationEventListener orientationEventListener; private BroadcastReceiver broadcastReceiver; /** Factory method to create a device orientation manager. */ @@ -63,7 +59,7 @@ private DeviceOrientationManager( * *

    When orientation information is updated the new orientation is send to the client using the * {@link DartMessenger}. This latest value can also be retrieved through the {@link - * #getMediaOrientation()} accessor. + * #getVideoOrientation()} accessor. * *

    If the device's ACCELEROMETER_ROTATION setting is enabled the {@link * DeviceOrientationManager} will report orientation updates based on the sensor information. If @@ -71,52 +67,103 @@ private DeviceOrientationManager( * the deliver orientation updates based on the UI orientation. */ public void start() { - startSensorListener(); - startUIListener(); + if (broadcastReceiver != null) { + return; + } + broadcastReceiver = + new BroadcastReceiver() { + @Override + public void onReceive(Context context, Intent intent) { + handleUIOrientationChange(); + } + }; + activity.registerReceiver(broadcastReceiver, orientationIntentFilter); + broadcastReceiver.onReceive(activity, null); } /** Stops listening for orientation updates. */ public void stop() { - stopSensorListener(); - stopUIListener(); + if (broadcastReceiver == null) { + return; + } + activity.unregisterReceiver(broadcastReceiver); + broadcastReceiver = null; } /** - * Returns the last captured orientation in degrees based on sensor or UI information. + * Returns the device's photo orientation in degrees based on the sensor orientation and the last + * known UI orientation. + * + *

    Returns one of 0, 90, 180 or 270. * - *

    The orientation is returned in degrees and could be one of the following values: + * @return The device's photo orientation in degrees. + */ + public int getPhotoOrientation() { + return this.getPhotoOrientation(this.lastOrientation); + } + + /** + * Returns the device's photo orientation in degrees based on the sensor orientation and the + * supplied {@link PlatformChannel.DeviceOrientation} value. * - *

      - *
    • 0: Indicates the device is currently in portrait. - *
    • 90: Indicates the device is currently in landscape left. - *
    • 180: Indicates the device is currently in portrait down. - *
    • 270: Indicates the device is currently in landscape right. - *
    + *

    Returns one of 0, 90, 180 or 270. * - * @return The last captured orientation in degrees + * @param orientation The {@link PlatformChannel.DeviceOrientation} value that is to be converted + * into degrees. + * @return The device's photo orientation in degrees. */ - public int getMediaOrientation() { - return this.getMediaOrientation(this.lastOrientation); + public int getPhotoOrientation(PlatformChannel.DeviceOrientation orientation) { + int angle = 0; + // Fallback to device orientation when the orientation value is null + if (orientation == null) { + orientation = getUIOrientation(); + } + + switch (orientation) { + case PORTRAIT_UP: + angle = 90; + break; + case PORTRAIT_DOWN: + angle = 270; + break; + case LANDSCAPE_LEFT: + angle = isFrontFacing ? 180 : 0; + break; + case LANDSCAPE_RIGHT: + angle = isFrontFacing ? 0 : 180; + break; + } + + // 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 (angle + sensorOrientation + 270) % 360; } /** - * Returns the device's orientation in degrees based on the supplied {@link - * PlatformChannel.DeviceOrientation} value. + * Returns the device's video orientation in degrees based on the sensor orientation and the last + * known UI orientation. + * + *

    Returns one of 0, 90, 180 or 270. * - *

    + * @return The device's video orientation in degrees. + */ + public int getVideoOrientation() { + return this.getVideoOrientation(this.lastOrientation); + } + + /** + * Returns the device's video orientation in degrees based on the sensor orientation and the + * supplied {@link PlatformChannel.DeviceOrientation} value. * - *

      - *
    • PORTRAIT_UP: converts to 0 degrees. - *
    • LANDSCAPE_LEFT: converts to 90 degrees. - *
    • PORTRAIT_DOWN: converts to 180 degrees. - *
    • LANDSCAPE_RIGHT: converts to 270 degrees. - *
    + *

    Returns one of 0, 90, 180 or 270. * * @param orientation The {@link PlatformChannel.DeviceOrientation} value that is to be converted * into degrees. - * @return The device's orientation in degrees. + * @return The device's video orientation in degrees. */ - public int getMediaOrientation(PlatformChannel.DeviceOrientation orientation) { + public int getVideoOrientation(PlatformChannel.DeviceOrientation orientation) { int angle = 0; // Fallback to device orientation when the orientation value is null @@ -146,53 +193,6 @@ public int getMediaOrientation(PlatformChannel.DeviceOrientation orientation) { return (angle + sensorOrientation + 360) % 360; } - private void startSensorListener() { - if (orientationEventListener != null) { - return; - } - orientationEventListener = - new OrientationEventListener(activity, SensorManager.SENSOR_DELAY_NORMAL) { - @Override - public void onOrientationChanged(int angle) { - handleSensorOrientationChange(angle); - } - }; - if (orientationEventListener.canDetectOrientation()) { - orientationEventListener.enable(); - } - } - - private void startUIListener() { - if (broadcastReceiver != null) { - return; - } - broadcastReceiver = - new BroadcastReceiver() { - @Override - public void onReceive(Context context, Intent intent) { - handleUIOrientationChange(); - } - }; - activity.registerReceiver(broadcastReceiver, orientationIntentFilter); - broadcastReceiver.onReceive(activity, null); - } - - /** - * Handles orientation changes based on information from the device's sensors. - * - *

    This method is visible for testing purposes only and should never be used outside this - * class. - * - * @param angle of the current orientation. - */ - @VisibleForTesting - void handleSensorOrientationChange(int angle) { - if (!isAccelerometerRotationLocked()) { - PlatformChannel.DeviceOrientation orientation = calculateSensorOrientation(angle); - lastOrientation = handleOrientationChange(orientation, lastOrientation, messenger); - } - } - /** * Handles orientation changes based on change events triggered by the OrientationIntentFilter. * @@ -201,10 +201,8 @@ void handleSensorOrientationChange(int angle) { */ @VisibleForTesting void handleUIOrientationChange() { - if (isAccelerometerRotationLocked()) { - PlatformChannel.DeviceOrientation orientation = getUIOrientation(); - lastOrientation = handleOrientationChange(orientation, lastOrientation, messenger); - } + PlatformChannel.DeviceOrientation orientation = getUIOrientation(); + lastOrientation = handleOrientationChange(orientation, lastOrientation, messenger); } /** @@ -220,34 +218,12 @@ static DeviceOrientation handleOrientationChange( DeviceOrientation previousOrientation, DartMessenger messenger) { if (!newOrientation.equals(previousOrientation)) { - messenger.sendDeviceOrientationChangeEvent(newOrientation); + messenger.sendDeviceUIOrientationChangeEvent(newOrientation); } return newOrientation; } - private void stopSensorListener() { - if (orientationEventListener == null) { - return; - } - orientationEventListener.disable(); - orientationEventListener = null; - } - - private void stopUIListener() { - if (broadcastReceiver == null) { - return; - } - activity.unregisterReceiver(broadcastReceiver); - broadcastReceiver = null; - } - - private boolean isAccelerometerRotationLocked() { - return android.provider.Settings.System.getInt( - activity.getContentResolver(), Settings.System.ACCELEROMETER_ROTATION, 0) - != 1; - } - /** * Gets the current user interface orientation. * diff --git a/packages/camera/camera/example/ios/Runner.xcodeproj/project.pbxproj b/packages/camera/camera/example/ios/Runner.xcodeproj/project.pbxproj index aead167a5e99..1a3a9195ad1e 100644 --- a/packages/camera/camera/example/ios/Runner.xcodeproj/project.pbxproj +++ b/packages/camera/camera/example/ios/Runner.xcodeproj/project.pbxproj @@ -235,6 +235,7 @@ }; 97C146ED1CF9000F007C117D = { CreatedOnToolsVersion = 7.3.1; + DevelopmentTeam = 7624MWN53C; }; }; }; @@ -563,6 +564,7 @@ baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */; buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + DEVELOPMENT_TEAM = 7624MWN53C; ENABLE_BITCODE = NO; FRAMEWORK_SEARCH_PATHS = ( "$(inherited)", @@ -584,6 +586,7 @@ baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + DEVELOPMENT_TEAM = 7624MWN53C; ENABLE_BITCODE = NO; FRAMEWORK_SEARCH_PATHS = ( "$(inherited)", diff --git a/packages/camera/camera/ios/Classes/CameraPlugin.m b/packages/camera/camera/ios/Classes/CameraPlugin.m index ebd5366ba78d..d5a9448901f0 100644 --- a/packages/camera/camera/ios/Classes/CameraPlugin.m +++ b/packages/camera/camera/ios/Classes/CameraPlugin.m @@ -1306,7 +1306,7 @@ - (void)orientationChanged:(NSNotification *)note { - (void)sendDeviceOrientation:(UIDeviceOrientation)orientation { [_deviceEventMethodChannel - invokeMethod:@"orientation_changed" + invokeMethod:@"ui_orientation_changed" arguments:@{@"orientation" : getStringForUIDeviceOrientation(orientation)}]; } diff --git a/packages/camera/camera/lib/src/camera_controller.dart b/packages/camera/camera/lib/src/camera_controller.dart index 3284a9b01fa2..1c9902c75cf0 100644 --- a/packages/camera/camera/lib/src/camera_controller.dart +++ b/packages/camera/camera/lib/src/camera_controller.dart @@ -44,7 +44,7 @@ class CameraValue { required this.focusMode, required this.exposurePointSupported, required this.focusPointSupported, - required this.deviceOrientation, + required this.deviceUIOrientation, this.lockedCaptureOrientation, this.recordingOrientation, }) : _isRecordingPaused = isRecordingPaused; @@ -62,7 +62,7 @@ class CameraValue { exposurePointSupported: false, focusMode: FocusMode.auto, focusPointSupported: false, - deviceOrientation: DeviceOrientation.portraitUp, + deviceUIOrientation: DeviceOrientation.portraitUp, ); /// True after [CameraController.initialize] has completed successfully. @@ -118,8 +118,8 @@ class CameraValue { /// Whether setting the focus point is supported. final bool focusPointSupported; - /// The current device orientation. - final DeviceOrientation deviceOrientation; + /// The current device UI orientation. + final DeviceOrientation deviceUIOrientation; /// The currently locked capture orientation. final DeviceOrientation? lockedCaptureOrientation; @@ -147,7 +147,7 @@ class CameraValue { FocusMode? focusMode, bool? exposurePointSupported, bool? focusPointSupported, - DeviceOrientation? deviceOrientation, + DeviceOrientation? deviceUIOrientation, Optional? lockedCaptureOrientation, Optional? recordingOrientation, }) { @@ -165,7 +165,7 @@ class CameraValue { exposurePointSupported: exposurePointSupported ?? this.exposurePointSupported, focusPointSupported: focusPointSupported ?? this.focusPointSupported, - deviceOrientation: deviceOrientation ?? this.deviceOrientation, + deviceUIOrientation: deviceUIOrientation ?? this.deviceUIOrientation, lockedCaptureOrientation: lockedCaptureOrientation == null ? this.lockedCaptureOrientation : lockedCaptureOrientation.orNull, @@ -188,7 +188,7 @@ class CameraValue { 'focusMode: $focusMode, ' 'exposurePointSupported: $exposurePointSupported, ' 'focusPointSupported: $focusPointSupported, ' - 'deviceOrientation: $deviceOrientation, ' + 'deviceUIOrientation: $deviceUIOrientation, ' 'lockedCaptureOrientation: $lockedCaptureOrientation, ' 'recordingOrientation: $recordingOrientation)'; } @@ -237,7 +237,7 @@ class CameraController extends ValueNotifier { bool _isDisposed = false; StreamSubscription? _imageStreamSubscription; FutureOr? _initCalled; - StreamSubscription? _deviceOrientationSubscription; + StreamSubscription? _deviceUIOrientationSubscription; /// Checks whether [CameraController.dispose] has completed successfully. /// @@ -262,10 +262,11 @@ class CameraController extends ValueNotifier { try { Completer _initializeCompleter = Completer(); - _deviceOrientationSubscription = - CameraPlatform.instance.onDeviceOrientationChanged().listen((event) { + _deviceUIOrientationSubscription = CameraPlatform.instance + .onDeviceUIOrientationChanged() + .listen((event) { value = value.copyWith( - deviceOrientation: event.orientation, + deviceUIOrientation: event.orientation, ); }); @@ -457,7 +458,7 @@ class CameraController extends ValueNotifier { isRecordingVideo: true, isRecordingPaused: false, recordingOrientation: Optional.fromNullable( - value.lockedCaptureOrientation ?? value.deviceOrientation)); + value.lockedCaptureOrientation ?? value.deviceUIOrientation)); } on PlatformException catch (e) { throw CameraException(e.code, e.message); } @@ -695,10 +696,10 @@ class CameraController extends ValueNotifier { Future lockCaptureOrientation([DeviceOrientation? orientation]) async { try { await CameraPlatform.instance.lockCaptureOrientation( - _cameraId, orientation ?? value.deviceOrientation); + _cameraId, orientation ?? value.deviceUIOrientation); value = value.copyWith( lockedCaptureOrientation: - Optional.fromNullable(orientation ?? value.deviceOrientation)); + Optional.fromNullable(orientation ?? value.deviceUIOrientation)); } on PlatformException catch (e) { throw CameraException(e.code, e.message); } @@ -755,7 +756,7 @@ class CameraController extends ValueNotifier { if (_isDisposed) { return; } - unawaited(_deviceOrientationSubscription?.cancel()); + unawaited(_deviceUIOrientationSubscription?.cancel()); _isDisposed = true; super.dispose(); if (_initCalled != null) { diff --git a/packages/camera/camera/lib/src/camera_preview.dart b/packages/camera/camera/lib/src/camera_preview.dart index e2f1ff931e42..6d863ad8444c 100644 --- a/packages/camera/camera/lib/src/camera_preview.dart +++ b/packages/camera/camera/lib/src/camera_preview.dart @@ -55,9 +55,9 @@ class CameraPreview extends StatelessWidget { int _getQuarterTurns() { Map turns = { DeviceOrientation.portraitUp: 0, - DeviceOrientation.landscapeLeft: 1, + DeviceOrientation.landscapeRight: 1, DeviceOrientation.portraitDown: 2, - DeviceOrientation.landscapeRight: 3, + DeviceOrientation.landscapeLeft: 3, }; return turns[_getApplicableOrientation()]!; } @@ -66,6 +66,6 @@ class CameraPreview extends StatelessWidget { return controller.value.isRecordingVideo ? controller.value.recordingOrientation! : (controller.value.lockedCaptureOrientation ?? - controller.value.deviceOrientation); + controller.value.deviceUIOrientation); } } diff --git a/packages/camera/camera/pubspec.yaml b/packages/camera/camera/pubspec.yaml index 78cad5215afb..71524d84acc5 100644 --- a/packages/camera/camera/pubspec.yaml +++ b/packages/camera/camera/pubspec.yaml @@ -20,7 +20,9 @@ flutter: pluginClass: CameraPlugin dependencies: - camera_platform_interface: ^2.0.0 +# camera_platform_interface: ^2.0.0 + camera_platform_interface: + path: ../camera_platform_interface flutter: sdk: flutter pedantic: ^1.10.0 diff --git a/packages/camera/camera/test/camera_preview_test.dart b/packages/camera/camera/test/camera_preview_test.dart index d579341c0e58..8e48d8ecf921 100644 --- a/packages/camera/camera/test/camera_preview_test.dart +++ b/packages/camera/camera/test/camera_preview_test.dart @@ -128,7 +128,7 @@ void main() { controller.value = controller.value.copyWith( isInitialized: true, isRecordingVideo: true, - deviceOrientation: DeviceOrientation.portraitUp, + deviceUIOrientation: DeviceOrientation.portraitUp, lockedCaptureOrientation: Optional.fromNullable(DeviceOrientation.landscapeRight), recordingOrientation: @@ -161,7 +161,7 @@ void main() { final FakeController controller = FakeController(); controller.value = controller.value.copyWith( isInitialized: true, - deviceOrientation: DeviceOrientation.portraitUp, + deviceUIOrientation: DeviceOrientation.portraitUp, lockedCaptureOrientation: Optional.fromNullable(DeviceOrientation.landscapeRight), recordingOrientation: @@ -194,7 +194,7 @@ void main() { final FakeController controller = FakeController(); controller.value = controller.value.copyWith( isInitialized: true, - deviceOrientation: DeviceOrientation.portraitUp, + deviceUIOrientation: DeviceOrientation.portraitUp, lockedCaptureOrientation: null, recordingOrientation: Optional.fromNullable(DeviceOrientation.landscapeLeft), diff --git a/packages/camera/camera/test/camera_test.dart b/packages/camera/camera/test/camera_test.dart index 26382a9b7d60..c8088d28d977 100644 --- a/packages/camera/camera/test/camera_test.dart +++ b/packages/camera/camera/test/camera_test.dart @@ -39,7 +39,7 @@ get mockOnCameraInitializedEvent => CameraInitializedEvent( ); get mockOnDeviceOrientationChangedEvent => - DeviceOrientationChangedEvent(DeviceOrientation.portraitUp); + DeviceUIOrientationChangedEvent(DeviceOrientation.portraitUp); get mockOnCameraClosingEvent => null; @@ -1287,7 +1287,7 @@ class MockCameraPlatform extends Mock Stream.value(mockOnCameraErrorEvent); @override - Stream onDeviceOrientationChanged() => + Stream onDeviceUIOrientationChanged() => Stream.value(mockOnDeviceOrientationChangedEvent); @override diff --git a/packages/camera/camera/test/camera_value_test.dart b/packages/camera/camera/test/camera_value_test.dart index e0378cca2cb9..3d61c1587497 100644 --- a/packages/camera/camera/test/camera_value_test.dart +++ b/packages/camera/camera/test/camera_value_test.dart @@ -25,7 +25,7 @@ void main() { exposureMode: ExposureMode.auto, exposurePointSupported: true, focusMode: FocusMode.auto, - deviceOrientation: DeviceOrientation.portraitUp, + deviceUIOrientation: DeviceOrientation.portraitUp, lockedCaptureOrientation: DeviceOrientation.portraitUp, recordingOrientation: DeviceOrientation.portraitUp, focusPointSupported: true, @@ -42,7 +42,7 @@ void main() { expect(cameraValue.flashMode, FlashMode.auto); expect(cameraValue.exposureMode, ExposureMode.auto); expect(cameraValue.exposurePointSupported, true); - expect(cameraValue.deviceOrientation, DeviceOrientation.portraitUp); + expect(cameraValue.deviceUIOrientation, DeviceOrientation.portraitUp); expect( cameraValue.lockedCaptureOrientation, DeviceOrientation.portraitUp); expect(cameraValue.recordingOrientation, DeviceOrientation.portraitUp); @@ -63,7 +63,7 @@ void main() { expect(cameraValue.exposureMode, ExposureMode.auto); expect(cameraValue.exposurePointSupported, false); expect(cameraValue.focusMode, FocusMode.auto); - expect(cameraValue.deviceOrientation, DeviceOrientation.portraitUp); + expect(cameraValue.deviceUIOrientation, DeviceOrientation.portraitUp); expect(cameraValue.lockedCaptureOrientation, null); expect(cameraValue.recordingOrientation, null); }); @@ -84,7 +84,7 @@ void main() { expect(cameraValue.focusMode, FocusMode.auto); expect(cameraValue.exposureMode, ExposureMode.auto); expect(cameraValue.exposurePointSupported, false); - expect(cameraValue.deviceOrientation, DeviceOrientation.portraitUp); + expect(cameraValue.deviceUIOrientation, DeviceOrientation.portraitUp); expect(cameraValue.lockedCaptureOrientation, null); expect(cameraValue.recordingOrientation, null); }); @@ -129,13 +129,13 @@ void main() { focusMode: FocusMode.auto, exposurePointSupported: true, focusPointSupported: true, - deviceOrientation: DeviceOrientation.portraitUp, + deviceUIOrientation: DeviceOrientation.portraitUp, lockedCaptureOrientation: DeviceOrientation.portraitUp, recordingOrientation: DeviceOrientation.portraitUp, ); expect(cameraValue.toString(), - 'CameraValue(isRecordingVideo: false, isInitialized: false, errorDescription: null, previewSize: Size(10.0, 10.0), isStreamingImages: false, flashMode: FlashMode.auto, exposureMode: ExposureMode.auto, focusMode: FocusMode.auto, exposurePointSupported: true, focusPointSupported: true, deviceOrientation: DeviceOrientation.portraitUp, lockedCaptureOrientation: DeviceOrientation.portraitUp, recordingOrientation: DeviceOrientation.portraitUp)'); + 'CameraValue(isRecordingVideo: false, isInitialized: false, errorDescription: null, previewSize: Size(10.0, 10.0), isStreamingImages: false, flashMode: FlashMode.auto, exposureMode: ExposureMode.auto, focusMode: FocusMode.auto, exposurePointSupported: true, focusPointSupported: true, deviceUIOrientation: DeviceOrientation.portraitUp, lockedCaptureOrientation: DeviceOrientation.portraitUp, recordingOrientation: DeviceOrientation.portraitUp)'); }); }); } diff --git a/packages/camera/camera_platform_interface/lib/src/events/device_event.dart b/packages/camera/camera_platform_interface/lib/src/events/device_event.dart index c6cedd135fed..a59491304f58 100644 --- a/packages/camera/camera_platform_interface/lib/src/events/device_event.dart +++ b/packages/camera/camera_platform_interface/lib/src/events/device_event.dart @@ -20,21 +20,20 @@ import 'package:flutter/services.dart'; /// They can be (and in fact, are) filtered by the `instanceof`-operator. abstract class DeviceEvent {} -/// The [DeviceOrientationChangedEvent] is fired every time the user changes the -/// physical orientation of the device. -class DeviceOrientationChangedEvent extends DeviceEvent { +/// The [DeviceUIOrientationChangedEvent] is fired every time the orientation of the device UI changes. +class DeviceUIOrientationChangedEvent extends DeviceEvent { /// The new orientation of the device final DeviceOrientation orientation; /// Build a new orientation changed event. - DeviceOrientationChangedEvent(this.orientation); + DeviceUIOrientationChangedEvent(this.orientation); - /// Converts the supplied [Map] to an instance of the [DeviceOrientationChangedEvent] + /// Converts the supplied [Map] to an instance of the [DeviceUIOrientationChangedEvent] /// class. - DeviceOrientationChangedEvent.fromJson(Map json) + DeviceUIOrientationChangedEvent.fromJson(Map json) : orientation = deserializeDeviceOrientation(json['orientation']); - /// Converts the [DeviceOrientationChangedEvent] instance into a [Map] instance that + /// Converts the [DeviceUIOrientationChangedEvent] instance into a [Map] instance that /// can be serialized to JSON. Map toJson() => { 'orientation': serializeDeviceOrientation(orientation), @@ -43,7 +42,7 @@ class DeviceOrientationChangedEvent extends DeviceEvent { @override bool operator ==(Object other) => identical(this, other) || - other is DeviceOrientationChangedEvent && + other is DeviceUIOrientationChangedEvent && runtimeType == other.runtimeType && orientation == other.orientation; diff --git a/packages/camera/camera_platform_interface/lib/src/method_channel/method_channel_camera.dart b/packages/camera/camera_platform_interface/lib/src/method_channel/method_channel_camera.dart index c6c363a56d65..927edf573c13 100644 --- a/packages/camera/camera_platform_interface/lib/src/method_channel/method_channel_camera.dart +++ b/packages/camera/camera_platform_interface/lib/src/method_channel/method_channel_camera.dart @@ -169,9 +169,9 @@ class MethodChannelCamera extends CameraPlatform { } @override - Stream onDeviceOrientationChanged() { + Stream onDeviceUIOrientationChanged() { return deviceEventStreamController.stream - .whereType(); + .whereType(); } @override @@ -446,8 +446,8 @@ class MethodChannelCamera extends CameraPlatform { /// the plugin as it may break or change at any time. Future handleDeviceMethodCall(MethodCall call) async { switch (call.method) { - case 'orientation_changed': - deviceEventStreamController.add(DeviceOrientationChangedEvent( + case 'ui_orientation_changed': + deviceEventStreamController.add(DeviceUIOrientationChangedEvent( deserializeDeviceOrientation(call.arguments['orientation']))); break; default: diff --git a/packages/camera/camera_platform_interface/lib/src/platform_interface/camera_platform.dart b/packages/camera/camera_platform_interface/lib/src/platform_interface/camera_platform.dart index 4437d3b0593a..ba73fd87028c 100644 --- a/packages/camera/camera_platform_interface/lib/src/platform_interface/camera_platform.dart +++ b/packages/camera/camera_platform_interface/lib/src/platform_interface/camera_platform.dart @@ -96,14 +96,13 @@ abstract class CameraPlatform extends PlatformInterface { throw UnimplementedError('onCameraTimeLimitReached() is not implemented.'); } - /// The device orientation changed. + /// The ui orientation changed. /// /// Implementations for this: /// - Should support all 4 orientations. - /// - Should not emit new values when the screen orientation is locked. - Stream onDeviceOrientationChanged() { + Stream onDeviceUIOrientationChanged() { throw UnimplementedError( - 'onDeviceOrientationChanged() is not implemented.'); + 'onDeviceUIOrientationChanged() is not implemented.'); } /// Locks the capture orientation. diff --git a/packages/camera/camera_platform_interface/test/camera_platform_interface_test.dart b/packages/camera/camera_platform_interface/test/camera_platform_interface_test.dart index c8f38efc4e2d..259f84fb394c 100644 --- a/packages/camera/camera_platform_interface/test/camera_platform_interface_test.dart +++ b/packages/camera/camera_platform_interface/test/camera_platform_interface_test.dart @@ -98,7 +98,7 @@ void main() { // Act & Assert expect( - () => cameraPlatform.onDeviceOrientationChanged(), + () => cameraPlatform.onDeviceUIOrientationChanged(), throwsUnimplementedError, ); }); diff --git a/packages/camera/camera_platform_interface/test/events/device_event_test.dart b/packages/camera/camera_platform_interface/test/events/device_event_test.dart index f7cb657725a9..3e5e8fa013d0 100644 --- a/packages/camera/camera_platform_interface/test/events/device_event_test.dart +++ b/packages/camera/camera_platform_interface/test/events/device_event_test.dart @@ -11,13 +11,14 @@ void main() { group('DeviceOrientationChangedEvent tests', () { test('Constructor should initialize all properties', () { - final event = DeviceOrientationChangedEvent(DeviceOrientation.portraitUp); + final event = + DeviceUIOrientationChangedEvent(DeviceOrientation.portraitUp); expect(event.orientation, DeviceOrientation.portraitUp); }); test('fromJson should initialize all properties', () { - final event = DeviceOrientationChangedEvent.fromJson({ + final event = DeviceUIOrientationChangedEvent.fromJson({ 'orientation': 'portraitUp', }); @@ -25,7 +26,8 @@ void main() { }); test('toJson should return a map with all fields', () { - final event = DeviceOrientationChangedEvent(DeviceOrientation.portraitUp); + final event = + DeviceUIOrientationChangedEvent(DeviceOrientation.portraitUp); final jsonMap = event.toJson(); @@ -35,24 +37,25 @@ void main() { test('equals should return true if objects are the same', () { final firstEvent = - DeviceOrientationChangedEvent(DeviceOrientation.portraitUp); + DeviceUIOrientationChangedEvent(DeviceOrientation.portraitUp); final secondEvent = - DeviceOrientationChangedEvent(DeviceOrientation.portraitUp); + DeviceUIOrientationChangedEvent(DeviceOrientation.portraitUp); expect(firstEvent == secondEvent, true); }); test('equals should return false if orientation is different', () { final firstEvent = - DeviceOrientationChangedEvent(DeviceOrientation.portraitUp); + DeviceUIOrientationChangedEvent(DeviceOrientation.portraitUp); final secondEvent = - DeviceOrientationChangedEvent(DeviceOrientation.landscapeLeft); + DeviceUIOrientationChangedEvent(DeviceOrientation.landscapeLeft); expect(firstEvent == secondEvent, false); }); test('hashCode should match hashCode of all properties', () { - final event = DeviceOrientationChangedEvent(DeviceOrientation.portraitUp); + final event = + DeviceUIOrientationChangedEvent(DeviceOrientation.portraitUp); final expectedHashCode = event.orientation.hashCode; expect(event.hashCode, expectedHashCode); diff --git a/packages/camera/camera_platform_interface/test/method_channel/method_channel_camera_test.dart b/packages/camera/camera_platform_interface/test/method_channel/method_channel_camera_test.dart index 8a618545535b..b0f038ce9afe 100644 --- a/packages/camera/camera_platform_interface/test/method_channel/method_channel_camera_test.dart +++ b/packages/camera/camera_platform_interface/test/method_channel/method_channel_camera_test.dart @@ -356,7 +356,7 @@ void main() { // Emit test events final event = - DeviceOrientationChangedEvent(DeviceOrientation.portraitUp); + DeviceUIOrientationChangedEvent(DeviceOrientation.portraitUp); await camera.handleDeviceMethodCall( MethodCall('orientation_changed', event.toJson())); await camera.handleDeviceMethodCall( From 5e3eac84571fb1bc636a0861bdf9c76dab34ab15 Mon Sep 17 00:00:00 2001 From: "Bodhi Mulders (BeMacized)" Date: Tue, 29 Jun 2021 16:59:57 +0200 Subject: [PATCH 52/63] Fix unit tests --- .../plugins/camera/DartMessengerTest.java | 4 +- .../DeviceOrientationManagerTest.java | 104 +++++++++--------- .../camera/test/camera_preview_test.dart | 4 +- 3 files changed, 54 insertions(+), 58 deletions(-) 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 0a2fc43d03cb..b89563cc1553 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 @@ -105,12 +105,12 @@ public void sendCameraClosingEvent() { @Test public void sendDeviceOrientationChangedEvent() { doAnswer(createPostHandlerAnswer()).when(mockHandler).post(any(Runnable.class)); - dartMessenger.sendDeviceOrientationChangeEvent(PlatformChannel.DeviceOrientation.PORTRAIT_UP); + dartMessenger.sendDeviceUIOrientationChangeEvent(PlatformChannel.DeviceOrientation.PORTRAIT_UP); List sentMessages = fakeBinaryMessenger.getMessages(); assertEquals(1, sentMessages.size()); MethodCall call = decodeSentMessage(sentMessages.get(0)); - assertEquals("orientation_changed", call.method); + assertEquals("ui_orientation_changed", call.method); assertEquals(call.argument("orientation"), "portraitUp"); } diff --git a/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/features/sensororientation/DeviceOrientationManagerTest.java b/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/features/sensororientation/DeviceOrientationManagerTest.java index 6e8d04d20e99..910759e6e57f 100644 --- a/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/features/sensororientation/DeviceOrientationManagerTest.java +++ b/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/features/sensororientation/DeviceOrientationManagerTest.java @@ -50,15 +50,15 @@ public void before() { } @Test - public void getMediaOrientation_when_natural_screen_orientation_equals_portrait_up() { + public void getVideoOrientation_when_natural_screen_orientation_equals_portrait_up() { int degreesPortraitUp = - deviceOrientationManager.getMediaOrientation(DeviceOrientation.PORTRAIT_UP); + deviceOrientationManager.getVideoOrientation(DeviceOrientation.PORTRAIT_UP); int degreesPortraitDown = - deviceOrientationManager.getMediaOrientation(DeviceOrientation.PORTRAIT_DOWN); + deviceOrientationManager.getVideoOrientation(DeviceOrientation.PORTRAIT_DOWN); int degreesLandscapeLeft = - deviceOrientationManager.getMediaOrientation(DeviceOrientation.LANDSCAPE_LEFT); + deviceOrientationManager.getVideoOrientation(DeviceOrientation.LANDSCAPE_LEFT); int degreesLandscapeRight = - deviceOrientationManager.getMediaOrientation(DeviceOrientation.LANDSCAPE_RIGHT); + deviceOrientationManager.getVideoOrientation(DeviceOrientation.LANDSCAPE_RIGHT); assertEquals(0, degreesPortraitUp); assertEquals(90, degreesLandscapeLeft); @@ -67,17 +67,17 @@ public void getMediaOrientation_when_natural_screen_orientation_equals_portrait_ } @Test - public void getMediaOrientation_when_natural_screen_orientation_equals_landscape_left() { + public void getVideoOrientation_when_natural_screen_orientation_equals_landscape_left() { DeviceOrientationManager orientationManager = DeviceOrientationManager.create(mockActivity, mockDartMessenger, false, 90); - int degreesPortraitUp = orientationManager.getMediaOrientation(DeviceOrientation.PORTRAIT_UP); + int degreesPortraitUp = orientationManager.getVideoOrientation(DeviceOrientation.PORTRAIT_UP); int degreesPortraitDown = - orientationManager.getMediaOrientation(DeviceOrientation.PORTRAIT_DOWN); + orientationManager.getVideoOrientation(DeviceOrientation.PORTRAIT_DOWN); int degreesLandscapeLeft = - orientationManager.getMediaOrientation(DeviceOrientation.LANDSCAPE_LEFT); + orientationManager.getVideoOrientation(DeviceOrientation.LANDSCAPE_LEFT); int degreesLandscapeRight = - orientationManager.getMediaOrientation(DeviceOrientation.LANDSCAPE_RIGHT); + orientationManager.getVideoOrientation(DeviceOrientation.LANDSCAPE_RIGHT); assertEquals(90, degreesPortraitUp); assertEquals(180, degreesLandscapeLeft); @@ -86,79 +86,75 @@ public void getMediaOrientation_when_natural_screen_orientation_equals_landscape } @Test - public void getMediaOrientation_should_fallback_to_sensor_orientation_when_orientation_is_null() { + public void getVideoOrientation_should_fallback_to_sensor_orientation_when_orientation_is_null() { setUpUIOrientationMocks(Configuration.ORIENTATION_LANDSCAPE, Surface.ROTATION_0); - int degrees = deviceOrientationManager.getMediaOrientation(null); + int degrees = deviceOrientationManager.getVideoOrientation(null); assertEquals(90, degrees); } @Test - public void handleSensorOrientationChange_should_send_message_when_sensor_access_is_allowed() { - try (MockedStatic mockedSystem = mockStatic(Settings.System.class)) { - mockedSystem - .when( - () -> - Settings.System.getInt(any(), eq(Settings.System.ACCELEROMETER_ROTATION), eq(0))) - .thenReturn(1); - setUpUIOrientationMocks(Configuration.ORIENTATION_PORTRAIT, Surface.ROTATION_0); - - deviceOrientationManager.handleSensorOrientationChange(90); - } + public void getPhotoOrientation_when_natural_screen_orientation_equals_portrait_up() { + int degreesPortraitUp = + deviceOrientationManager.getPhotoOrientation(DeviceOrientation.PORTRAIT_UP); + int degreesPortraitDown = + deviceOrientationManager.getPhotoOrientation(DeviceOrientation.PORTRAIT_DOWN); + int degreesLandscapeLeft = + deviceOrientationManager.getPhotoOrientation(DeviceOrientation.LANDSCAPE_LEFT); + int degreesLandscapeRight = + deviceOrientationManager.getPhotoOrientation(DeviceOrientation.LANDSCAPE_RIGHT); - verify(mockDartMessenger, times(1)) - .sendDeviceOrientationChangeEvent(DeviceOrientation.LANDSCAPE_LEFT); + assertEquals(0, degreesPortraitUp); + assertEquals(90, degreesLandscapeRight); + assertEquals(180, degreesPortraitDown); + assertEquals(270, degreesLandscapeLeft); } @Test - public void - handleSensorOrientationChange_should_send_message_when_sensor_access_is_not_allowed() { - try (MockedStatic mockedSystem = mockStatic(Settings.System.class)) { - mockedSystem - .when( - () -> - Settings.System.getInt(any(), eq(Settings.System.ACCELEROMETER_ROTATION), eq(0))) - .thenReturn(0); - setUpUIOrientationMocks(Configuration.ORIENTATION_PORTRAIT, Surface.ROTATION_0); + public void getPhotoOrientation_when_natural_screen_orientation_equals_landscape_left() { + DeviceOrientationManager orientationManager = + DeviceOrientationManager.create(mockActivity, mockDartMessenger, false, 90); - deviceOrientationManager.handleSensorOrientationChange(90); - } + int degreesPortraitUp = orientationManager.getPhotoOrientation(DeviceOrientation.PORTRAIT_UP); + int degreesPortraitDown = + orientationManager.getPhotoOrientation(DeviceOrientation.PORTRAIT_DOWN); + int degreesLandscapeLeft = + orientationManager.getPhotoOrientation(DeviceOrientation.LANDSCAPE_LEFT); + int degreesLandscapeRight = + orientationManager.getPhotoOrientation(DeviceOrientation.LANDSCAPE_RIGHT); - verify(mockDartMessenger, never()).sendDeviceOrientationChangeEvent(any()); + assertEquals(90, degreesPortraitUp); + assertEquals(180, degreesLandscapeRight); + assertEquals(270, degreesPortraitDown); + assertEquals(0, degreesLandscapeLeft); } @Test - public void handleUIOrientationChange_should_send_message_when_sensor_access_is_allowed() { - try (MockedStatic mockedSystem = mockStatic(Settings.System.class)) { - mockedSystem - .when( - () -> - Settings.System.getInt(any(), eq(Settings.System.ACCELEROMETER_ROTATION), eq(0))) - .thenReturn(0); - setUpUIOrientationMocks(Configuration.ORIENTATION_LANDSCAPE, Surface.ROTATION_0); + public void + getPhotoOrientation_should_fallback_to_current_orientation_when_orientation_is_null() { + setUpUIOrientationMocks(Configuration.ORIENTATION_LANDSCAPE, Surface.ROTATION_0); - deviceOrientationManager.handleUIOrientationChange(); - } + int degrees = deviceOrientationManager.getPhotoOrientation(null); - verify(mockDartMessenger, times(1)) - .sendDeviceOrientationChangeEvent(DeviceOrientation.LANDSCAPE_LEFT); + assertEquals(270, degrees); } @Test - public void handleUIOrientationChange_should_send_message_when_sensor_access_is_not_allowed() { + public void handleUIOrientationChange_should_send_message_when_sensor_access_is_allowed() { try (MockedStatic mockedSystem = mockStatic(Settings.System.class)) { mockedSystem .when( () -> Settings.System.getInt(any(), eq(Settings.System.ACCELEROMETER_ROTATION), eq(0))) - .thenReturn(1); + .thenReturn(0); setUpUIOrientationMocks(Configuration.ORIENTATION_LANDSCAPE, Surface.ROTATION_0); deviceOrientationManager.handleUIOrientationChange(); } - verify(mockDartMessenger, never()).sendDeviceOrientationChangeEvent(any()); + verify(mockDartMessenger, times(1)) + .sendDeviceUIOrientationChangeEvent(DeviceOrientation.LANDSCAPE_LEFT); } @Test @@ -170,7 +166,7 @@ public void handleOrientationChange_should_send_message_when_orientation_is_upda DeviceOrientationManager.handleOrientationChange( newOrientation, previousOrientation, mockDartMessenger); - verify(mockDartMessenger, times(1)).sendDeviceOrientationChangeEvent(newOrientation); + verify(mockDartMessenger, times(1)).sendDeviceUIOrientationChangeEvent(newOrientation); assertEquals(newOrientation, orientation); } @@ -183,7 +179,7 @@ public void handleOrientationChange_should_not_send_message_when_orientation_is_ DeviceOrientationManager.handleOrientationChange( newOrientation, previousOrientation, mockDartMessenger); - verify(mockDartMessenger, never()).sendDeviceOrientationChangeEvent(any()); + verify(mockDartMessenger, never()).sendDeviceUIOrientationChangeEvent(any()); assertEquals(newOrientation, orientation); } diff --git a/packages/camera/camera/test/camera_preview_test.dart b/packages/camera/camera/test/camera_preview_test.dart index 8e48d8ecf921..290230a18609 100644 --- a/packages/camera/camera/test/camera_preview_test.dart +++ b/packages/camera/camera/test/camera_preview_test.dart @@ -146,7 +146,7 @@ void main() { RotatedBox rotatedBox = tester.widget(find.byType(RotatedBox)); - expect(rotatedBox.quarterTurns, 1); + expect(rotatedBox.quarterTurns, 3); debugDefaultTargetPlatformOverride = null; }); @@ -179,7 +179,7 @@ void main() { RotatedBox rotatedBox = tester.widget(find.byType(RotatedBox)); - expect(rotatedBox.quarterTurns, 3); + expect(rotatedBox.quarterTurns, 1); debugDefaultTargetPlatformOverride = null; }); From 9779a28756d0d73054ef1c4f2132eb74aa0f5fbe Mon Sep 17 00:00:00 2001 From: BeMacized Date: Thu, 8 Jul 2021 13:58:06 +0200 Subject: [PATCH 53/63] Fix rotation issues with exposure- and focus point features. --- .../io/flutter/plugins/camera/Camera.java | 23 +-- .../plugins/camera/CameraRegionUtils.java | 25 +++- .../camera/features/CameraFeatureFactory.java | 10 +- .../features/CameraFeatureFactoryImpl.java | 130 ++++++++--------- .../exposurepoint/ExposurePointFeature.java | 14 +- .../focuspoint/FocusPointFeature.java | 131 ++++++++++-------- .../DeviceOrientationManager.java | 14 +- 7 files changed, 204 insertions(+), 143 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 b663891331bf..ae589f21e1a9 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 @@ -198,20 +198,21 @@ public Camera( cameraFeatureFactory.createExposureLockFeature(cameraProperties)); this.cameraFeatures.setExposureOffset( cameraFeatureFactory.createExposureOffsetFeature(cameraProperties)); + SensorOrientationFeature sensorOrientationFeature = cameraFeatureFactory.createSensorOrientationFeature( + cameraProperties, activity, dartMessenger); + this.cameraFeatures.setSensorOrientation(sensorOrientationFeature); this.cameraFeatures.setExposurePoint( - cameraFeatureFactory.createExposurePointFeature(cameraProperties)); + cameraFeatureFactory.createExposurePointFeature(cameraProperties, sensorOrientationFeature)); this.cameraFeatures.setFlash(cameraFeatureFactory.createFlashFeature(cameraProperties)); this.cameraFeatures.setFocusPoint( - cameraFeatureFactory.createFocusPointFeature(cameraProperties)); + cameraFeatureFactory.createFocusPointFeature(cameraProperties, sensorOrientationFeature)); this.cameraFeatures.setFpsRange(cameraFeatureFactory.createFpsRangeFeature(cameraProperties)); this.cameraFeatures.setNoiseReduction( cameraFeatureFactory.createNoiseReductionFeature(cameraProperties)); this.cameraFeatures.setResolution( cameraFeatureFactory.createResolutionFeature( cameraProperties, resolutionPreset, cameraProperties.getCameraName())); - this.cameraFeatures.setSensorOrientation( - cameraFeatureFactory.createSensorOrientationFeature( - cameraProperties, activity, dartMessenger)); + this.cameraFeatures.setZoomLevel(cameraFeatureFactory.createZoomLevelFeature(cameraProperties)); // Create capture callback @@ -888,7 +889,7 @@ public double getExposureOffsetStepSize() { * @param result Flutter result. * @param newMode New mode. */ - public void setFocusMode(@NonNull final Result result, FocusMode newMode) { + public void setFocusMode(final Result result, FocusMode newMode) { final AutoFocusFeature autoFocusFeature = cameraFeatures.getAutoFocus(); autoFocusFeature.setValue(newMode); autoFocusFeature.updateBuilder(previewRequestBuilder); @@ -910,7 +911,9 @@ public void setFocusMode(@NonNull final Result result, FocusMode newMode) { captureSession.setRepeatingRequest( previewRequestBuilder.build(), null, backgroundHandler); } catch (CameraAccessException e) { - result.error("setFocusModeFailed", "Error setting focus mode: " + e.getMessage(), null); + if (result != null) { + result.error("setFocusModeFailed", "Error setting focus mode: " + e.getMessage(), null); + } } break; @@ -920,7 +923,9 @@ public void setFocusMode(@NonNull final Result result, FocusMode newMode) { break; } - result.success(null); + if (result != null) { + result.success(null); + } } /** @@ -937,6 +942,8 @@ public void setFocusPoint(@NonNull final Result result, Point point) { refreshPreviewCaptureSession( () -> result.success(null), (code, message) -> result.error("setFocusPointFailed", "Could not set focus point.", null)); + + this.setFocusMode(null, cameraFeatures.getAutoFocus().getValue()); } /** diff --git a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/CameraRegionUtils.java b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/CameraRegionUtils.java index ff8a49f1d148..9bfe8066addd 100644 --- a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/CameraRegionUtils.java +++ b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/CameraRegionUtils.java @@ -8,11 +8,14 @@ import android.hardware.camera2.CaptureRequest; import android.hardware.camera2.params.MeteringRectangle; import android.os.Build; +import android.util.Log; import android.util.Size; import androidx.annotation.NonNull; import androidx.annotation.VisibleForTesting; import java.util.Arrays; +import io.flutter.embedding.engine.systemchannels.PlatformChannel; + /** * Utility class offering functions to calculate values regarding the camera boundaries. * @@ -69,11 +72,29 @@ && supportsDistortionCorrection(cameraProperties)) { * boundaries. */ public static MeteringRectangle convertPointToMeteringRectangle( - @NonNull Size boundaries, double x, double y) { + @NonNull Size boundaries, double x, double y, @NonNull PlatformChannel.DeviceOrientation orientation) { assert (boundaries.getWidth() > 0 && boundaries.getHeight() > 0); assert (x >= 0 && x <= 1); assert (y >= 0 && y <= 1); - + // Rotate the coordinates to match the device orientation + double oldX = x, oldY = y; + switch(orientation) { + case PORTRAIT_UP: // 90 ccw + y = 1 - oldX; + x = oldY; + break; + case PORTRAIT_DOWN: // 90 cw + x = 1 - oldY; + y = oldX; + break; + case LANDSCAPE_LEFT: + // No rotation required + break; + case LANDSCAPE_RIGHT: // 180 + x = 1 - x; + y = 1 - y; + break; + } // Interpolate the target coordinate. int targetX = (int) Math.round(x * ((double) (boundaries.getWidth() - 1))); int targetY = (int) Math.round(y * ((double) (boundaries.getHeight() - 1))); 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 8d10c445788c..97af51c6d384 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 @@ -5,6 +5,8 @@ package io.flutter.plugins.camera.features; import android.app.Activity; +import android.hardware.Sensor; + import androidx.annotation.NonNull; import io.flutter.plugins.camera.CameraProperties; import io.flutter.plugins.camera.DartMessenger; @@ -84,9 +86,11 @@ ResolutionFeature createResolutionFeature( * * @param cameraProperties instance of the CameraProperties class containing information about the * cameras features. + * @param sensorOrientationFeature instance of the SensorOrientationFeature class containing + * information about the sensor and device orientation. * @return newly created instance of the FocusPointFeature class. */ - FocusPointFeature createFocusPointFeature(@NonNull CameraProperties cameraProperties); + FocusPointFeature createFocusPointFeature(@NonNull CameraProperties cameraProperties, @NonNull SensorOrientationFeature sensorOrientationFeature); /** * Creates a new instance of the FPS range feature. @@ -126,9 +130,11 @@ SensorOrientationFeature createSensorOrientationFeature( * * @param cameraProperties instance of the CameraProperties class containing information about the * cameras features. + * @param sensorOrientationFeature instance of the SensorOrientationFeature class containing + * information about the sensor and device orientation. * @return newly created instance of the ExposurePointFeature class. */ - ExposurePointFeature createExposurePointFeature(@NonNull CameraProperties cameraProperties); + ExposurePointFeature createExposurePointFeature(@NonNull CameraProperties cameraProperties, @NonNull SensorOrientationFeature sensorOrientationFeature); /** * Creates a new instance of the noise reduction feature. 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 b12ad3626226..d42c189a7166 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 @@ -5,7 +5,9 @@ package io.flutter.plugins.camera.features; import android.app.Activity; + import androidx.annotation.NonNull; + import io.flutter.plugins.camera.CameraProperties; import io.flutter.plugins.camera.DartMessenger; import io.flutter.plugins.camera.features.autofocus.AutoFocusFeature; @@ -28,68 +30,68 @@ */ 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) { - return new FocusPointFeature(cameraProperties); - } - - @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 ExposurePointFeature createExposurePointFeature( - @NonNull CameraProperties cameraProperties) { - return new ExposurePointFeature(cameraProperties); - } - - @Override - public NoiseReductionFeature createNoiseReductionFeature( - @NonNull CameraProperties cameraProperties) { - return new NoiseReductionFeature(cameraProperties); - } + @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, @NonNull SensorOrientationFeature sensorOrientationFeature) { + return new FocusPointFeature(cameraProperties, sensorOrientationFeature); + } + + @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 ExposurePointFeature createExposurePointFeature( + @NonNull CameraProperties cameraProperties, @NonNull SensorOrientationFeature sensorOrientationFeature) { + return new ExposurePointFeature(cameraProperties, sensorOrientationFeature); + } + + @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/exposurepoint/ExposurePointFeature.java b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/exposurepoint/ExposurePointFeature.java index 8c2ee6167846..6828494d04b9 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 @@ -8,10 +8,13 @@ import android.hardware.camera2.params.MeteringRectangle; import android.util.Size; import androidx.annotation.NonNull; + +import io.flutter.embedding.engine.systemchannels.PlatformChannel; import io.flutter.plugins.camera.CameraProperties; import io.flutter.plugins.camera.CameraRegionUtils; import io.flutter.plugins.camera.features.CameraFeature; import io.flutter.plugins.camera.features.Point; +import io.flutter.plugins.camera.features.sensororientation.SensorOrientationFeature; /** Exposure point controls where in the frame exposure metering will come from. */ public class ExposurePointFeature extends CameraFeature { @@ -19,14 +22,16 @@ public class ExposurePointFeature extends CameraFeature { private Size cameraBoundaries; private Point exposurePoint; private MeteringRectangle exposureRectangle; + private final SensorOrientationFeature sensorOrientationFeature; /** * Creates a new instance of the {@link ExposurePointFeature}. * * @param cameraProperties Collection of the characteristics for the current camera device. */ - public ExposurePointFeature(CameraProperties cameraProperties) { + public ExposurePointFeature(CameraProperties cameraProperties, SensorOrientationFeature sensorOrientationFeature) { super(cameraProperties); + this.sensorOrientationFeature = sensorOrientationFeature; } /** @@ -80,9 +85,12 @@ private void buildExposureRectangle() { if (this.exposurePoint == null) { this.exposureRectangle = null; } else { + PlatformChannel.DeviceOrientation orientation = this.sensorOrientationFeature.getLockedCaptureOrientation(); + if (orientation == null) { + orientation = this.sensorOrientationFeature.getDeviceOrientationManager().getLastUIOrientation(); + } this.exposureRectangle = - CameraRegionUtils.convertPointToMeteringRectangle( - this.cameraBoundaries, this.exposurePoint.x, this.exposurePoint.y); + CameraRegionUtils.convertPointToMeteringRectangle(this.cameraBoundaries, this.exposurePoint.x, this.exposurePoint.y, orientation); } } } 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 92fcfa9f1132..d97ca2785762 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 @@ -7,82 +7,93 @@ import android.hardware.camera2.CaptureRequest; import android.hardware.camera2.params.MeteringRectangle; import android.util.Size; + import androidx.annotation.NonNull; + +import io.flutter.embedding.engine.systemchannels.PlatformChannel; import io.flutter.plugins.camera.CameraProperties; import io.flutter.plugins.camera.CameraRegionUtils; import io.flutter.plugins.camera.features.CameraFeature; import io.flutter.plugins.camera.features.Point; +import io.flutter.plugins.camera.features.sensororientation.SensorOrientationFeature; -/** Focus point controls where in the frame focus will come from. */ +/** + * Focus point controls where in the frame focus will come from. + */ public class FocusPointFeature extends CameraFeature { - private Size cameraBoundaries; - private Point focusPoint; - private MeteringRectangle focusRectangle; - - /** - * Creates a new instance of the {@link FocusPointFeature}. - * - * @param cameraProperties Collection of the characteristics for the current camera device. - */ - public FocusPointFeature(CameraProperties cameraProperties) { - super(cameraProperties); - } + private Size cameraBoundaries; + private Point focusPoint; + private MeteringRectangle focusRectangle; + private final SensorOrientationFeature sensorOrientationFeature; - /** - * Sets the camera boundaries that are required for the focus point feature to function. - * - * @param cameraBoundaries - The camera boundaries to set. - */ - public void setCameraBoundaries(@NonNull Size cameraBoundaries) { - this.cameraBoundaries = cameraBoundaries; - this.buildFocusRectangle(); - } + /** + * Creates a new instance of the {@link FocusPointFeature}. + * + * @param cameraProperties Collection of the characteristics for the current camera device. + */ + public FocusPointFeature(CameraProperties cameraProperties, SensorOrientationFeature sensorOrientationFeature) { + super(cameraProperties); + this.sensorOrientationFeature = sensorOrientationFeature; + } - @Override - public String getDebugName() { - return "FocusPointFeature"; - } + /** + * Sets the camera boundaries that are required for the focus point feature to function. + * + * @param cameraBoundaries - The camera boundaries to set. + */ + public void setCameraBoundaries(@NonNull Size cameraBoundaries) { + this.cameraBoundaries = cameraBoundaries; + this.buildFocusRectangle(); + } - @Override - public Point getValue() { - return focusPoint; - } + @Override + public String getDebugName() { + return "FocusPointFeature"; + } - @Override - public void setValue(Point value) { - this.focusPoint = value == null || value.x == null || value.y == null ? null : value; - this.buildFocusRectangle(); - } + @Override + public Point getValue() { + return focusPoint; + } - // Whether or not this camera can set the focus point. - @Override - public boolean checkIsSupported() { - Integer supportedRegions = cameraProperties.getControlMaxRegionsAutoFocus(); - return supportedRegions != null && supportedRegions > 0; - } + @Override + public void setValue(Point value) { + this.focusPoint = value == null || value.x == null || value.y == null ? null : value; + this.buildFocusRectangle(); + } - @Override - public void updateBuilder(CaptureRequest.Builder requestBuilder) { - if (!checkIsSupported()) { - return; + // Whether or not this camera can set the focus point. + @Override + public boolean checkIsSupported() { + Integer supportedRegions = cameraProperties.getControlMaxRegionsAutoFocus(); + return supportedRegions != null && supportedRegions > 0; } - requestBuilder.set( - CaptureRequest.CONTROL_AF_REGIONS, - focusRectangle == null ? null : new MeteringRectangle[] {focusRectangle}); - } - private void buildFocusRectangle() { - if (this.cameraBoundaries == null) { - throw new AssertionError( - "The cameraBoundaries should be set (using `FocusPointFeature.setCameraBoundaries(Size)`) before updating the focus point."); + @Override + public void updateBuilder(CaptureRequest.Builder requestBuilder) { + if (!checkIsSupported()) { + return; + } + requestBuilder.set( + CaptureRequest.CONTROL_AF_REGIONS, + focusRectangle == null ? null : new MeteringRectangle[]{focusRectangle}); } - if (this.focusPoint == null) { - this.focusRectangle = null; - } else { - this.focusRectangle = - CameraRegionUtils.convertPointToMeteringRectangle( - this.cameraBoundaries, this.focusPoint.x, this.focusPoint.y); + + private void buildFocusRectangle() { + if (this.cameraBoundaries == null) { + throw new AssertionError( + "The cameraBoundaries should be set (using `FocusPointFeature.setCameraBoundaries(Size)`) before updating the focus point."); + } + if (this.focusPoint == null) { + this.focusRectangle = null; + } else { + PlatformChannel.DeviceOrientation orientation = this.sensorOrientationFeature.getLockedCaptureOrientation(); + if (orientation == null) { + orientation = this.sensorOrientationFeature.getDeviceOrientationManager().getLastUIOrientation(); + } + this.focusRectangle = + CameraRegionUtils.convertPointToMeteringRectangle(this.cameraBoundaries, this.focusPoint.x, this.focusPoint.y, orientation); + } } - } } 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 9223b89e7905..1b5f202bdec1 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 @@ -193,6 +193,13 @@ public int getVideoOrientation(PlatformChannel.DeviceOrientation orientation) { return (angle + sensorOrientation + 360) % 360; } + /** + * @return the last received UI orientation. + */ + public PlatformChannel.DeviceOrientation getLastUIOrientation() { + return this.lastOrientation; + } + /** * Handles orientation changes based on change events triggered by the OrientationIntentFilter. * @@ -202,7 +209,8 @@ public int getVideoOrientation(PlatformChannel.DeviceOrientation orientation) { @VisibleForTesting void handleUIOrientationChange() { PlatformChannel.DeviceOrientation orientation = getUIOrientation(); - lastOrientation = handleOrientationChange(orientation, lastOrientation, messenger); + handleOrientationChange(orientation, lastOrientation, messenger); + lastOrientation = orientation; } /** @@ -213,15 +221,13 @@ void handleUIOrientationChange() { * class. */ @VisibleForTesting - static DeviceOrientation handleOrientationChange( + static void handleOrientationChange( DeviceOrientation newOrientation, DeviceOrientation previousOrientation, DartMessenger messenger) { if (!newOrientation.equals(previousOrientation)) { messenger.sendDeviceUIOrientationChangeEvent(newOrientation); } - - return newOrientation; } /** From 90000ceb3ee96ac60101c0dad66e3abc746b88c2 Mon Sep 17 00:00:00 2001 From: "Bodhi Mulders (BeMacized)" Date: Mon, 12 Jul 2021 15:27:58 +0200 Subject: [PATCH 54/63] Fix unit tests & Format --- .../io/flutter/plugins/camera/Camera.java | 10 +- .../plugins/camera/CameraRegionUtils.java | 13 +- .../camera/features/CameraFeatureFactory.java | 12 +- .../features/CameraFeatureFactoryImpl.java | 133 ++++++++--------- .../exposurepoint/ExposurePointFeature.java | 13 +- .../focuspoint/FocusPointFeature.java | 138 +++++++++--------- .../DeviceOrientationManager.java | 4 +- .../plugins/camera/CameraRegionUtilsTest.java | 54 +++++-- .../io/flutter/plugins/camera/CameraTest.java | 33 ++++- .../ExposurePointFeatureTest.java | 84 ++++++++--- .../focuspoint/FocusPointFeatureTest.java | 82 ++++++++--- .../DeviceOrientationManagerTest.java | 12 +- 12 files changed, 359 insertions(+), 229 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 ae589f21e1a9..6234b4abb81a 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 @@ -198,11 +198,13 @@ public Camera( cameraFeatureFactory.createExposureLockFeature(cameraProperties)); this.cameraFeatures.setExposureOffset( cameraFeatureFactory.createExposureOffsetFeature(cameraProperties)); - SensorOrientationFeature sensorOrientationFeature = cameraFeatureFactory.createSensorOrientationFeature( + SensorOrientationFeature sensorOrientationFeature = + cameraFeatureFactory.createSensorOrientationFeature( cameraProperties, activity, dartMessenger); this.cameraFeatures.setSensorOrientation(sensorOrientationFeature); this.cameraFeatures.setExposurePoint( - cameraFeatureFactory.createExposurePointFeature(cameraProperties, sensorOrientationFeature)); + cameraFeatureFactory.createExposurePointFeature( + cameraProperties, sensorOrientationFeature)); this.cameraFeatures.setFlash(cameraFeatureFactory.createFlashFeature(cameraProperties)); this.cameraFeatures.setFocusPoint( cameraFeatureFactory.createFocusPointFeature(cameraProperties, sensorOrientationFeature)); @@ -212,7 +214,7 @@ public Camera( this.cameraFeatures.setResolution( cameraFeatureFactory.createResolutionFeature( cameraProperties, resolutionPreset, cameraProperties.getCameraName())); - + this.cameraFeatures.setZoomLevel(cameraFeatureFactory.createZoomLevelFeature(cameraProperties)); // Create capture callback @@ -912,7 +914,7 @@ public void setFocusMode(final Result result, FocusMode newMode) { previewRequestBuilder.build(), null, backgroundHandler); } catch (CameraAccessException e) { if (result != null) { - result.error("setFocusModeFailed", "Error setting focus mode: " + e.getMessage(), null); + result.error("setFocusModeFailed", "Error setting focus mode: " + e.getMessage(), null); } } break; diff --git a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/CameraRegionUtils.java b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/CameraRegionUtils.java index 9bfe8066addd..53df2776559b 100644 --- a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/CameraRegionUtils.java +++ b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/CameraRegionUtils.java @@ -8,13 +8,11 @@ import android.hardware.camera2.CaptureRequest; import android.hardware.camera2.params.MeteringRectangle; import android.os.Build; -import android.util.Log; import android.util.Size; import androidx.annotation.NonNull; import androidx.annotation.VisibleForTesting; -import java.util.Arrays; - import io.flutter.embedding.engine.systemchannels.PlatformChannel; +import java.util.Arrays; /** * Utility class offering functions to calculate values regarding the camera boundaries. @@ -72,13 +70,16 @@ && supportsDistortionCorrection(cameraProperties)) { * boundaries. */ public static MeteringRectangle convertPointToMeteringRectangle( - @NonNull Size boundaries, double x, double y, @NonNull PlatformChannel.DeviceOrientation orientation) { + @NonNull Size boundaries, + double x, + double y, + @NonNull PlatformChannel.DeviceOrientation orientation) { assert (boundaries.getWidth() > 0 && boundaries.getHeight() > 0); assert (x >= 0 && x <= 1); assert (y >= 0 && y <= 1); // Rotate the coordinates to match the device orientation double oldX = x, oldY = y; - switch(orientation) { + switch (orientation) { case PORTRAIT_UP: // 90 ccw y = 1 - oldX; x = oldY; @@ -88,7 +89,7 @@ public static MeteringRectangle convertPointToMeteringRectangle( y = oldX; break; case LANDSCAPE_LEFT: - // No rotation required + // No rotation required break; case LANDSCAPE_RIGHT: // 180 x = 1 - x; 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 97af51c6d384..b91f9a1c03f7 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 @@ -5,8 +5,6 @@ package io.flutter.plugins.camera.features; import android.app.Activity; -import android.hardware.Sensor; - import androidx.annotation.NonNull; import io.flutter.plugins.camera.CameraProperties; import io.flutter.plugins.camera.DartMessenger; @@ -90,7 +88,9 @@ ResolutionFeature createResolutionFeature( * information about the sensor and device orientation. * @return newly created instance of the FocusPointFeature class. */ - FocusPointFeature createFocusPointFeature(@NonNull CameraProperties cameraProperties, @NonNull SensorOrientationFeature sensorOrientationFeature); + FocusPointFeature createFocusPointFeature( + @NonNull CameraProperties cameraProperties, + @NonNull SensorOrientationFeature sensorOrientationFeature); /** * Creates a new instance of the FPS range feature. @@ -130,11 +130,13 @@ SensorOrientationFeature createSensorOrientationFeature( * * @param cameraProperties instance of the CameraProperties class containing information about the * cameras features. - * @param sensorOrientationFeature instance of the SensorOrientationFeature class containing + * @param sensorOrientationFeature instance of the SensorOrientationFeature class containing * information about the sensor and device orientation. * @return newly created instance of the ExposurePointFeature class. */ - ExposurePointFeature createExposurePointFeature(@NonNull CameraProperties cameraProperties, @NonNull SensorOrientationFeature sensorOrientationFeature); + ExposurePointFeature createExposurePointFeature( + @NonNull CameraProperties cameraProperties, + @NonNull SensorOrientationFeature sensorOrientationFeature); /** * Creates a new instance of the noise reduction feature. 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 d42c189a7166..95a8c06caa0a 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 @@ -5,9 +5,7 @@ package io.flutter.plugins.camera.features; import android.app.Activity; - import androidx.annotation.NonNull; - import io.flutter.plugins.camera.CameraProperties; import io.flutter.plugins.camera.DartMessenger; import io.flutter.plugins.camera.features.autofocus.AutoFocusFeature; @@ -30,68 +28,71 @@ */ 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, @NonNull SensorOrientationFeature sensorOrientationFeature) { - return new FocusPointFeature(cameraProperties, sensorOrientationFeature); - } - - @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 ExposurePointFeature createExposurePointFeature( - @NonNull CameraProperties cameraProperties, @NonNull SensorOrientationFeature sensorOrientationFeature) { - return new ExposurePointFeature(cameraProperties, sensorOrientationFeature); - } - - @Override - public NoiseReductionFeature createNoiseReductionFeature( - @NonNull CameraProperties cameraProperties) { - return new NoiseReductionFeature(cameraProperties); - } + @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, + @NonNull SensorOrientationFeature sensorOrientationFeature) { + return new FocusPointFeature(cameraProperties, sensorOrientationFeature); + } + + @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 ExposurePointFeature createExposurePointFeature( + @NonNull CameraProperties cameraProperties, + @NonNull SensorOrientationFeature sensorOrientationFeature) { + return new ExposurePointFeature(cameraProperties, sensorOrientationFeature); + } + + @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/exposurepoint/ExposurePointFeature.java b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/exposurepoint/ExposurePointFeature.java index 6828494d04b9..336e756e9ed8 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 @@ -8,7 +8,6 @@ import android.hardware.camera2.params.MeteringRectangle; import android.util.Size; import androidx.annotation.NonNull; - import io.flutter.embedding.engine.systemchannels.PlatformChannel; import io.flutter.plugins.camera.CameraProperties; import io.flutter.plugins.camera.CameraRegionUtils; @@ -29,7 +28,8 @@ public class ExposurePointFeature extends CameraFeature { * * @param cameraProperties Collection of the characteristics for the current camera device. */ - public ExposurePointFeature(CameraProperties cameraProperties, SensorOrientationFeature sensorOrientationFeature) { + public ExposurePointFeature( + CameraProperties cameraProperties, SensorOrientationFeature sensorOrientationFeature) { super(cameraProperties); this.sensorOrientationFeature = sensorOrientationFeature; } @@ -85,12 +85,15 @@ private void buildExposureRectangle() { if (this.exposurePoint == null) { this.exposureRectangle = null; } else { - PlatformChannel.DeviceOrientation orientation = this.sensorOrientationFeature.getLockedCaptureOrientation(); + PlatformChannel.DeviceOrientation orientation = + this.sensorOrientationFeature.getLockedCaptureOrientation(); if (orientation == null) { - orientation = this.sensorOrientationFeature.getDeviceOrientationManager().getLastUIOrientation(); + orientation = + this.sensorOrientationFeature.getDeviceOrientationManager().getLastUIOrientation(); } this.exposureRectangle = - CameraRegionUtils.convertPointToMeteringRectangle(this.cameraBoundaries, this.exposurePoint.x, this.exposurePoint.y, orientation); + CameraRegionUtils.convertPointToMeteringRectangle( + this.cameraBoundaries, this.exposurePoint.x, this.exposurePoint.y, orientation); } } } 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 d97ca2785762..a3a0172d3c37 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 @@ -7,9 +7,7 @@ import android.hardware.camera2.CaptureRequest; import android.hardware.camera2.params.MeteringRectangle; import android.util.Size; - import androidx.annotation.NonNull; - import io.flutter.embedding.engine.systemchannels.PlatformChannel; import io.flutter.plugins.camera.CameraProperties; import io.flutter.plugins.camera.CameraRegionUtils; @@ -17,83 +15,85 @@ import io.flutter.plugins.camera.features.Point; import io.flutter.plugins.camera.features.sensororientation.SensorOrientationFeature; -/** - * Focus point controls where in the frame focus will come from. - */ +/** Focus point controls where in the frame focus will come from. */ public class FocusPointFeature extends CameraFeature { - private Size cameraBoundaries; - private Point focusPoint; - private MeteringRectangle focusRectangle; - private final SensorOrientationFeature sensorOrientationFeature; + private Size cameraBoundaries; + private Point focusPoint; + private MeteringRectangle focusRectangle; + private final SensorOrientationFeature sensorOrientationFeature; - /** - * Creates a new instance of the {@link FocusPointFeature}. - * - * @param cameraProperties Collection of the characteristics for the current camera device. - */ - public FocusPointFeature(CameraProperties cameraProperties, SensorOrientationFeature sensorOrientationFeature) { - super(cameraProperties); - this.sensorOrientationFeature = sensorOrientationFeature; - } + /** + * Creates a new instance of the {@link FocusPointFeature}. + * + * @param cameraProperties Collection of the characteristics for the current camera device. + */ + public FocusPointFeature( + CameraProperties cameraProperties, SensorOrientationFeature sensorOrientationFeature) { + super(cameraProperties); + this.sensorOrientationFeature = sensorOrientationFeature; + } - /** - * Sets the camera boundaries that are required for the focus point feature to function. - * - * @param cameraBoundaries - The camera boundaries to set. - */ - public void setCameraBoundaries(@NonNull Size cameraBoundaries) { - this.cameraBoundaries = cameraBoundaries; - this.buildFocusRectangle(); - } + /** + * Sets the camera boundaries that are required for the focus point feature to function. + * + * @param cameraBoundaries - The camera boundaries to set. + */ + public void setCameraBoundaries(@NonNull Size cameraBoundaries) { + this.cameraBoundaries = cameraBoundaries; + this.buildFocusRectangle(); + } - @Override - public String getDebugName() { - return "FocusPointFeature"; - } + @Override + public String getDebugName() { + return "FocusPointFeature"; + } - @Override - public Point getValue() { - return focusPoint; - } + @Override + public Point getValue() { + return focusPoint; + } - @Override - public void setValue(Point value) { - this.focusPoint = value == null || value.x == null || value.y == null ? null : value; - this.buildFocusRectangle(); - } + @Override + public void setValue(Point value) { + this.focusPoint = value == null || value.x == null || value.y == null ? null : value; + this.buildFocusRectangle(); + } - // Whether or not this camera can set the focus point. - @Override - public boolean checkIsSupported() { - Integer supportedRegions = cameraProperties.getControlMaxRegionsAutoFocus(); - return supportedRegions != null && supportedRegions > 0; - } + // Whether or not this camera can set the focus point. + @Override + public boolean checkIsSupported() { + Integer supportedRegions = cameraProperties.getControlMaxRegionsAutoFocus(); + return supportedRegions != null && supportedRegions > 0; + } - @Override - public void updateBuilder(CaptureRequest.Builder requestBuilder) { - if (!checkIsSupported()) { - return; - } - requestBuilder.set( - CaptureRequest.CONTROL_AF_REGIONS, - focusRectangle == null ? null : new MeteringRectangle[]{focusRectangle}); + @Override + public void updateBuilder(CaptureRequest.Builder requestBuilder) { + if (!checkIsSupported()) { + return; } + requestBuilder.set( + CaptureRequest.CONTROL_AF_REGIONS, + focusRectangle == null ? null : new MeteringRectangle[] {focusRectangle}); + } - private void buildFocusRectangle() { - if (this.cameraBoundaries == null) { - throw new AssertionError( - "The cameraBoundaries should be set (using `FocusPointFeature.setCameraBoundaries(Size)`) before updating the focus point."); - } - if (this.focusPoint == null) { - this.focusRectangle = null; - } else { - PlatformChannel.DeviceOrientation orientation = this.sensorOrientationFeature.getLockedCaptureOrientation(); - if (orientation == null) { - orientation = this.sensorOrientationFeature.getDeviceOrientationManager().getLastUIOrientation(); - } - this.focusRectangle = - CameraRegionUtils.convertPointToMeteringRectangle(this.cameraBoundaries, this.focusPoint.x, this.focusPoint.y, orientation); - } + private void buildFocusRectangle() { + if (this.cameraBoundaries == null) { + throw new AssertionError( + "The cameraBoundaries should be set (using `FocusPointFeature.setCameraBoundaries(Size)`) before updating the focus point."); + } + if (this.focusPoint == null) { + this.focusRectangle = null; + } else { + PlatformChannel.DeviceOrientation orientation = + this.sensorOrientationFeature.getLockedCaptureOrientation(); + if (orientation == null) { + orientation = + this.sensorOrientationFeature.getDeviceOrientationManager().getLastUIOrientation(); + } + this.focusRectangle = + CameraRegionUtils.convertPointToMeteringRectangle( + this.cameraBoundaries, this.focusPoint.x, this.focusPoint.y, orientation); } + } } 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 1b5f202bdec1..def5a0ad7174 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 @@ -193,9 +193,7 @@ public int getVideoOrientation(PlatformChannel.DeviceOrientation orientation) { return (angle + sensorOrientation + 360) % 360; } - /** - * @return the last received UI orientation. - */ + /** @return the last received UI orientation. */ public PlatformChannel.DeviceOrientation getLastUIOrientation() { return this.lastOrientation; } diff --git a/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/CameraRegionUtilsTest.java b/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/CameraRegionUtilsTest.java index c0ff47d8f9f1..af86e1169fbd 100644 --- a/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/CameraRegionUtilsTest.java +++ b/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/CameraRegionUtilsTest.java @@ -18,6 +18,7 @@ import android.hardware.camera2.params.MeteringRectangle; import android.os.Build; import android.util.Size; +import io.flutter.embedding.engine.systemchannels.PlatformChannel; import io.flutter.plugins.camera.utils.TestUtils; import org.junit.Before; import org.junit.Test; @@ -248,22 +249,26 @@ public void setUp() { @Test(expected = AssertionError.class) public void convertPointToMeteringRectangle_should_throw_for_x_upper_bound() { - CameraRegionUtils.convertPointToMeteringRectangle(this.mockCameraBoundaries, 1.5, 0); + CameraRegionUtils.convertPointToMeteringRectangle( + this.mockCameraBoundaries, 1.5, 0, PlatformChannel.DeviceOrientation.PORTRAIT_UP); } @Test(expected = AssertionError.class) public void convertPointToMeteringRectangle_should_throw_for_x_lower_bound() { - CameraRegionUtils.convertPointToMeteringRectangle(this.mockCameraBoundaries, -0.5, 0); + CameraRegionUtils.convertPointToMeteringRectangle( + this.mockCameraBoundaries, -0.5, 0, PlatformChannel.DeviceOrientation.PORTRAIT_UP); } @Test(expected = AssertionError.class) public void convertPointToMeteringRectangle_should_throw_for_y_upper_bound() { - CameraRegionUtils.convertPointToMeteringRectangle(this.mockCameraBoundaries, 0, 1.5); + CameraRegionUtils.convertPointToMeteringRectangle( + this.mockCameraBoundaries, 0, 1.5, PlatformChannel.DeviceOrientation.PORTRAIT_UP); } @Test(expected = AssertionError.class) public void convertPointToMeteringRectangle_should_throw_for_y_lower_bound() { - CameraRegionUtils.convertPointToMeteringRectangle(this.mockCameraBoundaries, 0, -0.5); + CameraRegionUtils.convertPointToMeteringRectangle( + this.mockCameraBoundaries, 0, -0.5, PlatformChannel.DeviceOrientation.PORTRAIT_UP); } @Test @@ -316,23 +321,48 @@ public Boolean answer(InvocationOnMock equalsInvocation) MeteringRectangle r; // Center - r = CameraRegionUtils.convertPointToMeteringRectangle(this.mockCameraBoundaries, 0.5, 0.5); + r = + CameraRegionUtils.convertPointToMeteringRectangle( + this.mockCameraBoundaries, + 0.5, + 0.5, + PlatformChannel.DeviceOrientation.LANDSCAPE_LEFT); assertTrue(CameraRegionUtils.MeteringRectangleFactory.create(45, 45, 10, 10, 1).equals(r)); // Top left - r = CameraRegionUtils.convertPointToMeteringRectangle(this.mockCameraBoundaries, 0.0, 0.0); + r = + CameraRegionUtils.convertPointToMeteringRectangle( + this.mockCameraBoundaries, + 0.0, + 0.0, + PlatformChannel.DeviceOrientation.LANDSCAPE_LEFT); assertTrue(CameraRegionUtils.MeteringRectangleFactory.create(0, 0, 10, 10, 1).equals(r)); // Bottom right - r = CameraRegionUtils.convertPointToMeteringRectangle(this.mockCameraBoundaries, 1.0, 1.0); + r = + CameraRegionUtils.convertPointToMeteringRectangle( + this.mockCameraBoundaries, + 1.0, + 1.0, + PlatformChannel.DeviceOrientation.LANDSCAPE_LEFT); assertTrue(CameraRegionUtils.MeteringRectangleFactory.create(89, 89, 10, 10, 1).equals(r)); // Top left - r = CameraRegionUtils.convertPointToMeteringRectangle(this.mockCameraBoundaries, 0.0, 1.0); + r = + CameraRegionUtils.convertPointToMeteringRectangle( + this.mockCameraBoundaries, + 0.0, + 1.0, + PlatformChannel.DeviceOrientation.LANDSCAPE_LEFT); assertTrue(CameraRegionUtils.MeteringRectangleFactory.create(0, 89, 10, 10, 1).equals(r)); // Top right - r = CameraRegionUtils.convertPointToMeteringRectangle(this.mockCameraBoundaries, 1.0, 0.0); + r = + CameraRegionUtils.convertPointToMeteringRectangle( + this.mockCameraBoundaries, + 1.0, + 0.0, + PlatformChannel.DeviceOrientation.LANDSCAPE_LEFT); assertTrue(CameraRegionUtils.MeteringRectangleFactory.create(89, 0, 10, 10, 1).equals(r)); } } @@ -342,7 +372,8 @@ public void convertPointToMeteringRectangle_should_throw_for_0_width_boundary() Size mockCameraBoundaries = mock(Size.class); when(mockCameraBoundaries.getWidth()).thenReturn(0); when(mockCameraBoundaries.getHeight()).thenReturn(50); - CameraRegionUtils.convertPointToMeteringRectangle(mockCameraBoundaries, 0, -0.5); + CameraRegionUtils.convertPointToMeteringRectangle( + mockCameraBoundaries, 0, -0.5, PlatformChannel.DeviceOrientation.PORTRAIT_UP); } @Test(expected = AssertionError.class) @@ -350,7 +381,8 @@ public void convertPointToMeteringRectangle_should_throw_for_0_height_boundary() Size mockCameraBoundaries = mock(Size.class); when(mockCameraBoundaries.getWidth()).thenReturn(50); when(mockCameraBoundaries.getHeight()).thenReturn(0); - CameraRegionUtils.convertPointToMeteringRectangle(this.mockCameraBoundaries, 0, -0.5); + CameraRegionUtils.convertPointToMeteringRectangle( + this.mockCameraBoundaries, 0, -0.5, PlatformChannel.DeviceOrientation.PORTRAIT_UP); } private static void updateSdkVersion(int version) { 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 d561e70dd994..d3596378cd4a 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 @@ -6,6 +6,7 @@ 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.times; @@ -20,6 +21,7 @@ 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.autofocus.FocusMode; import io.flutter.plugins.camera.features.exposurelock.ExposureLockFeature; import io.flutter.plugins.camera.features.exposurelock.ExposureMode; import io.flutter.plugins.camera.features.exposureoffset.ExposureOffsetFeature; @@ -81,6 +83,9 @@ public void should_create_camera_plugin_and_set_all_features() { final boolean enableAudio = false; when(mockCameraProperties.getCameraName()).thenReturn(cameraName); + SensorOrientationFeature mockSensorOrientationFeature = mock(SensorOrientationFeature.class); + when(mockCameraFeatureFactory.createSensorOrientationFeature(any(), any(), any())) + .thenReturn(mockSensorOrientationFeature); Camera camera = new Camera( @@ -92,18 +97,20 @@ public void should_create_camera_plugin_and_set_all_features() { resolutionPreset, enableAudio); + verify(mockCameraFeatureFactory, times(1)) + .createSensorOrientationFeature(mockCameraProperties, mockActivity, mockDartMessenger); verify(mockCameraFeatureFactory, times(1)).createAutoFocusFeature(mockCameraProperties, false); verify(mockCameraFeatureFactory, times(1)).createExposureLockFeature(mockCameraProperties); - verify(mockCameraFeatureFactory, times(1)).createExposurePointFeature(eq(mockCameraProperties)); + verify(mockCameraFeatureFactory, times(1)) + .createExposurePointFeature(eq(mockCameraProperties), eq(mockSensorOrientationFeature)); verify(mockCameraFeatureFactory, times(1)).createExposureOffsetFeature(mockCameraProperties); verify(mockCameraFeatureFactory, times(1)).createFlashFeature(mockCameraProperties); - verify(mockCameraFeatureFactory, times(1)).createFocusPointFeature(eq(mockCameraProperties)); + verify(mockCameraFeatureFactory, times(1)) + .createFocusPointFeature(eq(mockCameraProperties), eq(mockSensorOrientationFeature)); 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); assertNotNull("should create a camera", camera); } @@ -222,8 +229,10 @@ public void setExposureMode_Should_update_exposure_lock_feature_and_update_build @Test public void setExposurePoint_Should_update_exposure_point_feature_and_update_builder() { + SensorOrientationFeature mockSensorOrientationFeature = mock(SensorOrientationFeature.class); ExposurePointFeature mockExposurePointFeature = - mockCameraFeatureFactory.createExposurePointFeature(mockCameraProperties); + mockCameraFeatureFactory.createExposurePointFeature( + mockCameraProperties, mockSensorOrientationFeature); MethodChannel.Result mockResult = mock(MethodChannel.Result.class); Point point = new Point(42d, 42d); @@ -248,10 +257,15 @@ public void setFlashMode_Should_update_flash_feature_and_update_builder() { @Test public void setFocusPoint_Should_update_focus_point_feature_and_update_builder() { + SensorOrientationFeature mockSensorOrientationFeature = mock(SensorOrientationFeature.class); FocusPointFeature mockFocusPointFeature = - mockCameraFeatureFactory.createFocusPointFeature(mockCameraProperties); + mockCameraFeatureFactory.createFocusPointFeature( + mockCameraProperties, mockSensorOrientationFeature); + AutoFocusFeature mockAutoFocusFeature = + mockCameraFeatureFactory.createAutoFocusFeature(mockCameraProperties, false); MethodChannel.Result mockResult = mock(MethodChannel.Result.class); Point point = new Point(42d, 42d); + when(mockAutoFocusFeature.getValue()).thenReturn(FocusMode.auto); camera.setFocusPoint(mockResult, point); @@ -336,7 +350,9 @@ public ResolutionFeature createResolutionFeature( } @Override - public FocusPointFeature createFocusPointFeature(@NonNull CameraProperties cameraProperties) { + public FocusPointFeature createFocusPointFeature( + @NonNull CameraProperties cameraProperties, + @NonNull SensorOrientationFeature sensorOrientationFeature) { return mockFocusPointFeature; } @@ -360,7 +376,8 @@ public ZoomLevelFeature createZoomLevelFeature(@NonNull CameraProperties cameraP @Override public ExposurePointFeature createExposurePointFeature( - @NonNull CameraProperties cameraProperties) { + @NonNull CameraProperties cameraProperties, + @NonNull SensorOrientationFeature sensorOrientationFeature) { return mockExposurePointFeature; } 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 4a515c6fd0ec..0e5904ebf90a 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 @@ -19,9 +19,12 @@ import android.hardware.camera2.CaptureRequest; import android.hardware.camera2.params.MeteringRectangle; import android.util.Size; +import io.flutter.embedding.engine.systemchannels.PlatformChannel; import io.flutter.plugins.camera.CameraProperties; import io.flutter.plugins.camera.CameraRegionUtils; import io.flutter.plugins.camera.features.Point; +import io.flutter.plugins.camera.features.sensororientation.DeviceOrientationManager; +import io.flutter.plugins.camera.features.sensororientation.SensorOrientationFeature; import org.junit.Before; import org.junit.Test; import org.mockito.MockedStatic; @@ -30,19 +33,27 @@ public class ExposurePointFeatureTest { Size mockCameraBoundaries; + SensorOrientationFeature mockSensorOrientationFeature; + DeviceOrientationManager mockDeviceOrientationManager; @Before public void setUp() { this.mockCameraBoundaries = mock(Size.class); when(this.mockCameraBoundaries.getWidth()).thenReturn(100); when(this.mockCameraBoundaries.getHeight()).thenReturn(100); + mockSensorOrientationFeature = mock(SensorOrientationFeature.class); + mockDeviceOrientationManager = mock(DeviceOrientationManager.class); + when(mockSensorOrientationFeature.getDeviceOrientationManager()) + .thenReturn(mockDeviceOrientationManager); + when(mockDeviceOrientationManager.getLastUIOrientation()) + .thenReturn(PlatformChannel.DeviceOrientation.LANDSCAPE_LEFT); } @Test public void getDebugName_should_return_the_name_of_the_feature() { CameraProperties mockCameraProperties = mock(CameraProperties.class); - CameraRegionUtils mockCameraRegions = mock(CameraRegionUtils.class); - ExposurePointFeature exposurePointFeature = new ExposurePointFeature(mockCameraProperties); + ExposurePointFeature exposurePointFeature = + new ExposurePointFeature(mockCameraProperties, mockSensorOrientationFeature); assertEquals("ExposurePointFeature", exposurePointFeature.getDebugName()); } @@ -50,15 +61,16 @@ public void getDebugName_should_return_the_name_of_the_feature() { @Test public void getValue_should_return_null_if_not_set() { CameraProperties mockCameraProperties = mock(CameraProperties.class); - ExposurePointFeature exposurePointFeature = new ExposurePointFeature(mockCameraProperties); - Point actualPoint = exposurePointFeature.getValue(); + ExposurePointFeature exposurePointFeature = + new ExposurePointFeature(mockCameraProperties, mockSensorOrientationFeature); assertNull(exposurePointFeature.getValue()); } @Test public void getValue_should_echo_the_set_value() { CameraProperties mockCameraProperties = mock(CameraProperties.class); - ExposurePointFeature exposurePointFeature = new ExposurePointFeature(mockCameraProperties); + ExposurePointFeature exposurePointFeature = + new ExposurePointFeature(mockCameraProperties, mockSensorOrientationFeature); exposurePointFeature.setCameraBoundaries(this.mockCameraBoundaries); Point expectedPoint = new Point(0.0, 0.0); @@ -71,7 +83,8 @@ public void getValue_should_echo_the_set_value() { @Test public void setValue_should_reset_point_when_x_coord_is_null() { CameraProperties mockCameraProperties = mock(CameraProperties.class); - ExposurePointFeature exposurePointFeature = new ExposurePointFeature(mockCameraProperties); + ExposurePointFeature exposurePointFeature = + new ExposurePointFeature(mockCameraProperties, mockSensorOrientationFeature); exposurePointFeature.setCameraBoundaries(this.mockCameraBoundaries); exposurePointFeature.setValue(new Point(null, 0.0)); @@ -82,7 +95,8 @@ public void setValue_should_reset_point_when_x_coord_is_null() { @Test public void setValue_should_reset_point_when_y_coord_is_null() { CameraProperties mockCameraProperties = mock(CameraProperties.class); - ExposurePointFeature exposurePointFeature = new ExposurePointFeature(mockCameraProperties); + ExposurePointFeature exposurePointFeature = + new ExposurePointFeature(mockCameraProperties, mockSensorOrientationFeature); exposurePointFeature.setCameraBoundaries(this.mockCameraBoundaries); exposurePointFeature.setValue(new Point(0.0, null)); @@ -93,7 +107,8 @@ public void setValue_should_reset_point_when_y_coord_is_null() { @Test public void setValue_should_set_point_when_valid_coords_are_supplied() { CameraProperties mockCameraProperties = mock(CameraProperties.class); - ExposurePointFeature exposurePointFeature = new ExposurePointFeature(mockCameraProperties); + ExposurePointFeature exposurePointFeature = + new ExposurePointFeature(mockCameraProperties, mockSensorOrientationFeature); exposurePointFeature.setCameraBoundaries(this.mockCameraBoundaries); Point point = new Point(0.0, 0.0); @@ -107,7 +122,8 @@ public void setValue_should_set_point_when_valid_coords_are_supplied() { setValue_should_determine_metering_rectangle_when_valid_boundaries_and_coords_are_supplied() { CameraProperties mockCameraProperties = mock(CameraProperties.class); when(mockCameraProperties.getControlMaxRegionsAutoExposure()).thenReturn(1); - ExposurePointFeature exposurePointFeature = new ExposurePointFeature(mockCameraProperties); + ExposurePointFeature exposurePointFeature = + new ExposurePointFeature(mockCameraProperties, mockSensorOrientationFeature); Size mockedCameraBoundaries = mock(Size.class); exposurePointFeature.setCameraBoundaries(mockedCameraBoundaries); @@ -117,7 +133,12 @@ public void setValue_should_set_point_when_valid_coords_are_supplied() { exposurePointFeature.setValue(new Point(0.5, 0.5)); mockedCameraRegionUtils.verify( - () -> CameraRegionUtils.convertPointToMeteringRectangle(mockedCameraBoundaries, 0.5, 0.5), + () -> + CameraRegionUtils.convertPointToMeteringRectangle( + mockedCameraBoundaries, + 0.5, + 0.5, + PlatformChannel.DeviceOrientation.LANDSCAPE_LEFT), times(1)); } } @@ -126,7 +147,8 @@ public void setValue_should_set_point_when_valid_coords_are_supplied() { public void setValue_should_throw_assertion_error_when_no_valid_boundaries_are_set() { CameraProperties mockCameraProperties = mock(CameraProperties.class); when(mockCameraProperties.getControlMaxRegionsAutoExposure()).thenReturn(1); - ExposurePointFeature exposurePointFeature = new ExposurePointFeature(mockCameraProperties); + ExposurePointFeature exposurePointFeature = + new ExposurePointFeature(mockCameraProperties, mockSensorOrientationFeature); try (MockedStatic mockedCameraRegionUtils = Mockito.mockStatic(CameraRegionUtils.class)) { @@ -138,7 +160,8 @@ public void setValue_should_throw_assertion_error_when_no_valid_boundaries_are_s public void setValue_should_not_determine_metering_rectangle_when_null_coords_are_set() { CameraProperties mockCameraProperties = mock(CameraProperties.class); when(mockCameraProperties.getControlMaxRegionsAutoExposure()).thenReturn(1); - ExposurePointFeature exposurePointFeature = new ExposurePointFeature(mockCameraProperties); + ExposurePointFeature exposurePointFeature = + new ExposurePointFeature(mockCameraProperties, mockSensorOrientationFeature); Size mockedCameraBoundaries = mock(Size.class); exposurePointFeature.setCameraBoundaries(mockedCameraBoundaries); @@ -158,7 +181,8 @@ public void setValue_should_not_determine_metering_rectangle_when_null_coords_ar setCameraBoundaries_should_determine_metering_rectangle_when_valid_boundaries_and_coords_are_supplied() { CameraProperties mockCameraProperties = mock(CameraProperties.class); when(mockCameraProperties.getControlMaxRegionsAutoExposure()).thenReturn(1); - ExposurePointFeature exposurePointFeature = new ExposurePointFeature(mockCameraProperties); + ExposurePointFeature exposurePointFeature = + new ExposurePointFeature(mockCameraProperties, mockSensorOrientationFeature); exposurePointFeature.setCameraBoundaries(this.mockCameraBoundaries); exposurePointFeature.setValue(new Point(0.5, 0.5)); Size mockedCameraBoundaries = mock(Size.class); @@ -169,7 +193,12 @@ public void setValue_should_not_determine_metering_rectangle_when_null_coords_ar exposurePointFeature.setCameraBoundaries(mockedCameraBoundaries); mockedCameraRegionUtils.verify( - () -> CameraRegionUtils.convertPointToMeteringRectangle(mockedCameraBoundaries, 0.5, 0.5), + () -> + CameraRegionUtils.convertPointToMeteringRectangle( + mockedCameraBoundaries, + 0.5, + 0.5, + PlatformChannel.DeviceOrientation.LANDSCAPE_LEFT), times(1)); } } @@ -177,7 +206,8 @@ public void setValue_should_not_determine_metering_rectangle_when_null_coords_ar @Test public void checkIsSupported_should_return_false_when_max_regions_is_null() { CameraProperties mockCameraProperties = mock(CameraProperties.class); - ExposurePointFeature exposurePointFeature = new ExposurePointFeature(mockCameraProperties); + ExposurePointFeature exposurePointFeature = + new ExposurePointFeature(mockCameraProperties, mockSensorOrientationFeature); exposurePointFeature.setCameraBoundaries(new Size(100, 100)); when(mockCameraProperties.getControlMaxRegionsAutoExposure()).thenReturn(null); @@ -188,7 +218,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); + ExposurePointFeature exposurePointFeature = + new ExposurePointFeature(mockCameraProperties, mockSensorOrientationFeature); exposurePointFeature.setCameraBoundaries(new Size(100, 100)); when(mockCameraProperties.getControlMaxRegionsAutoExposure()).thenReturn(0); @@ -199,7 +230,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); + ExposurePointFeature exposurePointFeature = + new ExposurePointFeature(mockCameraProperties, mockSensorOrientationFeature); exposurePointFeature.setCameraBoundaries(new Size(100, 100)); when(mockCameraProperties.getControlMaxRegionsAutoExposure()).thenReturn(1); @@ -211,7 +243,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); CaptureRequest.Builder mockCaptureRequestBuilder = mock(CaptureRequest.Builder.class); - ExposurePointFeature exposurePointFeature = new ExposurePointFeature(mockCameraProperties); + ExposurePointFeature exposurePointFeature = + new ExposurePointFeature(mockCameraProperties, mockSensorOrientationFeature); when(mockCameraProperties.getControlMaxRegionsAutoExposure()).thenReturn(0); @@ -226,7 +259,8 @@ public void updateBuilder_should_return_when_checkIsSupported_is_false() { CameraProperties mockCameraProperties = mock(CameraProperties.class); when(mockCameraProperties.getControlMaxRegionsAutoExposure()).thenReturn(1); CaptureRequest.Builder mockCaptureRequestBuilder = mock(CaptureRequest.Builder.class); - ExposurePointFeature exposurePointFeature = new ExposurePointFeature(mockCameraProperties); + ExposurePointFeature exposurePointFeature = + new ExposurePointFeature(mockCameraProperties, mockSensorOrientationFeature); Size mockedCameraBoundaries = mock(Size.class); MeteringRectangle mockedMeteringRectangle = mock(MeteringRectangle.class); @@ -236,7 +270,10 @@ public void updateBuilder_should_return_when_checkIsSupported_is_false() { .when( () -> CameraRegionUtils.convertPointToMeteringRectangle( - mockedCameraBoundaries, 0.5, 0.5)) + mockedCameraBoundaries, + 0.5, + 0.5, + PlatformChannel.DeviceOrientation.LANDSCAPE_LEFT)) .thenReturn(mockedMeteringRectangle); exposurePointFeature.setCameraBoundaries(mockedCameraBoundaries); exposurePointFeature.setValue(new Point(0.5, 0.5)); @@ -254,8 +291,8 @@ public void updateBuilder_should_return_when_checkIsSupported_is_false() { CameraProperties mockCameraProperties = mock(CameraProperties.class); when(mockCameraProperties.getControlMaxRegionsAutoExposure()).thenReturn(1); CaptureRequest.Builder mockCaptureRequestBuilder = mock(CaptureRequest.Builder.class); - ExposurePointFeature exposurePointFeature = new ExposurePointFeature(mockCameraProperties); - MeteringRectangle mockedMeteringRectangle = mock(MeteringRectangle.class); + ExposurePointFeature exposurePointFeature = + new ExposurePointFeature(mockCameraProperties, mockSensorOrientationFeature); exposurePointFeature.updateBuilder(mockCaptureRequestBuilder); @@ -267,7 +304,8 @@ public void updateBuilder_should_not_set_metering_rectangle_when_no_valid_coords CameraProperties mockCameraProperties = mock(CameraProperties.class); when(mockCameraProperties.getControlMaxRegionsAutoExposure()).thenReturn(1); CaptureRequest.Builder mockCaptureRequestBuilder = mock(CaptureRequest.Builder.class); - ExposurePointFeature exposurePointFeature = new ExposurePointFeature(mockCameraProperties); + ExposurePointFeature exposurePointFeature = + new ExposurePointFeature(mockCameraProperties, mockSensorOrientationFeature); exposurePointFeature.setCameraBoundaries(this.mockCameraBoundaries); exposurePointFeature.setValue(null); 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 d158336ef235..d2afdef5886d 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 @@ -19,9 +19,12 @@ import android.hardware.camera2.CaptureRequest; import android.hardware.camera2.params.MeteringRectangle; import android.util.Size; +import io.flutter.embedding.engine.systemchannels.PlatformChannel; import io.flutter.plugins.camera.CameraProperties; import io.flutter.plugins.camera.CameraRegionUtils; import io.flutter.plugins.camera.features.Point; +import io.flutter.plugins.camera.features.sensororientation.DeviceOrientationManager; +import io.flutter.plugins.camera.features.sensororientation.SensorOrientationFeature; import org.junit.Before; import org.junit.Test; import org.mockito.MockedStatic; @@ -30,19 +33,27 @@ public class FocusPointFeatureTest { Size mockCameraBoundaries; + SensorOrientationFeature mockSensorOrientationFeature; + DeviceOrientationManager mockDeviceOrientationManager; @Before public void setUp() { this.mockCameraBoundaries = mock(Size.class); when(this.mockCameraBoundaries.getWidth()).thenReturn(100); when(this.mockCameraBoundaries.getHeight()).thenReturn(100); + mockSensorOrientationFeature = mock(SensorOrientationFeature.class); + mockDeviceOrientationManager = mock(DeviceOrientationManager.class); + when(mockSensorOrientationFeature.getDeviceOrientationManager()) + .thenReturn(mockDeviceOrientationManager); + when(mockDeviceOrientationManager.getLastUIOrientation()) + .thenReturn(PlatformChannel.DeviceOrientation.LANDSCAPE_LEFT); } @Test public void getDebugName_should_return_the_name_of_the_feature() { CameraProperties mockCameraProperties = mock(CameraProperties.class); - CameraRegionUtils mockCameraRegions = mock(CameraRegionUtils.class); - FocusPointFeature focusPointFeature = new FocusPointFeature(mockCameraProperties); + FocusPointFeature focusPointFeature = + new FocusPointFeature(mockCameraProperties, mockSensorOrientationFeature); assertEquals("FocusPointFeature", focusPointFeature.getDebugName()); } @@ -50,7 +61,8 @@ public void getDebugName_should_return_the_name_of_the_feature() { @Test public void getValue_should_return_null_if_not_set() { CameraProperties mockCameraProperties = mock(CameraProperties.class); - FocusPointFeature focusPointFeature = new FocusPointFeature(mockCameraProperties); + FocusPointFeature focusPointFeature = + new FocusPointFeature(mockCameraProperties, mockSensorOrientationFeature); Point actualPoint = focusPointFeature.getValue(); assertNull(focusPointFeature.getValue()); } @@ -58,7 +70,8 @@ public void getValue_should_return_null_if_not_set() { @Test public void getValue_should_echo_the_set_value() { CameraProperties mockCameraProperties = mock(CameraProperties.class); - FocusPointFeature focusPointFeature = new FocusPointFeature(mockCameraProperties); + FocusPointFeature focusPointFeature = + new FocusPointFeature(mockCameraProperties, mockSensorOrientationFeature); focusPointFeature.setCameraBoundaries(this.mockCameraBoundaries); Point expectedPoint = new Point(0.0, 0.0); @@ -71,7 +84,8 @@ public void getValue_should_echo_the_set_value() { @Test public void setValue_should_reset_point_when_x_coord_is_null() { CameraProperties mockCameraProperties = mock(CameraProperties.class); - FocusPointFeature focusPointFeature = new FocusPointFeature(mockCameraProperties); + FocusPointFeature focusPointFeature = + new FocusPointFeature(mockCameraProperties, mockSensorOrientationFeature); focusPointFeature.setCameraBoundaries(this.mockCameraBoundaries); focusPointFeature.setValue(new Point(null, 0.0)); @@ -82,7 +96,8 @@ public void setValue_should_reset_point_when_x_coord_is_null() { @Test public void setValue_should_reset_point_when_y_coord_is_null() { CameraProperties mockCameraProperties = mock(CameraProperties.class); - FocusPointFeature focusPointFeature = new FocusPointFeature(mockCameraProperties); + FocusPointFeature focusPointFeature = + new FocusPointFeature(mockCameraProperties, mockSensorOrientationFeature); focusPointFeature.setCameraBoundaries(this.mockCameraBoundaries); focusPointFeature.setValue(new Point(0.0, null)); @@ -93,7 +108,8 @@ public void setValue_should_reset_point_when_y_coord_is_null() { @Test public void setValue_should_set_point_when_valid_coords_are_supplied() { CameraProperties mockCameraProperties = mock(CameraProperties.class); - FocusPointFeature focusPointFeature = new FocusPointFeature(mockCameraProperties); + FocusPointFeature focusPointFeature = + new FocusPointFeature(mockCameraProperties, mockSensorOrientationFeature); focusPointFeature.setCameraBoundaries(this.mockCameraBoundaries); Point point = new Point(0.0, 0.0); @@ -107,7 +123,8 @@ public void setValue_should_set_point_when_valid_coords_are_supplied() { setValue_should_determine_metering_rectangle_when_valid_boundaries_and_coords_are_supplied() { CameraProperties mockCameraProperties = mock(CameraProperties.class); when(mockCameraProperties.getControlMaxRegionsAutoFocus()).thenReturn(1); - FocusPointFeature focusPointFeature = new FocusPointFeature(mockCameraProperties); + FocusPointFeature focusPointFeature = + new FocusPointFeature(mockCameraProperties, mockSensorOrientationFeature); Size mockedCameraBoundaries = mock(Size.class); focusPointFeature.setCameraBoundaries(mockedCameraBoundaries); @@ -117,7 +134,12 @@ public void setValue_should_set_point_when_valid_coords_are_supplied() { focusPointFeature.setValue(new Point(0.5, 0.5)); mockedCameraRegionUtils.verify( - () -> CameraRegionUtils.convertPointToMeteringRectangle(mockedCameraBoundaries, 0.5, 0.5), + () -> + CameraRegionUtils.convertPointToMeteringRectangle( + mockedCameraBoundaries, + 0.5, + 0.5, + PlatformChannel.DeviceOrientation.LANDSCAPE_LEFT), times(1)); } } @@ -126,7 +148,8 @@ public void setValue_should_set_point_when_valid_coords_are_supplied() { public void setValue_should_throw_assertion_error_when_no_valid_boundaries_are_set() { CameraProperties mockCameraProperties = mock(CameraProperties.class); when(mockCameraProperties.getControlMaxRegionsAutoFocus()).thenReturn(1); - FocusPointFeature focusPointFeature = new FocusPointFeature(mockCameraProperties); + FocusPointFeature focusPointFeature = + new FocusPointFeature(mockCameraProperties, mockSensorOrientationFeature); try (MockedStatic mockedCameraRegionUtils = Mockito.mockStatic(CameraRegionUtils.class)) { @@ -138,7 +161,8 @@ public void setValue_should_throw_assertion_error_when_no_valid_boundaries_are_s public void setValue_should_not_determine_metering_rectangle_when_null_coords_are_set() { CameraProperties mockCameraProperties = mock(CameraProperties.class); when(mockCameraProperties.getControlMaxRegionsAutoFocus()).thenReturn(1); - FocusPointFeature focusPointFeature = new FocusPointFeature(mockCameraProperties); + FocusPointFeature focusPointFeature = + new FocusPointFeature(mockCameraProperties, mockSensorOrientationFeature); Size mockedCameraBoundaries = mock(Size.class); focusPointFeature.setCameraBoundaries(mockedCameraBoundaries); @@ -158,7 +182,8 @@ public void setValue_should_not_determine_metering_rectangle_when_null_coords_ar setCameraBoundaries_should_determine_metering_rectangle_when_valid_boundaries_and_coords_are_supplied() { CameraProperties mockCameraProperties = mock(CameraProperties.class); when(mockCameraProperties.getControlMaxRegionsAutoFocus()).thenReturn(1); - FocusPointFeature focusPointFeature = new FocusPointFeature(mockCameraProperties); + FocusPointFeature focusPointFeature = + new FocusPointFeature(mockCameraProperties, mockSensorOrientationFeature); focusPointFeature.setCameraBoundaries(this.mockCameraBoundaries); focusPointFeature.setValue(new Point(0.5, 0.5)); Size mockedCameraBoundaries = mock(Size.class); @@ -169,7 +194,12 @@ public void setValue_should_not_determine_metering_rectangle_when_null_coords_ar focusPointFeature.setCameraBoundaries(mockedCameraBoundaries); mockedCameraRegionUtils.verify( - () -> CameraRegionUtils.convertPointToMeteringRectangle(mockedCameraBoundaries, 0.5, 0.5), + () -> + CameraRegionUtils.convertPointToMeteringRectangle( + mockedCameraBoundaries, + 0.5, + 0.5, + PlatformChannel.DeviceOrientation.LANDSCAPE_LEFT), times(1)); } } @@ -177,7 +207,8 @@ public void setValue_should_not_determine_metering_rectangle_when_null_coords_ar @Test public void checkIsSupported_should_return_false_when_max_regions_is_null() { CameraProperties mockCameraProperties = mock(CameraProperties.class); - FocusPointFeature focusPointFeature = new FocusPointFeature(mockCameraProperties); + FocusPointFeature focusPointFeature = + new FocusPointFeature(mockCameraProperties, mockSensorOrientationFeature); focusPointFeature.setCameraBoundaries(new Size(100, 100)); when(mockCameraProperties.getControlMaxRegionsAutoFocus()).thenReturn(null); @@ -188,7 +219,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); - FocusPointFeature focusPointFeature = new FocusPointFeature(mockCameraProperties); + FocusPointFeature focusPointFeature = + new FocusPointFeature(mockCameraProperties, mockSensorOrientationFeature); focusPointFeature.setCameraBoundaries(new Size(100, 100)); when(mockCameraProperties.getControlMaxRegionsAutoFocus()).thenReturn(0); @@ -199,7 +231,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); - FocusPointFeature focusPointFeature = new FocusPointFeature(mockCameraProperties); + FocusPointFeature focusPointFeature = + new FocusPointFeature(mockCameraProperties, mockSensorOrientationFeature); focusPointFeature.setCameraBoundaries(new Size(100, 100)); when(mockCameraProperties.getControlMaxRegionsAutoFocus()).thenReturn(1); @@ -211,7 +244,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); CaptureRequest.Builder mockCaptureRequestBuilder = mock(CaptureRequest.Builder.class); - FocusPointFeature focusPointFeature = new FocusPointFeature(mockCameraProperties); + FocusPointFeature focusPointFeature = + new FocusPointFeature(mockCameraProperties, mockSensorOrientationFeature); when(mockCameraProperties.getControlMaxRegionsAutoFocus()).thenReturn(0); @@ -226,7 +260,8 @@ public void updateBuilder_should_return_when_checkIsSupported_is_false() { CameraProperties mockCameraProperties = mock(CameraProperties.class); when(mockCameraProperties.getControlMaxRegionsAutoFocus()).thenReturn(1); CaptureRequest.Builder mockCaptureRequestBuilder = mock(CaptureRequest.Builder.class); - FocusPointFeature focusPointFeature = new FocusPointFeature(mockCameraProperties); + FocusPointFeature focusPointFeature = + new FocusPointFeature(mockCameraProperties, mockSensorOrientationFeature); Size mockedCameraBoundaries = mock(Size.class); MeteringRectangle mockedMeteringRectangle = mock(MeteringRectangle.class); @@ -236,7 +271,10 @@ public void updateBuilder_should_return_when_checkIsSupported_is_false() { .when( () -> CameraRegionUtils.convertPointToMeteringRectangle( - mockedCameraBoundaries, 0.5, 0.5)) + mockedCameraBoundaries, + 0.5, + 0.5, + PlatformChannel.DeviceOrientation.LANDSCAPE_LEFT)) .thenReturn(mockedMeteringRectangle); focusPointFeature.setCameraBoundaries(mockedCameraBoundaries); focusPointFeature.setValue(new Point(0.5, 0.5)); @@ -254,7 +292,8 @@ public void updateBuilder_should_return_when_checkIsSupported_is_false() { CameraProperties mockCameraProperties = mock(CameraProperties.class); when(mockCameraProperties.getControlMaxRegionsAutoFocus()).thenReturn(1); CaptureRequest.Builder mockCaptureRequestBuilder = mock(CaptureRequest.Builder.class); - FocusPointFeature focusPointFeature = new FocusPointFeature(mockCameraProperties); + FocusPointFeature focusPointFeature = + new FocusPointFeature(mockCameraProperties, mockSensorOrientationFeature); MeteringRectangle mockedMeteringRectangle = mock(MeteringRectangle.class); focusPointFeature.updateBuilder(mockCaptureRequestBuilder); @@ -267,7 +306,8 @@ public void updateBuilder_should_not_set_metering_rectangle_when_no_valid_coords CameraProperties mockCameraProperties = mock(CameraProperties.class); when(mockCameraProperties.getControlMaxRegionsAutoFocus()).thenReturn(1); CaptureRequest.Builder mockCaptureRequestBuilder = mock(CaptureRequest.Builder.class); - FocusPointFeature focusPointFeature = new FocusPointFeature(mockCameraProperties); + FocusPointFeature focusPointFeature = + new FocusPointFeature(mockCameraProperties, mockSensorOrientationFeature); focusPointFeature.setCameraBoundaries(this.mockCameraBoundaries); focusPointFeature.setValue(null); diff --git a/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/features/sensororientation/DeviceOrientationManagerTest.java b/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/features/sensororientation/DeviceOrientationManagerTest.java index 910759e6e57f..0634e7f200be 100644 --- a/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/features/sensororientation/DeviceOrientationManagerTest.java +++ b/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/features/sensororientation/DeviceOrientationManagerTest.java @@ -162,12 +162,10 @@ public void handleOrientationChange_should_send_message_when_orientation_is_upda DeviceOrientation previousOrientation = DeviceOrientation.PORTRAIT_UP; DeviceOrientation newOrientation = DeviceOrientation.LANDSCAPE_LEFT; - DeviceOrientation orientation = - DeviceOrientationManager.handleOrientationChange( - newOrientation, previousOrientation, mockDartMessenger); + DeviceOrientationManager.handleOrientationChange( + newOrientation, previousOrientation, mockDartMessenger); verify(mockDartMessenger, times(1)).sendDeviceUIOrientationChangeEvent(newOrientation); - assertEquals(newOrientation, orientation); } @Test @@ -175,12 +173,10 @@ public void handleOrientationChange_should_not_send_message_when_orientation_is_ DeviceOrientation previousOrientation = DeviceOrientation.PORTRAIT_UP; DeviceOrientation newOrientation = DeviceOrientation.PORTRAIT_UP; - DeviceOrientation orientation = - DeviceOrientationManager.handleOrientationChange( - newOrientation, previousOrientation, mockDartMessenger); + DeviceOrientationManager.handleOrientationChange( + newOrientation, previousOrientation, mockDartMessenger); verify(mockDartMessenger, never()).sendDeviceUIOrientationChangeEvent(any()); - assertEquals(newOrientation, orientation); } @Test From 3b7f053699acfb897c09cddeb7f50bb490451296 Mon Sep 17 00:00:00 2001 From: "Bodhi Mulders (BeMacized)" Date: Mon, 12 Jul 2021 15:47:52 +0200 Subject: [PATCH 55/63] Add unit tests for metering rectangle rotation --- .../plugins/camera/CameraRegionUtilsTest.java | 129 +++++++++++------- 1 file changed, 83 insertions(+), 46 deletions(-) diff --git a/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/CameraRegionUtilsTest.java b/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/CameraRegionUtilsTest.java index af86e1169fbd..ed1f0e56adc2 100644 --- a/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/CameraRegionUtilsTest.java +++ b/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/CameraRegionUtilsTest.java @@ -28,6 +28,7 @@ public class CameraRegionUtilsTest { + private MockedStatic mockedMeteringRectangleFactory; Size mockCameraBoundaries; @Before @@ -273,52 +274,8 @@ public void convertPointToMeteringRectangle_should_throw_for_y_lower_bound() { @Test public void convertPointToMeteringRectangle_should_return_valid_MeteringRectangle() { - try (MockedStatic mockedMeteringRectangleFactory = - mockStatic(CameraRegionUtils.MeteringRectangleFactory.class)) { - - mockedMeteringRectangleFactory - .when( - () -> - CameraRegionUtils.MeteringRectangleFactory.create( - anyInt(), anyInt(), anyInt(), anyInt(), anyInt())) - .thenAnswer( - new Answer() { - @Override - public MeteringRectangle answer(InvocationOnMock createInvocation) - throws Throwable { - MeteringRectangle mockMeteringRectangle = mock(MeteringRectangle.class); - when(mockMeteringRectangle.getX()).thenReturn(createInvocation.getArgument(0)); - when(mockMeteringRectangle.getY()).thenReturn(createInvocation.getArgument(1)); - when(mockMeteringRectangle.getWidth()) - .thenReturn(createInvocation.getArgument(2)); - when(mockMeteringRectangle.getHeight()) - .thenReturn(createInvocation.getArgument(3)); - when(mockMeteringRectangle.getMeteringWeight()) - .thenReturn(createInvocation.getArgument(4)); - when(mockMeteringRectangle.equals(any())) - .thenAnswer( - new Answer() { - @Override - public Boolean answer(InvocationOnMock equalsInvocation) - throws Throwable { - MeteringRectangle otherMockMeteringRectangle = - equalsInvocation.getArgument(0); - return mockMeteringRectangle.getX() - == otherMockMeteringRectangle.getX() - && mockMeteringRectangle.getY() - == otherMockMeteringRectangle.getY() - && mockMeteringRectangle.getWidth() - == otherMockMeteringRectangle.getWidth() - && mockMeteringRectangle.getHeight() - == otherMockMeteringRectangle.getHeight() - && mockMeteringRectangle.getMeteringWeight() - == otherMockMeteringRectangle.getMeteringWeight(); - } - }); - return mockMeteringRectangle; - } - }); - + setUpTestMeteringRectangleFactory(); + try { MeteringRectangle r; // Center r = @@ -364,6 +321,40 @@ public Boolean answer(InvocationOnMock equalsInvocation) 0.0, PlatformChannel.DeviceOrientation.LANDSCAPE_LEFT); assertTrue(CameraRegionUtils.MeteringRectangleFactory.create(89, 0, 10, 10, 1).equals(r)); + } finally { + tearDownTestMeteringRectangleFactory(); + } + } + + @Test() + public void + convertPointToMeteringRectangle_should_rotate_metering_rectangle_according_to_ui_orientation() { + setUpTestMeteringRectangleFactory(); + try { + MeteringRectangle r; + // PORTRAIT_UP + r = + CameraRegionUtils.convertPointToMeteringRectangle( + this.mockCameraBoundaries, 1, 1, PlatformChannel.DeviceOrientation.PORTRAIT_UP); + assertTrue(CameraRegionUtils.MeteringRectangleFactory.create(89, 0, 10, 10, 1).equals(r)); + // PORTRAIT_DOWN + r = + CameraRegionUtils.convertPointToMeteringRectangle( + this.mockCameraBoundaries, 1, 1, PlatformChannel.DeviceOrientation.PORTRAIT_DOWN); + assertTrue(CameraRegionUtils.MeteringRectangleFactory.create(0, 89, 10, 10, 1).equals(r)); + // PORTRAIT_UP + r = + CameraRegionUtils.convertPointToMeteringRectangle( + this.mockCameraBoundaries, 1, 1, PlatformChannel.DeviceOrientation.LANDSCAPE_RIGHT); + assertTrue(CameraRegionUtils.MeteringRectangleFactory.create(0, 0, 10, 10, 1).equals(r)); + // LANDSCAPE_LEFT (no rotation) + r = + CameraRegionUtils.convertPointToMeteringRectangle( + this.mockCameraBoundaries, 1, 1, PlatformChannel.DeviceOrientation.LANDSCAPE_LEFT); + assertTrue(CameraRegionUtils.MeteringRectangleFactory.create(89, 89, 10, 10, 1).equals(r)); + + } finally { + tearDownTestMeteringRectangleFactory(); } } @@ -388,4 +379,50 @@ public void convertPointToMeteringRectangle_should_throw_for_0_height_boundary() private static void updateSdkVersion(int version) { TestUtils.setFinalStatic(Build.VERSION.class, "SDK_INT", version); } + + private void tearDownTestMeteringRectangleFactory() { + mockedMeteringRectangleFactory.close(); + } + + private void setUpTestMeteringRectangleFactory() { + mockedMeteringRectangleFactory = mockStatic(CameraRegionUtils.MeteringRectangleFactory.class); + + mockedMeteringRectangleFactory + .when( + () -> + CameraRegionUtils.MeteringRectangleFactory.create( + anyInt(), anyInt(), anyInt(), anyInt(), anyInt())) + .thenAnswer( + new Answer() { + @Override + public MeteringRectangle answer(InvocationOnMock createInvocation) throws Throwable { + MeteringRectangle mockMeteringRectangle = mock(MeteringRectangle.class); + when(mockMeteringRectangle.getX()).thenReturn(createInvocation.getArgument(0)); + when(mockMeteringRectangle.getY()).thenReturn(createInvocation.getArgument(1)); + when(mockMeteringRectangle.getWidth()).thenReturn(createInvocation.getArgument(2)); + when(mockMeteringRectangle.getHeight()).thenReturn(createInvocation.getArgument(3)); + when(mockMeteringRectangle.getMeteringWeight()) + .thenReturn(createInvocation.getArgument(4)); + when(mockMeteringRectangle.equals(any())) + .thenAnswer( + new Answer() { + @Override + public Boolean answer(InvocationOnMock equalsInvocation) + throws Throwable { + MeteringRectangle otherMockMeteringRectangle = + equalsInvocation.getArgument(0); + return mockMeteringRectangle.getX() == otherMockMeteringRectangle.getX() + && mockMeteringRectangle.getY() == otherMockMeteringRectangle.getY() + && mockMeteringRectangle.getWidth() + == otherMockMeteringRectangle.getWidth() + && mockMeteringRectangle.getHeight() + == otherMockMeteringRectangle.getHeight() + && mockMeteringRectangle.getMeteringWeight() + == otherMockMeteringRectangle.getMeteringWeight(); + } + }); + return mockMeteringRectangle; + } + }); + } } From 36d7173d50f1b0eb81489e31169c4a1db7753981 Mon Sep 17 00:00:00 2001 From: "Bodhi Mulders (BeMacized)" Date: Tue, 13 Jul 2021 09:39:15 +0200 Subject: [PATCH 56/63] Revert adjustments to platform interface to prevent unnecessary breaking change --- .../flutter/plugins/camera/DartMessenger.java | 2 +- .../plugins/camera/DartMessengerTest.java | 2 +- .../camera/camera/ios/Classes/CameraPlugin.m | 2 +- .../camera/lib/src/camera_controller.dart | 29 +++++++++---------- .../camera/camera/lib/src/camera_preview.dart | 2 +- .../camera/test/camera_preview_test.dart | 6 ++-- packages/camera/camera/test/camera_test.dart | 4 +-- .../camera/camera/test/camera_value_test.dart | 12 ++++---- .../lib/src/events/device_event.dart | 14 ++++----- .../method_channel/method_channel_camera.dart | 8 ++--- .../platform_interface/camera_platform.dart | 4 +-- .../test/camera_platform_interface_test.dart | 2 +- .../test/events/device_event_test.dart | 19 +++++------- .../method_channel_camera_test.dart | 2 +- 14 files changed, 52 insertions(+), 56 deletions(-) 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 12eae0b9901b..6e1f3c8ccf9f 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 @@ -25,7 +25,7 @@ public class DartMessenger { /** Specifies the different device related message types. */ enum DeviceEventType { /** Indicates the device's orientation has changed. */ - UI_ORIENTATION_CHANGED("ui_orientation_changed"); + UI_ORIENTATION_CHANGED("orientation_changed"); private final String method; DeviceEventType(String method) { 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 b89563cc1553..7519d6865cb2 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 @@ -110,7 +110,7 @@ public void sendDeviceOrientationChangedEvent() { List sentMessages = fakeBinaryMessenger.getMessages(); assertEquals(1, sentMessages.size()); MethodCall call = decodeSentMessage(sentMessages.get(0)); - assertEquals("ui_orientation_changed", call.method); + assertEquals("orientation_changed", call.method); assertEquals(call.argument("orientation"), "portraitUp"); } diff --git a/packages/camera/camera/ios/Classes/CameraPlugin.m b/packages/camera/camera/ios/Classes/CameraPlugin.m index d5a9448901f0..ebd5366ba78d 100644 --- a/packages/camera/camera/ios/Classes/CameraPlugin.m +++ b/packages/camera/camera/ios/Classes/CameraPlugin.m @@ -1306,7 +1306,7 @@ - (void)orientationChanged:(NSNotification *)note { - (void)sendDeviceOrientation:(UIDeviceOrientation)orientation { [_deviceEventMethodChannel - invokeMethod:@"ui_orientation_changed" + invokeMethod:@"orientation_changed" arguments:@{@"orientation" : getStringForUIDeviceOrientation(orientation)}]; } diff --git a/packages/camera/camera/lib/src/camera_controller.dart b/packages/camera/camera/lib/src/camera_controller.dart index 1c9902c75cf0..37869fe78528 100644 --- a/packages/camera/camera/lib/src/camera_controller.dart +++ b/packages/camera/camera/lib/src/camera_controller.dart @@ -44,7 +44,7 @@ class CameraValue { required this.focusMode, required this.exposurePointSupported, required this.focusPointSupported, - required this.deviceUIOrientation, + required this.deviceOrientation, this.lockedCaptureOrientation, this.recordingOrientation, }) : _isRecordingPaused = isRecordingPaused; @@ -62,7 +62,7 @@ class CameraValue { exposurePointSupported: false, focusMode: FocusMode.auto, focusPointSupported: false, - deviceUIOrientation: DeviceOrientation.portraitUp, + deviceOrientation: DeviceOrientation.portraitUp, ); /// True after [CameraController.initialize] has completed successfully. @@ -119,7 +119,7 @@ class CameraValue { final bool focusPointSupported; /// The current device UI orientation. - final DeviceOrientation deviceUIOrientation; + final DeviceOrientation deviceOrientation; /// The currently locked capture orientation. final DeviceOrientation? lockedCaptureOrientation; @@ -147,7 +147,7 @@ class CameraValue { FocusMode? focusMode, bool? exposurePointSupported, bool? focusPointSupported, - DeviceOrientation? deviceUIOrientation, + DeviceOrientation? deviceOrientation, Optional? lockedCaptureOrientation, Optional? recordingOrientation, }) { @@ -165,7 +165,7 @@ class CameraValue { exposurePointSupported: exposurePointSupported ?? this.exposurePointSupported, focusPointSupported: focusPointSupported ?? this.focusPointSupported, - deviceUIOrientation: deviceUIOrientation ?? this.deviceUIOrientation, + deviceOrientation: deviceOrientation ?? this.deviceOrientation, lockedCaptureOrientation: lockedCaptureOrientation == null ? this.lockedCaptureOrientation : lockedCaptureOrientation.orNull, @@ -188,7 +188,7 @@ class CameraValue { 'focusMode: $focusMode, ' 'exposurePointSupported: $exposurePointSupported, ' 'focusPointSupported: $focusPointSupported, ' - 'deviceUIOrientation: $deviceUIOrientation, ' + 'deviceOrientation: $deviceOrientation, ' 'lockedCaptureOrientation: $lockedCaptureOrientation, ' 'recordingOrientation: $recordingOrientation)'; } @@ -237,7 +237,7 @@ class CameraController extends ValueNotifier { bool _isDisposed = false; StreamSubscription? _imageStreamSubscription; FutureOr? _initCalled; - StreamSubscription? _deviceUIOrientationSubscription; + StreamSubscription? _deviceOrientationSubscription; /// Checks whether [CameraController.dispose] has completed successfully. /// @@ -262,11 +262,10 @@ class CameraController extends ValueNotifier { try { Completer _initializeCompleter = Completer(); - _deviceUIOrientationSubscription = CameraPlatform.instance - .onDeviceUIOrientationChanged() - .listen((event) { + _deviceOrientationSubscription = + CameraPlatform.instance.onDeviceOrientationChanged().listen((event) { value = value.copyWith( - deviceUIOrientation: event.orientation, + deviceOrientation: event.orientation, ); }); @@ -458,7 +457,7 @@ class CameraController extends ValueNotifier { isRecordingVideo: true, isRecordingPaused: false, recordingOrientation: Optional.fromNullable( - value.lockedCaptureOrientation ?? value.deviceUIOrientation)); + value.lockedCaptureOrientation ?? value.deviceOrientation)); } on PlatformException catch (e) { throw CameraException(e.code, e.message); } @@ -696,10 +695,10 @@ class CameraController extends ValueNotifier { Future lockCaptureOrientation([DeviceOrientation? orientation]) async { try { await CameraPlatform.instance.lockCaptureOrientation( - _cameraId, orientation ?? value.deviceUIOrientation); + _cameraId, orientation ?? value.deviceOrientation); value = value.copyWith( lockedCaptureOrientation: - Optional.fromNullable(orientation ?? value.deviceUIOrientation)); + Optional.fromNullable(orientation ?? value.deviceOrientation)); } on PlatformException catch (e) { throw CameraException(e.code, e.message); } @@ -756,7 +755,7 @@ class CameraController extends ValueNotifier { if (_isDisposed) { return; } - unawaited(_deviceUIOrientationSubscription?.cancel()); + unawaited(_deviceOrientationSubscription?.cancel()); _isDisposed = true; super.dispose(); if (_initCalled != null) { diff --git a/packages/camera/camera/lib/src/camera_preview.dart b/packages/camera/camera/lib/src/camera_preview.dart index 6d863ad8444c..ba55ef8c811f 100644 --- a/packages/camera/camera/lib/src/camera_preview.dart +++ b/packages/camera/camera/lib/src/camera_preview.dart @@ -66,6 +66,6 @@ class CameraPreview extends StatelessWidget { return controller.value.isRecordingVideo ? controller.value.recordingOrientation! : (controller.value.lockedCaptureOrientation ?? - controller.value.deviceUIOrientation); + controller.value.deviceOrientation); } } diff --git a/packages/camera/camera/test/camera_preview_test.dart b/packages/camera/camera/test/camera_preview_test.dart index 290230a18609..8275461192b4 100644 --- a/packages/camera/camera/test/camera_preview_test.dart +++ b/packages/camera/camera/test/camera_preview_test.dart @@ -128,7 +128,7 @@ void main() { controller.value = controller.value.copyWith( isInitialized: true, isRecordingVideo: true, - deviceUIOrientation: DeviceOrientation.portraitUp, + deviceOrientation: DeviceOrientation.portraitUp, lockedCaptureOrientation: Optional.fromNullable(DeviceOrientation.landscapeRight), recordingOrientation: @@ -161,7 +161,7 @@ void main() { final FakeController controller = FakeController(); controller.value = controller.value.copyWith( isInitialized: true, - deviceUIOrientation: DeviceOrientation.portraitUp, + deviceOrientation: DeviceOrientation.portraitUp, lockedCaptureOrientation: Optional.fromNullable(DeviceOrientation.landscapeRight), recordingOrientation: @@ -194,7 +194,7 @@ void main() { final FakeController controller = FakeController(); controller.value = controller.value.copyWith( isInitialized: true, - deviceUIOrientation: DeviceOrientation.portraitUp, + deviceOrientation: DeviceOrientation.portraitUp, lockedCaptureOrientation: null, recordingOrientation: Optional.fromNullable(DeviceOrientation.landscapeLeft), diff --git a/packages/camera/camera/test/camera_test.dart b/packages/camera/camera/test/camera_test.dart index c8088d28d977..26382a9b7d60 100644 --- a/packages/camera/camera/test/camera_test.dart +++ b/packages/camera/camera/test/camera_test.dart @@ -39,7 +39,7 @@ get mockOnCameraInitializedEvent => CameraInitializedEvent( ); get mockOnDeviceOrientationChangedEvent => - DeviceUIOrientationChangedEvent(DeviceOrientation.portraitUp); + DeviceOrientationChangedEvent(DeviceOrientation.portraitUp); get mockOnCameraClosingEvent => null; @@ -1287,7 +1287,7 @@ class MockCameraPlatform extends Mock Stream.value(mockOnCameraErrorEvent); @override - Stream onDeviceUIOrientationChanged() => + Stream onDeviceOrientationChanged() => Stream.value(mockOnDeviceOrientationChangedEvent); @override diff --git a/packages/camera/camera/test/camera_value_test.dart b/packages/camera/camera/test/camera_value_test.dart index 3d61c1587497..e0378cca2cb9 100644 --- a/packages/camera/camera/test/camera_value_test.dart +++ b/packages/camera/camera/test/camera_value_test.dart @@ -25,7 +25,7 @@ void main() { exposureMode: ExposureMode.auto, exposurePointSupported: true, focusMode: FocusMode.auto, - deviceUIOrientation: DeviceOrientation.portraitUp, + deviceOrientation: DeviceOrientation.portraitUp, lockedCaptureOrientation: DeviceOrientation.portraitUp, recordingOrientation: DeviceOrientation.portraitUp, focusPointSupported: true, @@ -42,7 +42,7 @@ void main() { expect(cameraValue.flashMode, FlashMode.auto); expect(cameraValue.exposureMode, ExposureMode.auto); expect(cameraValue.exposurePointSupported, true); - expect(cameraValue.deviceUIOrientation, DeviceOrientation.portraitUp); + expect(cameraValue.deviceOrientation, DeviceOrientation.portraitUp); expect( cameraValue.lockedCaptureOrientation, DeviceOrientation.portraitUp); expect(cameraValue.recordingOrientation, DeviceOrientation.portraitUp); @@ -63,7 +63,7 @@ void main() { expect(cameraValue.exposureMode, ExposureMode.auto); expect(cameraValue.exposurePointSupported, false); expect(cameraValue.focusMode, FocusMode.auto); - expect(cameraValue.deviceUIOrientation, DeviceOrientation.portraitUp); + expect(cameraValue.deviceOrientation, DeviceOrientation.portraitUp); expect(cameraValue.lockedCaptureOrientation, null); expect(cameraValue.recordingOrientation, null); }); @@ -84,7 +84,7 @@ void main() { expect(cameraValue.focusMode, FocusMode.auto); expect(cameraValue.exposureMode, ExposureMode.auto); expect(cameraValue.exposurePointSupported, false); - expect(cameraValue.deviceUIOrientation, DeviceOrientation.portraitUp); + expect(cameraValue.deviceOrientation, DeviceOrientation.portraitUp); expect(cameraValue.lockedCaptureOrientation, null); expect(cameraValue.recordingOrientation, null); }); @@ -129,13 +129,13 @@ void main() { focusMode: FocusMode.auto, exposurePointSupported: true, focusPointSupported: true, - deviceUIOrientation: DeviceOrientation.portraitUp, + deviceOrientation: DeviceOrientation.portraitUp, lockedCaptureOrientation: DeviceOrientation.portraitUp, recordingOrientation: DeviceOrientation.portraitUp, ); expect(cameraValue.toString(), - 'CameraValue(isRecordingVideo: false, isInitialized: false, errorDescription: null, previewSize: Size(10.0, 10.0), isStreamingImages: false, flashMode: FlashMode.auto, exposureMode: ExposureMode.auto, focusMode: FocusMode.auto, exposurePointSupported: true, focusPointSupported: true, deviceUIOrientation: DeviceOrientation.portraitUp, lockedCaptureOrientation: DeviceOrientation.portraitUp, recordingOrientation: DeviceOrientation.portraitUp)'); + 'CameraValue(isRecordingVideo: false, isInitialized: false, errorDescription: null, previewSize: Size(10.0, 10.0), isStreamingImages: false, flashMode: FlashMode.auto, exposureMode: ExposureMode.auto, focusMode: FocusMode.auto, exposurePointSupported: true, focusPointSupported: true, deviceOrientation: DeviceOrientation.portraitUp, lockedCaptureOrientation: DeviceOrientation.portraitUp, recordingOrientation: DeviceOrientation.portraitUp)'); }); }); } diff --git a/packages/camera/camera_platform_interface/lib/src/events/device_event.dart b/packages/camera/camera_platform_interface/lib/src/events/device_event.dart index a59491304f58..ac1c66e4df82 100644 --- a/packages/camera/camera_platform_interface/lib/src/events/device_event.dart +++ b/packages/camera/camera_platform_interface/lib/src/events/device_event.dart @@ -20,20 +20,20 @@ import 'package:flutter/services.dart'; /// They can be (and in fact, are) filtered by the `instanceof`-operator. abstract class DeviceEvent {} -/// The [DeviceUIOrientationChangedEvent] is fired every time the orientation of the device UI changes. -class DeviceUIOrientationChangedEvent extends DeviceEvent { +/// The [DeviceOrientationChangedEvent] is fired every time the orientation of the device UI changes. +class DeviceOrientationChangedEvent extends DeviceEvent { /// The new orientation of the device final DeviceOrientation orientation; /// Build a new orientation changed event. - DeviceUIOrientationChangedEvent(this.orientation); + DeviceOrientationChangedEvent(this.orientation); - /// Converts the supplied [Map] to an instance of the [DeviceUIOrientationChangedEvent] + /// Converts the supplied [Map] to an instance of the [DeviceOrientationChangedEvent] /// class. - DeviceUIOrientationChangedEvent.fromJson(Map json) + DeviceOrientationChangedEvent.fromJson(Map json) : orientation = deserializeDeviceOrientation(json['orientation']); - /// Converts the [DeviceUIOrientationChangedEvent] instance into a [Map] instance that + /// Converts the [DeviceOrientationChangedEvent] instance into a [Map] instance that /// can be serialized to JSON. Map toJson() => { 'orientation': serializeDeviceOrientation(orientation), @@ -42,7 +42,7 @@ class DeviceUIOrientationChangedEvent extends DeviceEvent { @override bool operator ==(Object other) => identical(this, other) || - other is DeviceUIOrientationChangedEvent && + other is DeviceOrientationChangedEvent && runtimeType == other.runtimeType && orientation == other.orientation; diff --git a/packages/camera/camera_platform_interface/lib/src/method_channel/method_channel_camera.dart b/packages/camera/camera_platform_interface/lib/src/method_channel/method_channel_camera.dart index 927edf573c13..c6c363a56d65 100644 --- a/packages/camera/camera_platform_interface/lib/src/method_channel/method_channel_camera.dart +++ b/packages/camera/camera_platform_interface/lib/src/method_channel/method_channel_camera.dart @@ -169,9 +169,9 @@ class MethodChannelCamera extends CameraPlatform { } @override - Stream onDeviceUIOrientationChanged() { + Stream onDeviceOrientationChanged() { return deviceEventStreamController.stream - .whereType(); + .whereType(); } @override @@ -446,8 +446,8 @@ class MethodChannelCamera extends CameraPlatform { /// the plugin as it may break or change at any time. Future handleDeviceMethodCall(MethodCall call) async { switch (call.method) { - case 'ui_orientation_changed': - deviceEventStreamController.add(DeviceUIOrientationChangedEvent( + case 'orientation_changed': + deviceEventStreamController.add(DeviceOrientationChangedEvent( deserializeDeviceOrientation(call.arguments['orientation']))); break; default: diff --git a/packages/camera/camera_platform_interface/lib/src/platform_interface/camera_platform.dart b/packages/camera/camera_platform_interface/lib/src/platform_interface/camera_platform.dart index ba73fd87028c..e61fd78f6cde 100644 --- a/packages/camera/camera_platform_interface/lib/src/platform_interface/camera_platform.dart +++ b/packages/camera/camera_platform_interface/lib/src/platform_interface/camera_platform.dart @@ -100,9 +100,9 @@ abstract class CameraPlatform extends PlatformInterface { /// /// Implementations for this: /// - Should support all 4 orientations. - Stream onDeviceUIOrientationChanged() { + Stream onDeviceOrientationChanged() { throw UnimplementedError( - 'onDeviceUIOrientationChanged() is not implemented.'); + 'onDeviceOrientationChanged() is not implemented.'); } /// Locks the capture orientation. diff --git a/packages/camera/camera_platform_interface/test/camera_platform_interface_test.dart b/packages/camera/camera_platform_interface/test/camera_platform_interface_test.dart index 259f84fb394c..c8f38efc4e2d 100644 --- a/packages/camera/camera_platform_interface/test/camera_platform_interface_test.dart +++ b/packages/camera/camera_platform_interface/test/camera_platform_interface_test.dart @@ -98,7 +98,7 @@ void main() { // Act & Assert expect( - () => cameraPlatform.onDeviceUIOrientationChanged(), + () => cameraPlatform.onDeviceOrientationChanged(), throwsUnimplementedError, ); }); diff --git a/packages/camera/camera_platform_interface/test/events/device_event_test.dart b/packages/camera/camera_platform_interface/test/events/device_event_test.dart index 3e5e8fa013d0..f7cb657725a9 100644 --- a/packages/camera/camera_platform_interface/test/events/device_event_test.dart +++ b/packages/camera/camera_platform_interface/test/events/device_event_test.dart @@ -11,14 +11,13 @@ void main() { group('DeviceOrientationChangedEvent tests', () { test('Constructor should initialize all properties', () { - final event = - DeviceUIOrientationChangedEvent(DeviceOrientation.portraitUp); + final event = DeviceOrientationChangedEvent(DeviceOrientation.portraitUp); expect(event.orientation, DeviceOrientation.portraitUp); }); test('fromJson should initialize all properties', () { - final event = DeviceUIOrientationChangedEvent.fromJson({ + final event = DeviceOrientationChangedEvent.fromJson({ 'orientation': 'portraitUp', }); @@ -26,8 +25,7 @@ void main() { }); test('toJson should return a map with all fields', () { - final event = - DeviceUIOrientationChangedEvent(DeviceOrientation.portraitUp); + final event = DeviceOrientationChangedEvent(DeviceOrientation.portraitUp); final jsonMap = event.toJson(); @@ -37,25 +35,24 @@ void main() { test('equals should return true if objects are the same', () { final firstEvent = - DeviceUIOrientationChangedEvent(DeviceOrientation.portraitUp); + DeviceOrientationChangedEvent(DeviceOrientation.portraitUp); final secondEvent = - DeviceUIOrientationChangedEvent(DeviceOrientation.portraitUp); + DeviceOrientationChangedEvent(DeviceOrientation.portraitUp); expect(firstEvent == secondEvent, true); }); test('equals should return false if orientation is different', () { final firstEvent = - DeviceUIOrientationChangedEvent(DeviceOrientation.portraitUp); + DeviceOrientationChangedEvent(DeviceOrientation.portraitUp); final secondEvent = - DeviceUIOrientationChangedEvent(DeviceOrientation.landscapeLeft); + DeviceOrientationChangedEvent(DeviceOrientation.landscapeLeft); expect(firstEvent == secondEvent, false); }); test('hashCode should match hashCode of all properties', () { - final event = - DeviceUIOrientationChangedEvent(DeviceOrientation.portraitUp); + final event = DeviceOrientationChangedEvent(DeviceOrientation.portraitUp); final expectedHashCode = event.orientation.hashCode; expect(event.hashCode, expectedHashCode); diff --git a/packages/camera/camera_platform_interface/test/method_channel/method_channel_camera_test.dart b/packages/camera/camera_platform_interface/test/method_channel/method_channel_camera_test.dart index b0f038ce9afe..8a618545535b 100644 --- a/packages/camera/camera_platform_interface/test/method_channel/method_channel_camera_test.dart +++ b/packages/camera/camera_platform_interface/test/method_channel/method_channel_camera_test.dart @@ -356,7 +356,7 @@ void main() { // Emit test events final event = - DeviceUIOrientationChangedEvent(DeviceOrientation.portraitUp); + DeviceOrientationChangedEvent(DeviceOrientation.portraitUp); await camera.handleDeviceMethodCall( MethodCall('orientation_changed', event.toJson())); await camera.handleDeviceMethodCall( From a2607d1c3957a4f990cdcac3930fecbc5e8899f2 Mon Sep 17 00:00:00 2001 From: "Bodhi Mulders (BeMacized)" Date: Tue, 13 Jul 2021 09:41:35 +0200 Subject: [PATCH 57/63] Revert naming refactor --- .../main/java/io/flutter/plugins/camera/DartMessenger.java | 6 +++--- .../sensororientation/DeviceOrientationManager.java | 2 +- .../java/io/flutter/plugins/camera/DartMessengerTest.java | 2 +- .../sensororientation/DeviceOrientationManagerTest.java | 6 +++--- 4 files changed, 8 insertions(+), 8 deletions(-) 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 6e1f3c8ccf9f..dc62fce524d3 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 @@ -25,7 +25,7 @@ public class DartMessenger { /** Specifies the different device related message types. */ enum DeviceEventType { /** Indicates the device's orientation has changed. */ - UI_ORIENTATION_CHANGED("orientation_changed"); + ORIENTATION_CHANGED("orientation_changed"); private final String method; DeviceEventType(String method) { @@ -74,10 +74,10 @@ enum CameraEventType { * * @param orientation specifies the new orientation of the device. */ - public void sendDeviceUIOrientationChangeEvent(PlatformChannel.DeviceOrientation orientation) { + public void sendDeviceOrientationChangeEvent(PlatformChannel.DeviceOrientation orientation) { assert (orientation != null); this.send( - DeviceEventType.UI_ORIENTATION_CHANGED, + DeviceEventType.ORIENTATION_CHANGED, new HashMap() { { put("orientation", CameraUtils.serializeDeviceOrientation(orientation)); 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 def5a0ad7174..5a2d9b9b7740 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 @@ -224,7 +224,7 @@ static void handleOrientationChange( DeviceOrientation previousOrientation, DartMessenger messenger) { if (!newOrientation.equals(previousOrientation)) { - messenger.sendDeviceUIOrientationChangeEvent(newOrientation); + messenger.sendDeviceOrientationChangeEvent(newOrientation); } } 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 7519d6865cb2..0a2fc43d03cb 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 @@ -105,7 +105,7 @@ public void sendCameraClosingEvent() { @Test public void sendDeviceOrientationChangedEvent() { doAnswer(createPostHandlerAnswer()).when(mockHandler).post(any(Runnable.class)); - dartMessenger.sendDeviceUIOrientationChangeEvent(PlatformChannel.DeviceOrientation.PORTRAIT_UP); + dartMessenger.sendDeviceOrientationChangeEvent(PlatformChannel.DeviceOrientation.PORTRAIT_UP); List sentMessages = fakeBinaryMessenger.getMessages(); assertEquals(1, sentMessages.size()); diff --git a/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/features/sensororientation/DeviceOrientationManagerTest.java b/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/features/sensororientation/DeviceOrientationManagerTest.java index 0634e7f200be..1fd9a2380883 100644 --- a/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/features/sensororientation/DeviceOrientationManagerTest.java +++ b/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/features/sensororientation/DeviceOrientationManagerTest.java @@ -154,7 +154,7 @@ public void handleUIOrientationChange_should_send_message_when_sensor_access_is_ } verify(mockDartMessenger, times(1)) - .sendDeviceUIOrientationChangeEvent(DeviceOrientation.LANDSCAPE_LEFT); + .sendDeviceOrientationChangeEvent(DeviceOrientation.LANDSCAPE_LEFT); } @Test @@ -165,7 +165,7 @@ public void handleOrientationChange_should_send_message_when_orientation_is_upda DeviceOrientationManager.handleOrientationChange( newOrientation, previousOrientation, mockDartMessenger); - verify(mockDartMessenger, times(1)).sendDeviceUIOrientationChangeEvent(newOrientation); + verify(mockDartMessenger, times(1)).sendDeviceOrientationChangeEvent(newOrientation); } @Test @@ -176,7 +176,7 @@ public void handleOrientationChange_should_not_send_message_when_orientation_is_ DeviceOrientationManager.handleOrientationChange( newOrientation, previousOrientation, mockDartMessenger); - verify(mockDartMessenger, never()).sendDeviceUIOrientationChangeEvent(any()); + verify(mockDartMessenger, never()).sendDeviceOrientationChangeEvent(any()); } @Test From 9fb1019a8c64c694c85343c5533eca89049a04a8 Mon Sep 17 00:00:00 2001 From: Bodhi Mulders Date: Tue, 13 Jul 2021 09:48:44 +0200 Subject: [PATCH 58/63] Update project.pbxproj --- .../camera/camera/example/ios/Runner.xcodeproj/project.pbxproj | 3 --- 1 file changed, 3 deletions(-) diff --git a/packages/camera/camera/example/ios/Runner.xcodeproj/project.pbxproj b/packages/camera/camera/example/ios/Runner.xcodeproj/project.pbxproj index 1a3a9195ad1e..aead167a5e99 100644 --- a/packages/camera/camera/example/ios/Runner.xcodeproj/project.pbxproj +++ b/packages/camera/camera/example/ios/Runner.xcodeproj/project.pbxproj @@ -235,7 +235,6 @@ }; 97C146ED1CF9000F007C117D = { CreatedOnToolsVersion = 7.3.1; - DevelopmentTeam = 7624MWN53C; }; }; }; @@ -564,7 +563,6 @@ baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */; buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; - DEVELOPMENT_TEAM = 7624MWN53C; ENABLE_BITCODE = NO; FRAMEWORK_SEARCH_PATHS = ( "$(inherited)", @@ -586,7 +584,6 @@ baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; - DEVELOPMENT_TEAM = 7624MWN53C; ENABLE_BITCODE = NO; FRAMEWORK_SEARCH_PATHS = ( "$(inherited)", From 777c696ede2505b71d48cb96ce6153b713dad476 Mon Sep 17 00:00:00 2001 From: "Bodhi Mulders (BeMacized)" Date: Tue, 13 Jul 2021 09:53:59 +0200 Subject: [PATCH 59/63] Undo version bump --- packages/camera/camera/CHANGELOG.md | 6 ------ packages/camera/camera/pubspec.yaml | 2 +- 2 files changed, 1 insertion(+), 7 deletions(-) diff --git a/packages/camera/camera/CHANGELOG.md b/packages/camera/camera/CHANGELOG.md index da6235518ee0..1f30104218e3 100644 --- a/packages/camera/camera/CHANGELOG.md +++ b/packages/camera/camera/CHANGELOG.md @@ -1,9 +1,3 @@ -## 0.8.2 - -* 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. - ## 0.8.1+4 * Silenced warnings that may occur during build when using a very diff --git a/packages/camera/camera/pubspec.yaml b/packages/camera/camera/pubspec.yaml index 78cad5215afb..789910e2c79b 100644 --- a/packages/camera/camera/pubspec.yaml +++ b/packages/camera/camera/pubspec.yaml @@ -4,7 +4,7 @@ description: A Flutter plugin for getting information about and controlling the and streaming image buffers to dart. repository: https://github.com/flutter/plugins/tree/master/packages/camera/camera issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+camera%22 -version: 0.8.2 +version: 0.8.1+4 environment: sdk: ">=2.12.0 <3.0.0" From 5d9fb6d56215e823c0532627e9ae292f535fdea5 Mon Sep 17 00:00:00 2001 From: "Bodhi Mulders (BeMacized)" Date: Tue, 13 Jul 2021 09:57:32 +0200 Subject: [PATCH 60/63] Updated changelog and pubspec version --- packages/camera/camera/CHANGELOG.md | 6 ++++++ packages/camera/camera/pubspec.yaml | 6 ++---- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/packages/camera/camera/CHANGELOG.md b/packages/camera/camera/CHANGELOG.md index 1f30104218e3..12c83aa7dc77 100644 --- a/packages/camera/camera/CHANGELOG.md +++ b/packages/camera/camera/CHANGELOG.md @@ -1,3 +1,9 @@ +## 0.9.0 + +* Complete rewrite of Android plugin to fix many capture, focus, flash, orientation 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. + ## 0.8.1+4 * Silenced warnings that may occur during build when using a very diff --git a/packages/camera/camera/pubspec.yaml b/packages/camera/camera/pubspec.yaml index ac0fc7177075..710dd0dd07f8 100644 --- a/packages/camera/camera/pubspec.yaml +++ b/packages/camera/camera/pubspec.yaml @@ -4,7 +4,7 @@ description: A Flutter plugin for getting information about and controlling the and streaming image buffers to dart. repository: https://github.com/flutter/plugins/tree/master/packages/camera/camera issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+camera%22 -version: 0.8.1+4 +version: 0.9.0 environment: sdk: ">=2.12.0 <3.0.0" @@ -20,9 +20,7 @@ flutter: pluginClass: CameraPlugin dependencies: -# camera_platform_interface: ^2.0.0 - camera_platform_interface: - path: ../camera_platform_interface + camera_platform_interface: ^2.0.0 flutter: sdk: flutter pedantic: ^1.10.0 From a3adf8ff1d53c90ed6071821e71c6c80b52e9429 Mon Sep 17 00:00:00 2001 From: "Bodhi Mulders (BeMacized)" Date: Mon, 26 Jul 2021 12:56:12 +0200 Subject: [PATCH 61/63] Expand camera platform interface to support pausing the camera preview. --- .../camera_platform_interface/CHANGELOG.md | 4 +++ .../method_channel/method_channel_camera.dart | 16 ++++++++++ .../platform_interface/camera_platform.dart | 10 ++++++ .../camera_platform_interface/pubspec.yaml | 2 +- .../test/camera_platform_interface_test.dart | 26 +++++++++++++++ .../method_channel_camera_test.dart | 32 +++++++++++++++++++ 6 files changed, 89 insertions(+), 1 deletion(-) diff --git a/packages/camera/camera_platform_interface/CHANGELOG.md b/packages/camera/camera_platform_interface/CHANGELOG.md index 49214d24d18e..6567d00aa852 100644 --- a/packages/camera/camera_platform_interface/CHANGELOG.md +++ b/packages/camera/camera_platform_interface/CHANGELOG.md @@ -1,3 +1,7 @@ +## 2.1.0 + +* Introduces interface methods for pausing and resuming the camera preview. + ## 2.0.1 * Update platform_plugin_interface version requirement. diff --git a/packages/camera/camera_platform_interface/lib/src/method_channel/method_channel_camera.dart b/packages/camera/camera_platform_interface/lib/src/method_channel/method_channel_camera.dart index c6c363a56d65..f932f253f491 100644 --- a/packages/camera/camera_platform_interface/lib/src/method_channel/method_channel_camera.dart +++ b/packages/camera/camera_platform_interface/lib/src/method_channel/method_channel_camera.dart @@ -399,6 +399,22 @@ class MethodChannelCamera extends CameraPlatform { } } + @override + Future pausePreview(int cameraId) async { + await _channel.invokeMethod( + 'pausePreview', + {'cameraId': cameraId}, + ); + } + + @override + Future resumePreview(int cameraId) async { + await _channel.invokeMethod( + 'resumePreview', + {'cameraId': cameraId}, + ); + } + @override Widget buildPreview(int cameraId) { return Texture(textureId: cameraId); diff --git a/packages/camera/camera_platform_interface/lib/src/platform_interface/camera_platform.dart b/packages/camera/camera_platform_interface/lib/src/platform_interface/camera_platform.dart index e61fd78f6cde..7a7bbf3da592 100644 --- a/packages/camera/camera_platform_interface/lib/src/platform_interface/camera_platform.dart +++ b/packages/camera/camera_platform_interface/lib/src/platform_interface/camera_platform.dart @@ -234,6 +234,16 @@ abstract class CameraPlatform extends PlatformInterface { throw UnimplementedError('setZoomLevel() is not implemented.'); } + /// Pause the active preview on the current frame for the selected camera. + Future pausePreview(int cameraId) { + throw UnimplementedError('pausePreview() is not implemented.'); + } + + /// Resume the paused preview for the selected camera. + Future resumePreview(int cameraId) { + throw UnimplementedError('pausePreview() is not implemented.'); + } + /// Returns a widget showing a live camera preview. Widget buildPreview(int cameraId) { throw UnimplementedError('buildView() has not been implemented.'); diff --git a/packages/camera/camera_platform_interface/pubspec.yaml b/packages/camera/camera_platform_interface/pubspec.yaml index def06019c268..d691afd41c21 100644 --- a/packages/camera/camera_platform_interface/pubspec.yaml +++ b/packages/camera/camera_platform_interface/pubspec.yaml @@ -4,7 +4,7 @@ repository: https://github.com/flutter/plugins/tree/master/packages/camera/camer issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+camera%22 # 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.1.0 environment: sdk: '>=2.12.0 <3.0.0' diff --git a/packages/camera/camera_platform_interface/test/camera_platform_interface_test.dart b/packages/camera/camera_platform_interface/test/camera_platform_interface_test.dart index c8f38efc4e2d..750c27200692 100644 --- a/packages/camera/camera_platform_interface/test/camera_platform_interface_test.dart +++ b/packages/camera/camera_platform_interface/test/camera_platform_interface_test.dart @@ -408,6 +408,32 @@ void main() { throwsUnimplementedError, ); }); + + test( + 'Default implementation of pausePreview() should throw unimplemented error', + () { + // Arrange + final cameraPlatform = ExtendsCameraPlatform(); + + // Act & Assert + expect( + () => cameraPlatform.pausePreview(1), + throwsUnimplementedError, + ); + }); + + test( + 'Default implementation of resumePreview() should throw unimplemented error', + () { + // Arrange + final cameraPlatform = ExtendsCameraPlatform(); + + // Act & Assert + expect( + () => cameraPlatform.resumePreview(1), + throwsUnimplementedError, + ); + }); }); } diff --git a/packages/camera/camera_platform_interface/test/method_channel/method_channel_camera_test.dart b/packages/camera/camera_platform_interface/test/method_channel/method_channel_camera_test.dart index 8a618545535b..ec71aa173fff 100644 --- a/packages/camera/camera_platform_interface/test/method_channel/method_channel_camera_test.dart +++ b/packages/camera/camera_platform_interface/test/method_channel/method_channel_camera_test.dart @@ -923,6 +923,38 @@ void main() { arguments: {'cameraId': cameraId}), ]); }); + + test('Should pause the camera preview', () async { + // Arrange + MethodChannelMock channel = MethodChannelMock( + channelName: 'plugins.flutter.io/camera', + methods: {'pausePreview': null}, + ); + + // Act + await camera.pausePreview(cameraId); + + // Assert + expect(channel.log, [ + isMethodCall('pausePreview', arguments: {'cameraId': cameraId}), + ]); + }); + + test('Should resume the camera preview', () async { + // Arrange + MethodChannelMock channel = MethodChannelMock( + channelName: 'plugins.flutter.io/camera', + methods: {'resumePreview': null}, + ); + + // Act + await camera.resumePreview(cameraId); + + // Assert + expect(channel.log, [ + isMethodCall('resumePreview', arguments: {'cameraId': cameraId}), + ]); + }); }); }); } From 98378d325bbc0d14afdb7b54e0c05b67c0cdff42 Mon Sep 17 00:00:00 2001 From: "Bodhi Mulders (BeMacized)" Date: Mon, 26 Jul 2021 12:58:13 +0200 Subject: [PATCH 62/63] Add Android & iOS implementations for pausing the camera preview --- packages/camera/camera/CHANGELOG.md | 4 ++ .../io/flutter/plugins/camera/Camera.java | 67 ++++++++++++------- .../plugins/camera/MethodCallHandlerImpl.java | 20 ++++++ packages/camera/camera/example/lib/main.dart | 28 +++++++- .../camera/camera/ios/Classes/CameraPlugin.m | 19 +++++- .../camera/lib/src/camera_controller.dart | 48 ++++++++++++- .../camera/camera/lib/src/camera_preview.dart | 3 +- packages/camera/camera/pubspec.yaml | 4 +- .../camera/test/camera_preview_test.dart | 6 ++ .../camera/camera/test/camera_value_test.dart | 43 +++++++----- 10 files changed, 196 insertions(+), 46 deletions(-) diff --git a/packages/camera/camera/CHANGELOG.md b/packages/camera/camera/CHANGELOG.md index 8c19fb193966..2c78f765d707 100644 --- a/packages/camera/camera/CHANGELOG.md +++ b/packages/camera/camera/CHANGELOG.md @@ -1,3 +1,7 @@ +## 0.9.1 + +* Added functions to pause and resume the camera preview + ## 0.9.0 * Complete rewrite of Android plugin to fix many capture, focus, flash, orientation and exposure issues. 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 6234b4abb81a..200b107f2295 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 @@ -162,6 +162,8 @@ public void onError(String errorCode, String errorMessage) { private MediaRecorder mediaRecorder; /** True when recording video. */ private boolean recordingVideo; + /** True when the preview is paused. */ + private boolean pausedPreview; private File captureFile; @@ -189,6 +191,7 @@ public Camera( this.applicationContext = activity.getApplicationContext(); this.cameraProperties = cameraProperties; this.cameraFeatureFactory = cameraFeatureFactory; + this.pausedPreview = false; // Setup camera features this.cameraFeatures = new CameraFeatures(); @@ -484,8 +487,10 @@ private void refreshPreviewCaptureSession( } try { - captureSession.setRepeatingRequest( - previewRequestBuilder.build(), cameraCaptureCallback, backgroundHandler); + if (!pausedPreview) { + captureSession.setRepeatingRequest( + previewRequestBuilder.build(), cameraCaptureCallback, backgroundHandler); + } if (onSuccessCallback != null) { onSuccessCallback.run(); @@ -900,29 +905,32 @@ public void setFocusMode(final Result result, FocusMode newMode) { * 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 - previewRequestBuilder.set( - CaptureRequest.CONTROL_AF_TRIGGER, CameraMetadata.CONTROL_AF_TRIGGER_IDLE); - - try { - captureSession.setRepeatingRequest( - previewRequestBuilder.build(), null, backgroundHandler); - } catch (CameraAccessException e) { - if (result != null) { - result.error("setFocusModeFailed", "Error setting focus mode: " + e.getMessage(), null); + if (!pausedPreview) { + switch (newMode) { + case locked: + // Perform a single focus trigger + lockAutoFocus(); + + // Set AF state to idle again + previewRequestBuilder.set( + CaptureRequest.CONTROL_AF_TRIGGER, CameraMetadata.CONTROL_AF_TRIGGER_IDLE); + + try { + captureSession.setRepeatingRequest( + previewRequestBuilder.build(), null, backgroundHandler); + } catch (CameraAccessException e) { + if (result != null) { + result.error( + "setFocusModeFailed", "Error setting focus mode: " + e.getMessage(), null); + } } - } - break; + break; - case auto: - // Cancel current AF trigger and set AF to idle again - unlockAutoFocus(); - break; + case auto: + // Cancel current AF trigger and set AF to idle again + unlockAutoFocus(); + break; + } } if (result != null) { @@ -1026,6 +1034,19 @@ public void unlockCaptureOrientation() { cameraFeatures.getSensorOrientation().unlockCaptureOrientation(); } + /** Pause the preview from dart. */ + public void pausePreview() throws CameraAccessException { + this.pausedPreview = true; + this.captureSession.stopRepeating(); + } + + /** Resume the preview from dart. */ + public void resumePreview() throws CameraAccessException { + this.pausedPreview = false; + this.refreshPreviewCaptureSession( + null, (code, message) -> dartMessenger.sendCameraErrorEvent(message)); + } + public void startPreview() throws CameraAccessException { if (pictureImageReader == null || pictureImageReader.getSurface() == null) return; Log.i(TAG, "startPreview"); 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 ac6af3ded19c..1426bcc41af4 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 @@ -334,6 +334,26 @@ public void onMethodCall(@NonNull MethodCall call, @NonNull final Result result) } break; } + case "pausePreview": + { + try { + camera.pausePreview(); + result.success(null); + } catch (Exception e) { + handleException(e, result); + } + break; + } + case "resumePreview": + { + try { + camera.resumePreview(); + result.success(null); + } catch (Exception e) { + handleException(e, result); + } + break; + } case "dispose": { if (camera != null) { diff --git a/packages/camera/camera/example/lib/main.dart b/packages/camera/camera/example/lib/main.dart index 00ac2251ba2a..a7e8472c3ec1 100644 --- a/packages/camera/camera/example/lib/main.dart +++ b/packages/camera/camera/example/lib/main.dart @@ -530,7 +530,16 @@ class _CameraExampleHomeState extends State cameraController.value.isRecordingVideo ? onStopButtonPressed : null, - ) + ), + IconButton( + icon: const Icon(Icons.pause_presentation), + color: + cameraController != null && cameraController.value.isPreviewPaused + ? Colors.red + : Colors.blue, + onPressed: + cameraController == null ? null : onPausePreviewButtonPressed, + ), ], ); } @@ -741,6 +750,23 @@ class _CameraExampleHomeState extends State }); } + Future onPausePreviewButtonPressed() async { + final CameraController? cameraController = controller; + + if (cameraController == null || !cameraController.value.isInitialized) { + showInSnackBar('Error: select a camera first.'); + return; + } + + if (cameraController.value.isPreviewPaused) { + await cameraController.resumePreview(); + } else { + await cameraController.pausePreview(); + } + + if (mounted) setState(() {}); + } + void onPauseButtonPressed() { pauseVideoRecording().then((_) { if (mounted) setState(() {}); diff --git a/packages/camera/camera/ios/Classes/CameraPlugin.m b/packages/camera/camera/ios/Classes/CameraPlugin.m index d88eb45945fe..1f5e369a9b11 100644 --- a/packages/camera/camera/ios/Classes/CameraPlugin.m +++ b/packages/camera/camera/ios/Classes/CameraPlugin.m @@ -330,6 +330,7 @@ @interface FLTCam : NSObject isRecordingVideo && _isRecordingPaused; @@ -150,6 +159,8 @@ class CameraValue { DeviceOrientation? deviceOrientation, Optional? lockedCaptureOrientation, Optional? recordingOrientation, + bool? isPreviewPaused, + Optional? previewPauseOrientation, }) { return CameraValue( isInitialized: isInitialized ?? this.isInitialized, @@ -172,6 +183,10 @@ class CameraValue { recordingOrientation: recordingOrientation == null ? this.recordingOrientation : recordingOrientation.orNull, + isPreviewPaused: isPreviewPaused ?? this.isPreviewPaused, + previewPauseOrientation: previewPauseOrientation == null + ? this.previewPauseOrientation + : previewPauseOrientation.orNull, ); } @@ -190,7 +205,9 @@ class CameraValue { 'focusPointSupported: $focusPointSupported, ' 'deviceOrientation: $deviceOrientation, ' 'lockedCaptureOrientation: $lockedCaptureOrientation, ' - 'recordingOrientation: $recordingOrientation)'; + 'recordingOrientation: $recordingOrientation, ' + 'isPreviewPaused: $isPreviewPaused, ' + 'previewPausedOrientation: $previewPauseOrientation)'; } } @@ -325,6 +342,35 @@ class CameraController extends ValueNotifier { await CameraPlatform.instance.prepareForVideoRecording(); } + /// Pauses the current camera preview + Future pausePreview() async { + if (value.isPreviewPaused) { + return; + } + try { + await CameraPlatform.instance.pausePreview(_cameraId); + value = value.copyWith( + isPreviewPaused: true, + previewPauseOrientation: Optional.of(this.value.deviceOrientation)); + } on PlatformException catch (e) { + throw CameraException(e.code, e.message); + } + } + + /// Resumes the current camera preview + Future resumePreview() async { + if (!value.isPreviewPaused) { + return; + } + try { + await CameraPlatform.instance.resumePreview(_cameraId); + value = value.copyWith( + isPreviewPaused: false, previewPauseOrientation: Optional.absent()); + } on PlatformException catch (e) { + throw CameraException(e.code, e.message); + } + } + /// Captures an image and returns the file where it was saved. /// /// Throws a [CameraException] if the capture fails. diff --git a/packages/camera/camera/lib/src/camera_preview.dart b/packages/camera/camera/lib/src/camera_preview.dart index ba55ef8c811f..bc4221c3d97a 100644 --- a/packages/camera/camera/lib/src/camera_preview.dart +++ b/packages/camera/camera/lib/src/camera_preview.dart @@ -65,7 +65,8 @@ class CameraPreview extends StatelessWidget { DeviceOrientation _getApplicableOrientation() { return controller.value.isRecordingVideo ? controller.value.recordingOrientation! - : (controller.value.lockedCaptureOrientation ?? + : (controller.value.previewPauseOrientation ?? + controller.value.lockedCaptureOrientation ?? controller.value.deviceOrientation); } } diff --git a/packages/camera/camera/pubspec.yaml b/packages/camera/camera/pubspec.yaml index 710dd0dd07f8..d27ec170b11d 100644 --- a/packages/camera/camera/pubspec.yaml +++ b/packages/camera/camera/pubspec.yaml @@ -4,7 +4,7 @@ description: A Flutter plugin for getting information about and controlling the and streaming image buffers to dart. repository: https://github.com/flutter/plugins/tree/master/packages/camera/camera issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+camera%22 -version: 0.9.0 +version: 0.9.1 environment: sdk: ">=2.12.0 <3.0.0" @@ -20,7 +20,7 @@ flutter: pluginClass: CameraPlugin dependencies: - camera_platform_interface: ^2.0.0 + camera_platform_interface: ^2.1.0 flutter: sdk: flutter pedantic: ^1.10.0 diff --git a/packages/camera/camera/test/camera_preview_test.dart b/packages/camera/camera/test/camera_preview_test.dart index 8275461192b4..14afddaea070 100644 --- a/packages/camera/camera/test/camera_preview_test.dart +++ b/packages/camera/camera/test/camera_preview_test.dart @@ -113,6 +113,12 @@ class FakeController extends ValueNotifier @override Future unlockCaptureOrientation() async {} + + @override + Future pausePreview() async {} + + @override + Future resumePreview() async {} } void main() { diff --git a/packages/camera/camera/test/camera_value_test.dart b/packages/camera/camera/test/camera_value_test.dart index e0378cca2cb9..4718d8943c34 100644 --- a/packages/camera/camera/test/camera_value_test.dart +++ b/packages/camera/camera/test/camera_value_test.dart @@ -29,6 +29,8 @@ void main() { lockedCaptureOrientation: DeviceOrientation.portraitUp, recordingOrientation: DeviceOrientation.portraitUp, focusPointSupported: true, + isPreviewPaused: false, + previewPauseOrientation: DeviceOrientation.portraitUp, ); expect(cameraValue, isA()); @@ -46,6 +48,8 @@ void main() { expect( cameraValue.lockedCaptureOrientation, DeviceOrientation.portraitUp); expect(cameraValue.recordingOrientation, DeviceOrientation.portraitUp); + expect(cameraValue.isPreviewPaused, false); + expect(cameraValue.previewPauseOrientation, DeviceOrientation.portraitUp); }); test('Can be created as uninitialized', () { @@ -66,6 +70,8 @@ void main() { expect(cameraValue.deviceOrientation, DeviceOrientation.portraitUp); expect(cameraValue.lockedCaptureOrientation, null); expect(cameraValue.recordingOrientation, null); + expect(cameraValue.isPreviewPaused, isFalse); + expect(cameraValue.previewPauseOrientation, null); }); test('Can be copied with isInitialized', () { @@ -87,6 +93,8 @@ void main() { expect(cameraValue.deviceOrientation, DeviceOrientation.portraitUp); expect(cameraValue.lockedCaptureOrientation, null); expect(cameraValue.recordingOrientation, null); + expect(cameraValue.isPreviewPaused, isFalse); + expect(cameraValue.previewPauseOrientation, null); }); test('Has aspectRatio after setting size', () { @@ -117,25 +125,26 @@ void main() { test('toString() works as expected', () { var cameraValue = const CameraValue( - isInitialized: false, - errorDescription: null, - previewSize: Size(10, 10), - isRecordingPaused: false, - isRecordingVideo: false, - isTakingPicture: false, - isStreamingImages: false, - flashMode: FlashMode.auto, - exposureMode: ExposureMode.auto, - focusMode: FocusMode.auto, - exposurePointSupported: true, - focusPointSupported: true, - deviceOrientation: DeviceOrientation.portraitUp, - lockedCaptureOrientation: DeviceOrientation.portraitUp, - recordingOrientation: DeviceOrientation.portraitUp, - ); + isInitialized: false, + errorDescription: null, + previewSize: Size(10, 10), + isRecordingPaused: false, + isRecordingVideo: false, + isTakingPicture: false, + isStreamingImages: false, + flashMode: FlashMode.auto, + exposureMode: ExposureMode.auto, + focusMode: FocusMode.auto, + exposurePointSupported: true, + focusPointSupported: true, + deviceOrientation: DeviceOrientation.portraitUp, + lockedCaptureOrientation: DeviceOrientation.portraitUp, + recordingOrientation: DeviceOrientation.portraitUp, + isPreviewPaused: true, + previewPauseOrientation: DeviceOrientation.portraitUp); expect(cameraValue.toString(), - 'CameraValue(isRecordingVideo: false, isInitialized: false, errorDescription: null, previewSize: Size(10.0, 10.0), isStreamingImages: false, flashMode: FlashMode.auto, exposureMode: ExposureMode.auto, focusMode: FocusMode.auto, exposurePointSupported: true, focusPointSupported: true, deviceOrientation: DeviceOrientation.portraitUp, lockedCaptureOrientation: DeviceOrientation.portraitUp, recordingOrientation: DeviceOrientation.portraitUp)'); + 'CameraValue(isRecordingVideo: false, isInitialized: false, errorDescription: null, previewSize: Size(10.0, 10.0), isStreamingImages: false, flashMode: FlashMode.auto, exposureMode: ExposureMode.auto, focusMode: FocusMode.auto, exposurePointSupported: true, focusPointSupported: true, deviceOrientation: DeviceOrientation.portraitUp, lockedCaptureOrientation: DeviceOrientation.portraitUp, recordingOrientation: DeviceOrientation.portraitUp, isPreviewPaused: true, previewPausedOrientation: DeviceOrientation.portraitUp)'); }); }); } From 08137da1bdde3c9b771ce69d97985d668ca5583e Mon Sep 17 00:00:00 2001 From: BeMacized Date: Tue, 24 Aug 2021 12:28:58 +0200 Subject: [PATCH 63/63] Fix formatting --- .../io/flutter/plugins/camera/Camera.java | 57 ++++++++++--------- 1 file changed, 29 insertions(+), 28 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 44398c088912..da54cffdf51a 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 @@ -834,36 +834,37 @@ public void setFocusMode(final Result result, @NonNull FocusMode newMode) { * For focus mode an extra step of actually locking/unlocking the * focus has to be done, in order to ensure it goes into the correct state. */ - if (!pausedPreview) { - switch (newMode) { - case locked: - // Perform a single focus trigger. - if (captureSession == null) { - Log.i(TAG, "[unlockAutoFocus] captureSession null, returning"); - return; - } - lockAutoFocus(); - - // Set AF state to idle again. - previewRequestBuilder.set( - CaptureRequest.CONTROL_AF_TRIGGER, CameraMetadata.CONTROL_AF_TRIGGER_IDLE); - - try { - captureSession.setRepeatingRequest( - previewRequestBuilder.build(), null, backgroundHandler); - } catch (CameraAccessException e) { - if (result != null) { - result.error("setFocusModeFailed", "Error setting focus mode: " + e.getMessage(), null); - } - return; + if (!pausedPreview) { + switch (newMode) { + case locked: + // Perform a single focus trigger. + if (captureSession == null) { + Log.i(TAG, "[unlockAutoFocus] captureSession null, returning"); + return; + } + lockAutoFocus(); + + // Set AF state to idle again. + previewRequestBuilder.set( + CaptureRequest.CONTROL_AF_TRIGGER, CameraMetadata.CONTROL_AF_TRIGGER_IDLE); + + try { + captureSession.setRepeatingRequest( + previewRequestBuilder.build(), null, backgroundHandler); + } catch (CameraAccessException e) { + if (result != null) { + 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; - } + return; + } + break; + case auto: + // Cancel current AF trigger and set AF to idle again. + unlockAutoFocus(); + break; } + } if (result != null) { result.success(null);