diff --git a/packages/camera/camera_android_camerax/CHANGELOG.md b/packages/camera/camera_android_camerax/CHANGELOG.md index e97f2a55030..4f63c42d1f9 100644 --- a/packages/camera/camera_android_camerax/CHANGELOG.md +++ b/packages/camera/camera_android_camerax/CHANGELOG.md @@ -1,3 +1,7 @@ +## 0.5.0+34 + +* Implements `setFocusPoint`, `setExposurePoint`, and `setExposureOffset`. + ## 0.5.0+33 * Fixes typo in `README.md`. diff --git a/packages/camera/camera_android_camerax/README.md b/packages/camera/camera_android_camerax/README.md index 13967faa200..48674de44cf 100644 --- a/packages/camera/camera_android_camerax/README.md +++ b/packages/camera/camera_android_camerax/README.md @@ -24,20 +24,19 @@ dependencies: ## Missing features and limitations - ### 240p resolution configuration for video recording 240p resolution configuration for video recording is unsupported by CameraX, and thus, the plugin will fall back to 480p if configured with a `ResolutionPreset`. -### Exposure mode, point, & offset configuration \[[Issue #120468][120468]\] +### Exposure mode configuration \[[Issue #120468][120468]\] -`setExposureMode`, `setExposurePoint`, & `setExposureOffset` are unimplemented. +`setExposureMode`is unimplemented. -### Focus mode & point configuration \[[Issue #120467][120467]\] +### Focus mode configuration \[[Issue #120467][120467]\] -`setFocusMode` & `setFocusPoint` are unimplemented. +`setFocusMode` is unimplemented. ### Setting maximum duration and stream options for video capture diff --git a/packages/camera/camera_android_camerax/android/src/main/java/io/flutter/plugins/camerax/CameraAndroidCameraxPlugin.java b/packages/camera/camera_android_camerax/android/src/main/java/io/flutter/plugins/camerax/CameraAndroidCameraxPlugin.java index b0e0493cbe2..ea9f3ed4d06 100644 --- a/packages/camera/camera_android_camerax/android/src/main/java/io/flutter/plugins/camerax/CameraAndroidCameraxPlugin.java +++ b/packages/camera/camera_android_camerax/android/src/main/java/io/flutter/plugins/camerax/CameraAndroidCameraxPlugin.java @@ -27,6 +27,7 @@ public final class CameraAndroidCameraxPlugin implements FlutterPlugin, Activity @VisibleForTesting @Nullable public ImageCaptureHostApiImpl imageCaptureHostApiImpl; @VisibleForTesting @Nullable public CameraControlHostApiImpl cameraControlHostApiImpl; @VisibleForTesting @Nullable public SystemServicesHostApiImpl systemServicesHostApiImpl; + @VisibleForTesting @Nullable public MeteringPointHostApiImpl meteringPointHostApiImpl; @VisibleForTesting public @Nullable DeviceOrientationManagerHostApiImpl deviceOrientationManagerHostApiImpl; @@ -119,6 +120,12 @@ public void setUp( cameraControlHostApiImpl = new CameraControlHostApiImpl(binaryMessenger, instanceManager, context); GeneratedCameraXLibrary.CameraControlHostApi.setup(binaryMessenger, cameraControlHostApiImpl); + GeneratedCameraXLibrary.FocusMeteringActionHostApi.setup( + binaryMessenger, new FocusMeteringActionHostApiImpl(instanceManager)); + GeneratedCameraXLibrary.FocusMeteringResultHostApi.setup( + binaryMessenger, new FocusMeteringResultHostApiImpl(instanceManager)); + meteringPointHostApiImpl = new MeteringPointHostApiImpl(instanceManager); + GeneratedCameraXLibrary.MeteringPointHostApi.setup(binaryMessenger, meteringPointHostApiImpl); } @Override @@ -238,5 +245,8 @@ public void updateActivity(@Nullable Activity activity) { if (deviceOrientationManagerHostApiImpl != null) { deviceOrientationManagerHostApiImpl.setActivity(activity); } + if (meteringPointHostApiImpl != null) { + meteringPointHostApiImpl.setActivity(activity); + } } } diff --git a/packages/camera/camera_android_camerax/android/src/main/java/io/flutter/plugins/camerax/CameraControlHostApiImpl.java b/packages/camera/camera_android_camerax/android/src/main/java/io/flutter/plugins/camerax/CameraControlHostApiImpl.java index ba26e91c962..ba4318a8927 100644 --- a/packages/camera/camera_android_camerax/android/src/main/java/io/flutter/plugins/camerax/CameraControlHostApiImpl.java +++ b/packages/camera/camera_android_camerax/android/src/main/java/io/flutter/plugins/camerax/CameraControlHostApiImpl.java @@ -83,6 +83,12 @@ public void onSuccess(Void voidResult) { } public void onFailure(Throwable t) { + if (t instanceof CameraControl.OperationCanceledException) { + // Operation was canceled due to camera being closed or a new request was submitted, which + // is not actionable and should not block a new value from potentially being submitted. + result.success(null); + return; + } result.error(t); } }, @@ -94,6 +100,9 @@ public void onFailure(Throwable t) { * *

Will trigger an auto focus action and enable auto focus/auto exposure/auto white balance * metering regions. + * + *

