diff --git a/packages/camera/camera/CHANGELOG.md b/packages/camera/camera/CHANGELOG.md index e6495a8fc796..29750f9cefff 100644 --- a/packages/camera/camera/CHANGELOG.md +++ b/packages/camera/camera/CHANGELOG.md @@ -1,5 +1,8 @@ ## 0.9.4+3 +* Change Android compileSdkVersion to 31. +* Remove usages of deprecated Android API `CamcorderProfile`. +* Update gradle version to 7.0.2 on Android. * Fix registerTexture and result being called on background thread on iOS. ## 0.9.4+2 diff --git a/packages/camera/camera/android/build.gradle b/packages/camera/camera/android/build.gradle index 25285ad33205..264f7f8edc7d 100644 --- a/packages/camera/camera/android/build.gradle +++ b/packages/camera/camera/android/build.gradle @@ -9,7 +9,7 @@ buildscript { } dependencies { - classpath 'com.android.tools.build:gradle:3.5.0' + classpath 'com.android.tools.build:gradle:7.0.2' } } @@ -27,9 +27,10 @@ project.getTasks().withType(JavaCompile){ apply plugin: 'com.android.library' android { - compileSdkVersion 29 + compileSdkVersion 31 defaultConfig { + targetSdkVersion 31 minSdkVersion 21 testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" } @@ -60,7 +61,7 @@ android { dependencies { compileOnly 'androidx.annotation:annotation:1.1.0' testImplementation 'junit:junit:4.12' - testImplementation 'org.mockito:mockito-inline:3.12.4' + testImplementation 'org.mockito:mockito-inline:4.0.0' testImplementation 'androidx.test:core:1.3.0' - testImplementation 'org.robolectric:robolectric:4.3' + testImplementation 'org.robolectric:robolectric:4.5' } diff --git a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/Camera.java b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/Camera.java index 75ced531b08a..e31903955b4e 100644 --- a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/Camera.java +++ b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/Camera.java @@ -20,6 +20,7 @@ import android.hardware.camera2.params.OutputConfiguration; import android.hardware.camera2.params.SessionConfiguration; import android.media.CamcorderProfile; +import android.media.EncoderProfiles; import android.media.Image; import android.media.ImageReader; import android.media.MediaRecorder; @@ -199,8 +200,16 @@ private void prepareMediaRecorder(String outputFilePath) throws IOException { ((SensorOrientationFeature) cameraFeatures.getSensorOrientation()) .getLockedCaptureOrientation(); + MediaRecorderBuilder mediaRecorderBuilder; + + if (Build.VERSION.SDK_INT >= 31) { + mediaRecorderBuilder = new MediaRecorderBuilder(getRecordingProfile(), outputFilePath); + } else { + mediaRecorderBuilder = new MediaRecorderBuilder(getRecordingProfileLegacy(), outputFilePath); + } + mediaRecorder = - new MediaRecorderBuilder(getRecordingProfile(), outputFilePath) + mediaRecorderBuilder .setEnableAudio(enableAudio) .setMediaOrientation( lockedOrientation == null @@ -918,8 +927,12 @@ public float getMinZoomLevel() { return cameraFeatures.getZoomLevel().getMinimumZoomLevel(); } - /** Shortcut to get current recording profile. */ - CamcorderProfile getRecordingProfile() { + /** Shortcut to get current recording profile. Legacy method provides support for SDK < 31. */ + CamcorderProfile getRecordingProfileLegacy() { + return cameraFeatures.getResolution().getRecordingProfileLegacy(); + } + + EncoderProfiles getRecordingProfile() { return cameraFeatures.getResolution().getRecordingProfile(); } 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 67763dde9be4..afbd7c3758a6 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 @@ -4,12 +4,16 @@ package io.flutter.plugins.camera.features.resolution; +import android.annotation.TargetApi; import android.hardware.camera2.CaptureRequest; import android.media.CamcorderProfile; +import android.media.EncoderProfiles; +import android.os.Build; import android.util.Size; import androidx.annotation.VisibleForTesting; import io.flutter.plugins.camera.CameraProperties; import io.flutter.plugins.camera.features.CameraFeature; +import java.util.List; /** * Controls the resolutions configuration on the {@link android.hardware.camera2} API. @@ -21,7 +25,8 @@ public class ResolutionFeature extends CameraFeature { private Size captureSize; private Size previewSize; - private CamcorderProfile recordingProfile; + private CamcorderProfile recordingProfileLegacy; + private EncoderProfiles recordingProfile; private ResolutionPreset currentSetting; private int cameraId; @@ -51,7 +56,11 @@ public ResolutionFeature( * * @return Resolution information to configure the {@link android.hardware.camera2} API. */ - public CamcorderProfile getRecordingProfile() { + public CamcorderProfile getRecordingProfileLegacy() { + return this.recordingProfileLegacy; + } + + public EncoderProfiles getRecordingProfile() { return this.recordingProfile; } @@ -100,19 +109,29 @@ public void updateBuilder(CaptureRequest.Builder requestBuilder) { } @VisibleForTesting - static Size computeBestPreviewSize(int cameraId, ResolutionPreset preset) { + static Size computeBestPreviewSize(int cameraId, ResolutionPreset preset) + throws IndexOutOfBoundsException { if (preset.ordinal() > ResolutionPreset.high.ordinal()) { preset = ResolutionPreset.high; } + if (Build.VERSION.SDK_INT >= 31) { + EncoderProfiles profile = + getBestAvailableCamcorderProfileForResolutionPreset(cameraId, preset); + List videoProfiles = profile.getVideoProfiles(); + EncoderProfiles.VideoProfile defaultVideoProfile = videoProfiles.get(0); - CamcorderProfile profile = - getBestAvailableCamcorderProfileForResolutionPreset(cameraId, preset); - return new Size(profile.videoFrameWidth, profile.videoFrameHeight); + return new Size(defaultVideoProfile.getWidth(), defaultVideoProfile.getHeight()); + } else { + @SuppressWarnings("deprecation") + CamcorderProfile profile = + getBestAvailableCamcorderProfileForResolutionPresetLegacy(cameraId, preset); + return new Size(profile.videoFrameWidth, profile.videoFrameHeight); + } } /** * Gets the best possible {@link android.media.CamcorderProfile} for the supplied {@link - * ResolutionPreset}. + * ResolutionPreset}. Supports SDK < 31. * * @param cameraId Camera identifier which indicates the device's camera for which to select a * {@link android.media.CamcorderProfile}. @@ -121,7 +140,7 @@ static Size computeBestPreviewSize(int cameraId, ResolutionPreset preset) { * @return The best possible {@link android.media.CamcorderProfile} that matches the supplied * {@link ResolutionPreset}. */ - public static CamcorderProfile getBestAvailableCamcorderProfileForResolutionPreset( + public static CamcorderProfile getBestAvailableCamcorderProfileForResolutionPresetLegacy( int cameraId, ResolutionPreset preset) { if (cameraId < 0) { throw new AssertionError( @@ -164,13 +183,74 @@ public static CamcorderProfile getBestAvailableCamcorderProfileForResolutionPres } } - private void configureResolution(ResolutionPreset resolutionPreset, int cameraId) { + @TargetApi(Build.VERSION_CODES.S) + public static EncoderProfiles getBestAvailableCamcorderProfileForResolutionPreset( + int cameraId, ResolutionPreset preset) { + if (cameraId < 0) { + throw new AssertionError( + "getBestAvailableCamcorderProfileForResolutionPreset can only be used with valid (>=0) camera identifiers."); + } + + String cameraIdString = Integer.toString(cameraId); + + 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.getAll(cameraIdString, CamcorderProfile.QUALITY_HIGH); + } + case ultraHigh: + if (CamcorderProfile.hasProfile(cameraId, CamcorderProfile.QUALITY_2160P)) { + return CamcorderProfile.getAll(cameraIdString, CamcorderProfile.QUALITY_2160P); + } + case veryHigh: + if (CamcorderProfile.hasProfile(cameraId, CamcorderProfile.QUALITY_1080P)) { + return CamcorderProfile.getAll(cameraIdString, CamcorderProfile.QUALITY_1080P); + } + case high: + if (CamcorderProfile.hasProfile(cameraId, CamcorderProfile.QUALITY_720P)) { + return CamcorderProfile.getAll(cameraIdString, CamcorderProfile.QUALITY_720P); + } + case medium: + if (CamcorderProfile.hasProfile(cameraId, CamcorderProfile.QUALITY_480P)) { + return CamcorderProfile.getAll(cameraIdString, CamcorderProfile.QUALITY_480P); + } + case low: + if (CamcorderProfile.hasProfile(cameraId, CamcorderProfile.QUALITY_QVGA)) { + return CamcorderProfile.getAll(cameraIdString, CamcorderProfile.QUALITY_QVGA); + } + default: + if (CamcorderProfile.hasProfile(cameraId, CamcorderProfile.QUALITY_LOW)) { + return CamcorderProfile.getAll(cameraIdString, CamcorderProfile.QUALITY_LOW); + } + + throw new IllegalArgumentException( + "No capture session available for current capture session."); + } + } + + private void configureResolution(ResolutionPreset resolutionPreset, int cameraId) + throws IndexOutOfBoundsException { if (!checkIsSupported()) { return; } - recordingProfile = - getBestAvailableCamcorderProfileForResolutionPreset(cameraId, resolutionPreset); - captureSize = new Size(recordingProfile.videoFrameWidth, recordingProfile.videoFrameHeight); + + if (Build.VERSION.SDK_INT >= 31) { + recordingProfile = + getBestAvailableCamcorderProfileForResolutionPreset(cameraId, resolutionPreset); + List videoProfiles = recordingProfile.getVideoProfiles(); + + EncoderProfiles.VideoProfile defaultVideoProfile = videoProfiles.get(0); + captureSize = new Size(defaultVideoProfile.getWidth(), defaultVideoProfile.getHeight()); + } else { + @SuppressWarnings("deprecation") + CamcorderProfile camcorderProfile = + getBestAvailableCamcorderProfileForResolutionPresetLegacy(cameraId, resolutionPreset); + recordingProfileLegacy = camcorderProfile; + captureSize = + new Size(recordingProfileLegacy.videoFrameWidth, recordingProfileLegacy.videoFrameHeight); + } + previewSize = computeBestPreviewSize(cameraId, resolutionPreset); } } diff --git a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/media/MediaRecorderBuilder.java b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/media/MediaRecorderBuilder.java index a78c2b47b7ad..0aebfee39e0a 100644 --- a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/media/MediaRecorderBuilder.java +++ b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/media/MediaRecorderBuilder.java @@ -5,11 +5,14 @@ package io.flutter.plugins.camera.media; import android.media.CamcorderProfile; +import android.media.EncoderProfiles; import android.media.MediaRecorder; +import android.os.Build; import androidx.annotation.NonNull; import java.io.IOException; public class MediaRecorderBuilder { + @SuppressWarnings("deprecation") static class MediaRecorderFactory { MediaRecorder makeMediaRecorder() { return new MediaRecorder(); @@ -17,23 +20,40 @@ MediaRecorder makeMediaRecorder() { } private final String outputFilePath; - private final CamcorderProfile recordingProfile; + private final CamcorderProfile camcorderProfile; + private final EncoderProfiles encoderProfiles; private final MediaRecorderFactory recorderFactory; private boolean enableAudio; private int mediaOrientation; public MediaRecorderBuilder( - @NonNull CamcorderProfile recordingProfile, @NonNull String outputFilePath) { - this(recordingProfile, outputFilePath, new MediaRecorderFactory()); + @NonNull CamcorderProfile camcorderProfile, @NonNull String outputFilePath) { + this(camcorderProfile, outputFilePath, new MediaRecorderFactory()); + } + + public MediaRecorderBuilder( + @NonNull EncoderProfiles encoderProfiles, @NonNull String outputFilePath) { + this(encoderProfiles, outputFilePath, new MediaRecorderFactory()); } MediaRecorderBuilder( - @NonNull CamcorderProfile recordingProfile, + @NonNull CamcorderProfile camcorderProfile, @NonNull String outputFilePath, MediaRecorderFactory helper) { this.outputFilePath = outputFilePath; - this.recordingProfile = recordingProfile; + this.camcorderProfile = camcorderProfile; + this.encoderProfiles = null; + this.recorderFactory = helper; + } + + MediaRecorderBuilder( + @NonNull EncoderProfiles encoderProfiles, + @NonNull String outputFilePath, + MediaRecorderFactory helper) { + this.outputFilePath = outputFilePath; + this.encoderProfiles = encoderProfiles; + this.camcorderProfile = null; this.recorderFactory = helper; } @@ -47,23 +67,43 @@ public MediaRecorderBuilder setMediaOrientation(int orientation) { return this; } - public MediaRecorder build() throws IOException { + public MediaRecorder build() throws IOException, NullPointerException, IndexOutOfBoundsException { MediaRecorder mediaRecorder = recorderFactory.makeMediaRecorder(); // There's a fixed order that mediaRecorder expects. Only change these functions accordingly. // You can find the specifics here: https://developer.android.com/reference/android/media/MediaRecorder. if (enableAudio) mediaRecorder.setAudioSource(MediaRecorder.AudioSource.MIC); mediaRecorder.setVideoSource(MediaRecorder.VideoSource.SURFACE); - mediaRecorder.setOutputFormat(recordingProfile.fileFormat); - if (enableAudio) { - mediaRecorder.setAudioEncoder(recordingProfile.audioCodec); - mediaRecorder.setAudioEncodingBitRate(recordingProfile.audioBitRate); - mediaRecorder.setAudioSamplingRate(recordingProfile.audioSampleRate); + + if (Build.VERSION.SDK_INT >= 31) { + EncoderProfiles.VideoProfile videoProfile = encoderProfiles.getVideoProfiles().get(0); + EncoderProfiles.AudioProfile audioProfile = encoderProfiles.getAudioProfiles().get(0); + + mediaRecorder.setOutputFormat(encoderProfiles.getRecommendedFileFormat()); + if (enableAudio) { + mediaRecorder.setAudioEncoder(audioProfile.getCodec()); + mediaRecorder.setAudioEncodingBitRate(audioProfile.getBitrate()); + mediaRecorder.setAudioSamplingRate(audioProfile.getSampleRate()); + } + mediaRecorder.setVideoEncoder(videoProfile.getCodec()); + mediaRecorder.setVideoEncodingBitRate(videoProfile.getBitrate()); + mediaRecorder.setVideoFrameRate(videoProfile.getFrameRate()); + mediaRecorder.setVideoSize(videoProfile.getWidth(), videoProfile.getHeight()); + mediaRecorder.setVideoSize(videoProfile.getWidth(), videoProfile.getHeight()); + } else { + mediaRecorder.setOutputFormat(camcorderProfile.fileFormat); + if (enableAudio) { + mediaRecorder.setAudioEncoder(camcorderProfile.audioCodec); + mediaRecorder.setAudioEncodingBitRate(camcorderProfile.audioBitRate); + mediaRecorder.setAudioSamplingRate(camcorderProfile.audioSampleRate); + } + mediaRecorder.setVideoEncoder(camcorderProfile.videoCodec); + mediaRecorder.setVideoEncodingBitRate(camcorderProfile.videoBitRate); + mediaRecorder.setVideoFrameRate(camcorderProfile.videoFrameRate); + mediaRecorder.setVideoSize( + camcorderProfile.videoFrameWidth, camcorderProfile.videoFrameHeight); } - mediaRecorder.setVideoEncoder(recordingProfile.videoCodec); - mediaRecorder.setVideoEncodingBitRate(recordingProfile.videoBitRate); - mediaRecorder.setVideoFrameRate(recordingProfile.videoFrameRate); - mediaRecorder.setVideoSize(recordingProfile.videoFrameWidth, recordingProfile.videoFrameHeight); + mediaRecorder.setOutputFile(outputFilePath); mediaRecorder.setOrientationHint(this.mediaOrientation); diff --git a/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/CameraTest.java b/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/CameraTest.java index 9d973195435e..1ed2e4c11d7b 100644 --- a/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/CameraTest.java +++ b/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/CameraTest.java @@ -22,7 +22,6 @@ import android.hardware.camera2.CameraCaptureSession; import android.hardware.camera2.CameraMetadata; import android.hardware.camera2.CaptureRequest; -import android.media.CamcorderProfile; import android.media.MediaRecorder; import android.os.Build; import android.os.Handler; @@ -249,20 +248,6 @@ public void getMinZoomLevel() { assertEquals(expectedMinZoomLevel, actualMinZoomLevel, 0); } - @Test - public void getRecordingProfile() { - ResolutionFeature mockResolutionFeature = - mockCameraFeatureFactory.createResolutionFeature(mockCameraProperties, null, null); - CamcorderProfile mockCamcorderProfile = mock(CamcorderProfile.class); - - when(mockResolutionFeature.getRecordingProfile()).thenReturn(mockCamcorderProfile); - - CamcorderProfile actualRecordingProfile = camera.getRecordingProfile(); - - verify(mockResolutionFeature, times(1)).getRecordingProfile(); - assertEquals(mockCamcorderProfile, actualRecordingProfile); - } - @Test public void setExposureMode_shouldUpdateExposureLockFeature() { ExposureLockFeature mockExposureLockFeature = diff --git a/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/CameraTest_getRecordingProfileTest.java b/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/CameraTest_getRecordingProfileTest.java new file mode 100644 index 000000000000..04bab14f26ac --- /dev/null +++ b/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/CameraTest_getRecordingProfileTest.java @@ -0,0 +1,205 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +package io.flutter.plugins.camera; + +import static org.junit.Assert.assertEquals; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import android.app.Activity; +import android.hardware.camera2.CameraCaptureSession; +import android.hardware.camera2.CaptureRequest; +import android.media.CamcorderProfile; +import android.media.EncoderProfiles; +import android.os.Handler; +import android.os.HandlerThread; +import androidx.annotation.NonNull; +import io.flutter.plugins.camera.features.CameraFeatureFactory; +import io.flutter.plugins.camera.features.autofocus.AutoFocusFeature; +import io.flutter.plugins.camera.features.exposurelock.ExposureLockFeature; +import io.flutter.plugins.camera.features.exposureoffset.ExposureOffsetFeature; +import io.flutter.plugins.camera.features.exposurepoint.ExposurePointFeature; +import io.flutter.plugins.camera.features.flash.FlashFeature; +import io.flutter.plugins.camera.features.focuspoint.FocusPointFeature; +import io.flutter.plugins.camera.features.fpsrange.FpsRangeFeature; +import io.flutter.plugins.camera.features.noisereduction.NoiseReductionFeature; +import io.flutter.plugins.camera.features.resolution.ResolutionFeature; +import io.flutter.plugins.camera.features.resolution.ResolutionPreset; +import io.flutter.plugins.camera.features.sensororientation.SensorOrientationFeature; +import io.flutter.plugins.camera.features.zoomlevel.ZoomLevelFeature; +import io.flutter.view.TextureRegistry; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.MockedStatic; +import org.robolectric.RobolectricTestRunner; +import org.robolectric.annotation.Config; + +@RunWith(RobolectricTestRunner.class) +public class CameraTest_getRecordingProfileTest { + + private CameraProperties mockCameraProperties; + private CameraFeatureFactory mockCameraFeatureFactory; + private DartMessenger mockDartMessenger; + private Camera camera; + private CameraCaptureSession mockCaptureSession; + private CaptureRequest.Builder mockPreviewRequestBuilder; + private MockedStatic mockHandlerThreadFactory; + private HandlerThread mockHandlerThread; + private MockedStatic mockHandlerFactory; + private Handler mockHandler; + + @Before + public void before() { + mockCameraProperties = mock(CameraProperties.class); + mockCameraFeatureFactory = new TestCameraFeatureFactory(); + mockDartMessenger = mock(DartMessenger.class); + + final Activity mockActivity = mock(Activity.class); + final TextureRegistry.SurfaceTextureEntry mockFlutterTexture = + mock(TextureRegistry.SurfaceTextureEntry.class); + final ResolutionPreset resolutionPreset = ResolutionPreset.high; + final boolean enableAudio = false; + + camera = + new Camera( + mockActivity, + mockFlutterTexture, + mockCameraFeatureFactory, + mockDartMessenger, + mockCameraProperties, + resolutionPreset, + enableAudio); + } + + @Config(maxSdk = 30) + @Test + public void getRecordingProfileLegacy() { + ResolutionFeature mockResolutionFeature = + mockCameraFeatureFactory.createResolutionFeature(mockCameraProperties, null, null); + CamcorderProfile mockCamcorderProfile = mock(CamcorderProfile.class); + + when(mockResolutionFeature.getRecordingProfileLegacy()).thenReturn(mockCamcorderProfile); + + CamcorderProfile actualRecordingProfile = camera.getRecordingProfileLegacy(); + + verify(mockResolutionFeature, times(1)).getRecordingProfileLegacy(); + assertEquals(mockCamcorderProfile, actualRecordingProfile); + } + + @Config(minSdk = 31) + @Test + public void getRecordingProfile() { + ResolutionFeature mockResolutionFeature = + mockCameraFeatureFactory.createResolutionFeature(mockCameraProperties, null, null); + EncoderProfiles mockRecordingProfile = mock(EncoderProfiles.class); + + when(mockResolutionFeature.getRecordingProfile()).thenReturn(mockRecordingProfile); + + EncoderProfiles actualRecordingProfile = camera.getRecordingProfile(); + + verify(mockResolutionFeature, times(1)).getRecordingProfile(); + assertEquals(mockRecordingProfile, actualRecordingProfile); + } + + private static class TestCameraFeatureFactory implements CameraFeatureFactory { + private final AutoFocusFeature mockAutoFocusFeature; + private final ExposureLockFeature mockExposureLockFeature; + private final ExposureOffsetFeature mockExposureOffsetFeature; + private final ExposurePointFeature mockExposurePointFeature; + private final FlashFeature mockFlashFeature; + private final FocusPointFeature mockFocusPointFeature; + private final FpsRangeFeature mockFpsRangeFeature; + private final NoiseReductionFeature mockNoiseReductionFeature; + private final ResolutionFeature mockResolutionFeature; + private final SensorOrientationFeature mockSensorOrientationFeature; + private final ZoomLevelFeature mockZoomLevelFeature; + + public TestCameraFeatureFactory() { + this.mockAutoFocusFeature = mock(AutoFocusFeature.class); + this.mockExposureLockFeature = mock(ExposureLockFeature.class); + this.mockExposureOffsetFeature = mock(ExposureOffsetFeature.class); + this.mockExposurePointFeature = mock(ExposurePointFeature.class); + this.mockFlashFeature = mock(FlashFeature.class); + this.mockFocusPointFeature = mock(FocusPointFeature.class); + this.mockFpsRangeFeature = mock(FpsRangeFeature.class); + this.mockNoiseReductionFeature = mock(NoiseReductionFeature.class); + this.mockResolutionFeature = mock(ResolutionFeature.class); + this.mockSensorOrientationFeature = mock(SensorOrientationFeature.class); + this.mockZoomLevelFeature = mock(ZoomLevelFeature.class); + } + + @Override + public AutoFocusFeature createAutoFocusFeature( + @NonNull CameraProperties cameraProperties, boolean recordingVideo) { + return mockAutoFocusFeature; + } + + @Override + public ExposureLockFeature createExposureLockFeature( + @NonNull CameraProperties cameraProperties) { + return mockExposureLockFeature; + } + + @Override + public ExposureOffsetFeature createExposureOffsetFeature( + @NonNull CameraProperties cameraProperties) { + return mockExposureOffsetFeature; + } + + @Override + public FlashFeature createFlashFeature(@NonNull CameraProperties cameraProperties) { + return mockFlashFeature; + } + + @Override + public ResolutionFeature createResolutionFeature( + @NonNull CameraProperties cameraProperties, + ResolutionPreset initialSetting, + String cameraName) { + return mockResolutionFeature; + } + + @Override + public FocusPointFeature createFocusPointFeature( + @NonNull CameraProperties cameraProperties, + @NonNull SensorOrientationFeature sensorOrienttionFeature) { + return mockFocusPointFeature; + } + + @Override + public FpsRangeFeature createFpsRangeFeature(@NonNull CameraProperties cameraProperties) { + return mockFpsRangeFeature; + } + + @Override + public SensorOrientationFeature createSensorOrientationFeature( + @NonNull CameraProperties cameraProperties, + @NonNull Activity activity, + @NonNull DartMessenger dartMessenger) { + return mockSensorOrientationFeature; + } + + @Override + public ZoomLevelFeature createZoomLevelFeature(@NonNull CameraProperties cameraProperties) { + return mockZoomLevelFeature; + } + + @Override + public ExposurePointFeature createExposurePointFeature( + @NonNull CameraProperties cameraProperties, + @NonNull SensorOrientationFeature sensorOrientationFeature) { + return mockExposurePointFeature; + } + + @Override + public NoiseReductionFeature createNoiseReductionFeature( + @NonNull CameraProperties cameraProperties) { + return mockNoiseReductionFeature; + } + } +} 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 e09223dfabe9..957b57a66435 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 @@ -8,24 +8,33 @@ import static org.junit.Assert.assertTrue; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.mockStatic; +import static org.mockito.Mockito.when; import android.media.CamcorderProfile; +import android.media.EncoderProfiles; import io.flutter.plugins.camera.CameraProperties; +import java.util.List; import org.junit.After; import org.junit.Before; import org.junit.Test; +import org.junit.runner.RunWith; import org.mockito.MockedStatic; +import org.robolectric.RobolectricTestRunner; +import org.robolectric.annotation.Config; +@RunWith(RobolectricTestRunner.class) public class ResolutionFeatureTest { private static final String cameraName = "1"; - private CamcorderProfile mockProfileLow; + private CamcorderProfile mockProfileLowLegacy; + private EncoderProfiles mockProfileLow; private MockedStatic mockedStaticProfile; @Before - public void before() { + @SuppressWarnings("deprecation") + public void beforeLegacy() { mockedStaticProfile = mockStatic(CamcorderProfile.class); - mockProfileLow = mock(CamcorderProfile.class); - CamcorderProfile mockProfile = mock(CamcorderProfile.class); + mockProfileLowLegacy = mock(CamcorderProfile.class); + CamcorderProfile mockProfileLegacy = mock(CamcorderProfile.class); mockedStaticProfile .when(() -> CamcorderProfile.hasProfile(1, CamcorderProfile.QUALITY_HIGH)) @@ -51,25 +60,58 @@ public void before() { mockedStaticProfile .when(() -> CamcorderProfile.get(1, CamcorderProfile.QUALITY_HIGH)) - .thenReturn(mockProfile); + .thenReturn(mockProfileLegacy); mockedStaticProfile .when(() -> CamcorderProfile.get(1, CamcorderProfile.QUALITY_2160P)) - .thenReturn(mockProfile); + .thenReturn(mockProfileLegacy); mockedStaticProfile .when(() -> CamcorderProfile.get(1, CamcorderProfile.QUALITY_1080P)) - .thenReturn(mockProfile); + .thenReturn(mockProfileLegacy); mockedStaticProfile .when(() -> CamcorderProfile.get(1, CamcorderProfile.QUALITY_720P)) - .thenReturn(mockProfile); + .thenReturn(mockProfileLegacy); mockedStaticProfile .when(() -> CamcorderProfile.get(1, CamcorderProfile.QUALITY_480P)) - .thenReturn(mockProfile); + .thenReturn(mockProfileLegacy); mockedStaticProfile .when(() -> CamcorderProfile.get(1, CamcorderProfile.QUALITY_QVGA)) - .thenReturn(mockProfile); + .thenReturn(mockProfileLegacy); mockedStaticProfile .when(() -> CamcorderProfile.get(1, CamcorderProfile.QUALITY_LOW)) + .thenReturn(mockProfileLowLegacy); + } + + public void before() { + mockProfileLow = mock(EncoderProfiles.class); + EncoderProfiles mockProfile = mock(EncoderProfiles.class); + EncoderProfiles.VideoProfile mockVideoProfile = mock(EncoderProfiles.VideoProfile.class); + List mockVideoProfilesList = List.of(mockVideoProfile); + + mockedStaticProfile + .when(() -> CamcorderProfile.getAll("1", CamcorderProfile.QUALITY_HIGH)) + .thenReturn(mockProfile); + mockedStaticProfile + .when(() -> CamcorderProfile.getAll("1", CamcorderProfile.QUALITY_2160P)) + .thenReturn(mockProfile); + mockedStaticProfile + .when(() -> CamcorderProfile.getAll("1", CamcorderProfile.QUALITY_1080P)) + .thenReturn(mockProfile); + mockedStaticProfile + .when(() -> CamcorderProfile.getAll("1", CamcorderProfile.QUALITY_720P)) + .thenReturn(mockProfile); + mockedStaticProfile + .when(() -> CamcorderProfile.getAll("1", CamcorderProfile.QUALITY_480P)) + .thenReturn(mockProfile); + mockedStaticProfile + .when(() -> CamcorderProfile.getAll("1", CamcorderProfile.QUALITY_QVGA)) + .thenReturn(mockProfile); + mockedStaticProfile + .when(() -> CamcorderProfile.getAll("1", CamcorderProfile.QUALITY_LOW)) .thenReturn(mockProfileLow); + + when(mockProfile.getVideoProfiles()).thenReturn(mockVideoProfilesList); + when(mockVideoProfile.getHeight()).thenReturn(100); + when(mockVideoProfile.getWidth()).thenReturn(100); } @After @@ -116,6 +158,39 @@ public void checkIsSupport_returnsTrue() { assertTrue(resolutionFeature.checkIsSupported()); } + @Config(maxSdk = 30) + @SuppressWarnings("deprecation") + @Test + public void getBestAvailableCamcorderProfileForResolutionPreset_shouldFallThroughLegacy() { + 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( + mockProfileLowLegacy, + ResolutionFeature.getBestAvailableCamcorderProfileForResolutionPresetLegacy( + 1, ResolutionPreset.max)); + } + + @Config(minSdk = 31) @Test public void getBestAvailableCamcorderProfileForResolutionPreset_shouldFallThrough() { mockedStaticProfile @@ -146,45 +221,112 @@ public void getBestAvailableCamcorderProfileForResolutionPreset_shouldFallThroug 1, ResolutionPreset.max)); } + @Config(maxSdk = 30) + @SuppressWarnings("deprecation") + @Test + public void computeBestPreviewSize_shouldUse720PWhenResolutionPresetMaxLegacy() { + ResolutionFeature.computeBestPreviewSize(1, ResolutionPreset.max); + + mockedStaticProfile.verify(() -> CamcorderProfile.get(1, CamcorderProfile.QUALITY_720P)); + } + + @Config(minSdk = 31) @Test public void computeBestPreviewSize_shouldUse720PWhenResolutionPresetMax() { + before(); ResolutionFeature.computeBestPreviewSize(1, ResolutionPreset.max); + mockedStaticProfile.verify(() -> CamcorderProfile.getAll("1", CamcorderProfile.QUALITY_720P)); + } + + @Config(maxSdk = 30) + @SuppressWarnings("deprecation") + @Test + public void computeBestPreviewSize_shouldUse720PWhenResolutionPresetUltraHighLegacy() { + ResolutionFeature.computeBestPreviewSize(1, ResolutionPreset.ultraHigh); + mockedStaticProfile.verify(() -> CamcorderProfile.get(1, CamcorderProfile.QUALITY_720P)); } + @Config(minSdk = 31) @Test public void computeBestPreviewSize_shouldUse720PWhenResolutionPresetUltraHigh() { + before(); ResolutionFeature.computeBestPreviewSize(1, ResolutionPreset.ultraHigh); + mockedStaticProfile.verify(() -> CamcorderProfile.getAll("1", CamcorderProfile.QUALITY_720P)); + } + + @Config(maxSdk = 30) + @SuppressWarnings("deprecation") + @Test + public void computeBestPreviewSize_shouldUse720PWhenResolutionPresetVeryHighLegacy() { + ResolutionFeature.computeBestPreviewSize(1, ResolutionPreset.veryHigh); + mockedStaticProfile.verify(() -> CamcorderProfile.get(1, CamcorderProfile.QUALITY_720P)); } + @Config(minSdk = 31) + @SuppressWarnings("deprecation") @Test public void computeBestPreviewSize_shouldUse720PWhenResolutionPresetVeryHigh() { + before(); ResolutionFeature.computeBestPreviewSize(1, ResolutionPreset.veryHigh); + mockedStaticProfile.verify(() -> CamcorderProfile.getAll("1", CamcorderProfile.QUALITY_720P)); + } + + @Config(maxSdk = 30) + @SuppressWarnings("deprecation") + @Test + public void computeBestPreviewSize_shouldUse720PWhenResolutionPresetHighLegacy() { + ResolutionFeature.computeBestPreviewSize(1, ResolutionPreset.high); + mockedStaticProfile.verify(() -> CamcorderProfile.get(1, CamcorderProfile.QUALITY_720P)); } + @Config(minSdk = 31) @Test public void computeBestPreviewSize_shouldUse720PWhenResolutionPresetHigh() { + before(); ResolutionFeature.computeBestPreviewSize(1, ResolutionPreset.high); - mockedStaticProfile.verify(() -> CamcorderProfile.get(1, CamcorderProfile.QUALITY_720P)); + mockedStaticProfile.verify(() -> CamcorderProfile.getAll("1", CamcorderProfile.QUALITY_720P)); } + @Config(maxSdk = 30) + @SuppressWarnings("deprecation") @Test - public void computeBestPreviewSize_shouldUse480PWhenResolutionPresetMedium() { + public void computeBestPreviewSize_shouldUse480PWhenResolutionPresetMediumLegacy() { ResolutionFeature.computeBestPreviewSize(1, ResolutionPreset.medium); mockedStaticProfile.verify(() -> CamcorderProfile.get(1, CamcorderProfile.QUALITY_480P)); } + @Config(minSdk = 31) @Test - public void computeBestPreviewSize_shouldUseQVGAWhenResolutionPresetLow() { + public void computeBestPreviewSize_shouldUse480PWhenResolutionPresetMedium() { + before(); + ResolutionFeature.computeBestPreviewSize(1, ResolutionPreset.medium); + + mockedStaticProfile.verify(() -> CamcorderProfile.getAll("1", CamcorderProfile.QUALITY_480P)); + } + + @Config(maxSdk = 30) + @SuppressWarnings("deprecation") + @Test + public void computeBestPreviewSize_shouldUseQVGAWhenResolutionPresetLowLegacy() { ResolutionFeature.computeBestPreviewSize(1, ResolutionPreset.low); mockedStaticProfile.verify(() -> CamcorderProfile.get(1, CamcorderProfile.QUALITY_QVGA)); } + + @Config(minSdk = 31) + @Test + public void computeBestPreviewSize_shouldUseQVGAWhenResolutionPresetLow() { + before(); + ResolutionFeature.computeBestPreviewSize(1, ResolutionPreset.low); + + mockedStaticProfile.verify(() -> CamcorderProfile.getAll("1", CamcorderProfile.QUALITY_QVGA)); + } } 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 58f17cb758bf..82449a10188a 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 @@ -36,6 +36,7 @@ public class DeviceOrientationManagerTest { private DeviceOrientationManager deviceOrientationManager; @Before + @SuppressWarnings("deprecation") public void before() { mockActivity = mock(Activity.class); mockDartMessenger = mock(DartMessenger.class); diff --git a/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/media/MediaRecorderBuilderTest.java b/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/media/MediaRecorderBuilderTest.java index 5425409c2f3a..6cc58ee823d9 100644 --- a/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/media/MediaRecorderBuilderTest.java +++ b/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/media/MediaRecorderBuilderTest.java @@ -8,23 +8,42 @@ import static org.mockito.Mockito.*; import android.media.CamcorderProfile; +import android.media.EncoderProfiles; import android.media.MediaRecorder; import java.io.IOException; import java.lang.reflect.Constructor; +import java.util.List; import org.junit.Test; +import org.junit.runner.RunWith; import org.mockito.InOrder; +import org.robolectric.RobolectricTestRunner; +import org.robolectric.annotation.Config; +@RunWith(RobolectricTestRunner.class) public class MediaRecorderBuilderTest { + @Config(maxSdk = 30) + @SuppressWarnings("deprecation") @Test - public void ctor_test() { + public void ctor_testLegacy() { MediaRecorderBuilder builder = new MediaRecorderBuilder(CamcorderProfile.get(CamcorderProfile.QUALITY_1080P), ""); assertNotNull(builder); } + @Config(minSdk = 31) @Test - public void build_shouldSetValuesInCorrectOrderWhenAudioIsDisabled() throws IOException { + public void ctor_test() { + MediaRecorderBuilder builder = + new MediaRecorderBuilder(CamcorderProfile.getAll("0", CamcorderProfile.QUALITY_1080P), ""); + + assertNotNull(builder); + } + + @Config(maxSdk = 30) + @SuppressWarnings("deprecation") + @Test + public void build_shouldSetValuesInCorrectOrderWhenAudioIsDisabledLegacy() throws IOException { CamcorderProfile recorderProfile = getEmptyCamcorderProfile(); MediaRecorderBuilder.MediaRecorderFactory mockFactory = mock(MediaRecorderBuilder.MediaRecorderFactory.class); @@ -54,8 +73,67 @@ public void build_shouldSetValuesInCorrectOrderWhenAudioIsDisabled() throws IOEx inOrder.verify(recorder).prepare(); } + @Config(minSdk = 31) @Test - public void build_shouldSetValuesInCorrectOrderWhenAudioIsEnabled() throws IOException { + public void build_shouldSetValuesInCorrectOrderWhenAudioIsDisabled() throws IOException { + EncoderProfiles recorderProfile = mock(EncoderProfiles.class); + List mockVideoProfiles = + List.of(mock(EncoderProfiles.VideoProfile.class)); + List mockAudioProfiles = + List.of(mock(EncoderProfiles.AudioProfile.class)); + MediaRecorderBuilder.MediaRecorderFactory mockFactory = + mock(MediaRecorderBuilder.MediaRecorderFactory.class); + MediaRecorder mockMediaRecorder = mock(MediaRecorder.class); + String outputFilePath = "mock_video_file_path"; + int mediaOrientation = 1; + MediaRecorderBuilder builder = + new MediaRecorderBuilder(recorderProfile, outputFilePath, mockFactory) + .setEnableAudio(false) + .setMediaOrientation(mediaOrientation); + + when(mockFactory.makeMediaRecorder()).thenReturn(mockMediaRecorder); + when(recorderProfile.getVideoProfiles()).thenReturn(mockVideoProfiles); + when(recorderProfile.getAudioProfiles()).thenReturn(mockAudioProfiles); + + MediaRecorder recorder = builder.build(); + + EncoderProfiles.VideoProfile videoProfile = mockVideoProfiles.get(0); + + InOrder inOrder = inOrder(recorder); + inOrder.verify(recorder).setVideoSource(MediaRecorder.VideoSource.SURFACE); + inOrder.verify(recorder).setOutputFormat(recorderProfile.getRecommendedFileFormat()); + inOrder.verify(recorder).setVideoEncoder(videoProfile.getCodec()); + inOrder.verify(recorder).setVideoEncodingBitRate(videoProfile.getBitrate()); + inOrder.verify(recorder).setVideoFrameRate(videoProfile.getFrameRate()); + inOrder.verify(recorder).setVideoSize(videoProfile.getWidth(), videoProfile.getHeight()); + inOrder.verify(recorder).setOutputFile(outputFilePath); + inOrder.verify(recorder).setOrientationHint(mediaOrientation); + inOrder.verify(recorder).prepare(); + } + + @Config(minSdk = 31) + @Test(expected = IndexOutOfBoundsException.class) + public void build_shouldThrowExceptionWithoutVideoOrAudioProfiles() throws IOException { + EncoderProfiles recorderProfile = mock(EncoderProfiles.class); + MediaRecorderBuilder.MediaRecorderFactory mockFactory = + mock(MediaRecorderBuilder.MediaRecorderFactory.class); + MediaRecorder mockMediaRecorder = mock(MediaRecorder.class); + String outputFilePath = "mock_video_file_path"; + int mediaOrientation = 1; + MediaRecorderBuilder builder = + new MediaRecorderBuilder(recorderProfile, outputFilePath, mockFactory) + .setEnableAudio(false) + .setMediaOrientation(mediaOrientation); + + when(mockFactory.makeMediaRecorder()).thenReturn(mockMediaRecorder); + + MediaRecorder recorder = builder.build(); + } + + @Config(maxSdk = 30) + @SuppressWarnings("deprecation") + @Test + public void build_shouldSetValuesInCorrectOrderWhenAudioIsEnabledLegacy() throws IOException { CamcorderProfile recorderProfile = getEmptyCamcorderProfile(); MediaRecorderBuilder.MediaRecorderFactory mockFactory = mock(MediaRecorderBuilder.MediaRecorderFactory.class); @@ -89,6 +167,49 @@ public void build_shouldSetValuesInCorrectOrderWhenAudioIsEnabled() throws IOExc inOrder.verify(recorder).prepare(); } + @Config(minSdk = 31) + @Test + public void build_shouldSetValuesInCorrectOrderWhenAudioIsEnabled() throws IOException { + EncoderProfiles recorderProfile = mock(EncoderProfiles.class); + List mockVideoProfiles = + List.of(mock(EncoderProfiles.VideoProfile.class)); + List mockAudioProfiles = + List.of(mock(EncoderProfiles.AudioProfile.class)); + MediaRecorderBuilder.MediaRecorderFactory mockFactory = + mock(MediaRecorderBuilder.MediaRecorderFactory.class); + MediaRecorder mockMediaRecorder = mock(MediaRecorder.class); + String outputFilePath = "mock_video_file_path"; + int mediaOrientation = 1; + MediaRecorderBuilder builder = + new MediaRecorderBuilder(recorderProfile, outputFilePath, mockFactory) + .setEnableAudio(true) + .setMediaOrientation(mediaOrientation); + + when(mockFactory.makeMediaRecorder()).thenReturn(mockMediaRecorder); + when(recorderProfile.getVideoProfiles()).thenReturn(mockVideoProfiles); + when(recorderProfile.getAudioProfiles()).thenReturn(mockAudioProfiles); + + MediaRecorder recorder = builder.build(); + + EncoderProfiles.VideoProfile videoProfile = mockVideoProfiles.get(0); + EncoderProfiles.AudioProfile audioProfile = mockAudioProfiles.get(0); + + InOrder inOrder = inOrder(recorder); + inOrder.verify(recorder).setAudioSource(MediaRecorder.AudioSource.MIC); + inOrder.verify(recorder).setVideoSource(MediaRecorder.VideoSource.SURFACE); + inOrder.verify(recorder).setOutputFormat(recorderProfile.getRecommendedFileFormat()); + inOrder.verify(recorder).setAudioEncoder(audioProfile.getCodec()); + inOrder.verify(recorder).setAudioEncodingBitRate(audioProfile.getBitrate()); + inOrder.verify(recorder).setAudioSamplingRate(audioProfile.getSampleRate()); + inOrder.verify(recorder).setVideoEncoder(videoProfile.getCodec()); + inOrder.verify(recorder).setVideoEncodingBitRate(videoProfile.getBitrate()); + inOrder.verify(recorder).setVideoFrameRate(videoProfile.getFrameRate()); + inOrder.verify(recorder).setVideoSize(videoProfile.getWidth(), videoProfile.getHeight()); + inOrder.verify(recorder).setOutputFile(outputFilePath); + inOrder.verify(recorder).setOrientationHint(mediaOrientation); + inOrder.verify(recorder).prepare(); + } + private CamcorderProfile getEmptyCamcorderProfile() { try { Constructor constructor = diff --git a/packages/camera/camera/android/src/test/resources/robolectric.properties b/packages/camera/camera/android/src/test/resources/robolectric.properties new file mode 100644 index 000000000000..90fbd74370a7 --- /dev/null +++ b/packages/camera/camera/android/src/test/resources/robolectric.properties @@ -0,0 +1 @@ +sdk=30 \ No newline at end of file diff --git a/packages/camera/camera/example/android/app/build.gradle b/packages/camera/camera/example/android/app/build.gradle index 7d0e281b74e8..476d65373723 100644 --- a/packages/camera/camera/example/android/app/build.gradle +++ b/packages/camera/camera/example/android/app/build.gradle @@ -25,7 +25,7 @@ apply plugin: 'com.android.application' apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle" android { - compileSdkVersion 29 + compileSdkVersion 31 lintOptions { disable 'InvalidPackage' diff --git a/packages/camera/camera/example/android/gradle.properties b/packages/camera/camera/example/android/gradle.properties index a6738207fd15..b253d8e5f746 100644 --- a/packages/camera/camera/example/android/gradle.properties +++ b/packages/camera/camera/example/android/gradle.properties @@ -1,4 +1,4 @@ org.gradle.jvmargs=-Xmx1536M android.useAndroidX=true -android.enableJetifier=true +android.enableJetifier=false android.enableR8=true