Skip to content

Commit e07eb50

Browse files
authored
[camerax] Implements setExposureMode (flutter#6110)
Implements `setExposureMode`. Fixes flutter#120468. ~To be landed after (1) flutter/packages#6059 then (2) flutter/packages#6109 Done :)
1 parent bc51dea commit e07eb50

File tree

11 files changed

+176
-11
lines changed

11 files changed

+176
-11
lines changed

packages/camera/camera_android_camerax/CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,7 @@
1+
## 0.5.0+36
2+
3+
* Implements `setExposureMode`.
4+
15
## 0.5.0+35
26

37
* Modifies `CameraInitializedEvent` that is sent when the camera is initialized to indicate that the initial focus

packages/camera/camera_android_camerax/README.md

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -30,10 +30,6 @@ dependencies:
3030
and thus, the plugin will fall back to 480p if configured with a
3131
`ResolutionPreset`.
3232

33-
### Exposure mode configuration \[[Issue #120468][120468]\]
34-
35-
`setExposureMode`is unimplemented.
36-
3733
### Focus mode configuration \[[Issue #120467][120467]\]
3834

3935
`setFocusMode` is unimplemented.

packages/camera/camera_android_camerax/android/src/main/java/io/flutter/plugins/camerax/CameraAndroidCameraxPlugin.java

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,9 @@ public final class CameraAndroidCameraxPlugin implements FlutterPlugin, Activity
2929
@VisibleForTesting @Nullable public SystemServicesHostApiImpl systemServicesHostApiImpl;
3030
@VisibleForTesting @Nullable public MeteringPointHostApiImpl meteringPointHostApiImpl;
3131

32+
@VisibleForTesting @Nullable
33+
public Camera2CameraControlHostApiImpl camera2CameraControlHostApiImpl;
34+
3235
@VisibleForTesting
3336
public @Nullable DeviceOrientationManagerHostApiImpl deviceOrientationManagerHostApiImpl;
3437

@@ -120,6 +123,11 @@ public void setUp(
120123
cameraControlHostApiImpl =
121124
new CameraControlHostApiImpl(binaryMessenger, instanceManager, context);
122125
GeneratedCameraXLibrary.CameraControlHostApi.setup(binaryMessenger, cameraControlHostApiImpl);
126+
camera2CameraControlHostApiImpl = new Camera2CameraControlHostApiImpl(instanceManager, context);
127+
GeneratedCameraXLibrary.Camera2CameraControlHostApi.setup(
128+
binaryMessenger, camera2CameraControlHostApiImpl);
129+
GeneratedCameraXLibrary.CaptureRequestOptionsHostApi.setup(
130+
binaryMessenger, new CaptureRequestOptionsHostApiImpl(instanceManager));
123131
GeneratedCameraXLibrary.FocusMeteringActionHostApi.setup(
124132
binaryMessenger, new FocusMeteringActionHostApiImpl(instanceManager));
125133
GeneratedCameraXLibrary.FocusMeteringResultHostApi.setup(
@@ -217,6 +225,9 @@ public void updateContext(@NonNull Context context) {
217225
if (cameraControlHostApiImpl != null) {
218226
cameraControlHostApiImpl.setContext(context);
219227
}
228+
if (camera2CameraControlHostApiImpl != null) {
229+
camera2CameraControlHostApiImpl.setContext(context);
230+
}
220231
}
221232

222233
/** Sets {@code LifecycleOwner} that is used to control the lifecycle of the camera by CameraX. */

packages/camera/camera_android_camerax/android/src/main/java/io/flutter/plugins/camerax/CaptureRequestOptionsHostApiImpl.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -110,8 +110,8 @@ public void create(@NonNull Long identifier, @NonNull Map<Long, Object> options)
110110
Map<CaptureRequestKeySupportedType, Object> decodedOptions =
111111
new HashMap<CaptureRequestKeySupportedType, Object>();
112112
for (Map.Entry<Long, Object> option : options.entrySet()) {
113-
decodedOptions.put(
114-
CaptureRequestKeySupportedType.values()[option.getKey().intValue()], option.getValue());
113+
Integer index = ((Number) option.getKey()).intValue();
114+
decodedOptions.put(CaptureRequestKeySupportedType.values()[index], option.getValue());
115115
}
116116
instanceManager.addDartCreatedInstance(proxy.create(decodedOptions), identifier);
117117
}

packages/camera/camera_android_camerax/android/src/test/java/io/flutter/plugins/camerax/CameraAndroidCameraxPluginTest.java

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -169,6 +169,8 @@ public void onAttachedToActivity_setsActivityAsNeededAndPermissionsRegistry() {
169169
mock(ImageAnalysisHostApiImpl.class);
170170
final CameraControlHostApiImpl mockCameraControlHostApiImpl =
171171
mock(CameraControlHostApiImpl.class);
172+
final Camera2CameraControlHostApiImpl mockCamera2CameraControlHostApiImpl =
173+
mock(Camera2CameraControlHostApiImpl.class);
172174

173175
when(flutterPluginBinding.getApplicationContext()).thenReturn(mockContext);
174176

@@ -180,6 +182,7 @@ public void onAttachedToActivity_setsActivityAsNeededAndPermissionsRegistry() {
180182
plugin.imageAnalysisHostApiImpl = mockImageAnalysisHostApiImpl;
181183
plugin.cameraControlHostApiImpl = mockCameraControlHostApiImpl;
182184
plugin.liveDataHostApiImpl = mock(LiveDataHostApiImpl.class);
185+
plugin.camera2CameraControlHostApiImpl = mockCamera2CameraControlHostApiImpl;
183186

184187
plugin.onAttachedToEngine(flutterPluginBinding);
185188
plugin.onDetachedFromActivityForConfigChanges();
@@ -191,6 +194,7 @@ public void onAttachedToActivity_setsActivityAsNeededAndPermissionsRegistry() {
191194
verify(mockImageCaptureHostApiImpl).setContext(mockContext);
192195
verify(mockImageAnalysisHostApiImpl).setContext(mockContext);
193196
verify(mockCameraControlHostApiImpl).setContext(mockContext);
197+
verify(mockCamera2CameraControlHostApiImpl).setContext(mockContext);
194198
}
195199

196200
@Test
@@ -259,6 +263,8 @@ public void onReattachedToActivityForConfigChanges_setsActivityAndPermissionsReg
259263
mock(CameraControlHostApiImpl.class);
260264
final DeviceOrientationManagerHostApiImpl mockDeviceOrientationManagerHostApiImpl =
261265
mock(DeviceOrientationManagerHostApiImpl.class);
266+
final Camera2CameraControlHostApiImpl mockCamera2CameraControlHostApiImpl =
267+
mock(Camera2CameraControlHostApiImpl.class);
262268
final MeteringPointHostApiImpl mockMeteringPointHostApiImpl =
263269
mock(MeteringPointHostApiImpl.class);
264270
final ArgumentCaptor<PermissionsRegistry> permissionsRegistryCaptor =
@@ -277,6 +283,7 @@ public void onReattachedToActivityForConfigChanges_setsActivityAndPermissionsReg
277283
plugin.deviceOrientationManagerHostApiImpl = mockDeviceOrientationManagerHostApiImpl;
278284
plugin.meteringPointHostApiImpl = mockMeteringPointHostApiImpl;
279285
plugin.liveDataHostApiImpl = mock(LiveDataHostApiImpl.class);
286+
plugin.camera2CameraControlHostApiImpl = mockCamera2CameraControlHostApiImpl;
280287

281288
plugin.onAttachedToEngine(flutterPluginBinding);
282289
plugin.onReattachedToActivityForConfigChanges(activityPluginBinding);
@@ -294,6 +301,7 @@ public void onReattachedToActivityForConfigChanges_setsActivityAndPermissionsReg
294301
verify(mockImageCaptureHostApiImpl).setContext(mockActivity);
295302
verify(mockImageAnalysisHostApiImpl).setContext(mockActivity);
296303
verify(mockCameraControlHostApiImpl).setContext(mockActivity);
304+
verify(mockCamera2CameraControlHostApiImpl).setContext(mockActivity);
297305

298306
// Check permissions registry reference is set.
299307
verify(mockSystemServicesHostApiImpl)
@@ -347,6 +355,8 @@ public void onDetachedFromActivity_setsContextReferencesBasedOnFlutterPluginBind
347355
final ImageCaptureHostApiImpl mockImageCaptureHostApiImpl = mock(ImageCaptureHostApiImpl.class);
348356
final CameraControlHostApiImpl mockCameraControlHostApiImpl =
349357
mock(CameraControlHostApiImpl.class);
358+
final Camera2CameraControlHostApiImpl mockCamera2CameraControlHostApiImpl =
359+
mock(Camera2CameraControlHostApiImpl.class);
350360
final ArgumentCaptor<PermissionsRegistry> permissionsRegistryCaptor =
351361
ArgumentCaptor.forClass(PermissionsRegistry.class);
352362

@@ -360,6 +370,7 @@ public void onDetachedFromActivity_setsContextReferencesBasedOnFlutterPluginBind
360370
plugin.imageAnalysisHostApiImpl = mockImageAnalysisHostApiImpl;
361371
plugin.cameraControlHostApiImpl = mockCameraControlHostApiImpl;
362372
plugin.liveDataHostApiImpl = mock(LiveDataHostApiImpl.class);
373+
plugin.camera2CameraControlHostApiImpl = mockCamera2CameraControlHostApiImpl;
363374

364375
plugin.onAttachedToEngine(flutterPluginBinding);
365376
plugin.onDetachedFromActivity();
@@ -371,5 +382,6 @@ public void onDetachedFromActivity_setsContextReferencesBasedOnFlutterPluginBind
371382
verify(mockImageCaptureHostApiImpl).setContext(mockContext);
372383
verify(mockImageAnalysisHostApiImpl).setContext(mockContext);
373384
verify(mockCameraControlHostApiImpl).setContext(mockContext);
385+
verify(mockCamera2CameraControlHostApiImpl).setContext(mockContext);
374386
}
375387
}

packages/camera/camera_android_camerax/example/lib/main.dart

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -393,8 +393,10 @@ class _CameraExampleHomeState extends State<CameraExampleHome>
393393
children: <Widget>[
394394
TextButton(
395395
style: styleAuto,
396-
onPressed:
397-
() {}, // TODO(camsim99): Add functionality back here.
396+
onPressed: controller != null
397+
? () =>
398+
onSetExposureModeButtonPressed(ExposureMode.auto)
399+
: null,
398400
onLongPress: () {
399401
if (controller != null) {
400402
CameraPlatform.instance
@@ -406,8 +408,10 @@ class _CameraExampleHomeState extends State<CameraExampleHome>
406408
),
407409
TextButton(
408410
style: styleLocked,
409-
onPressed:
410-
() {}, // TODO(camsim99): Add functionality back here.
411+
onPressed: controller != null
412+
? () =>
413+
onSetExposureModeButtonPressed(ExposureMode.locked)
414+
: null,
411415
child: const Text('LOCKED'),
412416
),
413417
TextButton(

packages/camera/camera_android_camerax/lib/src/android_camera_camerax.dart

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,12 +14,14 @@ import 'package:stream_transform/stream_transform.dart';
1414

1515
import 'analyzer.dart';
1616
import 'camera.dart';
17+
import 'camera2_camera_control.dart';
1718
import 'camera_control.dart';
1819
import 'camera_info.dart';
1920
import 'camera_selector.dart';
2021
import 'camera_state.dart';
2122
import 'camerax_library.g.dart';
2223
import 'camerax_proxy.dart';
24+
import 'capture_request_options.dart';
2325
import 'device_orientation_manager.dart';
2426
import 'exposure_state.dart';
2527
import 'fallback_strategy.dart';
@@ -545,6 +547,27 @@ class AndroidCameraCameraX extends CameraPlatform {
545547
point: point, meteringMode: FocusMeteringAction.flagAf);
546548
}
547549

550+
/// Sets the exposure mode for taking pictures.
551+
///
552+
/// Setting [ExposureMode.locked] will lock current exposure point until it
553+
/// is unset by setting [ExposureMode.auto].
554+
///
555+
/// [cameraId] is not used.
556+
@override
557+
Future<void> setExposureMode(int cameraId, ExposureMode mode) async {
558+
final Camera2CameraControl camera2Control =
559+
proxy.getCamera2CameraControl(cameraControl);
560+
final bool lockExposureMode = mode == ExposureMode.locked;
561+
562+
final CaptureRequestOptions captureRequestOptions = proxy
563+
.createCaptureRequestOptions(<(
564+
CaptureRequestKeySupportedType,
565+
Object?
566+
)>[(CaptureRequestKeySupportedType.controlAeLock, lockExposureMode)]);
567+
568+
await camera2Control.addCaptureRequestOptions(captureRequestOptions);
569+
}
570+
548571
/// Gets the maximum supported zoom level for the selected camera.
549572
///
550573
/// [cameraId] not used.

packages/camera/camera_android_camerax/lib/src/camerax_proxy.dart

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,13 @@
55
import 'dart:ui' show Size;
66

77
import 'analyzer.dart';
8+
import 'camera2_camera_control.dart';
9+
import 'camera_control.dart';
810
import 'camera_info.dart';
911
import 'camera_selector.dart';
1012
import 'camera_state.dart';
1113
import 'camerax_library.g.dart';
14+
import 'capture_request_options.dart';
1215
import 'device_orientation_manager.dart';
1316
import 'fallback_strategy.dart';
1417
import 'focus_metering_action.dart';
@@ -52,6 +55,8 @@ class CameraXProxy {
5255
_startListeningForDeviceOrientationChange,
5356
this.setPreviewSurfaceProvider = _setPreviewSurfaceProvider,
5457
this.getDefaultDisplayRotation = _getDefaultDisplayRotation,
58+
this.getCamera2CameraControl = _getCamera2CameraControl,
59+
this.createCaptureRequestOptions = _createCaptureRequestOptions,
5560
this.createMeteringPoint = _createMeteringPoint,
5661
this.createFocusMeteringAction = _createFocusMeteringAction,
5762
});
@@ -142,6 +147,15 @@ class CameraXProxy {
142147
/// rotation constants.
143148
Future<int> Function() getDefaultDisplayRotation;
144149

150+
/// Get [Camera2CameraControl] instance from [cameraControl].
151+
Camera2CameraControl Function(CameraControl cameraControl)
152+
getCamera2CameraControl;
153+
154+
/// Create [CapureRequestOptions] with specified options.
155+
CaptureRequestOptions Function(
156+
List<(CaptureRequestKeySupportedType, Object?)> options)
157+
createCaptureRequestOptions;
158+
145159
/// Returns a [MeteringPoint] with the specified coordinates based on
146160
/// [cameraInfo].
147161
MeteringPoint Function(double x, double y, CameraInfo cameraInfo)
@@ -255,6 +269,16 @@ class CameraXProxy {
255269
return DeviceOrientationManager.getDefaultDisplayRotation();
256270
}
257271

272+
static Camera2CameraControl _getCamera2CameraControl(
273+
CameraControl cameraControl) {
274+
return Camera2CameraControl(cameraControl: cameraControl);
275+
}
276+
277+
static CaptureRequestOptions _createCaptureRequestOptions(
278+
List<(CaptureRequestKeySupportedType, Object?)> options) {
279+
return CaptureRequestOptions(requestedOptions: options);
280+
}
281+
258282
static MeteringPoint _createMeteringPoint(
259283
double x, double y, CameraInfo cameraInfo) {
260284
return MeteringPoint(x: x, y: y, cameraInfo: cameraInfo);

packages/camera/camera_android_camerax/pubspec.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ name: camera_android_camerax
22
description: Android implementation of the camera plugin using the CameraX library.
33
repository: https://github.com/flutter/packages/tree/main/packages/camera/camera_android_camerax
44
issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+camera%22
5-
version: 0.5.0+35
5+
version: 0.5.0+36
66

77
environment:
88
sdk: ^3.1.0

packages/camera/camera_android_camerax/test/android_camera_camerax_test.dart

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,13 +9,15 @@ import 'package:async/async.dart';
99
import 'package:camera_android_camerax/camera_android_camerax.dart';
1010
import 'package:camera_android_camerax/src/analyzer.dart';
1111
import 'package:camera_android_camerax/src/camera.dart';
12+
import 'package:camera_android_camerax/src/camera2_camera_control.dart';
1213
import 'package:camera_android_camerax/src/camera_control.dart';
1314
import 'package:camera_android_camerax/src/camera_info.dart';
1415
import 'package:camera_android_camerax/src/camera_selector.dart';
1516
import 'package:camera_android_camerax/src/camera_state.dart';
1617
import 'package:camera_android_camerax/src/camera_state_error.dart';
1718
import 'package:camera_android_camerax/src/camerax_library.g.dart';
1819
import 'package:camera_android_camerax/src/camerax_proxy.dart';
20+
import 'package:camera_android_camerax/src/capture_request_options.dart';
1921
import 'package:camera_android_camerax/src/device_orientation_manager.dart';
2022
import 'package:camera_android_camerax/src/exposure_state.dart';
2123
import 'package:camera_android_camerax/src/fallback_strategy.dart';
@@ -78,6 +80,7 @@ import 'test_camerax_library.g.dart';
7880
MockSpec<TestInstanceManagerHostApi>(),
7981
MockSpec<TestSystemServicesHostApi>(),
8082
MockSpec<ZoomState>(),
83+
MockSpec<Camera2CameraControl>(),
8184
])
8285
@GenerateMocks(<Type>[], customMocks: <MockSpec<Object>>[
8386
MockSpec<LiveData<CameraState>>(as: #MockLiveCameraState),
@@ -2010,6 +2013,59 @@ void main() {
20102013
expect(camera.captureOrientationLocked, isFalse);
20112014
});
20122015

2016+
test('setExposureMode sets expected controlAeLock value via Camera2 interop',
2017+
() async {
2018+
final AndroidCameraCameraX camera = AndroidCameraCameraX();
2019+
const int cameraId = 78;
2020+
final MockCameraControl mockCameraControl = MockCameraControl();
2021+
final MockCamera2CameraControl mockCamera2CameraControl =
2022+
MockCamera2CameraControl();
2023+
2024+
// Set directly for test versus calling createCamera.
2025+
camera.camera = MockCamera();
2026+
camera.cameraControl = mockCameraControl;
2027+
2028+
// Tell plugin to create detached Camera2CameraControl and
2029+
// CaptureRequestOptions instances for testing.
2030+
camera.proxy = CameraXProxy(
2031+
getCamera2CameraControl: (CameraControl cameraControl) =>
2032+
cameraControl == mockCameraControl
2033+
? mockCamera2CameraControl
2034+
: Camera2CameraControl.detached(cameraControl: cameraControl),
2035+
createCaptureRequestOptions:
2036+
(List<(CaptureRequestKeySupportedType, Object?)> options) =>
2037+
CaptureRequestOptions.detached(requestedOptions: options),
2038+
);
2039+
2040+
// Test auto mode.
2041+
await camera.setExposureMode(cameraId, ExposureMode.auto);
2042+
2043+
VerificationResult verificationResult =
2044+
verify(mockCamera2CameraControl.addCaptureRequestOptions(captureAny));
2045+
CaptureRequestOptions capturedCaptureRequestOptions =
2046+
verificationResult.captured.single as CaptureRequestOptions;
2047+
List<(CaptureRequestKeySupportedType, Object?)> requestedOptions =
2048+
capturedCaptureRequestOptions.requestedOptions;
2049+
expect(requestedOptions.length, equals(1));
2050+
expect(requestedOptions.first.$1,
2051+
equals(CaptureRequestKeySupportedType.controlAeLock));
2052+
expect(requestedOptions.first.$2, equals(false));
2053+
2054+
// Test locked mode.
2055+
clearInteractions(mockCamera2CameraControl);
2056+
await camera.setExposureMode(cameraId, ExposureMode.locked);
2057+
2058+
verificationResult =
2059+
verify(mockCamera2CameraControl.addCaptureRequestOptions(captureAny));
2060+
capturedCaptureRequestOptions =
2061+
verificationResult.captured.single as CaptureRequestOptions;
2062+
requestedOptions = capturedCaptureRequestOptions.requestedOptions;
2063+
expect(requestedOptions.length, equals(1));
2064+
expect(requestedOptions.first.$1,
2065+
equals(CaptureRequestKeySupportedType.controlAeLock));
2066+
expect(requestedOptions.first.$2, equals(true));
2067+
});
2068+
20132069
test(
20142070
'setExposurePoint clears current auto-exposure metering point as expected',
20152071
() async {

0 commit comments

Comments
 (0)