Will send a {@link GeneratedCameraXLibrary.Result} with a null result if operation was + * canceled. */ public void startFocusAndMetering( @NonNull CameraControl cameraControl, @@ -117,6 +126,12 @@ public void onSuccess(FocusMeteringResult focusMeteringResult) { } public void onFailure(Throwable t) { + if (t instanceof CameraControl.OperationCanceledException) { + // Operation was canceled due to camera being closed or a new request was submitted, which + // is not actionable and should not block a new value from potentially being submitted. + result.success(null); + return; + } result.error(t); } }, @@ -152,6 +167,9 @@ public void onFailure(Throwable t) { *

The exposure compensation value set on the camera must be within the range of {@code * ExposureState#getExposureCompensationRange()} for the current {@code ExposureState} for the * call to succeed. + * + *

Will send a {@link GeneratedCameraXLibrary.Result} with a null result if operation was + * canceled. */ public void setExposureCompensationIndex( @NonNull CameraControl cameraControl, @NonNull Long index, @NonNull Result result) { @@ -166,6 +184,12 @@ public void onSuccess(Integer integerResult) { } public void onFailure(Throwable t) { + if (t instanceof CameraControl.OperationCanceledException) { + // Operation was canceled due to camera being closed or a new request was submitted, which + // is not actionable and should not block a new value from potentially being submitted. + result.success(null); + return; + } result.error(t); } }, diff --git a/packages/camera/camera_android_camerax/android/src/main/java/io/flutter/plugins/camerax/GeneratedCameraXLibrary.java b/packages/camera/camera_android_camerax/android/src/main/java/io/flutter/plugins/camerax/GeneratedCameraXLibrary.java index 08f8ea2da9f..ac92427314f 100644 --- a/packages/camera/camera_android_camerax/android/src/main/java/io/flutter/plugins/camerax/GeneratedCameraXLibrary.java +++ b/packages/camera/camera_android_camerax/android/src/main/java/io/flutter/plugins/camerax/GeneratedCameraXLibrary.java @@ -83,14 +83,14 @@ private CameraStateType(final int index) { *

If you need to add another type to support a type S to use a LiveData in this plugin, * ensure the following is done on the Dart side: * - *

* In `../lib/src/live_data.dart`, add new cases for S in + *

* In `camera_android_camerax/lib/src/live_data.dart`, add new cases for S in * `_LiveDataHostApiImpl#getValueFromInstances` to get the current value of type S from a * LiveData instance and in `LiveDataFlutterApiImpl#create` to create the expected type of * LiveData when requested. * *

On the native side, ensure the following is done: * - *

* Update `LiveDataHostApiImpl#getValue` is updated to properly return identifiers for + *

* Make sure `LiveDataHostApiImpl#getValue` is updated to properly return identifiers for * instances of type S. * Update `ObserverFlutterApiWrapper#onChanged` to properly handle * receiving calls with instances of type S if a LiveData instance is observed. */ @@ -146,6 +146,24 @@ private VideoResolutionFallbackRule(final int index) { } } + /** + * The types of capture request options this plugin currently supports. + * + *

If you need to add another option to support, ensure the following is done on the Dart side: + * + *

* In `camera_android_camerax/lib/src/capture_request_options.dart`, add new cases for this + * option in `_CaptureRequestOptionsHostApiImpl#createFromInstances` to create the expected Map + * entry of option key index and value to send to the native side. + * + *

On the native side, ensure the following is done: + * + *

* Update `CaptureRequestOptionsHostApiImpl#create` to set the correct `CaptureRequest` key + * with a valid value type for this option. + * + *

See https://developer.android.com/reference/android/hardware/camera2/CaptureRequest for the + * sorts of capture request options that can be supported via CameraX's interoperability with + * Camera2. + */ public enum CaptureRequestKeySupportedType { CONTROL_AE_LOCK(0); @@ -3899,7 +3917,11 @@ public void create(@NonNull Long identifierArg, @NonNull Reply callback) { public interface MeteringPointHostApi { void create( - @NonNull Long identifier, @NonNull Double x, @NonNull Double y, @Nullable Double size); + @NonNull Long identifier, + @NonNull Double x, + @NonNull Double y, + @Nullable Double size, + @NonNull Long cameraInfoId); @NonNull Double getDefaultPointSize(); @@ -3927,12 +3949,14 @@ static void setup( Double xArg = (Double) args.get(1); Double yArg = (Double) args.get(2); Double sizeArg = (Double) args.get(3); + Number cameraInfoIdArg = (Number) args.get(4); try { api.create( (identifierArg == null) ? null : identifierArg.longValue(), xArg, yArg, - sizeArg); + sizeArg, + (cameraInfoIdArg == null) ? null : cameraInfoIdArg.longValue()); wrapped.add(0, null); } catch (Throwable exception) { ArrayList wrappedError = wrapError(exception); diff --git a/packages/camera/camera_android_camerax/android/src/main/java/io/flutter/plugins/camerax/MeteringPointHostApiImpl.java b/packages/camera/camera_android_camerax/android/src/main/java/io/flutter/plugins/camerax/MeteringPointHostApiImpl.java index 56ffcfb2e39..36306253ed1 100644 --- a/packages/camera/camera_android_camerax/android/src/main/java/io/flutter/plugins/camerax/MeteringPointHostApiImpl.java +++ b/packages/camera/camera_android_camerax/android/src/main/java/io/flutter/plugins/camerax/MeteringPointHostApiImpl.java @@ -4,13 +4,20 @@ package io.flutter.plugins.camerax; +import android.app.Activity; +import android.content.Context; +import android.os.Build; +import android.view.Display; +import android.view.WindowManager; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.annotation.VisibleForTesting; +import androidx.camera.core.CameraInfo; +import androidx.camera.core.DisplayOrientedMeteringPointFactory; import androidx.camera.core.MeteringPoint; import androidx.camera.core.MeteringPointFactory; -import androidx.camera.core.SurfaceOrientedMeteringPointFactory; import io.flutter.plugins.camerax.GeneratedCameraXLibrary.MeteringPointHostApi; +import java.util.Objects; /** * Host API implementation for {@link MeteringPoint}. @@ -25,17 +32,32 @@ public class MeteringPointHostApiImpl implements MeteringPointHostApi { /** Proxy for constructor and static methods of {@link MeteringPoint}. */ @VisibleForTesting public static class MeteringPointProxy { + Activity activity; /** * Creates a surface oriented {@link MeteringPoint} with the specified x, y, and size. * - *

A {@link SurfaceOrientedMeteringPointFactory} is used to construct the {@link - * MeteringPoint} because underlying the camera preview that this plugin uses is a Flutter - * texture that is backed by a {@link Surface} created by the Flutter Android embedding. + *

A {@link DisplayOrientedMeteringPointFactory} is used to construct the {@link + * MeteringPoint} because this factory handles the transformation of specified coordinates based + * on camera information and the device orientation automatically. */ @NonNull - public MeteringPoint create(@NonNull Double x, @NonNull Double y, @Nullable Double size) { - SurfaceOrientedMeteringPointFactory factory = getSurfaceOrientedMeteringPointFactory(1f, 1f); + public MeteringPoint create( + @NonNull Double x, + @NonNull Double y, + @Nullable Double size, + @NonNull CameraInfo cameraInfo) { + Display display = null; + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { + display = activity.getDisplay(); + } else { + display = getDefaultDisplayForAndroidVersionBelowR(activity); + } + + DisplayOrientedMeteringPointFactory factory = + getDisplayOrientedMeteringPointFactory(display, cameraInfo, 1f, 1f); + if (size == null) { return factory.createPoint(x.floatValue(), y.floatValue()); } else { @@ -43,11 +65,18 @@ public MeteringPoint create(@NonNull Double x, @NonNull Double y, @Nullable Doub } } + @NonNull + @SuppressWarnings("deprecation") + private Display getDefaultDisplayForAndroidVersionBelowR(@NonNull Activity activity) { + return ((WindowManager) activity.getSystemService(Context.WINDOW_SERVICE)) + .getDefaultDisplay(); + } + @VisibleForTesting @NonNull - public SurfaceOrientedMeteringPointFactory getSurfaceOrientedMeteringPointFactory( - float width, float height) { - return new SurfaceOrientedMeteringPointFactory(width, height); + public DisplayOrientedMeteringPointFactory getDisplayOrientedMeteringPointFactory( + @NonNull Display display, @NonNull CameraInfo cameraInfo, float width, float height) { + return new DisplayOrientedMeteringPointFactory(display, cameraInfo, width, height); } /** @@ -81,10 +110,23 @@ public MeteringPointHostApiImpl(@NonNull InstanceManager instanceManager) { this.proxy = proxy; } + public void setActivity(@NonNull Activity activity) { + this.proxy.activity = activity; + } + @Override public void create( - @NonNull Long identifier, @NonNull Double x, @NonNull Double y, @Nullable Double size) { - MeteringPoint meteringPoint = proxy.create(x, y, size); + @NonNull Long identifier, + @NonNull Double x, + @NonNull Double y, + @Nullable Double size, + @NonNull Long cameraInfoId) { + MeteringPoint meteringPoint = + proxy.create( + x, + y, + size, + (CameraInfo) Objects.requireNonNull(instanceManager.getInstance(cameraInfoId))); instanceManager.addDartCreatedInstance(meteringPoint, identifier); } diff --git a/packages/camera/camera_android_camerax/android/src/test/java/io/flutter/plugins/camerax/CameraAndroidCameraxPluginTest.java b/packages/camera/camera_android_camerax/android/src/test/java/io/flutter/plugins/camerax/CameraAndroidCameraxPluginTest.java index 8c9daf8e014..f9f4be6db6e 100644 --- a/packages/camera/camera_android_camerax/android/src/test/java/io/flutter/plugins/camerax/CameraAndroidCameraxPluginTest.java +++ b/packages/camera/camera_android_camerax/android/src/test/java/io/flutter/plugins/camerax/CameraAndroidCameraxPluginTest.java @@ -92,6 +92,8 @@ public void onAttachedToActivity_setsActivityAsNeededAndPermissionsRegistry() { mock(SystemServicesHostApiImpl.class); final DeviceOrientationManagerHostApiImpl mockDeviceOrientationManagerHostApiImpl = mock(DeviceOrientationManagerHostApiImpl.class); + final MeteringPointHostApiImpl mockMeteringPointHostApiImpl = + mock(MeteringPointHostApiImpl.class); final ArgumentCaptor permissionsRegistryCaptor = ArgumentCaptor.forClass(PermissionsRegistry.class); @@ -103,6 +105,7 @@ public void onAttachedToActivity_setsActivityAsNeededAndPermissionsRegistry() { plugin.liveDataHostApiImpl = mock(LiveDataHostApiImpl.class); plugin.systemServicesHostApiImpl = mockSystemServicesHostApiImpl; plugin.deviceOrientationManagerHostApiImpl = mockDeviceOrientationManagerHostApiImpl; + plugin.meteringPointHostApiImpl = mockMeteringPointHostApiImpl; plugin.onAttachedToEngine(flutterPluginBinding); plugin.onAttachedToActivity(activityPluginBinding); @@ -110,6 +113,7 @@ public void onAttachedToActivity_setsActivityAsNeededAndPermissionsRegistry() { // Check Activity references are set. verify(mockSystemServicesHostApiImpl).setActivity(mockActivity); verify(mockDeviceOrientationManagerHostApiImpl).setActivity(mockActivity); + verify(mockMeteringPointHostApiImpl).setActivity(mockActivity); // Check permissions registry reference is set. verify(mockSystemServicesHostApiImpl) @@ -129,11 +133,14 @@ public void onAttachedToActivity_setsActivityAsNeededAndPermissionsRegistry() { mock(SystemServicesHostApiImpl.class); final DeviceOrientationManagerHostApiImpl mockDeviceOrientationManagerHostApiImpl = mock(DeviceOrientationManagerHostApiImpl.class); + final MeteringPointHostApiImpl mockMeteringPointHostApiImpl = + mock(MeteringPointHostApiImpl.class); plugin.processCameraProviderHostApiImpl = mockProcessCameraProviderHostApiImpl; plugin.liveDataHostApiImpl = mockLiveDataHostApiImpl; plugin.systemServicesHostApiImpl = mockSystemServicesHostApiImpl; plugin.deviceOrientationManagerHostApiImpl = mockDeviceOrientationManagerHostApiImpl; + plugin.meteringPointHostApiImpl = mockMeteringPointHostApiImpl; plugin.onAttachedToEngine(flutterPluginBinding); plugin.onDetachedFromActivityForConfigChanges(); @@ -142,6 +149,7 @@ public void onAttachedToActivity_setsActivityAsNeededAndPermissionsRegistry() { verify(mockLiveDataHostApiImpl).setLifecycleOwner(null); verify(mockSystemServicesHostApiImpl).setActivity(null); verify(mockDeviceOrientationManagerHostApiImpl).setActivity(null); + verify(mockMeteringPointHostApiImpl).setActivity(null); } @Test @@ -251,6 +259,8 @@ public void onReattachedToActivityForConfigChanges_setsActivityAndPermissionsReg mock(CameraControlHostApiImpl.class); final DeviceOrientationManagerHostApiImpl mockDeviceOrientationManagerHostApiImpl = mock(DeviceOrientationManagerHostApiImpl.class); + final MeteringPointHostApiImpl mockMeteringPointHostApiImpl = + mock(MeteringPointHostApiImpl.class); final ArgumentCaptor permissionsRegistryCaptor = ArgumentCaptor.forClass(PermissionsRegistry.class); @@ -265,6 +275,7 @@ public void onReattachedToActivityForConfigChanges_setsActivityAndPermissionsReg plugin.imageAnalysisHostApiImpl = mockImageAnalysisHostApiImpl; plugin.cameraControlHostApiImpl = mockCameraControlHostApiImpl; plugin.deviceOrientationManagerHostApiImpl = mockDeviceOrientationManagerHostApiImpl; + plugin.meteringPointHostApiImpl = mockMeteringPointHostApiImpl; plugin.liveDataHostApiImpl = mock(LiveDataHostApiImpl.class); plugin.onAttachedToEngine(flutterPluginBinding); @@ -273,6 +284,7 @@ public void onReattachedToActivityForConfigChanges_setsActivityAndPermissionsReg // Check Activity references are set. verify(mockSystemServicesHostApiImpl).setActivity(mockActivity); verify(mockDeviceOrientationManagerHostApiImpl).setActivity(mockActivity); + verify(mockMeteringPointHostApiImpl).setActivity(mockActivity); // Check Activity as Context references are set. verify(mockProcessCameraProviderHostApiImpl).setContext(mockActivity); @@ -300,11 +312,14 @@ public void onDetachedFromActivity_removesReferencesToActivityPluginBindingAndAc final LiveDataHostApiImpl mockLiveDataHostApiImpl = mock(LiveDataHostApiImpl.class); final DeviceOrientationManagerHostApiImpl mockDeviceOrientationManagerHostApiImpl = mock(DeviceOrientationManagerHostApiImpl.class); + final MeteringPointHostApiImpl mockMeteringPointHostApiImpl = + mock(MeteringPointHostApiImpl.class); plugin.processCameraProviderHostApiImpl = mockProcessCameraProviderHostApiImpl; plugin.liveDataHostApiImpl = mockLiveDataHostApiImpl; plugin.systemServicesHostApiImpl = mockSystemServicesHostApiImpl; plugin.deviceOrientationManagerHostApiImpl = mockDeviceOrientationManagerHostApiImpl; + plugin.meteringPointHostApiImpl = mockMeteringPointHostApiImpl; plugin.onAttachedToEngine(flutterPluginBinding); plugin.onDetachedFromActivityForConfigChanges(); @@ -313,6 +328,7 @@ public void onDetachedFromActivity_removesReferencesToActivityPluginBindingAndAc verify(mockLiveDataHostApiImpl).setLifecycleOwner(null); verify(mockSystemServicesHostApiImpl).setActivity(null); verify(mockDeviceOrientationManagerHostApiImpl).setActivity(null); + verify(mockMeteringPointHostApiImpl).setActivity(null); } @Test diff --git a/packages/camera/camera_android_camerax/android/src/test/java/io/flutter/plugins/camerax/CameraControlTest.java b/packages/camera/camera_android_camerax/android/src/test/java/io/flutter/plugins/camerax/CameraControlTest.java index 42ace7a51fb..a0fe8f977ee 100644 --- a/packages/camera/camera_android_camerax/android/src/test/java/io/flutter/plugins/camerax/CameraControlTest.java +++ b/packages/camera/camera_android_camerax/android/src/test/java/io/flutter/plugins/camerax/CameraControlTest.java @@ -149,6 +149,22 @@ public void setZoomRatio_setsZoomAsExpected() { failedSetZoomRatioCallback.onFailure(testThrowable); verify(failedMockResult).error(testThrowable); + + // Test response to canceled operation. + @SuppressWarnings("unchecked") + final GeneratedCameraXLibrary.Result canceledOpResult = + mock(GeneratedCameraXLibrary.Result.class); + final CameraControl.OperationCanceledException canceledOpThrowable = + mock(CameraControl.OperationCanceledException.class); + cameraControlHostApiImpl.setZoomRatio(cameraControlIdentifier, zoomRatio, canceledOpResult); + mockedFutures.verify( + () -> Futures.addCallback(eq(setZoomRatioFuture), futureCallbackCaptor.capture(), any())); + mockedFutures.clearInvocations(); + + FutureCallback canceledOpCallback = futureCallbackCaptor.getValue(); + + canceledOpCallback.onFailure(canceledOpThrowable); + verify(canceledOpResult).success(null); } } @@ -212,6 +228,25 @@ public void startFocusAndMetering_startsFocusAndMeteringAsExpected() { failedCallback.onFailure(testThrowable); verify(failedMockResult).error(testThrowable); + + // Test response to canceled operation. + @SuppressWarnings("unchecked") + final GeneratedCameraXLibrary.Result canceledOpResult = + mock(GeneratedCameraXLibrary.Result.class); + final CameraControl.OperationCanceledException canceledOpThrowable = + mock(CameraControl.OperationCanceledException.class); + cameraControlHostApiImpl.startFocusAndMetering( + cameraControlIdentifier, mockActionId, canceledOpResult); + mockedFutures.verify( + () -> + Futures.addCallback( + eq(startFocusAndMeteringFuture), futureCallbackCaptor.capture(), any())); + mockedFutures.clearInvocations(); + + FutureCallback canceledOpCallback = futureCallbackCaptor.getValue(); + + canceledOpCallback.onFailure(canceledOpThrowable); + verify(canceledOpResult).success(null); } } @@ -326,6 +361,25 @@ public void setExposureCompensationIndex_setsExposureCompensationIndexAsExpected failedCallback.onFailure(testThrowable); verify(failedMockResult).error(testThrowable); + + // Test response to canceled operation. + @SuppressWarnings("unchecked") + final GeneratedCameraXLibrary.Result canceledOpResult = + mock(GeneratedCameraXLibrary.Result.class); + final CameraControl.OperationCanceledException canceledOpThrowable = + mock(CameraControl.OperationCanceledException.class); + cameraControlHostApiImpl.setExposureCompensationIndex( + cameraControlIdentifier, index, canceledOpResult); + mockedFutures.verify( + () -> + Futures.addCallback( + eq(setExposureCompensationIndexFuture), futureCallbackCaptor.capture(), any())); + mockedFutures.clearInvocations(); + + FutureCallback canceledOpCallback = futureCallbackCaptor.getValue(); + + canceledOpCallback.onFailure(canceledOpThrowable); + verify(canceledOpResult).success(null); } } diff --git a/packages/camera/camera_android_camerax/android/src/test/java/io/flutter/plugins/camerax/MeteringPointTest.java b/packages/camera/camera_android_camerax/android/src/test/java/io/flutter/plugins/camerax/MeteringPointTest.java index f245eb53124..0734f6ba6a9 100644 --- a/packages/camera/camera_android_camerax/android/src/test/java/io/flutter/plugins/camerax/MeteringPointTest.java +++ b/packages/camera/camera_android_camerax/android/src/test/java/io/flutter/plugins/camerax/MeteringPointTest.java @@ -10,21 +10,30 @@ import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; +import android.app.Activity; +import android.content.Context; +import android.view.Display; +import android.view.WindowManager; +import androidx.camera.core.CameraInfo; +import androidx.camera.core.DisplayOrientedMeteringPointFactory; import androidx.camera.core.MeteringPoint; import androidx.camera.core.MeteringPointFactory; -import androidx.camera.core.SurfaceOrientedMeteringPointFactory; import io.flutter.plugin.common.BinaryMessenger; import org.junit.After; import org.junit.Before; import org.junit.Rule; import org.junit.Test; +import org.junit.runner.RunWith; import org.mockito.Mock; import org.mockito.MockedStatic; import org.mockito.Mockito; import org.mockito.junit.MockitoJUnit; import org.mockito.junit.MockitoRule; import org.mockito.stubbing.Answer; +import org.robolectric.RobolectricTestRunner; +import org.robolectric.annotation.Config; +@RunWith(RobolectricTestRunner.class) public class MeteringPointTest { @Rule public MockitoRule mockitoRule = MockitoJUnit.rule(); @@ -44,49 +53,158 @@ public void tearDown() { } @Test - public void hostApiCreate_createsExpectedMeteringPointWithSizeSpecified() { - MeteringPointHostApiImpl.MeteringPointProxy proxySpy = + @Config(sdk = 30) + public void hostApiCreate_createsExpectedMeteringPointWithSizeSpecified_AboveAndroid30() { + final MeteringPointHostApiImpl.MeteringPointProxy proxySpy = spy(new MeteringPointHostApiImpl.MeteringPointProxy()); - MeteringPointHostApiImpl hostApi = new MeteringPointHostApiImpl(testInstanceManager, proxySpy); + final MeteringPointHostApiImpl hostApi = + new MeteringPointHostApiImpl(testInstanceManager, proxySpy); final Long meteringPointIdentifier = 78L; - final Float x = 0.3f; - final Float y = 0.2f; - final Float size = 6f; + final Float x = 0.25f; + final Float y = 0.18f; + final Float size = 0.6f; final Float surfaceWidth = 1f; final Float surfaceHeight = 1f; - SurfaceOrientedMeteringPointFactory mockSurfaceOrientedMeteringPointFactory = - mock(SurfaceOrientedMeteringPointFactory.class); - - when(proxySpy.getSurfaceOrientedMeteringPointFactory(surfaceWidth, surfaceHeight)) - .thenReturn(mockSurfaceOrientedMeteringPointFactory); - when(mockSurfaceOrientedMeteringPointFactory.createPoint(x, y, size)).thenReturn(meteringPoint); - - hostApi.create(meteringPointIdentifier, x.doubleValue(), y.doubleValue(), size.doubleValue()); - - verify(mockSurfaceOrientedMeteringPointFactory).createPoint(x, y, size); + final DisplayOrientedMeteringPointFactory mockDisplayOrientedMeteringPointFactory = + mock(DisplayOrientedMeteringPointFactory.class); + final Activity mockActivity = mock(Activity.class); + final Display mockDisplay = mock(Display.class); + final CameraInfo mockCameraInfo = mock(CameraInfo.class); + final long mockCameraInfoId = 55L; + + hostApi.setActivity(mockActivity); + testInstanceManager.addDartCreatedInstance(mockCameraInfo, mockCameraInfoId); + + when(mockActivity.getDisplay()).thenReturn(mockDisplay); + when(proxySpy.getDisplayOrientedMeteringPointFactory( + mockDisplay, mockCameraInfo, surfaceWidth, surfaceHeight)) + .thenReturn(mockDisplayOrientedMeteringPointFactory); + when(mockDisplayOrientedMeteringPointFactory.createPoint(x, y, size)).thenReturn(meteringPoint); + + hostApi.create( + meteringPointIdentifier, + x.doubleValue(), + y.doubleValue(), + size.doubleValue(), + mockCameraInfoId); + + verify(mockDisplayOrientedMeteringPointFactory).createPoint(x, y, size); assertEquals(testInstanceManager.getInstance(meteringPointIdentifier), meteringPoint); } @Test - public void hostApiCreate_createsExpectedMeteringPointWithoutSizeSpecified() { - MeteringPointHostApiImpl.MeteringPointProxy proxySpy = + @Config(sdk = 29) + @SuppressWarnings("deprecation") + public void hostApiCreate_createsExpectedMeteringPointWithSizeSpecified_BelowAndroid30() { + final MeteringPointHostApiImpl.MeteringPointProxy proxySpy = spy(new MeteringPointHostApiImpl.MeteringPointProxy()); - MeteringPointHostApiImpl hostApi = new MeteringPointHostApiImpl(testInstanceManager, proxySpy); + final MeteringPointHostApiImpl hostApi = + new MeteringPointHostApiImpl(testInstanceManager, proxySpy); final Long meteringPointIdentifier = 78L; final Float x = 0.3f; final Float y = 0.2f; + final Float size = 6f; final Float surfaceWidth = 1f; final Float surfaceHeight = 1f; - SurfaceOrientedMeteringPointFactory mockSurfaceOrientedMeteringPointFactory = - mock(SurfaceOrientedMeteringPointFactory.class); - - when(proxySpy.getSurfaceOrientedMeteringPointFactory(surfaceWidth, surfaceHeight)) - .thenReturn(mockSurfaceOrientedMeteringPointFactory); - when(mockSurfaceOrientedMeteringPointFactory.createPoint(x, y)).thenReturn(meteringPoint); + final DisplayOrientedMeteringPointFactory mockDisplayOrientedMeteringPointFactory = + mock(DisplayOrientedMeteringPointFactory.class); + final Activity mockActivity = mock(Activity.class); + final WindowManager mockWindowManager = mock(WindowManager.class); + final Display mockDisplay = mock(Display.class); + final CameraInfo mockCameraInfo = mock(CameraInfo.class); + final long mockCameraInfoId = 5L; + + hostApi.setActivity(mockActivity); + testInstanceManager.addDartCreatedInstance(mockCameraInfo, mockCameraInfoId); + + when(mockActivity.getSystemService(Context.WINDOW_SERVICE)).thenReturn(mockWindowManager); + when(mockWindowManager.getDefaultDisplay()).thenReturn(mockDisplay); + when(proxySpy.getDisplayOrientedMeteringPointFactory( + mockDisplay, mockCameraInfo, surfaceWidth, surfaceHeight)) + .thenReturn(mockDisplayOrientedMeteringPointFactory); + when(mockDisplayOrientedMeteringPointFactory.createPoint(x, y, size)).thenReturn(meteringPoint); + + hostApi.create( + meteringPointIdentifier, + x.doubleValue(), + y.doubleValue(), + size.doubleValue(), + mockCameraInfoId); + + verify(mockDisplayOrientedMeteringPointFactory).createPoint(x, y, size); + assertEquals(testInstanceManager.getInstance(meteringPointIdentifier), meteringPoint); + } - hostApi.create(meteringPointIdentifier, x.doubleValue(), y.doubleValue(), null); + @Test + @Config(sdk = 30) + public void hostApiCreate_createsExpectedMeteringPointWithoutSizeSpecified_AboveAndroid30() { + final MeteringPointHostApiImpl.MeteringPointProxy proxySpy = + spy(new MeteringPointHostApiImpl.MeteringPointProxy()); + final MeteringPointHostApiImpl hostApi = + new MeteringPointHostApiImpl(testInstanceManager, proxySpy); + final Long meteringPointIdentifier = 78L; + final Float x = 0.23f; + final Float y = 0.32f; + final Float surfaceWidth = 1f; + final Float surfaceHeight = 1f; + final DisplayOrientedMeteringPointFactory mockDisplayOrientedMeteringPointFactory = + mock(DisplayOrientedMeteringPointFactory.class); + final Activity mockActivity = mock(Activity.class); + final Display mockDisplay = mock(Display.class); + final CameraInfo mockCameraInfo = mock(CameraInfo.class); + final long mockCameraInfoId = 6L; + + hostApi.setActivity(mockActivity); + testInstanceManager.addDartCreatedInstance(mockCameraInfo, mockCameraInfoId); + + when(mockActivity.getDisplay()).thenReturn(mockDisplay); + when(proxySpy.getDisplayOrientedMeteringPointFactory( + mockDisplay, mockCameraInfo, surfaceWidth, surfaceHeight)) + .thenReturn(mockDisplayOrientedMeteringPointFactory); + when(mockDisplayOrientedMeteringPointFactory.createPoint(x, y)).thenReturn(meteringPoint); + + hostApi.create( + meteringPointIdentifier, x.doubleValue(), y.doubleValue(), null, mockCameraInfoId); + + verify(mockDisplayOrientedMeteringPointFactory).createPoint(x, y); + assertEquals(testInstanceManager.getInstance(meteringPointIdentifier), meteringPoint); + } - verify(mockSurfaceOrientedMeteringPointFactory).createPoint(x, y); + @Test + @Config(sdk = 29) + @SuppressWarnings("deprecation") + public void hostApiCreate_createsExpectedMeteringPointWithoutSizeSpecified_BelowAndroid30() { + final MeteringPointHostApiImpl.MeteringPointProxy proxySpy = + spy(new MeteringPointHostApiImpl.MeteringPointProxy()); + final MeteringPointHostApiImpl hostApi = + new MeteringPointHostApiImpl(testInstanceManager, proxySpy); + final Long meteringPointIdentifier = 78L; + final Float x = 0.1f; + final Float y = 0.8f; + final Float surfaceWidth = 1f; + final Float surfaceHeight = 1f; + final DisplayOrientedMeteringPointFactory mockDisplayOrientedMeteringPointFactory = + mock(DisplayOrientedMeteringPointFactory.class); + final Activity mockActivity = mock(Activity.class); + final WindowManager mockWindowManager = mock(WindowManager.class); + final Display mockDisplay = mock(Display.class); + final CameraInfo mockCameraInfo = mock(CameraInfo.class); + final long mockCameraInfoId = 7L; + + hostApi.setActivity(mockActivity); + testInstanceManager.addDartCreatedInstance(mockCameraInfo, mockCameraInfoId); + + when(mockActivity.getSystemService(Context.WINDOW_SERVICE)).thenReturn(mockWindowManager); + when(mockWindowManager.getDefaultDisplay()).thenReturn(mockDisplay); + when(proxySpy.getDisplayOrientedMeteringPointFactory( + mockDisplay, mockCameraInfo, surfaceWidth, surfaceHeight)) + .thenReturn(mockDisplayOrientedMeteringPointFactory); + when(mockDisplayOrientedMeteringPointFactory.createPoint(x, y)).thenReturn(meteringPoint); + + hostApi.create( + meteringPointIdentifier, x.doubleValue(), y.doubleValue(), null, mockCameraInfoId); + + verify(mockDisplayOrientedMeteringPointFactory).createPoint(x, y); assertEquals(testInstanceManager.getInstance(meteringPointIdentifier), meteringPoint); } diff --git a/packages/camera/camera_android_camerax/example/lib/main.dart b/packages/camera/camera_android_camerax/example/lib/main.dart index 4484c5dac98..adb86837371 100644 --- a/packages/camera/camera_android_camerax/example/lib/main.dart +++ b/packages/camera/camera_android_camerax/example/lib/main.dart @@ -282,14 +282,15 @@ class _CameraExampleHomeState extends State IconButton( icon: const Icon(Icons.exposure), color: Colors.blue, - onPressed: - () {}, // TODO(camsim99): Add functionality back here. + onPressed: controller != null + ? onExposureModeButtonPressed + : null, ), IconButton( icon: const Icon(Icons.filter_center_focus), color: Colors.blue, onPressed: - () {}, // TODO(camsim99): Add functionality back here. + controller != null ? onFocusModeButtonPressed : null, ) ] : [], @@ -394,8 +395,13 @@ class _CameraExampleHomeState extends State style: styleAuto, onPressed: () {}, // TODO(camsim99): Add functionality back here. - onLongPress: - () {}, // TODO(camsim99): Add functionality back here., + onLongPress: () { + if (controller != null) { + CameraPlatform.instance + .setExposurePoint(controller!.cameraId, null); + showInSnackBar('Resetting exposure point'); + } + }, child: const Text('AUTO'), ), TextButton( @@ -406,8 +412,9 @@ class _CameraExampleHomeState extends State ), TextButton( style: styleLocked, - onPressed: - () {}, // TODO(camsim99): Add functionality back here. + onPressed: controller != null + ? () => controller!.setExposureOffset(0.0) + : null, child: const Text('RESET OFFSET'), ), ], @@ -468,8 +475,13 @@ class _CameraExampleHomeState extends State style: styleAuto, onPressed: () {}, // TODO(camsim99): Add functionality back here. - onLongPress: - () {}, // TODO(camsim99): Add functionality back here. + onLongPress: () { + if (controller != null) { + CameraPlatform.instance + .setFocusPoint(controller!.cameraId, null); + } + showInSnackBar('Resetting focus point'); + }, child: const Text('AUTO'), ), TextButton( diff --git a/packages/camera/camera_android_camerax/lib/src/android_camera_camerax.dart b/packages/camera/camera_android_camerax/lib/src/android_camera_camerax.dart index b05788d68a4..deddf4709c9 100644 --- a/packages/camera/camera_android_camerax/lib/src/android_camera_camerax.dart +++ b/packages/camera/camera_android_camerax/lib/src/android_camera_camerax.dart @@ -3,10 +3,12 @@ // found in the LICENSE file. import 'dart:async'; +import 'dart:math' show Point; import 'package:async/async.dart'; import 'package:camera_platform_interface/camera_platform_interface.dart'; -import 'package:flutter/services.dart' show DeviceOrientation; +import 'package:flutter/services.dart' + show DeviceOrientation, PlatformException; import 'package:flutter/widgets.dart'; import 'package:stream_transform/stream_transform.dart'; @@ -21,10 +23,12 @@ import 'camerax_proxy.dart'; import 'device_orientation_manager.dart'; import 'exposure_state.dart'; import 'fallback_strategy.dart'; +import 'focus_metering_action.dart'; import 'image_analysis.dart'; import 'image_capture.dart'; import 'image_proxy.dart'; import 'live_data.dart'; +import 'metering_point.dart'; import 'observer.dart'; import 'pending_recording.dart'; import 'plane_proxy.dart'; @@ -69,6 +73,9 @@ class AndroidCameraCameraX extends CameraPlatform { @visibleForTesting CameraInfo? cameraInfo; + /// The [CameraControl] instance that corresponds to the [camera] instance. + late CameraControl cameraControl; + /// The [LiveData] of the [CameraState] that represents the state of the /// [camera] instance. LiveData? liveCameraState; @@ -180,6 +187,16 @@ class AndroidCameraCameraX extends CameraPlatform { /// for an example on how setting target rotations for [UseCase]s works. bool shouldSetDefaultRotation = false; + /// The currently set [FocusMeteringAction] used to enable auto-focus and + /// auto-exposure. + @visibleForTesting + FocusMeteringAction? currentFocusMeteringAction; + + /// Error code indicating that exposure compensation is not supported by + /// CameraX for the device. + static const String exposureCompensationNotSupported = + 'exposureCompensationNotSupported'; + /// Returns list of all available cameras and their descriptions. @override Future> availableCameras() async { @@ -430,6 +447,18 @@ class AndroidCameraCameraX extends CameraPlatform { captureOrientationLocked = false; } + /// Sets the exposure point for automatically determining the exposure values. + /// + /// Supplying `null` for the [point] argument will result in resetting to the + /// original exposure point value. + /// + /// [cameraId] is not used. + @override + Future setExposurePoint(int cameraId, Point? point) async { + await _startFocusAndMeteringFor( + point: point, meteringMode: FocusMeteringAction.flagAe); + } + /// Gets the minimum supported exposure offset for the selected camera in EV units. /// /// [cameraId] not used. @@ -452,13 +481,71 @@ class AndroidCameraCameraX extends CameraPlatform { /// Gets the supported step size for exposure offset for the selected camera in EV units. /// - /// Returns 0 when exposure compensation is not supported. + /// Returns -1 if exposure compensation is not supported for the device. /// /// [cameraId] not used. @override Future getExposureOffsetStepSize(int cameraId) async { final ExposureState exposureState = await cameraInfo!.getExposureState(); - return exposureState.exposureCompensationStep; + final double exposureOffsetStepSize = + exposureState.exposureCompensationStep; + if (exposureOffsetStepSize == 0) { + // CameraX returns a step size of 0 if exposure compensation is not + // supported for the device. + return -1; + } + return exposureOffsetStepSize; + } + + /// Sets the exposure offset for the selected camera. + /// + /// The supplied [offset] value should be in EV units. 1 EV unit represents a + /// doubling in brightness. It should be between the minimum and maximum offsets + /// obtained through `getMinExposureOffset` and `getMaxExposureOffset` respectively. + /// Throws a `CameraException` when trying to set exposure offset on a device + /// that doesn't support exposure compensationan or if setting the offset fails, + /// like in the case that an illegal offset is supplied. + /// + /// When the supplied [offset] value does not align with the step size obtained + /// through `getExposureStepSize`, it will automatically be rounded to the nearest step. + /// + /// Returns the (rounded) offset value that was set. + @override + Future setExposureOffset(int cameraId, double offset) async { + final double exposureOffsetStepSize = + (await cameraInfo!.getExposureState()).exposureCompensationStep; + if (exposureOffsetStepSize == 0) { + throw CameraException(exposureCompensationNotSupported, + 'Exposure compensation not supported'); + } + + // (Exposure compensation index) * (exposure offset step size) = + // (exposure offset). + final int roundedExposureCompensationIndex = + (offset / exposureOffsetStepSize).round(); + + try { + await cameraControl + .setExposureCompensationIndex(roundedExposureCompensationIndex); + } on PlatformException catch (e) { + throw CameraException( + 'setExposureOffsetFailed', + e.message ?? + 'Setting the camera exposure compensation index failed.'); + } + return roundedExposureCompensationIndex * exposureOffsetStepSize; + } + + /// Sets the focus point for automatically determining the focus values. + /// + /// Supplying `null` for the [point] argument will result in resetting to the + /// original focus point value. + /// + /// [cameraId] is not used. + @override + Future setFocusPoint(int cameraId, Point? point) async { + await _startFocusAndMeteringFor( + point: point, meteringMode: FocusMeteringAction.flagAf); } /// Gets the maximum supported zoom level for the selected camera. @@ -502,7 +589,6 @@ class AndroidCameraCameraX extends CameraPlatform { /// Throws a `CameraException` when an illegal zoom level is supplied. @override Future setZoomLevel(int cameraId, double zoom) async { - final CameraControl cameraControl = await camera!.getCameraControl(); await cameraControl.setZoomRatio(zoom); } @@ -587,10 +673,8 @@ class AndroidCameraCameraX extends CameraPlatform { /// respectively. @override Future setFlashMode(int cameraId, FlashMode mode) async { - CameraControl? cameraControl; // Turn off torch mode if it is enabled and not being redundantly set. if (mode != FlashMode.torch && torchEnabled) { - cameraControl = await camera!.getCameraControl(); await cameraControl.enableTorch(false); torchEnabled = false; } @@ -608,7 +692,6 @@ class AndroidCameraCameraX extends CameraPlatform { // Torch mode enabled already. return; } - cameraControl = await camera!.getCameraControl(); await cameraControl.enableTorch(true); torchEnabled = true; } @@ -833,14 +916,15 @@ class AndroidCameraCameraX extends CameraPlatform { // Methods concerning camera state: - /// Updates [cameraInfo] to the information corresponding to [camera] and - /// adds observers to the [LiveData] of the [CameraState] of the current - /// [camera], saved as [liveCameraState]. + /// Updates [cameraInfo] and [cameraControl] to the information corresponding + /// to [camera] and adds observers to the [LiveData] of the [CameraState] of + /// the current [camera], saved as [liveCameraState]. /// /// If a previous [liveCameraState] was stored, existing observers are /// removed, as well. Future _updateCameraInfoAndLiveCameraState(int cameraId) async { cameraInfo = await camera!.getCameraInfo(); + cameraControl = await camera!.getCameraControl(); await liveCameraState?.removeObservers(); liveCameraState = await cameraInfo!.getCameraState(); await liveCameraState!.observe(_createCameraClosingObserver(cameraId)); @@ -981,4 +1065,87 @@ class AndroidCameraCameraX extends CameraPlatform { return proxy.createQualitySelector( videoQuality: videoQuality, fallbackStrategy: fallbackStrategy); } + + // Methods for configuring auto-focus and auto-exposure: + + /// Starts a focus and metering action. + /// + /// This method will modify and start the current action's metering points + /// overriden with the [point] provided for the specified [meteringMode] type + /// only, with all other points of other modes left untouched. Thus, the + /// focus and metering action started will contain only the one most recently + /// set point for each metering mode: AF, AE, AWB. + /// + /// Thus, if [point] is non-null, this action includes: + /// * metering points and their modes previously added to + /// [currentFocusMeteringAction] that do not share a metering mode with + /// [point] and + /// * [point] with the specified [meteringMode]. + /// If [point] is null, this action includes only metering points and + /// their modes previously added to [currentFocusMeteringAction] that do not + /// share a metering mode with [point]. If there are no such metering + /// points, then the previously enabled focus and metering actions will be + /// canceled. + Future _startFocusAndMeteringFor( + {required Point? point, required int meteringMode}) async { + if (point == null) { + // Try to clear any metering point from previous action with the specified + // meteringMode. + if (currentFocusMeteringAction == null) { + // Attempting to clear a metering point from a previous action, but no + // such action exists. + return; + } + + // Remove metering point with specified meteringMode from current focus + // and metering action, as only one focus or exposure point may be set + // at once in this plugin. + final List<(MeteringPoint, int?)> newMeteringPointInfos = + currentFocusMeteringAction!.meteringPointInfos + .where(((MeteringPoint, int?) meteringPointInfo) => + // meteringPointInfo may technically include points without a + // mode specified, but this logic is safe because this plugin + // only uses points that explicitly have mode + // FocusMeteringAction.flagAe or FocusMeteringAction.flagAf. + meteringPointInfo.$2 != meteringMode) + .toList(); + + if (newMeteringPointInfos.isEmpty) { + // If no other metering points were specified, cancel any previously + // started focus and metering actions. + await cameraControl.cancelFocusAndMetering(); + currentFocusMeteringAction = null; + return; + } + currentFocusMeteringAction = + proxy.createFocusMeteringAction(newMeteringPointInfos); + } else if (point.x < 0 || point.x > 1 || point.y < 0 || point.y > 1) { + throw CameraException('pointInvalid', + 'The coordinates of a metering point for an auto-focus or auto-exposure action must be within (0,0) and (1,1), but point $point was provided for metering mode $meteringMode.'); + } else { + // Add new metering point with specified meteringMode, which may involve + // replacing a metering point with the same specified meteringMode from + // the current focus and metering action. + List<(MeteringPoint, int?)> newMeteringPointInfos = + <(MeteringPoint, int?)>[]; + + if (currentFocusMeteringAction != null) { + newMeteringPointInfos = currentFocusMeteringAction!.meteringPointInfos + .where(((MeteringPoint, int?) meteringPointInfo) => + // meteringPointInfo may technically include points without a + // mode specified, but this logic is safe because this plugin + // only uses points that explicitly have mode + // FocusMeteringAction.flagAe or FocusMeteringAction.flagAf. + meteringPointInfo.$2 != meteringMode) + .toList(); + } + final MeteringPoint newMeteringPoint = + proxy.createMeteringPoint(point.x, point.y, cameraInfo!); + newMeteringPointInfos.add((newMeteringPoint, meteringMode)); + currentFocusMeteringAction = + proxy.createFocusMeteringAction(newMeteringPointInfos); + } + + await cameraControl.startFocusAndMetering(currentFocusMeteringAction!); + } } diff --git a/packages/camera/camera_android_camerax/lib/src/camera_control.dart b/packages/camera/camera_android_camerax/lib/src/camera_control.dart index 9c50ffa161f..a233011975f 100644 --- a/packages/camera/camera_android_camerax/lib/src/camera_control.dart +++ b/packages/camera/camera_android_camerax/lib/src/camera_control.dart @@ -56,6 +56,10 @@ class CameraControl extends JavaObject { /// Will trigger an auto focus action and enable auto focus/auto exposure/ /// auto white balance metering regions. /// + /// Only one [FocusMeteringAction] is allowed to run at a time; if multiple + /// are executed in a row, only the latest one will work and other actions + /// will be canceled. + /// /// Returns null if focus and metering could not be started. Future startFocusAndMetering( FocusMeteringAction action) { @@ -74,6 +78,10 @@ class CameraControl extends JavaObject { /// of the current [ExposureState]'s `exposureCompensationRange` for the call /// to succeed. /// + /// Only one [setExposureCompensationIndex] is allowed to run at a time; if + /// multiple are executed in a row, only the latest setting will be kept in + /// the camera. + /// /// Returns null if the exposure compensation index failed to be set. Future setExposureCompensationIndex(int index) async { return _api.setExposureCompensationIndexFromInstance(this, index); @@ -133,8 +141,13 @@ class _CameraControlHostApiImpl extends CameraControlHostApi { instanceManager.getIdentifier(instance)!; final int actionIdentifier = instanceManager.getIdentifier(action)!; try { - final int focusMeteringResultId = await startFocusAndMetering( + final int? focusMeteringResultId = await startFocusAndMetering( cameraControlIdentifier, actionIdentifier); + if (focusMeteringResultId == null) { + SystemServices.cameraErrorStreamController.add( + 'Starting focus and metering was canceled due to the camera being closed or a new request being submitted.'); + return Future.value(); + } return instanceManager.getInstanceWithWeakReference( focusMeteringResultId); } on PlatformException catch (e) { @@ -158,11 +171,20 @@ class _CameraControlHostApiImpl extends CameraControlHostApi { CameraControl instance, int index) async { final int identifier = instanceManager.getIdentifier(instance)!; try { - return setExposureCompensationIndex(identifier, index); + final int? exposureCompensationIndex = + await setExposureCompensationIndex(identifier, index); + if (exposureCompensationIndex == null) { + SystemServices.cameraErrorStreamController.add( + 'Setting exposure compensation index was canceled due to the camera being closed or a new request being submitted.'); + return Future.value(); + } + return exposureCompensationIndex; } on PlatformException catch (e) { SystemServices.cameraErrorStreamController.add(e.message ?? 'Setting the camera exposure compensation index failed.'); - return Future.value(); + // Surfacing error to plugin layer to maintain consistency of + // setExposureOffset implementation across platform implementations. + rethrow; } } } diff --git a/packages/camera/camera_android_camerax/lib/src/camerax_library.g.dart b/packages/camera/camera_android_camerax/lib/src/camerax_library.g.dart index 07515c0c14f..14239dd01b6 100644 --- a/packages/camera/camera_android_camerax/lib/src/camerax_library.g.dart +++ b/packages/camera/camera_android_camerax/lib/src/camerax_library.g.dart @@ -27,14 +27,14 @@ enum CameraStateType { /// If you need to add another type to support a type S to use a LiveData in /// this plugin, ensure the following is done on the Dart side: /// -/// * In `../lib/src/live_data.dart`, add new cases for S in +/// * In `camera_android_camerax/lib/src/live_data.dart`, add new cases for S in /// `_LiveDataHostApiImpl#getValueFromInstances` to get the current value of /// type S from a LiveData instance and in `LiveDataFlutterApiImpl#create` /// to create the expected type of LiveData when requested. /// /// On the native side, ensure the following is done: /// -/// * Update `LiveDataHostApiImpl#getValue` is updated to properly return +/// * Make sure `LiveDataHostApiImpl#getValue` is updated to properly return /// identifiers for instances of type S. /// * Update `ObserverFlutterApiWrapper#onChanged` to properly handle receiving /// calls with instances of type S if a LiveData instance is observed. @@ -68,6 +68,24 @@ enum VideoResolutionFallbackRule { lowerQualityThan, } +/// The types of capture request options this plugin currently supports. +/// +/// If you need to add another option to support, ensure the following is done +/// on the Dart side: +/// +/// * In `camera_android_camerax/lib/src/capture_request_options.dart`, add new cases for this +/// option in `_CaptureRequestOptionsHostApiImpl#createFromInstances` +/// to create the expected Map entry of option key index and value to send to +/// the native side. +/// +/// On the native side, ensure the following is done: +/// +/// * Update `CaptureRequestOptionsHostApiImpl#create` to set the correct +/// `CaptureRequest` key with a valid value type for this option. +/// +/// See https://developer.android.com/reference/android/hardware/camera2/CaptureRequest +/// for the sorts of capture request options that can be supported via CameraX's +/// interoperability with Camera2. enum CaptureRequestKeySupportedType { controlAeLock, } @@ -2884,7 +2902,7 @@ class CameraControlHostApi { } } - Future startFocusAndMetering( + Future startFocusAndMetering( int arg_identifier, int arg_focusMeteringActionId) async { final BasicMessageChannel channel = BasicMessageChannel( 'dev.flutter.pigeon.CameraControlHostApi.startFocusAndMetering', codec, @@ -2903,13 +2921,8 @@ class CameraControlHostApi { message: replyList[1] as String?, details: replyList[2], ); - } else if (replyList[0] == null) { - throw PlatformException( - code: 'null-error', - message: 'Host platform returned null value for non-null return value.', - ); } else { - return (replyList[0] as int?)!; + return (replyList[0] as int?); } } @@ -2935,7 +2948,7 @@ class CameraControlHostApi { } } - Future setExposureCompensationIndex( + Future setExposureCompensationIndex( int arg_identifier, int arg_index) async { final BasicMessageChannel channel = BasicMessageChannel( 'dev.flutter.pigeon.CameraControlHostApi.setExposureCompensationIndex', @@ -2954,13 +2967,8 @@ class CameraControlHostApi { message: replyList[1] as String?, details: replyList[2], ); - } else if (replyList[0] == null) { - throw PlatformException( - code: 'null-error', - message: 'Host platform returned null value for non-null return value.', - ); } else { - return (replyList[0] as int?)!; + return (replyList[0] as int?); } } } @@ -3130,14 +3138,14 @@ class MeteringPointHostApi { static const MessageCodec codec = StandardMessageCodec(); - Future create( - int arg_identifier, double arg_x, double arg_y, double? arg_size) async { + Future create(int arg_identifier, double arg_x, double arg_y, + double? arg_size, int arg_cameraInfoId) async { final BasicMessageChannel channel = BasicMessageChannel( 'dev.flutter.pigeon.MeteringPointHostApi.create', codec, binaryMessenger: _binaryMessenger); - final List? replyList = - await channel.send([arg_identifier, arg_x, arg_y, arg_size]) - as List?; + final List? replyList = await channel.send( + [arg_identifier, arg_x, arg_y, arg_size, arg_cameraInfoId]) + as List?; if (replyList == null) { throw PlatformException( code: 'channel-error', diff --git a/packages/camera/camera_android_camerax/lib/src/camerax_proxy.dart b/packages/camera/camera_android_camerax/lib/src/camerax_proxy.dart index 87d3680ff2c..072a753d8b4 100644 --- a/packages/camera/camera_android_camerax/lib/src/camerax_proxy.dart +++ b/packages/camera/camera_android_camerax/lib/src/camerax_proxy.dart @@ -5,14 +5,17 @@ import 'dart:ui' show Size; import 'analyzer.dart'; +import 'camera_info.dart'; import 'camera_selector.dart'; import 'camera_state.dart'; import 'camerax_library.g.dart'; import 'device_orientation_manager.dart'; import 'fallback_strategy.dart'; +import 'focus_metering_action.dart'; import 'image_analysis.dart'; import 'image_capture.dart'; import 'image_proxy.dart'; +import 'metering_point.dart'; import 'observer.dart'; import 'preview.dart'; import 'process_camera_provider.dart'; @@ -49,6 +52,8 @@ class CameraXProxy { _startListeningForDeviceOrientationChange, this.setPreviewSurfaceProvider = _setPreviewSurfaceProvider, this.getDefaultDisplayRotation = _getDefaultDisplayRotation, + this.createMeteringPoint = _createMeteringPoint, + this.createFocusMeteringAction = _createFocusMeteringAction, }); /// Returns a [ProcessCameraProvider] instance. @@ -137,6 +142,16 @@ class CameraXProxy { /// rotation constants. Future Function() getDefaultDisplayRotation; + /// Returns a [MeteringPoint] with the specified coordinates based on + /// [cameraInfo]. + MeteringPoint Function(double x, double y, CameraInfo cameraInfo) + createMeteringPoint; + + /// Returns a [FocusMeteringAction] based on the specified metering points + /// and their modes. + FocusMeteringAction Function(List<(MeteringPoint, int?)> meteringPointInfos) + createFocusMeteringAction; + static Future _getProcessCameraProvider() { return ProcessCameraProvider.getInstance(); } @@ -239,4 +254,14 @@ class CameraXProxy { static Future _getDefaultDisplayRotation() async { return DeviceOrientationManager.getDefaultDisplayRotation(); } + + static MeteringPoint _createMeteringPoint( + double x, double y, CameraInfo cameraInfo) { + return MeteringPoint(x: x, y: y, cameraInfo: cameraInfo); + } + + static FocusMeteringAction _createFocusMeteringAction( + List<(MeteringPoint, int?)> meteringPointInfos) { + return FocusMeteringAction(meteringPointInfos: meteringPointInfos); + } } diff --git a/packages/camera/camera_android_camerax/lib/src/focus_metering_action.dart b/packages/camera/camera_android_camerax/lib/src/focus_metering_action.dart index 6d9ebd97f01..fe09ebc0443 100644 --- a/packages/camera/camera_android_camerax/lib/src/focus_metering_action.dart +++ b/packages/camera/camera_android_camerax/lib/src/focus_metering_action.dart @@ -19,8 +19,7 @@ class FocusMeteringAction extends JavaObject { FocusMeteringAction({ BinaryMessenger? binaryMessenger, InstanceManager? instanceManager, - required List<(MeteringPoint meteringPoint, int? meteringMode)> - meteringPointInfos, + required this.meteringPointInfos, }) : super.detached( binaryMessenger: binaryMessenger, instanceManager: instanceManager, @@ -35,6 +34,7 @@ class FocusMeteringAction extends JavaObject { FocusMeteringAction.detached({ BinaryMessenger? binaryMessenger, InstanceManager? instanceManager, + required this.meteringPointInfos, }) : super.detached( binaryMessenger: binaryMessenger, instanceManager: instanceManager, @@ -45,6 +45,11 @@ class FocusMeteringAction extends JavaObject { late final _FocusMeteringActionHostApiImpl _api; + /// The requested [MeteringPoint]s and modes that are relevant to each of those + /// points. + final List<(MeteringPoint meteringPoint, int? meteringMode)> + meteringPointInfos; + /// Flag for metering mode that indicates the auto focus region is enabled. /// /// An autofocus scan is also triggered when [flagAf] is assigned. @@ -97,7 +102,9 @@ class _FocusMeteringActionHostApiImpl extends FocusMeteringActionHostApi { final int identifier = instanceManager.addDartCreatedInstance(instance, onCopy: (FocusMeteringAction original) { return FocusMeteringAction.detached( - binaryMessenger: binaryMessenger, instanceManager: instanceManager); + binaryMessenger: binaryMessenger, + instanceManager: instanceManager, + meteringPointInfos: original.meteringPointInfos); }); final List meteringPointInfosWithIds = diff --git a/packages/camera/camera_android_camerax/lib/src/metering_point.dart b/packages/camera/camera_android_camerax/lib/src/metering_point.dart index d699993a893..c5ee6061de9 100644 --- a/packages/camera/camera_android_camerax/lib/src/metering_point.dart +++ b/packages/camera/camera_android_camerax/lib/src/metering_point.dart @@ -6,6 +6,7 @@ import 'package:flutter/services.dart' show BinaryMessenger; import 'package:meta/meta.dart' show immutable; import 'android_camera_camerax_flutter_api_impls.dart'; +import 'camera_info.dart'; import 'camerax_library.g.dart'; import 'instance_manager.dart'; import 'java_object.dart'; @@ -23,13 +24,14 @@ class MeteringPoint extends JavaObject { required this.x, required this.y, this.size, + required this.cameraInfo, }) : super.detached( binaryMessenger: binaryMessenger, instanceManager: instanceManager, ) { _api = _MeteringPointHostApiImpl( binaryMessenger: binaryMessenger, instanceManager: instanceManager); - _api.createFromInstance(this, x, y, size); + _api.createFromInstance(this, x, y, size, cameraInfo); AndroidCameraXCameraFlutterApis.instance.ensureSetUp(); } @@ -41,6 +43,7 @@ class MeteringPoint extends JavaObject { required this.x, required this.y, this.size, + required this.cameraInfo, }) : super.detached( binaryMessenger: binaryMessenger, instanceManager: instanceManager, @@ -63,6 +66,10 @@ class MeteringPoint extends JavaObject { /// region width/height if crop region is set). final double? size; + /// The [CameraInfo] used to construct the metering point with a display- + /// oriented metering point factory. + final CameraInfo cameraInfo; + /// The default size of the [MeteringPoint] width and height (ranging from 0 /// to 1) which is a (normalized) percentage of the sensor width/height (or /// crop region width/height if crop region is set). @@ -100,8 +107,8 @@ class _MeteringPointHostApiImpl extends MeteringPointHostApi { /// Creates a [MeteringPoint] instance with the specified [x] and [y] /// coordinates as well as [size] if non-null. - Future createFromInstance( - MeteringPoint instance, double x, double y, double? size) { + Future createFromInstance(MeteringPoint instance, double x, double y, + double? size, CameraInfo cameraInfo) { int? identifier = instanceManager.getIdentifier(instance); identifier ??= instanceManager.addDartCreatedInstance(instance, onCopy: (MeteringPoint original) { @@ -110,9 +117,11 @@ class _MeteringPointHostApiImpl extends MeteringPointHostApi { instanceManager: instanceManager, x: original.x, y: original.y, + cameraInfo: original.cameraInfo, size: original.size); }); + final int? camInfoId = instanceManager.getIdentifier(cameraInfo); - return create(identifier, x, y, size); + return create(identifier, x, y, size, camInfoId!); } } diff --git a/packages/camera/camera_android_camerax/pigeons/camerax_library.dart b/packages/camera/camera_android_camerax/pigeons/camerax_library.dart index db6164044d3..fc78cae75d2 100644 --- a/packages/camera/camera_android_camerax/pigeons/camerax_library.dart +++ b/packages/camera/camera_android_camerax/pigeons/camerax_library.dart @@ -66,7 +66,7 @@ class CameraStateTypeData { /// If you need to add another type to support a type S to use a LiveData in /// this plugin, ensure the following is done on the Dart side: /// -/// * In `../lib/src/live_data.dart`, add new cases for S in +/// * In `camera_android_camerax/lib/src/live_data.dart`, add new cases for S in /// `_LiveDataHostApiImpl#getValueFromInstances` to get the current value of /// type S from a LiveData instance and in `LiveDataFlutterApiImpl#create` /// to create the expected type of LiveData when requested. @@ -148,7 +148,7 @@ class MeteringPointInfo { /// If you need to add another option to support, ensure the following is done /// on the Dart side: /// -/// * In `../lib/src/capture_request_options.dart`, add new cases for this +/// * In `camera_android_camerax/lib/src/capture_request_options.dart`, add new cases for this /// option in `_CaptureRequestOptionsHostApiImpl#createFromInstances` /// to create the expected Map entry of option key index and value to send to /// the native side. @@ -485,13 +485,13 @@ abstract class CameraControlHostApi { void setZoomRatio(int identifier, double ratio); @async - int startFocusAndMetering(int identifier, int focusMeteringActionId); + int? startFocusAndMetering(int identifier, int focusMeteringActionId); @async void cancelFocusAndMetering(int identifier); @async - int setExposureCompensationIndex(int identifier, int index); + int? setExposureCompensationIndex(int identifier, int index); } @FlutterApi() @@ -516,7 +516,8 @@ abstract class FocusMeteringResultFlutterApi { @HostApi(dartHostTestHandler: 'TestMeteringPointHostApi') abstract class MeteringPointHostApi { - void create(int identifier, double x, double y, double? size); + void create( + int identifier, double x, double y, double? size, int cameraInfoId); double getDefaultPointSize(); } diff --git a/packages/camera/camera_android_camerax/pubspec.yaml b/packages/camera/camera_android_camerax/pubspec.yaml index fee04bae156..3df56f5e1db 100644 --- a/packages/camera/camera_android_camerax/pubspec.yaml +++ b/packages/camera/camera_android_camerax/pubspec.yaml @@ -2,7 +2,7 @@ name: camera_android_camerax description: Android implementation of the camera plugin using the CameraX library. repository: https://github.com/flutter/packages/tree/main/packages/camera/camera_android_camerax issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+camera%22 -version: 0.5.0+33 +version: 0.5.0+34 environment: sdk: ^3.1.0 diff --git a/packages/camera/camera_android_camerax/test/android_camera_camerax_test.dart b/packages/camera/camera_android_camerax/test/android_camera_camerax_test.dart index bc6a0d1c6a3..0868916f3bf 100644 --- a/packages/camera/camera_android_camerax/test/android_camera_camerax_test.dart +++ b/packages/camera/camera_android_camerax/test/android_camera_camerax_test.dart @@ -3,6 +3,7 @@ // found in the LICENSE file. import 'dart:async'; +import 'dart:math' show Point; import 'package:async/async.dart'; import 'package:camera_android_camerax/camera_android_camerax.dart'; @@ -18,10 +19,12 @@ import 'package:camera_android_camerax/src/camerax_proxy.dart'; import 'package:camera_android_camerax/src/device_orientation_manager.dart'; import 'package:camera_android_camerax/src/exposure_state.dart'; import 'package:camera_android_camerax/src/fallback_strategy.dart'; +import 'package:camera_android_camerax/src/focus_metering_action.dart'; import 'package:camera_android_camerax/src/image_analysis.dart'; import 'package:camera_android_camerax/src/image_capture.dart'; import 'package:camera_android_camerax/src/image_proxy.dart'; import 'package:camera_android_camerax/src/live_data.dart'; +import 'package:camera_android_camerax/src/metering_point.dart'; import 'package:camera_android_camerax/src/observer.dart'; import 'package:camera_android_camerax/src/pending_recording.dart'; import 'package:camera_android_camerax/src/plane_proxy.dart'; @@ -38,7 +41,8 @@ import 'package:camera_android_camerax/src/use_case.dart'; import 'package:camera_android_camerax/src/video_capture.dart'; import 'package:camera_android_camerax/src/zoom_state.dart'; import 'package:camera_platform_interface/camera_platform_interface.dart'; -import 'package:flutter/services.dart' show DeviceOrientation, Uint8List; +import 'package:flutter/services.dart' + show DeviceOrientation, PlatformException, Uint8List; import 'package:flutter/widgets.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:mockito/annotations.dart'; @@ -113,6 +117,17 @@ void main() { return cameraClosingEventSent && cameraErrorSent; } + /// CameraXProxy for testing exposure and focus related controls. + /// + /// Modifies the creation of MeteringPoints and FocusMeteringActions to return + /// objects detached from a native object. + CameraXProxy getProxyForExposureAndFocus() => CameraXProxy( + createMeteringPoint: (double x, double y, CameraInfo cameraInfo) => + MeteringPoint.detached(x: x, y: y, cameraInfo: cameraInfo), + createFocusMeteringAction: (List<(MeteringPoint, int?)> + meteringPointInfos) => + FocusMeteringAction.detached(meteringPointInfos: meteringPointInfos)); + test('Should fetch CameraDescription instances for available cameras', () async { // Arrange @@ -331,6 +346,7 @@ void main() { final MockVideoCapture mockVideoCapture = MockVideoCapture(); final MockCamera mockCamera = MockCamera(); final MockCameraInfo mockCameraInfo = MockCameraInfo(); + final MockCameraControl mockCameraControl = MockCameraControl(); // Tell plugin to create mock/detached objects and stub method calls for the // testing of createCamera. @@ -377,6 +393,8 @@ void main() { when(mockCamera.getCameraInfo()).thenAnswer((_) async => mockCameraInfo); when(mockCameraInfo.getCameraState()) .thenAnswer((_) async => MockLiveCameraState()); + when(mockCamera.getCameraControl()) + .thenAnswer((_) async => mockCameraControl); camera.processCameraProvider = mockProcessCameraProvider; await camera.createCamera(testCameraDescription, testResolutionPreset, @@ -389,6 +407,9 @@ void main() { // Verify the camera's CameraInfo instance got updated. expect(camera.cameraInfo, equals(mockCameraInfo)); + // Verify camera's CameraControl instance got updated. + expect(camera.cameraControl, equals(mockCameraControl)); + // Verify preview has been marked as bound to the camera lifecycle by // createCamera. expect(camera.previewInitiallyBound, isTrue); @@ -940,6 +961,7 @@ void main() { MockProcessCameraProvider(); final MockCamera mockCamera = MockCamera(); final MockCameraInfo mockCameraInfo = MockCameraInfo(); + final MockCameraControl mockCameraControl = MockCameraControl(); final MockLiveCameraState mockLiveCameraState = MockLiveCameraState(); // Set directly for test versus calling createCamera. @@ -961,6 +983,8 @@ void main() { when(mockCamera.getCameraInfo()).thenAnswer((_) async => mockCameraInfo); when(mockCameraInfo.getCameraState()) .thenAnswer((_) async => mockLiveCameraState); + when(mockCamera.getCameraControl()) + .thenAnswer((_) async => mockCameraControl); await camera.resumePreview(78); @@ -974,6 +998,7 @@ void main() { as Observer), isTrue); expect(camera.cameraInfo, equals(mockCameraInfo)); + expect(camera.cameraControl, equals(mockCameraControl)); }); test( @@ -1017,6 +1042,7 @@ void main() { final MockCamera mockCamera = MockCamera(); final MockCamera newMockCamera = MockCamera(); final MockCameraInfo mockCameraInfo = MockCameraInfo(); + final MockCameraControl mockCameraControl = MockCameraControl(); final MockLiveCameraState mockLiveCameraState = MockLiveCameraState(); final MockLiveCameraState newMockLiveCameraState = MockLiveCameraState(); final TestSystemServicesHostApi mockSystemServicesApi = @@ -1055,6 +1081,8 @@ void main() { .thenAnswer((_) async => newMockCamera); when(newMockCamera.getCameraInfo()) .thenAnswer((_) async => mockCameraInfo); + when(newMockCamera.getCameraControl()) + .thenAnswer((_) async => mockCameraControl); when(mockCameraInfo.getCameraState()) .thenAnswer((_) async => newMockLiveCameraState); @@ -1066,6 +1094,7 @@ void main() { camera.cameraSelector!, [camera.videoCapture!])); expect(camera.camera, equals(newMockCamera)); expect(camera.cameraInfo, equals(mockCameraInfo)); + expect(camera.cameraControl, equals(mockCameraControl)); verify(mockLiveCameraState.removeObservers()); expect( await testCameraClosingObserver( @@ -1453,18 +1482,14 @@ void main() { () async { final AndroidCameraCameraX camera = AndroidCameraCameraX(); const int cameraId = 77; - final MockCameraControl mockCameraControl = MockCameraControl(); // Set directly for test versus calling createCamera. camera.imageCapture = MockImageCapture(); - camera.camera = MockCamera(); + camera.cameraControl = MockCameraControl(); // Ignore setting target rotation for this test; tested seprately. camera.captureOrientationLocked = true; - when(camera.camera!.getCameraControl()) - .thenAnswer((_) async => mockCameraControl); - await camera.setFlashMode(cameraId, FlashMode.torch); await camera.takePicture(cameraId); verify(camera.imageCapture!.setFlashMode(ImageCapture.flashModeOff)); @@ -1479,14 +1504,11 @@ void main() { // Set directly for test versus calling createCamera. camera.imageCapture = MockImageCapture(); - camera.camera = MockCamera(); + camera.cameraControl = mockCameraControl; // Ignore setting target rotation for this test; tested seprately. camera.captureOrientationLocked = true; - when(camera.camera!.getCameraControl()) - .thenAnswer((_) async => mockCameraControl); - for (final FlashMode flashMode in FlashMode.values) { await camera.setFlashMode(cameraId, flashMode); @@ -1520,10 +1542,7 @@ void main() { final MockCameraControl mockCameraControl = MockCameraControl(); // Set directly for test versus calling createCamera. - camera.camera = MockCamera(); - - when(camera.camera!.getCameraControl()) - .thenAnswer((_) async => mockCameraControl); + camera.cameraControl = mockCameraControl; await camera.setFlashMode(cameraId, FlashMode.torch); @@ -1538,10 +1557,7 @@ void main() { final MockCameraControl mockCameraControl = MockCameraControl(); // Set directly for test versus calling createCamera. - camera.camera = MockCamera(); - - when(camera.camera!.getCameraControl()) - .thenAnswer((_) async => mockCameraControl); + camera.cameraControl = mockCameraControl; for (final FlashMode flashMode in FlashMode.values) { camera.torchEnabled = true; @@ -1614,6 +1630,25 @@ void main() { expect(await camera.getExposureOffsetStepSize(55), 0.2); }); + test( + 'getExposureOffsetStepSize returns -1 when exposure compensation not supported on device', + () async { + final AndroidCameraCameraX camera = AndroidCameraCameraX(); + final MockCameraInfo mockCameraInfo = MockCameraInfo(); + final ExposureState exposureState = ExposureState.detached( + exposureCompensationRange: + ExposureCompensationRange(minCompensation: 0, maxCompensation: 0), + exposureCompensationStep: 0); + + // Set directly for test versus calling createCamera. + camera.cameraInfo = mockCameraInfo; + + when(mockCameraInfo.getExposureState()) + .thenAnswer((_) async => exposureState); + + expect(await camera.getExposureOffsetStepSize(55), -1); + }); + test('getMaxZoomLevel returns expected exposure offset', () async { final AndroidCameraCameraX camera = AndroidCameraCameraX(); final MockCameraInfo mockCameraInfo = MockCameraInfo(); @@ -1657,10 +1692,7 @@ void main() { final MockCameraControl mockCameraControl = MockCameraControl(); // Set directly for test versus calling createCamera. - camera.camera = MockCamera(); - - when(camera.camera!.getCameraControl()) - .thenAnswer((_) async => mockCameraControl); + camera.cameraControl = mockCameraControl; await camera.setZoomLevel(cameraId, zoomRatio); @@ -1981,4 +2013,452 @@ void main() { await camera.unlockCaptureOrientation(cameraId); expect(camera.captureOrientationLocked, isFalse); }); + + test( + 'setExposurePoint clears current auto-exposure metering point as expected', + () async { + final AndroidCameraCameraX camera = AndroidCameraCameraX(); + const int cameraId = 93; + final MockCameraControl mockCameraControl = MockCameraControl(); + final MockCameraInfo mockCameraInfo = MockCameraInfo(); + + // Set directly for test versus calling createCamera. + camera.cameraControl = mockCameraControl; + camera.cameraInfo = mockCameraInfo; + + camera.proxy = getProxyForExposureAndFocus(); + + // Verify nothing happens if no current focus and metering action has been + // enabled. + await camera.setExposurePoint(cameraId, null); + verifyNever(mockCameraControl.startFocusAndMetering(any)); + verifyNever(mockCameraControl.cancelFocusAndMetering()); + + // Verify current auto-exposure metering point is removed if previously set. + final (MeteringPoint, int?) autofocusMeteringPointInfo = ( + MeteringPoint.detached(x: 0.3, y: 0.7, cameraInfo: mockCameraInfo), + FocusMeteringAction.flagAf + ); + List<(MeteringPoint, int?)> meteringPointInfos = <(MeteringPoint, int?)>[ + ( + MeteringPoint.detached(x: 0.2, y: 0.5, cameraInfo: mockCameraInfo), + FocusMeteringAction.flagAe + ), + autofocusMeteringPointInfo + ]; + + camera.currentFocusMeteringAction = + FocusMeteringAction.detached(meteringPointInfos: meteringPointInfos); + + await camera.setExposurePoint(cameraId, null); + + final VerificationResult verificationResult = + verify(mockCameraControl.startFocusAndMetering(captureAny)); + final FocusMeteringAction capturedAction = + verificationResult.captured.single as FocusMeteringAction; + final List<(MeteringPoint, int?)> capturedMeteringPointInfos = + capturedAction.meteringPointInfos; + expect(capturedMeteringPointInfos.length, equals(1)); + expect( + capturedMeteringPointInfos.first, equals(autofocusMeteringPointInfo)); + + // Verify current focus and metering action is cleared if only previously + // set metering point was for auto-exposure. + meteringPointInfos = <(MeteringPoint, int?)>[ + ( + MeteringPoint.detached(x: 0.2, y: 0.5, cameraInfo: mockCameraInfo), + FocusMeteringAction.flagAe + ) + ]; + camera.currentFocusMeteringAction = + FocusMeteringAction.detached(meteringPointInfos: meteringPointInfos); + + await camera.setExposurePoint(cameraId, null); + + verify(mockCameraControl.cancelFocusAndMetering()); + }); + + test('setExposurePoint throws CameraException if invalid point specified', + () async { + final AndroidCameraCameraX camera = AndroidCameraCameraX(); + const int cameraId = 23; + final MockCameraControl mockCameraControl = MockCameraControl(); + const Point invalidExposurePoint = Point(3, -1); + + // Set directly for test versus calling createCamera. + camera.cameraControl = mockCameraControl; + + camera.proxy = getProxyForExposureAndFocus(); + + expect(() => camera.setExposurePoint(cameraId, invalidExposurePoint), + throwsA(isA())); + }); + + test( + 'setExposurePoint adds new exposure point to focus metering action to start as expected when previous metering points have been set', + () async { + final AndroidCameraCameraX camera = AndroidCameraCameraX(); + const int cameraId = 9; + final MockCameraControl mockCameraControl = MockCameraControl(); + final MockCameraInfo mockCameraInfo = MockCameraInfo(); + + // Set directly for test versus calling createCamera. + camera.cameraControl = mockCameraControl; + camera.cameraInfo = mockCameraInfo; + + camera.proxy = getProxyForExposureAndFocus(); + + // Verify current auto-exposure metering point is removed if previously set. + double exposurePointX = 0.8; + double exposurePointY = 0.1; + Point exposurePoint = Point(exposurePointX, exposurePointY); + final (MeteringPoint, int?) autofocusMeteringPointInfo = ( + MeteringPoint.detached(x: 0.3, y: 0.7, cameraInfo: mockCameraInfo), + FocusMeteringAction.flagAf + ); + List<(MeteringPoint, int?)> meteringPointInfos = <(MeteringPoint, int?)>[ + ( + MeteringPoint.detached(x: 0.2, y: 0.5, cameraInfo: mockCameraInfo), + FocusMeteringAction.flagAe + ), + autofocusMeteringPointInfo + ]; + + camera.currentFocusMeteringAction = + FocusMeteringAction.detached(meteringPointInfos: meteringPointInfos); + + await camera.setExposurePoint(cameraId, exposurePoint); + + VerificationResult verificationResult = + verify(mockCameraControl.startFocusAndMetering(captureAny)); + FocusMeteringAction capturedAction = + verificationResult.captured.single as FocusMeteringAction; + List<(MeteringPoint, int?)> capturedMeteringPointInfos = + capturedAction.meteringPointInfos; + expect(capturedMeteringPointInfos.length, equals(2)); + expect( + capturedMeteringPointInfos.first, equals(autofocusMeteringPointInfo)); + expect(capturedMeteringPointInfos[1].$1.x, equals(exposurePointX)); + expect(capturedMeteringPointInfos[1].$1.y, equals(exposurePointY)); + expect( + capturedMeteringPointInfos[1].$2, equals(FocusMeteringAction.flagAe)); + + // Verify exposure point is set when no auto-exposure metering point + // previously set, but an auto-focus point metering point has been. + exposurePointX = 0.2; + exposurePointY = 0.9; + exposurePoint = Point(exposurePointX, exposurePointY); + meteringPointInfos = <(MeteringPoint, int?)>[autofocusMeteringPointInfo]; + + camera.currentFocusMeteringAction = + FocusMeteringAction.detached(meteringPointInfos: meteringPointInfos); + + await camera.setExposurePoint(cameraId, exposurePoint); + + verificationResult = + verify(mockCameraControl.startFocusAndMetering(captureAny)); + capturedAction = verificationResult.captured.single as FocusMeteringAction; + capturedMeteringPointInfos = capturedAction.meteringPointInfos; + expect(capturedMeteringPointInfos.length, equals(2)); + expect( + capturedMeteringPointInfos.first, equals(autofocusMeteringPointInfo)); + expect(capturedMeteringPointInfos[1].$1.x, equals(exposurePointX)); + expect(capturedMeteringPointInfos[1].$1.y, equals(exposurePointY)); + expect( + capturedMeteringPointInfos[1].$2, equals(FocusMeteringAction.flagAe)); + }); + + test( + 'setExposurePoint adds new exposure point to focus metering action to start as expected when no previous metering points have been set', + () async { + final AndroidCameraCameraX camera = AndroidCameraCameraX(); + const int cameraId = 19; + final MockCameraControl mockCameraControl = MockCameraControl(); + const double exposurePointX = 0.8; + const double exposurePointY = 0.1; + const Point exposurePoint = + Point(exposurePointX, exposurePointY); + + // Set directly for test versus calling createCamera. + camera.cameraControl = mockCameraControl; + camera.cameraInfo = MockCameraInfo(); + camera.currentFocusMeteringAction = null; + + camera.proxy = getProxyForExposureAndFocus(); + + await camera.setExposurePoint(cameraId, exposurePoint); + + final VerificationResult verificationResult = + verify(mockCameraControl.startFocusAndMetering(captureAny)); + final FocusMeteringAction capturedAction = + verificationResult.captured.single as FocusMeteringAction; + final List<(MeteringPoint, int?)> capturedMeteringPointInfos = + capturedAction.meteringPointInfos; + expect(capturedMeteringPointInfos.length, equals(1)); + expect(capturedMeteringPointInfos.first.$1.x, equals(exposurePointX)); + expect(capturedMeteringPointInfos.first.$1.y, equals(exposurePointY)); + expect(capturedMeteringPointInfos.first.$2, + equals(FocusMeteringAction.flagAe)); + }); + + test( + 'setExposureOffset throws exception if exposure compensation not supported', + () async { + final AndroidCameraCameraX camera = AndroidCameraCameraX(); + const int cameraId = 6; + const double offset = 2; + final MockCameraInfo mockCameraInfo = MockCameraInfo(); + final ExposureState exposureState = ExposureState.detached( + exposureCompensationRange: + ExposureCompensationRange(minCompensation: 3, maxCompensation: 4), + exposureCompensationStep: 0); + + // Set directly for test versus calling createCamera. + camera.cameraInfo = mockCameraInfo; + + when(mockCameraInfo.getExposureState()) + .thenAnswer((_) async => exposureState); + + expect(() => camera.setExposureOffset(cameraId, offset), + throwsA(isA())); + }); + + test( + 'setExposureOffset throws exception if exposure compensation could not be set', + () async { + final AndroidCameraCameraX camera = AndroidCameraCameraX(); + const int cameraId = 11; + const double offset = 3; + final MockCameraInfo mockCameraInfo = MockCameraInfo(); + final CameraControl mockCameraControl = MockCameraControl(); + final ExposureState exposureState = ExposureState.detached( + exposureCompensationRange: + ExposureCompensationRange(minCompensation: 3, maxCompensation: 4), + exposureCompensationStep: 0.2); + + // Set directly for test versus calling createCamera. + camera.cameraInfo = mockCameraInfo; + camera.cameraControl = mockCameraControl; + + when(mockCameraInfo.getExposureState()) + .thenAnswer((_) async => exposureState); + when(mockCameraControl.setExposureCompensationIndex(15)).thenThrow( + PlatformException( + code: 'TEST_ERROR', + message: + 'This is a test error message indicating exposure offset could not be set.')); + + expect(() => camera.setExposureOffset(cameraId, offset), + throwsA(isA())); + }); + + test( + 'setExposureOffset behaves as expected to successful attempt to set exposure compensation index', + () async { + final AndroidCameraCameraX camera = AndroidCameraCameraX(); + const int cameraId = 11; + const double offset = 3; + final MockCameraInfo mockCameraInfo = MockCameraInfo(); + final CameraControl mockCameraControl = MockCameraControl(); + final ExposureState exposureState = ExposureState.detached( + exposureCompensationRange: + ExposureCompensationRange(minCompensation: 3, maxCompensation: 4), + exposureCompensationStep: 0.2); + + // Set directly for test versus calling createCamera. + camera.cameraInfo = mockCameraInfo; + camera.cameraControl = mockCameraControl; + + when(mockCameraInfo.getExposureState()) + .thenAnswer((_) async => exposureState); + + // Exposure index * exposure offset step size = exposure offset, i.e. + // 15 * 0.2 = 3. + expect(await camera.setExposureOffset(cameraId, offset), equals(3)); + }); + + test('setFocusPoint clears current auto-exposure metering point as expected', + () async { + final AndroidCameraCameraX camera = AndroidCameraCameraX(); + const int cameraId = 93; + final MockCameraControl mockCameraControl = MockCameraControl(); + final MockCameraInfo mockCameraInfo = MockCameraInfo(); + + // Set directly for test versus calling createCamera. + camera.cameraControl = mockCameraControl; + camera.cameraInfo = mockCameraInfo; + + camera.proxy = getProxyForExposureAndFocus(); + + // Verify nothing happens if no current focus and metering action has been + // enabled. + await camera.setFocusPoint(cameraId, null); + verifyNever(mockCameraControl.startFocusAndMetering(any)); + verifyNever(mockCameraControl.cancelFocusAndMetering()); + + // Verify current auto-exposure metering point is removed if previously set. + final (MeteringPoint, int?) autoexposureMeteringPointInfo = ( + MeteringPoint.detached(x: 0.3, y: 0.7, cameraInfo: mockCameraInfo), + FocusMeteringAction.flagAe + ); + List<(MeteringPoint, int?)> meteringPointInfos = <(MeteringPoint, int?)>[ + ( + MeteringPoint.detached(x: 0.2, y: 0.5, cameraInfo: mockCameraInfo), + FocusMeteringAction.flagAf + ), + autoexposureMeteringPointInfo + ]; + + camera.currentFocusMeteringAction = + FocusMeteringAction.detached(meteringPointInfos: meteringPointInfos); + + await camera.setFocusPoint(cameraId, null); + + final VerificationResult verificationResult = + verify(mockCameraControl.startFocusAndMetering(captureAny)); + final FocusMeteringAction capturedAction = + verificationResult.captured.single as FocusMeteringAction; + final List<(MeteringPoint, int?)> capturedMeteringPointInfos = + capturedAction.meteringPointInfos; + expect(capturedMeteringPointInfos.length, equals(1)); + expect(capturedMeteringPointInfos.first, + equals(autoexposureMeteringPointInfo)); + + // Verify current focus and metering action is cleared if only previously + // set metering point was for auto-exposure. + meteringPointInfos = <(MeteringPoint, int?)>[ + ( + MeteringPoint.detached(x: 0.2, y: 0.5, cameraInfo: mockCameraInfo), + FocusMeteringAction.flagAf + ) + ]; + camera.currentFocusMeteringAction = + FocusMeteringAction.detached(meteringPointInfos: meteringPointInfos); + + await camera.setFocusPoint(cameraId, null); + + verify(mockCameraControl.cancelFocusAndMetering()); + }); + + test('setFocusPoint throws CameraException if invalid point specified', + () async { + final AndroidCameraCameraX camera = AndroidCameraCameraX(); + const int cameraId = 23; + final MockCameraControl mockCameraControl = MockCameraControl(); + const Point invalidFocusPoint = Point(-3, 1); + + // Set directly for test versus calling createCamera. + camera.cameraControl = mockCameraControl; + + camera.proxy = getProxyForExposureAndFocus(); + + expect(() => camera.setFocusPoint(cameraId, invalidFocusPoint), + throwsA(isA())); + }); + + test( + 'setFocusPoint adds new exposure point to focus metering action to start as expected when previous metering points have been set', + () async { + final AndroidCameraCameraX camera = AndroidCameraCameraX(); + const int cameraId = 9; + final MockCameraControl mockCameraControl = MockCameraControl(); + final MockCameraInfo mockCameraInfo = MockCameraInfo(); + + // Set directly for test versus calling createCamera. + camera.cameraControl = mockCameraControl; + camera.cameraInfo = mockCameraInfo; + + camera.proxy = getProxyForExposureAndFocus(); + + // Verify current auto-exposure metering point is removed if previously set. + double focusPointX = 0.8; + double focusPointY = 0.1; + Point exposurePoint = Point(focusPointX, focusPointY); + final (MeteringPoint, int?) autoExposureMeteringPointInfo = ( + MeteringPoint.detached(x: 0.3, y: 0.7, cameraInfo: mockCameraInfo), + FocusMeteringAction.flagAe + ); + List<(MeteringPoint, int?)> meteringPointInfos = <(MeteringPoint, int?)>[ + ( + MeteringPoint.detached(x: 0.2, y: 0.5, cameraInfo: mockCameraInfo), + FocusMeteringAction.flagAf + ), + autoExposureMeteringPointInfo + ]; + + camera.currentFocusMeteringAction = + FocusMeteringAction.detached(meteringPointInfos: meteringPointInfos); + + await camera.setFocusPoint(cameraId, exposurePoint); + + VerificationResult verificationResult = + verify(mockCameraControl.startFocusAndMetering(captureAny)); + FocusMeteringAction capturedAction = + verificationResult.captured.single as FocusMeteringAction; + List<(MeteringPoint, int?)> capturedMeteringPointInfos = + capturedAction.meteringPointInfos; + expect(capturedMeteringPointInfos.length, equals(2)); + expect(capturedMeteringPointInfos.first, + equals(autoExposureMeteringPointInfo)); + expect(capturedMeteringPointInfos[1].$1.x, equals(focusPointX)); + expect(capturedMeteringPointInfos[1].$1.y, equals(focusPointY)); + expect( + capturedMeteringPointInfos[1].$2, equals(FocusMeteringAction.flagAf)); + + // Verify exposure point is set when no auto-exposure metering point + // previously set, but an auto-focus point metering point has been. + focusPointX = 0.2; + focusPointY = 0.9; + exposurePoint = Point(focusPointX, focusPointY); + meteringPointInfos = <(MeteringPoint, int?)>[autoExposureMeteringPointInfo]; + + camera.currentFocusMeteringAction = + FocusMeteringAction.detached(meteringPointInfos: meteringPointInfos); + + await camera.setFocusPoint(cameraId, exposurePoint); + + verificationResult = + verify(mockCameraControl.startFocusAndMetering(captureAny)); + capturedAction = verificationResult.captured.single as FocusMeteringAction; + capturedMeteringPointInfos = capturedAction.meteringPointInfos; + expect(capturedMeteringPointInfos.length, equals(2)); + expect(capturedMeteringPointInfos.first, + equals(autoExposureMeteringPointInfo)); + expect(capturedMeteringPointInfos[1].$1.x, equals(focusPointX)); + expect(capturedMeteringPointInfos[1].$1.y, equals(focusPointY)); + expect( + capturedMeteringPointInfos[1].$2, equals(FocusMeteringAction.flagAf)); + }); + + test( + 'setFocusPoint adds new exposure point to focus metering action to start as expected when no previous metering points have been set', + () async { + final AndroidCameraCameraX camera = AndroidCameraCameraX(); + const int cameraId = 19; + final MockCameraControl mockCameraControl = MockCameraControl(); + const double focusPointX = 0.8; + const double focusPointY = 0.1; + const Point exposurePoint = Point(focusPointX, focusPointY); + + // Set directly for test versus calling createCamera. + camera.cameraControl = mockCameraControl; + camera.cameraInfo = MockCameraInfo(); + camera.currentFocusMeteringAction = null; + + camera.proxy = getProxyForExposureAndFocus(); + + await camera.setFocusPoint(cameraId, exposurePoint); + + final VerificationResult verificationResult = + verify(mockCameraControl.startFocusAndMetering(captureAny)); + final FocusMeteringAction capturedAction = + verificationResult.captured.single as FocusMeteringAction; + final List<(MeteringPoint, int?)> capturedMeteringPointInfos = + capturedAction.meteringPointInfos; + expect(capturedMeteringPointInfos.length, equals(1)); + expect(capturedMeteringPointInfos.first.$1.x, equals(focusPointX)); + expect(capturedMeteringPointInfos.first.$1.y, equals(focusPointY)); + expect(capturedMeteringPointInfos.first.$2, + equals(FocusMeteringAction.flagAf)); + }); } diff --git a/packages/camera/camera_android_camerax/test/camera2_camera_control_test.mocks.dart b/packages/camera/camera_android_camerax/test/camera2_camera_control_test.mocks.dart index 09eb9436c7a..58d89c49b20 100644 --- a/packages/camera/camera_android_camerax/test/camera2_camera_control_test.mocks.dart +++ b/packages/camera/camera_android_camerax/test/camera2_camera_control_test.mocks.dart @@ -6,11 +6,13 @@ import 'dart:async' as _i3; import 'package:camera_android_camerax/src/camera_control.dart' as _i2; -import 'package:camera_android_camerax/src/camerax_library.g.dart' as _i5; -import 'package:camera_android_camerax/src/capture_request_options.dart' as _i4; +import 'package:camera_android_camerax/src/camerax_library.g.dart' as _i7; +import 'package:camera_android_camerax/src/capture_request_options.dart' as _i6; +import 'package:camera_android_camerax/src/focus_metering_action.dart' as _i5; +import 'package:camera_android_camerax/src/focus_metering_result.dart' as _i4; import 'package:mockito/mockito.dart' as _i1; -import 'test_camerax_library.g.dart' as _i6; +import 'test_camerax_library.g.dart' as _i8; // ignore_for_file: type=lint // ignore_for_file: avoid_redundant_argument_values @@ -53,6 +55,37 @@ class MockCameraControl extends _i1.Mock implements _i2.CameraControl { returnValue: _i3.Future.value(), returnValueForMissingStub: _i3.Future.value(), ) as _i3.Future); + + @override + _i3.Future<_i4.FocusMeteringResult?> startFocusAndMetering( + _i5.FocusMeteringAction? action) => + (super.noSuchMethod( + Invocation.method( + #startFocusAndMetering, + [action], + ), + returnValue: _i3.Future<_i4.FocusMeteringResult?>.value(), + ) as _i3.Future<_i4.FocusMeteringResult?>); + + @override + _i3.Future cancelFocusAndMetering() => (super.noSuchMethod( + Invocation.method( + #cancelFocusAndMetering, + [], + ), + returnValue: _i3.Future.value(), + returnValueForMissingStub: _i3.Future.value(), + ) as _i3.Future); + + @override + _i3.Future setExposureCompensationIndex(int? index) => + (super.noSuchMethod( + Invocation.method( + #setExposureCompensationIndex, + [index], + ), + returnValue: _i3.Future.value(), + ) as _i3.Future); } /// A class which mocks [CaptureRequestOptions]. @@ -60,36 +93,24 @@ class MockCameraControl extends _i1.Mock implements _i2.CameraControl { /// See the documentation for Mockito's code generation for more information. // ignore: must_be_immutable class MockCaptureRequestOptions extends _i1.Mock - implements _i4.CaptureRequestOptions { + implements _i6.CaptureRequestOptions { MockCaptureRequestOptions() { _i1.throwOnMissingStub(this); } @override - List<(_i5.CaptureRequestKeySupportedType, dynamic)> get requestedOptions => + List<(_i7.CaptureRequestKeySupportedType, Object?)> get requestedOptions => (super.noSuchMethod( Invocation.getter(#requestedOptions), - returnValue: <(_i5.CaptureRequestKeySupportedType, dynamic)>[], - ) as List<(_i5.CaptureRequestKeySupportedType, dynamic)>); - - @override - set requestedOptions( - List<(_i5.CaptureRequestKeySupportedType, dynamic)>? - _requestedOptions) => - super.noSuchMethod( - Invocation.setter( - #requestedOptions, - _requestedOptions, - ), - returnValueForMissingStub: null, - ); + returnValue: <(_i7.CaptureRequestKeySupportedType, Object?)>[], + ) as List<(_i7.CaptureRequestKeySupportedType, Object?)>); } /// A class which mocks [TestCamera2CameraControlHostApi]. /// /// See the documentation for Mockito's code generation for more information. class MockTestCamera2CameraControlHostApi extends _i1.Mock - implements _i6.TestCamera2CameraControlHostApi { + implements _i8.TestCamera2CameraControlHostApi { MockTestCamera2CameraControlHostApi() { _i1.throwOnMissingStub(this); } @@ -132,7 +153,7 @@ class MockTestCamera2CameraControlHostApi extends _i1.Mock /// /// See the documentation for Mockito's code generation for more information. class MockTestInstanceManagerHostApi extends _i1.Mock - implements _i6.TestInstanceManagerHostApi { + implements _i8.TestInstanceManagerHostApi { MockTestInstanceManagerHostApi() { _i1.throwOnMissingStub(this); } diff --git a/packages/camera/camera_android_camerax/test/camera_control_test.dart b/packages/camera/camera_android_camerax/test/camera_control_test.dart index 63c7a7d9aa9..22ba8aaf247 100644 --- a/packages/camera/camera_android_camerax/test/camera_control_test.dart +++ b/packages/camera/camera_android_camerax/test/camera_control_test.dart @@ -6,6 +6,7 @@ import 'package:camera_android_camerax/src/camera_control.dart'; import 'package:camera_android_camerax/src/focus_metering_action.dart'; import 'package:camera_android_camerax/src/focus_metering_result.dart'; import 'package:camera_android_camerax/src/instance_manager.dart'; +import 'package:flutter/services.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:mockito/annotations.dart'; import 'package:mockito/mockito.dart'; @@ -13,7 +14,11 @@ import 'package:mockito/mockito.dart'; import 'camera_control_test.mocks.dart'; import 'test_camerax_library.g.dart'; -@GenerateMocks([TestCameraControlHostApi, TestInstanceManagerHostApi]) +@GenerateMocks([ + TestCameraControlHostApi, + TestInstanceManagerHostApi, + FocusMeteringAction +]) void main() { TestWidgetsFlutterBinding.ensureInitialized(); @@ -90,8 +95,7 @@ void main() { instanceManager: instanceManager, ); const int cameraControlIdentifier = 75; - final FocusMeteringAction action = - FocusMeteringAction.detached(instanceManager: instanceManager); + final FocusMeteringAction action = MockFocusMeteringAction(); const int actionId = 5; final FocusMeteringResult result = FocusMeteringResult.detached(instanceManager: instanceManager); @@ -105,8 +109,7 @@ void main() { instanceManager.addHostCreatedInstance( action, actionId, - onCopy: (_) => - FocusMeteringAction.detached(instanceManager: instanceManager), + onCopy: (_) => MockFocusMeteringAction(), ); instanceManager.addHostCreatedInstance( result, @@ -122,6 +125,41 @@ void main() { verify(mockApi.startFocusAndMetering(cameraControlIdentifier, actionId)); }); + test('startFocusAndMetering returns null result if operation was canceled', + () async { + final MockTestCameraControlHostApi mockApi = + MockTestCameraControlHostApi(); + TestCameraControlHostApi.setup(mockApi); + + final InstanceManager instanceManager = InstanceManager( + onWeakReferenceRemoved: (_) {}, + ); + + final CameraControl cameraControl = CameraControl.detached( + instanceManager: instanceManager, + ); + const int cameraControlIdentifier = 75; + final FocusMeteringAction action = MockFocusMeteringAction(); + const int actionId = 5; + + instanceManager.addHostCreatedInstance( + cameraControl, + cameraControlIdentifier, + onCopy: (_) => CameraControl.detached(instanceManager: instanceManager), + ); + instanceManager.addHostCreatedInstance( + action, + actionId, + onCopy: (_) => MockFocusMeteringAction(), + ); + + when(mockApi.startFocusAndMetering(cameraControlIdentifier, actionId)) + .thenAnswer((_) => Future.value()); + + expect(await cameraControl.startFocusAndMetering(action), isNull); + verify(mockApi.startFocusAndMetering(cameraControlIdentifier, actionId)); + }); + test( 'cancelFocusAndMetering makes call on Java side to cancel focus and metering', () async { @@ -182,6 +220,72 @@ void main() { mockApi.setExposureCompensationIndex(cameraControlIdentifier, index)); }); + test( + 'setExposureCompensationIndex returns null when operation was canceled', + () async { + final MockTestCameraControlHostApi mockApi = + MockTestCameraControlHostApi(); + TestCameraControlHostApi.setup(mockApi); + + final InstanceManager instanceManager = InstanceManager( + onWeakReferenceRemoved: (_) {}, + ); + + final CameraControl cameraControl = CameraControl.detached( + instanceManager: instanceManager, + ); + const int cameraControlIdentifier = 40; + + instanceManager.addHostCreatedInstance( + cameraControl, + cameraControlIdentifier, + onCopy: (_) => CameraControl.detached(instanceManager: instanceManager), + ); + + const int index = 2; + when(mockApi.setExposureCompensationIndex(cameraControlIdentifier, index)) + .thenAnswer((_) => Future.value()); + + expect(await cameraControl.setExposureCompensationIndex(index), isNull); + verify( + mockApi.setExposureCompensationIndex(cameraControlIdentifier, index)); + }); + + test( + 'setExposureCompensationIndex throws PlatformException when one is thrown from native side', + () async { + final MockTestCameraControlHostApi mockApi = + MockTestCameraControlHostApi(); + TestCameraControlHostApi.setup(mockApi); + + final InstanceManager instanceManager = InstanceManager( + onWeakReferenceRemoved: (_) {}, + ); + + final CameraControl cameraControl = CameraControl.detached( + instanceManager: instanceManager, + ); + const int cameraControlIdentifier = 40; + + instanceManager.addHostCreatedInstance( + cameraControl, + cameraControlIdentifier, + onCopy: (_) => CameraControl.detached(instanceManager: instanceManager), + ); + + const int index = 1; + when(mockApi.setExposureCompensationIndex(cameraControlIdentifier, index)) + .thenThrow(PlatformException( + code: 'TEST_ERROR', + details: 'Platform exception thrown from Java side.')); + + expect(() => cameraControl.setExposureCompensationIndex(index), + throwsA(isA())); + + verify( + mockApi.setExposureCompensationIndex(cameraControlIdentifier, index)); + }); + test('flutterApiCreate makes call to add instance to instance manager', () { final InstanceManager instanceManager = InstanceManager( onWeakReferenceRemoved: (_) {}, diff --git a/packages/camera/camera_android_camerax/test/camera_control_test.mocks.dart b/packages/camera/camera_android_camerax/test/camera_control_test.mocks.dart index a11525af986..57040bcac62 100644 --- a/packages/camera/camera_android_camerax/test/camera_control_test.mocks.dart +++ b/packages/camera/camera_android_camerax/test/camera_control_test.mocks.dart @@ -5,6 +5,8 @@ // ignore_for_file: no_leading_underscores_for_library_prefixes import 'dart:async' as _i3; +import 'package:camera_android_camerax/src/focus_metering_action.dart' as _i4; +import 'package:camera_android_camerax/src/metering_point.dart' as _i5; import 'package:mockito/mockito.dart' as _i1; import 'test_camerax_library.g.dart' as _i2; @@ -66,7 +68,7 @@ class MockTestCameraControlHostApi extends _i1.Mock ) as _i3.Future); @override - _i3.Future startFocusAndMetering( + _i3.Future startFocusAndMetering( int? identifier, int? focusMeteringActionId, ) => @@ -78,8 +80,8 @@ class MockTestCameraControlHostApi extends _i1.Mock focusMeteringActionId, ], ), - returnValue: _i3.Future.value(0), - ) as _i3.Future); + returnValue: _i3.Future.value(), + ) as _i3.Future); @override _i3.Future cancelFocusAndMetering(int? identifier) => @@ -93,7 +95,7 @@ class MockTestCameraControlHostApi extends _i1.Mock ) as _i3.Future); @override - _i3.Future setExposureCompensationIndex( + _i3.Future setExposureCompensationIndex( int? identifier, int? index, ) => @@ -105,8 +107,8 @@ class MockTestCameraControlHostApi extends _i1.Mock index, ], ), - returnValue: _i3.Future.value(0), - ) as _i3.Future); + returnValue: _i3.Future.value(), + ) as _i3.Future); } /// A class which mocks [TestInstanceManagerHostApi]. @@ -127,3 +129,20 @@ class MockTestInstanceManagerHostApi extends _i1.Mock returnValueForMissingStub: null, ); } + +/// A class which mocks [FocusMeteringAction]. +/// +/// See the documentation for Mockito's code generation for more information. +// ignore: must_be_immutable +class MockFocusMeteringAction extends _i1.Mock + implements _i4.FocusMeteringAction { + MockFocusMeteringAction() { + _i1.throwOnMissingStub(this); + } + + @override + List<(_i5.MeteringPoint, int?)> get meteringPointInfos => (super.noSuchMethod( + Invocation.getter(#meteringPointInfos), + returnValue: <(_i5.MeteringPoint, int?)>[], + ) as List<(_i5.MeteringPoint, int?)>); +} diff --git a/packages/camera/camera_android_camerax/test/focus_metering_action_test.dart b/packages/camera/camera_android_camerax/test/focus_metering_action_test.dart index 657fb1aa3da..a74338e2735 100644 --- a/packages/camera/camera_android_camerax/test/focus_metering_action_test.dart +++ b/packages/camera/camera_android_camerax/test/focus_metering_action_test.dart @@ -37,6 +37,9 @@ void main() { ); FocusMeteringAction.detached( + meteringPointInfos: <(MeteringPoint, int?)>[ + (MockMeteringPoint(), FocusMeteringAction.flagAwb) + ], instanceManager: instanceManager, ); diff --git a/packages/camera/camera_android_camerax/test/focus_metering_action_test.mocks.dart b/packages/camera/camera_android_camerax/test/focus_metering_action_test.mocks.dart index 717215ca228..d1ceef9daad 100644 --- a/packages/camera/camera_android_camerax/test/focus_metering_action_test.mocks.dart +++ b/packages/camera/camera_android_camerax/test/focus_metering_action_test.mocks.dart @@ -1,13 +1,14 @@ -// Mocks generated by Mockito 5.4.3 from annotations +// Mocks generated by Mockito 5.4.4 from annotations // in camera_android_camerax/test/focus_metering_action_test.dart. // Do not manually edit this file. // ignore_for_file: no_leading_underscores_for_library_prefixes -import 'package:camera_android_camerax/src/camerax_library.g.dart' as _i4; -import 'package:camera_android_camerax/src/metering_point.dart' as _i2; +import 'package:camera_android_camerax/src/camera_info.dart' as _i2; +import 'package:camera_android_camerax/src/camerax_library.g.dart' as _i5; +import 'package:camera_android_camerax/src/metering_point.dart' as _i3; import 'package:mockito/mockito.dart' as _i1; -import 'test_camerax_library.g.dart' as _i3; +import 'test_camerax_library.g.dart' as _i4; // ignore_for_file: type=lint // ignore_for_file: avoid_redundant_argument_values @@ -22,11 +23,21 @@ import 'test_camerax_library.g.dart' as _i3; // ignore_for_file: camel_case_types // ignore_for_file: subtype_of_sealed_class +class _FakeCameraInfo_0 extends _i1.SmartFake implements _i2.CameraInfo { + _FakeCameraInfo_0( + Object parent, + Invocation parentInvocation, + ) : super( + parent, + parentInvocation, + ); +} + /// A class which mocks [MeteringPoint]. /// /// See the documentation for Mockito's code generation for more information. // ignore: must_be_immutable -class MockMeteringPoint extends _i1.Mock implements _i2.MeteringPoint { +class MockMeteringPoint extends _i1.Mock implements _i3.MeteringPoint { MockMeteringPoint() { _i1.throwOnMissingStub(this); } @@ -42,13 +53,22 @@ class MockMeteringPoint extends _i1.Mock implements _i2.MeteringPoint { Invocation.getter(#y), returnValue: 0.0, ) as double); + + @override + _i2.CameraInfo get cameraInfo => (super.noSuchMethod( + Invocation.getter(#cameraInfo), + returnValue: _FakeCameraInfo_0( + this, + Invocation.getter(#cameraInfo), + ), + ) as _i2.CameraInfo); } /// A class which mocks [TestFocusMeteringActionHostApi]. /// /// See the documentation for Mockito's code generation for more information. class MockTestFocusMeteringActionHostApi extends _i1.Mock - implements _i3.TestFocusMeteringActionHostApi { + implements _i4.TestFocusMeteringActionHostApi { MockTestFocusMeteringActionHostApi() { _i1.throwOnMissingStub(this); } @@ -56,7 +76,7 @@ class MockTestFocusMeteringActionHostApi extends _i1.Mock @override void create( int? identifier, - List<_i4.MeteringPointInfo?>? meteringPointInfos, + List<_i5.MeteringPointInfo?>? meteringPointInfos, ) => super.noSuchMethod( Invocation.method( @@ -74,7 +94,7 @@ class MockTestFocusMeteringActionHostApi extends _i1.Mock /// /// See the documentation for Mockito's code generation for more information. class MockTestInstanceManagerHostApi extends _i1.Mock - implements _i3.TestInstanceManagerHostApi { + implements _i4.TestInstanceManagerHostApi { MockTestInstanceManagerHostApi() { _i1.throwOnMissingStub(this); } diff --git a/packages/camera/camera_android_camerax/test/focus_metering_result_test.mocks.dart b/packages/camera/camera_android_camerax/test/focus_metering_result_test.mocks.dart index d35cdc15efb..be52b17bda4 100644 --- a/packages/camera/camera_android_camerax/test/focus_metering_result_test.mocks.dart +++ b/packages/camera/camera_android_camerax/test/focus_metering_result_test.mocks.dart @@ -1,12 +1,13 @@ -// Mocks generated by Mockito 5.4.3 from annotations +// Mocks generated by Mockito 5.4.4 from annotations // in camera_android_camerax/test/focus_metering_result_test.dart. // Do not manually edit this file. // ignore_for_file: no_leading_underscores_for_library_prefixes -import 'package:camera_android_camerax/src/metering_point.dart' as _i2; +import 'package:camera_android_camerax/src/camera_info.dart' as _i2; +import 'package:camera_android_camerax/src/metering_point.dart' as _i3; import 'package:mockito/mockito.dart' as _i1; -import 'test_camerax_library.g.dart' as _i3; +import 'test_camerax_library.g.dart' as _i4; // ignore_for_file: type=lint // ignore_for_file: avoid_redundant_argument_values @@ -21,11 +22,21 @@ import 'test_camerax_library.g.dart' as _i3; // ignore_for_file: camel_case_types // ignore_for_file: subtype_of_sealed_class +class _FakeCameraInfo_0 extends _i1.SmartFake implements _i2.CameraInfo { + _FakeCameraInfo_0( + Object parent, + Invocation parentInvocation, + ) : super( + parent, + parentInvocation, + ); +} + /// A class which mocks [MeteringPoint]. /// /// See the documentation for Mockito's code generation for more information. // ignore: must_be_immutable -class MockMeteringPoint extends _i1.Mock implements _i2.MeteringPoint { +class MockMeteringPoint extends _i1.Mock implements _i3.MeteringPoint { MockMeteringPoint() { _i1.throwOnMissingStub(this); } @@ -41,13 +52,22 @@ class MockMeteringPoint extends _i1.Mock implements _i2.MeteringPoint { Invocation.getter(#y), returnValue: 0.0, ) as double); + + @override + _i2.CameraInfo get cameraInfo => (super.noSuchMethod( + Invocation.getter(#cameraInfo), + returnValue: _FakeCameraInfo_0( + this, + Invocation.getter(#cameraInfo), + ), + ) as _i2.CameraInfo); } /// A class which mocks [TestFocusMeteringResultHostApi]. /// /// See the documentation for Mockito's code generation for more information. class MockTestFocusMeteringResultHostApi extends _i1.Mock - implements _i3.TestFocusMeteringResultHostApi { + implements _i4.TestFocusMeteringResultHostApi { MockTestFocusMeteringResultHostApi() { _i1.throwOnMissingStub(this); } @@ -66,7 +86,7 @@ class MockTestFocusMeteringResultHostApi extends _i1.Mock /// /// See the documentation for Mockito's code generation for more information. class MockTestInstanceManagerHostApi extends _i1.Mock - implements _i3.TestInstanceManagerHostApi { + implements _i4.TestInstanceManagerHostApi { MockTestInstanceManagerHostApi() { _i1.throwOnMissingStub(this); } diff --git a/packages/camera/camera_android_camerax/test/metering_point_test.dart b/packages/camera/camera_android_camerax/test/metering_point_test.dart index ba3daae96b1..5aa92164377 100644 --- a/packages/camera/camera_android_camerax/test/metering_point_test.dart +++ b/packages/camera/camera_android_camerax/test/metering_point_test.dart @@ -2,6 +2,7 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. +import 'package:camera_android_camerax/src/camera_info.dart'; import 'package:camera_android_camerax/src/instance_manager.dart'; import 'package:camera_android_camerax/src/metering_point.dart'; import 'package:flutter_test/flutter_test.dart'; @@ -11,7 +12,8 @@ import 'package:mockito/mockito.dart'; import 'metering_point_test.mocks.dart'; import 'test_camerax_library.g.dart'; -@GenerateMocks([TestInstanceManagerHostApi, TestMeteringPointHostApi]) +@GenerateMocks( + [TestInstanceManagerHostApi, TestMeteringPointHostApi, CameraInfo]) void main() { TestWidgetsFlutterBinding.ensureInitialized(); @@ -33,11 +35,12 @@ void main() { x: 0, y: 0.3, size: 4, + cameraInfo: MockCameraInfo(), instanceManager: instanceManager, ); verifyNever(mockApi.create(argThat(isA()), argThat(isA()), - argThat(isA()), argThat(isA()))); + argThat(isA()), argThat(isA()), argThat(isA()))); }); test('create calls create on the Java side', () async { @@ -52,14 +55,27 @@ void main() { const double x = 0.5; const double y = 0.6; const double size = 3; + final CameraInfo mockCameraInfo = MockCameraInfo(); + const int mockCameraInfoId = 4; + + instanceManager.addHostCreatedInstance(mockCameraInfo, mockCameraInfoId, + onCopy: (CameraInfo original) => MockCameraInfo()); + MeteringPoint( x: x, y: y, size: size, + cameraInfo: mockCameraInfo, instanceManager: instanceManager, ); - verify(mockApi.create(argThat(isA()), x, y, size)); + verify(mockApi.create( + argThat(isA()), + x, + y, + size, + mockCameraInfoId, + )); }); test('getDefaultPointSize returns expected size', () async { diff --git a/packages/camera/camera_android_camerax/test/metering_point_test.mocks.dart b/packages/camera/camera_android_camerax/test/metering_point_test.mocks.dart index ba199f66c63..e7a3d9fadb1 100644 --- a/packages/camera/camera_android_camerax/test/metering_point_test.mocks.dart +++ b/packages/camera/camera_android_camerax/test/metering_point_test.mocks.dart @@ -1,11 +1,18 @@ -// Mocks generated by Mockito 5.4.3 from annotations +// Mocks generated by Mockito 5.4.4 from annotations // in camera_android_camerax/test/metering_point_test.dart. // Do not manually edit this file. // ignore_for_file: no_leading_underscores_for_library_prefixes +import 'dart:async' as _i6; + +import 'package:camera_android_camerax/src/camera_info.dart' as _i5; +import 'package:camera_android_camerax/src/camera_state.dart' as _i7; +import 'package:camera_android_camerax/src/exposure_state.dart' as _i3; +import 'package:camera_android_camerax/src/live_data.dart' as _i2; +import 'package:camera_android_camerax/src/zoom_state.dart' as _i8; import 'package:mockito/mockito.dart' as _i1; -import 'test_camerax_library.g.dart' as _i2; +import 'test_camerax_library.g.dart' as _i4; // ignore_for_file: type=lint // ignore_for_file: avoid_redundant_argument_values @@ -20,11 +27,32 @@ import 'test_camerax_library.g.dart' as _i2; // ignore_for_file: camel_case_types // ignore_for_file: subtype_of_sealed_class +class _FakeLiveData_0 extends _i1.SmartFake + implements _i2.LiveData { + _FakeLiveData_0( + Object parent, + Invocation parentInvocation, + ) : super( + parent, + parentInvocation, + ); +} + +class _FakeExposureState_1 extends _i1.SmartFake implements _i3.ExposureState { + _FakeExposureState_1( + Object parent, + Invocation parentInvocation, + ) : super( + parent, + parentInvocation, + ); +} + /// A class which mocks [TestInstanceManagerHostApi]. /// /// See the documentation for Mockito's code generation for more information. class MockTestInstanceManagerHostApi extends _i1.Mock - implements _i2.TestInstanceManagerHostApi { + implements _i4.TestInstanceManagerHostApi { MockTestInstanceManagerHostApi() { _i1.throwOnMissingStub(this); } @@ -43,7 +71,7 @@ class MockTestInstanceManagerHostApi extends _i1.Mock /// /// See the documentation for Mockito's code generation for more information. class MockTestMeteringPointHostApi extends _i1.Mock - implements _i2.TestMeteringPointHostApi { + implements _i4.TestMeteringPointHostApi { MockTestMeteringPointHostApi() { _i1.throwOnMissingStub(this); } @@ -54,6 +82,7 @@ class MockTestMeteringPointHostApi extends _i1.Mock double? x, double? y, double? size, + int? cameraInfoId, ) => super.noSuchMethod( Invocation.method( @@ -63,6 +92,7 @@ class MockTestMeteringPointHostApi extends _i1.Mock x, y, size, + cameraInfoId, ], ), returnValueForMissingStub: null, @@ -77,3 +107,70 @@ class MockTestMeteringPointHostApi extends _i1.Mock returnValue: 0.0, ) as double); } + +/// A class which mocks [CameraInfo]. +/// +/// See the documentation for Mockito's code generation for more information. +// ignore: must_be_immutable +class MockCameraInfo extends _i1.Mock implements _i5.CameraInfo { + MockCameraInfo() { + _i1.throwOnMissingStub(this); + } + + @override + _i6.Future getSensorRotationDegrees() => (super.noSuchMethod( + Invocation.method( + #getSensorRotationDegrees, + [], + ), + returnValue: _i6.Future.value(0), + ) as _i6.Future); + + @override + _i6.Future<_i2.LiveData<_i7.CameraState>> getCameraState() => + (super.noSuchMethod( + Invocation.method( + #getCameraState, + [], + ), + returnValue: _i6.Future<_i2.LiveData<_i7.CameraState>>.value( + _FakeLiveData_0<_i7.CameraState>( + this, + Invocation.method( + #getCameraState, + [], + ), + )), + ) as _i6.Future<_i2.LiveData<_i7.CameraState>>); + + @override + _i6.Future<_i3.ExposureState> getExposureState() => (super.noSuchMethod( + Invocation.method( + #getExposureState, + [], + ), + returnValue: _i6.Future<_i3.ExposureState>.value(_FakeExposureState_1( + this, + Invocation.method( + #getExposureState, + [], + ), + )), + ) as _i6.Future<_i3.ExposureState>); + + @override + _i6.Future<_i2.LiveData<_i8.ZoomState>> getZoomState() => (super.noSuchMethod( + Invocation.method( + #getZoomState, + [], + ), + returnValue: _i6.Future<_i2.LiveData<_i8.ZoomState>>.value( + _FakeLiveData_0<_i8.ZoomState>( + this, + Invocation.method( + #getZoomState, + [], + ), + )), + ) as _i6.Future<_i2.LiveData<_i8.ZoomState>>); +} diff --git a/packages/camera/camera_android_camerax/test/test_camerax_library.g.dart b/packages/camera/camera_android_camerax/test/test_camerax_library.g.dart index 4450f97c82d..c847327cce5 100644 --- a/packages/camera/camera_android_camerax/test/test_camerax_library.g.dart +++ b/packages/camera/camera_android_camerax/test/test_camerax_library.g.dart @@ -1898,11 +1898,11 @@ abstract class TestCameraControlHostApi { Future setZoomRatio(int identifier, double ratio); - Future startFocusAndMetering(int identifier, int focusMeteringActionId); + Future startFocusAndMetering(int identifier, int focusMeteringActionId); Future cancelFocusAndMetering(int identifier); - Future setExposureCompensationIndex(int identifier, int index); + Future setExposureCompensationIndex(int identifier, int index); static void setup(TestCameraControlHostApi? api, {BinaryMessenger? binaryMessenger}) { @@ -1977,7 +1977,7 @@ abstract class TestCameraControlHostApi { final int? arg_focusMeteringActionId = (args[1] as int?); assert(arg_focusMeteringActionId != null, 'Argument for dev.flutter.pigeon.CameraControlHostApi.startFocusAndMetering was null, expected non-null int.'); - final int output = await api.startFocusAndMetering( + final int? output = await api.startFocusAndMetering( arg_identifier!, arg_focusMeteringActionId!); return [output]; }); @@ -2027,7 +2027,7 @@ abstract class TestCameraControlHostApi { final int? arg_index = (args[1] as int?); assert(arg_index != null, 'Argument for dev.flutter.pigeon.CameraControlHostApi.setExposureCompensationIndex was null, expected non-null int.'); - final int output = await api.setExposureCompensationIndex( + final int? output = await api.setExposureCompensationIndex( arg_identifier!, arg_index!); return [output]; }); @@ -2138,7 +2138,8 @@ abstract class TestMeteringPointHostApi { TestDefaultBinaryMessengerBinding.instance; static const MessageCodec codec = StandardMessageCodec(); - void create(int identifier, double x, double y, double? size); + void create( + int identifier, double x, double y, double? size, int cameraInfoId); double getDefaultPointSize(); @@ -2168,7 +2169,11 @@ abstract class TestMeteringPointHostApi { assert(arg_y != null, 'Argument for dev.flutter.pigeon.MeteringPointHostApi.create was null, expected non-null double.'); final double? arg_size = (args[3] as double?); - api.create(arg_identifier!, arg_x!, arg_y!, arg_size); + final int? arg_cameraInfoId = (args[4] as int?); + assert(arg_cameraInfoId != null, + 'Argument for dev.flutter.pigeon.MeteringPointHostApi.create was null, expected non-null int.'); + api.create( + arg_identifier!, arg_x!, arg_y!, arg_size, arg_cameraInfoId!); return []; }); }