From e07eb50a0fbce46f0de4bcd6f92f486154bed478 Mon Sep 17 00:00:00 2001 From: Camille Simon <43054281+camsim99@users.noreply.github.com> Date: Tue, 27 Feb 2024 16:45:09 -0500 Subject: [PATCH] [camerax] Implements `setExposureMode` (#6110) Implements `setExposureMode`. Fixes https://github.com/flutter/flutter/issues/120468. ~To be landed after (1) https://github.com/flutter/packages/pull/6059 then (2) https://github.com/flutter/packages/pull/6109.~ Done :) --- .../camera_android_camerax/CHANGELOG.md | 4 ++ .../camera/camera_android_camerax/README.md | 4 -- .../camerax/CameraAndroidCameraxPlugin.java | 11 ++++ .../CaptureRequestOptionsHostApiImpl.java | 4 +- .../CameraAndroidCameraxPluginTest.java | 12 ++++ .../example/lib/main.dart | 12 ++-- .../lib/src/android_camera_camerax.dart | 23 ++++++++ .../lib/src/camerax_proxy.dart | 24 ++++++++ .../camera_android_camerax/pubspec.yaml | 2 +- .../test/android_camera_camerax_test.dart | 56 +++++++++++++++++++ .../android_camera_camerax_test.mocks.dart | 35 ++++++++++++ 11 files changed, 176 insertions(+), 11 deletions(-) diff --git a/packages/camera/camera_android_camerax/CHANGELOG.md b/packages/camera/camera_android_camerax/CHANGELOG.md index 5c8002bae0..647a575599 100644 --- a/packages/camera/camera_android_camerax/CHANGELOG.md +++ b/packages/camera/camera_android_camerax/CHANGELOG.md @@ -1,3 +1,7 @@ +## 0.5.0+36 + +* Implements `setExposureMode`. + ## 0.5.0+35 * Modifies `CameraInitializedEvent` that is sent when the camera is initialized to indicate that the initial focus diff --git a/packages/camera/camera_android_camerax/README.md b/packages/camera/camera_android_camerax/README.md index 48674de44c..fdc50955de 100644 --- a/packages/camera/camera_android_camerax/README.md +++ b/packages/camera/camera_android_camerax/README.md @@ -30,10 +30,6 @@ dependencies: and thus, the plugin will fall back to 480p if configured with a `ResolutionPreset`. -### Exposure mode configuration \[[Issue #120468][120468]\] - -`setExposureMode`is unimplemented. - ### Focus mode configuration \[[Issue #120467][120467]\] `setFocusMode` is unimplemented. 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 ea9f3ed4d0..6a30074a2f 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 @@ -29,6 +29,9 @@ public final class CameraAndroidCameraxPlugin implements FlutterPlugin, Activity @VisibleForTesting @Nullable public SystemServicesHostApiImpl systemServicesHostApiImpl; @VisibleForTesting @Nullable public MeteringPointHostApiImpl meteringPointHostApiImpl; + @VisibleForTesting @Nullable + public Camera2CameraControlHostApiImpl camera2CameraControlHostApiImpl; + @VisibleForTesting public @Nullable DeviceOrientationManagerHostApiImpl deviceOrientationManagerHostApiImpl; @@ -120,6 +123,11 @@ public final class CameraAndroidCameraxPlugin implements FlutterPlugin, Activity cameraControlHostApiImpl = new CameraControlHostApiImpl(binaryMessenger, instanceManager, context); GeneratedCameraXLibrary.CameraControlHostApi.setup(binaryMessenger, cameraControlHostApiImpl); + camera2CameraControlHostApiImpl = new Camera2CameraControlHostApiImpl(instanceManager, context); + GeneratedCameraXLibrary.Camera2CameraControlHostApi.setup( + binaryMessenger, camera2CameraControlHostApiImpl); + GeneratedCameraXLibrary.CaptureRequestOptionsHostApi.setup( + binaryMessenger, new CaptureRequestOptionsHostApiImpl(instanceManager)); GeneratedCameraXLibrary.FocusMeteringActionHostApi.setup( binaryMessenger, new FocusMeteringActionHostApiImpl(instanceManager)); GeneratedCameraXLibrary.FocusMeteringResultHostApi.setup( @@ -217,6 +225,9 @@ public final class CameraAndroidCameraxPlugin implements FlutterPlugin, Activity if (cameraControlHostApiImpl != null) { cameraControlHostApiImpl.setContext(context); } + if (camera2CameraControlHostApiImpl != null) { + camera2CameraControlHostApiImpl.setContext(context); + } } /** Sets {@code LifecycleOwner} that is used to control the lifecycle of the camera by CameraX. */ diff --git a/packages/camera/camera_android_camerax/android/src/main/java/io/flutter/plugins/camerax/CaptureRequestOptionsHostApiImpl.java b/packages/camera/camera_android_camerax/android/src/main/java/io/flutter/plugins/camerax/CaptureRequestOptionsHostApiImpl.java index 6f8d8c91dd..f76dd5422e 100644 --- a/packages/camera/camera_android_camerax/android/src/main/java/io/flutter/plugins/camerax/CaptureRequestOptionsHostApiImpl.java +++ b/packages/camera/camera_android_camerax/android/src/main/java/io/flutter/plugins/camerax/CaptureRequestOptionsHostApiImpl.java @@ -110,8 +110,8 @@ public class CaptureRequestOptionsHostApiImpl implements CaptureRequestOptionsHo Map decodedOptions = new HashMap(); for (Map.Entry option : options.entrySet()) { - decodedOptions.put( - CaptureRequestKeySupportedType.values()[option.getKey().intValue()], option.getValue()); + Integer index = ((Number) option.getKey()).intValue(); + decodedOptions.put(CaptureRequestKeySupportedType.values()[index], option.getValue()); } instanceManager.addDartCreatedInstance(proxy.create(decodedOptions), 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 f9f4be6db6..fcca0f8eb0 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 @@ -169,6 +169,8 @@ public class CameraAndroidCameraxPluginTest { mock(ImageAnalysisHostApiImpl.class); final CameraControlHostApiImpl mockCameraControlHostApiImpl = mock(CameraControlHostApiImpl.class); + final Camera2CameraControlHostApiImpl mockCamera2CameraControlHostApiImpl = + mock(Camera2CameraControlHostApiImpl.class); when(flutterPluginBinding.getApplicationContext()).thenReturn(mockContext); @@ -180,6 +182,7 @@ public class CameraAndroidCameraxPluginTest { plugin.imageAnalysisHostApiImpl = mockImageAnalysisHostApiImpl; plugin.cameraControlHostApiImpl = mockCameraControlHostApiImpl; plugin.liveDataHostApiImpl = mock(LiveDataHostApiImpl.class); + plugin.camera2CameraControlHostApiImpl = mockCamera2CameraControlHostApiImpl; plugin.onAttachedToEngine(flutterPluginBinding); plugin.onDetachedFromActivityForConfigChanges(); @@ -191,6 +194,7 @@ public class CameraAndroidCameraxPluginTest { verify(mockImageCaptureHostApiImpl).setContext(mockContext); verify(mockImageAnalysisHostApiImpl).setContext(mockContext); verify(mockCameraControlHostApiImpl).setContext(mockContext); + verify(mockCamera2CameraControlHostApiImpl).setContext(mockContext); } @Test @@ -259,6 +263,8 @@ public class CameraAndroidCameraxPluginTest { mock(CameraControlHostApiImpl.class); final DeviceOrientationManagerHostApiImpl mockDeviceOrientationManagerHostApiImpl = mock(DeviceOrientationManagerHostApiImpl.class); + final Camera2CameraControlHostApiImpl mockCamera2CameraControlHostApiImpl = + mock(Camera2CameraControlHostApiImpl.class); final MeteringPointHostApiImpl mockMeteringPointHostApiImpl = mock(MeteringPointHostApiImpl.class); final ArgumentCaptor permissionsRegistryCaptor = @@ -277,6 +283,7 @@ public class CameraAndroidCameraxPluginTest { plugin.deviceOrientationManagerHostApiImpl = mockDeviceOrientationManagerHostApiImpl; plugin.meteringPointHostApiImpl = mockMeteringPointHostApiImpl; plugin.liveDataHostApiImpl = mock(LiveDataHostApiImpl.class); + plugin.camera2CameraControlHostApiImpl = mockCamera2CameraControlHostApiImpl; plugin.onAttachedToEngine(flutterPluginBinding); plugin.onReattachedToActivityForConfigChanges(activityPluginBinding); @@ -294,6 +301,7 @@ public class CameraAndroidCameraxPluginTest { verify(mockImageCaptureHostApiImpl).setContext(mockActivity); verify(mockImageAnalysisHostApiImpl).setContext(mockActivity); verify(mockCameraControlHostApiImpl).setContext(mockActivity); + verify(mockCamera2CameraControlHostApiImpl).setContext(mockActivity); // Check permissions registry reference is set. verify(mockSystemServicesHostApiImpl) @@ -347,6 +355,8 @@ public class CameraAndroidCameraxPluginTest { final ImageCaptureHostApiImpl mockImageCaptureHostApiImpl = mock(ImageCaptureHostApiImpl.class); final CameraControlHostApiImpl mockCameraControlHostApiImpl = mock(CameraControlHostApiImpl.class); + final Camera2CameraControlHostApiImpl mockCamera2CameraControlHostApiImpl = + mock(Camera2CameraControlHostApiImpl.class); final ArgumentCaptor permissionsRegistryCaptor = ArgumentCaptor.forClass(PermissionsRegistry.class); @@ -360,6 +370,7 @@ public class CameraAndroidCameraxPluginTest { plugin.imageAnalysisHostApiImpl = mockImageAnalysisHostApiImpl; plugin.cameraControlHostApiImpl = mockCameraControlHostApiImpl; plugin.liveDataHostApiImpl = mock(LiveDataHostApiImpl.class); + plugin.camera2CameraControlHostApiImpl = mockCamera2CameraControlHostApiImpl; plugin.onAttachedToEngine(flutterPluginBinding); plugin.onDetachedFromActivity(); @@ -371,5 +382,6 @@ public class CameraAndroidCameraxPluginTest { verify(mockImageCaptureHostApiImpl).setContext(mockContext); verify(mockImageAnalysisHostApiImpl).setContext(mockContext); verify(mockCameraControlHostApiImpl).setContext(mockContext); + verify(mockCamera2CameraControlHostApiImpl).setContext(mockContext); } } diff --git a/packages/camera/camera_android_camerax/example/lib/main.dart b/packages/camera/camera_android_camerax/example/lib/main.dart index adb8683737..ff1f620e03 100644 --- a/packages/camera/camera_android_camerax/example/lib/main.dart +++ b/packages/camera/camera_android_camerax/example/lib/main.dart @@ -393,8 +393,10 @@ class _CameraExampleHomeState extends State children: [ TextButton( style: styleAuto, - onPressed: - () {}, // TODO(camsim99): Add functionality back here. + onPressed: controller != null + ? () => + onSetExposureModeButtonPressed(ExposureMode.auto) + : null, onLongPress: () { if (controller != null) { CameraPlatform.instance @@ -406,8 +408,10 @@ class _CameraExampleHomeState extends State ), TextButton( style: styleLocked, - onPressed: - () {}, // TODO(camsim99): Add functionality back here. + onPressed: controller != null + ? () => + onSetExposureModeButtonPressed(ExposureMode.locked) + : null, child: const Text('LOCKED'), ), 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 ffff56b4d9..261525113d 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 @@ -14,12 +14,14 @@ import 'package:stream_transform/stream_transform.dart'; import 'analyzer.dart'; import 'camera.dart'; +import 'camera2_camera_control.dart'; import 'camera_control.dart'; import 'camera_info.dart'; import 'camera_selector.dart'; import 'camera_state.dart'; import 'camerax_library.g.dart'; import 'camerax_proxy.dart'; +import 'capture_request_options.dart'; import 'device_orientation_manager.dart'; import 'exposure_state.dart'; import 'fallback_strategy.dart'; @@ -545,6 +547,27 @@ class AndroidCameraCameraX extends CameraPlatform { point: point, meteringMode: FocusMeteringAction.flagAf); } + /// Sets the exposure mode for taking pictures. + /// + /// Setting [ExposureMode.locked] will lock current exposure point until it + /// is unset by setting [ExposureMode.auto]. + /// + /// [cameraId] is not used. + @override + Future setExposureMode(int cameraId, ExposureMode mode) async { + final Camera2CameraControl camera2Control = + proxy.getCamera2CameraControl(cameraControl); + final bool lockExposureMode = mode == ExposureMode.locked; + + final CaptureRequestOptions captureRequestOptions = proxy + .createCaptureRequestOptions(<( + CaptureRequestKeySupportedType, + Object? + )>[(CaptureRequestKeySupportedType.controlAeLock, lockExposureMode)]); + + await camera2Control.addCaptureRequestOptions(captureRequestOptions); + } + /// Gets the maximum supported zoom level for the selected camera. /// /// [cameraId] not used. 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 072a753d8b..2ccb353aa6 100644 --- a/packages/camera/camera_android_camerax/lib/src/camerax_proxy.dart +++ b/packages/camera/camera_android_camerax/lib/src/camerax_proxy.dart @@ -5,10 +5,13 @@ import 'dart:ui' show Size; import 'analyzer.dart'; +import 'camera2_camera_control.dart'; +import 'camera_control.dart'; import 'camera_info.dart'; import 'camera_selector.dart'; import 'camera_state.dart'; import 'camerax_library.g.dart'; +import 'capture_request_options.dart'; import 'device_orientation_manager.dart'; import 'fallback_strategy.dart'; import 'focus_metering_action.dart'; @@ -52,6 +55,8 @@ class CameraXProxy { _startListeningForDeviceOrientationChange, this.setPreviewSurfaceProvider = _setPreviewSurfaceProvider, this.getDefaultDisplayRotation = _getDefaultDisplayRotation, + this.getCamera2CameraControl = _getCamera2CameraControl, + this.createCaptureRequestOptions = _createCaptureRequestOptions, this.createMeteringPoint = _createMeteringPoint, this.createFocusMeteringAction = _createFocusMeteringAction, }); @@ -142,6 +147,15 @@ class CameraXProxy { /// rotation constants. Future Function() getDefaultDisplayRotation; + /// Get [Camera2CameraControl] instance from [cameraControl]. + Camera2CameraControl Function(CameraControl cameraControl) + getCamera2CameraControl; + + /// Create [CapureRequestOptions] with specified options. + CaptureRequestOptions Function( + List<(CaptureRequestKeySupportedType, Object?)> options) + createCaptureRequestOptions; + /// Returns a [MeteringPoint] with the specified coordinates based on /// [cameraInfo]. MeteringPoint Function(double x, double y, CameraInfo cameraInfo) @@ -255,6 +269,16 @@ class CameraXProxy { return DeviceOrientationManager.getDefaultDisplayRotation(); } + static Camera2CameraControl _getCamera2CameraControl( + CameraControl cameraControl) { + return Camera2CameraControl(cameraControl: cameraControl); + } + + static CaptureRequestOptions _createCaptureRequestOptions( + List<(CaptureRequestKeySupportedType, Object?)> options) { + return CaptureRequestOptions(requestedOptions: options); + } + static MeteringPoint _createMeteringPoint( double x, double y, CameraInfo cameraInfo) { return MeteringPoint(x: x, y: y, cameraInfo: cameraInfo); diff --git a/packages/camera/camera_android_camerax/pubspec.yaml b/packages/camera/camera_android_camerax/pubspec.yaml index 9e151c368e..660997d594 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+35 +version: 0.5.0+36 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 3998860bfb..05ee82abb4 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 @@ -9,6 +9,7 @@ import 'package:async/async.dart'; import 'package:camera_android_camerax/camera_android_camerax.dart'; import 'package:camera_android_camerax/src/analyzer.dart'; import 'package:camera_android_camerax/src/camera.dart'; +import 'package:camera_android_camerax/src/camera2_camera_control.dart'; import 'package:camera_android_camerax/src/camera_control.dart'; import 'package:camera_android_camerax/src/camera_info.dart'; import 'package:camera_android_camerax/src/camera_selector.dart'; @@ -16,6 +17,7 @@ import 'package:camera_android_camerax/src/camera_state.dart'; import 'package:camera_android_camerax/src/camera_state_error.dart'; import 'package:camera_android_camerax/src/camerax_library.g.dart'; import 'package:camera_android_camerax/src/camerax_proxy.dart'; +import 'package:camera_android_camerax/src/capture_request_options.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'; @@ -78,6 +80,7 @@ import 'test_camerax_library.g.dart'; MockSpec(), MockSpec(), MockSpec(), + MockSpec(), ]) @GenerateMocks([], customMocks: >[ MockSpec>(as: #MockLiveCameraState), @@ -2010,6 +2013,59 @@ void main() { expect(camera.captureOrientationLocked, isFalse); }); + test('setExposureMode sets expected controlAeLock value via Camera2 interop', + () async { + final AndroidCameraCameraX camera = AndroidCameraCameraX(); + const int cameraId = 78; + final MockCameraControl mockCameraControl = MockCameraControl(); + final MockCamera2CameraControl mockCamera2CameraControl = + MockCamera2CameraControl(); + + // Set directly for test versus calling createCamera. + camera.camera = MockCamera(); + camera.cameraControl = mockCameraControl; + + // Tell plugin to create detached Camera2CameraControl and + // CaptureRequestOptions instances for testing. + camera.proxy = CameraXProxy( + getCamera2CameraControl: (CameraControl cameraControl) => + cameraControl == mockCameraControl + ? mockCamera2CameraControl + : Camera2CameraControl.detached(cameraControl: cameraControl), + createCaptureRequestOptions: + (List<(CaptureRequestKeySupportedType, Object?)> options) => + CaptureRequestOptions.detached(requestedOptions: options), + ); + + // Test auto mode. + await camera.setExposureMode(cameraId, ExposureMode.auto); + + VerificationResult verificationResult = + verify(mockCamera2CameraControl.addCaptureRequestOptions(captureAny)); + CaptureRequestOptions capturedCaptureRequestOptions = + verificationResult.captured.single as CaptureRequestOptions; + List<(CaptureRequestKeySupportedType, Object?)> requestedOptions = + capturedCaptureRequestOptions.requestedOptions; + expect(requestedOptions.length, equals(1)); + expect(requestedOptions.first.$1, + equals(CaptureRequestKeySupportedType.controlAeLock)); + expect(requestedOptions.first.$2, equals(false)); + + // Test locked mode. + clearInteractions(mockCamera2CameraControl); + await camera.setExposureMode(cameraId, ExposureMode.locked); + + verificationResult = + verify(mockCamera2CameraControl.addCaptureRequestOptions(captureAny)); + capturedCaptureRequestOptions = + verificationResult.captured.single as CaptureRequestOptions; + requestedOptions = capturedCaptureRequestOptions.requestedOptions; + expect(requestedOptions.length, equals(1)); + expect(requestedOptions.first.$1, + equals(CaptureRequestKeySupportedType.controlAeLock)); + expect(requestedOptions.first.$2, equals(true)); + }); + test( 'setExposurePoint clears current auto-exposure metering point as expected', () async { diff --git a/packages/camera/camera_android_camerax/test/android_camera_camerax_test.mocks.dart b/packages/camera/camera_android_camerax/test/android_camera_camerax_test.mocks.dart index 4c90e940e7..be2541b8f9 100644 --- a/packages/camera/camera_android_camerax/test/android_camera_camerax_test.mocks.dart +++ b/packages/camera/camera_android_camerax/test/android_camera_camerax_test.mocks.dart @@ -8,11 +8,14 @@ import 'dart:typed_data' as _i29; import 'package:camera_android_camerax/src/analyzer.dart' as _i15; import 'package:camera_android_camerax/src/camera.dart' as _i9; +import 'package:camera_android_camerax/src/camera2_camera_control.dart' as _i38; import 'package:camera_android_camerax/src/camera_control.dart' as _i3; import 'package:camera_android_camerax/src/camera_info.dart' as _i2; import 'package:camera_android_camerax/src/camera_selector.dart' as _i22; import 'package:camera_android_camerax/src/camera_state.dart' as _i18; import 'package:camera_android_camerax/src/camerax_library.g.dart' as _i7; +import 'package:camera_android_camerax/src/capture_request_options.dart' + as _i39; import 'package:camera_android_camerax/src/exposure_state.dart' as _i5; import 'package:camera_android_camerax/src/fallback_strategy.dart' as _i23; import 'package:camera_android_camerax/src/focus_metering_action.dart' as _i21; @@ -1317,6 +1320,38 @@ class MockZoomState extends _i1.Mock implements _i19.ZoomState { ) as double); } +/// A class which mocks [Camera2CameraControl]. +/// +/// See the documentation for Mockito's code generation for more information. +// ignore: must_be_immutable +class MockCamera2CameraControl extends _i1.Mock + implements _i38.Camera2CameraControl { + @override + _i3.CameraControl get cameraControl => (super.noSuchMethod( + Invocation.getter(#cameraControl), + returnValue: _FakeCameraControl_1( + this, + Invocation.getter(#cameraControl), + ), + returnValueForMissingStub: _FakeCameraControl_1( + this, + Invocation.getter(#cameraControl), + ), + ) as _i3.CameraControl); + + @override + _i16.Future addCaptureRequestOptions( + _i39.CaptureRequestOptions? captureRequestOptions) => + (super.noSuchMethod( + Invocation.method( + #addCaptureRequestOptions, + [captureRequestOptions], + ), + returnValue: _i16.Future.value(), + returnValueForMissingStub: _i16.Future.value(), + ) as _i16.Future); +} + /// A class which mocks [LiveData]. /// /// See the documentation for Mockito's code generation for more information.