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/45] 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/45] 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/45] 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/45] 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/45] 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/45] 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/45] 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/45] 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/45] 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/45] 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/45] 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/45] 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/45] 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/45] 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/45] 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/45] 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/45] 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/45] 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/45] 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/45] 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/45] 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/45] 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/45] 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/45] 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/45] 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/45] 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/45] 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/45] 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/45] 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/45] 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/45] 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/45] 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/45] 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/45] 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/45] 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/45] 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/45] 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/45] 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/45] 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/45] 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 7f0180e7a389340a0a857df63d589115eb5d5461 Mon Sep 17 00:00:00 2001 From: Maurits van Beusekom Date: Wed, 16 Jun 2021 17:06:09 +0200 Subject: [PATCH 41/45] 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 42/45] 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 43/45] 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 44/45] 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 45/45] 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