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/23] 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/23] 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/23] 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/23] 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/23] 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/23] 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/23] 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/23] 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/23] 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/23] 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/23] 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/23] 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/23] 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/23] 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/23] 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/23] 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/23] 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/23] 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 7f0180e7a389340a0a857df63d589115eb5d5461 Mon Sep 17 00:00:00 2001 From: Maurits van Beusekom Date: Wed, 16 Jun 2021 17:06:09 +0200 Subject: [PATCH 19/23] 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 20/23] 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 8313dd0ccaa63c1fd03df67be1c14be19375d121 Mon Sep 17 00:00:00 2001 From: Maurits van Beusekom Date: Wed, 16 Jun 2021 18:58:31 +0200 Subject: [PATCH 21/23] Removed obsolete comment --- .../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 d5432da210da..90bd045e9629 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 @@ -136,7 +136,6 @@ public void setValue(ResolutionPreset value) { configureResolution(currentSetting, cameraId); } - // Always supported @Override public boolean checkIsSupported() { return cameraId >= 0; From a39c2e1f89ee6a76fc88bfa7aa37d7d5b9b90d1d Mon Sep 17 00:00:00 2001 From: Maurits van Beusekom Date: Wed, 16 Jun 2021 19:34:24 +0200 Subject: [PATCH 22/23] update method structure in class to a more logical order --- .../resolution/ResolutionFeature.java | 132 +++++++++--------- 1 file changed, 66 insertions(+), 66 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 90bd045e9629..a826ec266e9f 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 @@ -45,6 +45,71 @@ public ResolutionFeature( configureResolution(resolutionPreset, cameraId); } + /** + * Gets the {@link android.media.CamcorderProfile} containing the information to configure the + * resolution using the {@link android.hardware.camera2} API. + * + * @return Resolution information to configure the {@link android.hardware.camera2} API. + */ + public CamcorderProfile getRecordingProfile() { + return this.recordingProfile; + } + + /** + * Gets the optimal preview size based on the configured resolution. + * + * @return The optimal preview size. + */ + public Size getPreviewSize() { + return this.previewSize; + } + + /** + * Gets the optimal capture size based on the configured resolution. + * + * @return The optimal capture size. + */ + public Size getCaptureSize() { + return this.captureSize; + } + + @Override + public String getDebugName() { + return "ResolutionFeature"; + } + + @Override + public ResolutionPreset getValue() { + return currentSetting; + } + + @Override + public void setValue(ResolutionPreset value) { + this.currentSetting = value; + configureResolution(currentSetting, cameraId); + } + + @Override + public boolean checkIsSupported() { + return cameraId >= 0; + } + + @Override + public void updateBuilder(CaptureRequest.Builder requestBuilder) { + // No-op: when setting a resolution there is no need to update the request builder. + } + + @VisibleForTesting + static Size computeBestPreviewSize(int cameraId, ResolutionPreset preset) { + if (preset.ordinal() > ResolutionPreset.high.ordinal()) { + preset = ResolutionPreset.high; + } + + CamcorderProfile profile = + getBestAvailableCamcorderProfileForResolutionPreset(cameraId, preset); + return new Size(profile.videoFrameWidth, profile.videoFrameHeight); + } + /** * Gets the best possible {@link android.media.CamcorderProfile} for the supplied {@link * ResolutionPreset}. @@ -64,7 +129,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); @@ -99,17 +164,6 @@ public static CamcorderProfile getBestAvailableCamcorderProfileForResolutionPres } } - @VisibleForTesting - static Size computeBestPreviewSize(int cameraId, ResolutionPreset preset) { - if (preset.ordinal() > ResolutionPreset.high.ordinal()) { - preset = ResolutionPreset.high; - } - - CamcorderProfile profile = - getBestAvailableCamcorderProfileForResolutionPreset(cameraId, preset); - return new Size(profile.videoFrameWidth, profile.videoFrameHeight); - } - private void configureResolution(ResolutionPreset resolutionPreset, int cameraId) { if (!checkIsSupported()) { return; @@ -119,58 +173,4 @@ private void configureResolution(ResolutionPreset resolutionPreset, int cameraId captureSize = new Size(recordingProfile.videoFrameWidth, recordingProfile.videoFrameHeight); previewSize = computeBestPreviewSize(cameraId, resolutionPreset); } - - @Override - public String getDebugName() { - return "ResolutionFeature"; - } - - @Override - public ResolutionPreset getValue() { - return currentSetting; - } - - @Override - public void setValue(ResolutionPreset value) { - this.currentSetting = value; - configureResolution(currentSetting, cameraId); - } - - @Override - public boolean checkIsSupported() { - return cameraId >= 0; - } - - @Override - public void updateBuilder(CaptureRequest.Builder requestBuilder) { - // No-op: when setting a resolution there is no need to update the request builder. - } - - /** - * Gets the {@link android.media.CamcorderProfile} containing the information to configure the - * resolution using the {@link android.hardware.camera2} API. - * - * @return Resolution information to configure the {@link android.hardware.camera2} API. - */ - public CamcorderProfile getRecordingProfile() { - return this.recordingProfile; - } - - /** - * Gets the optimal preview size based on the configured resolution. - * - * @return The optimal preview size. - */ - public Size getPreviewSize() { - return this.previewSize; - } - - /** - * Gets the optimal capture size based on the configured resolution. - * - * @return The optimal capture size. - */ - public Size getCaptureSize() { - return this.captureSize; - } } From 7299b1d7132ad5738a393f01b82317060b472ff5 Mon Sep 17 00:00:00 2001 From: Maurits van Beusekom Date: Tue, 22 Jun 2021 09:29:31 +0200 Subject: [PATCH 23/23] Fix formatting --- .../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 a826ec266e9f..67763dde9be4 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 @@ -129,7 +129,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);