Skip to content

Commit 83d7fc6

Browse files
authored
[camerax] Implements setFocusPoint, setExposurePoint, setExposureOffset (flutter#6059)
This PR implements `setFocusPoint`, `setExposurePoint`, `setExposureOffset` and makes some small fixes here and there, each of which I have left a comment about for context. Part of flutter#120468 & flutter#120467. ~NOTE: Should land after flutter/packages#6068 done :)
1 parent ccb27be commit 83d7fc6

28 files changed

+1512
-185
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+34
2+
3+
* Implements `setFocusPoint`, `setExposurePoint`, and `setExposureOffset`.
4+
15
## 0.5.0+33
26

37
* Fixes typo in `README.md`.

packages/camera/camera_android_camerax/README.md

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -24,20 +24,19 @@ dependencies:
2424
2525
## Missing features and limitations
2626
27-
2827
### 240p resolution configuration for video recording
2928
3029
240p resolution configuration for video recording is unsupported by CameraX,
3130
and thus, the plugin will fall back to 480p if configured with a
3231
`ResolutionPreset`.
3332

34-
### Exposure mode, point, & offset configuration \[[Issue #120468][120468]\]
33+
### Exposure mode configuration \[[Issue #120468][120468]\]
3534

36-
`setExposureMode`, `setExposurePoint`, & `setExposureOffset` are unimplemented.
35+
`setExposureMode`is unimplemented.
3736

38-
### Focus mode & point configuration \[[Issue #120467][120467]\]
37+
### Focus mode configuration \[[Issue #120467][120467]\]
3938

40-
`setFocusMode` & `setFocusPoint` are unimplemented.
39+
`setFocusMode` is unimplemented.
4140

4241
### Setting maximum duration and stream options for video capture
4342

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

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ public final class CameraAndroidCameraxPlugin implements FlutterPlugin, Activity
2727
@VisibleForTesting @Nullable public ImageCaptureHostApiImpl imageCaptureHostApiImpl;
2828
@VisibleForTesting @Nullable public CameraControlHostApiImpl cameraControlHostApiImpl;
2929
@VisibleForTesting @Nullable public SystemServicesHostApiImpl systemServicesHostApiImpl;
30+
@VisibleForTesting @Nullable public MeteringPointHostApiImpl meteringPointHostApiImpl;
3031

3132
@VisibleForTesting
3233
public @Nullable DeviceOrientationManagerHostApiImpl deviceOrientationManagerHostApiImpl;
@@ -119,6 +120,12 @@ public void setUp(
119120
cameraControlHostApiImpl =
120121
new CameraControlHostApiImpl(binaryMessenger, instanceManager, context);
121122
GeneratedCameraXLibrary.CameraControlHostApi.setup(binaryMessenger, cameraControlHostApiImpl);
123+
GeneratedCameraXLibrary.FocusMeteringActionHostApi.setup(
124+
binaryMessenger, new FocusMeteringActionHostApiImpl(instanceManager));
125+
GeneratedCameraXLibrary.FocusMeteringResultHostApi.setup(
126+
binaryMessenger, new FocusMeteringResultHostApiImpl(instanceManager));
127+
meteringPointHostApiImpl = new MeteringPointHostApiImpl(instanceManager);
128+
GeneratedCameraXLibrary.MeteringPointHostApi.setup(binaryMessenger, meteringPointHostApiImpl);
122129
}
123130

124131
@Override
@@ -238,5 +245,8 @@ public void updateActivity(@Nullable Activity activity) {
238245
if (deviceOrientationManagerHostApiImpl != null) {
239246
deviceOrientationManagerHostApiImpl.setActivity(activity);
240247
}
248+
if (meteringPointHostApiImpl != null) {
249+
meteringPointHostApiImpl.setActivity(activity);
250+
}
241251
}
242252
}

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

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,12 @@ public void onSuccess(Void voidResult) {
8383
}
8484

8585
public void onFailure(Throwable t) {
86+
if (t instanceof CameraControl.OperationCanceledException) {
87+
// Operation was canceled due to camera being closed or a new request was submitted, which
88+
// is not actionable and should not block a new value from potentially being submitted.
89+
result.success(null);
90+
return;
91+
}
8692
result.error(t);
8793
}
8894
},
@@ -94,6 +100,9 @@ public void onFailure(Throwable t) {
94100
*
95101
* <p>Will trigger an auto focus action and enable auto focus/auto exposure/auto white balance
96102
* metering regions.
103+
*
104+
* <p>Will send a {@link GeneratedCameraXLibrary.Result} with a null result if operation was
105+
* canceled.
97106
*/
98107
public void startFocusAndMetering(
99108
@NonNull CameraControl cameraControl,
@@ -117,6 +126,12 @@ public void onSuccess(FocusMeteringResult focusMeteringResult) {
117126
}
118127

119128
public void onFailure(Throwable t) {
129+
if (t instanceof CameraControl.OperationCanceledException) {
130+
// Operation was canceled due to camera being closed or a new request was submitted, which
131+
// is not actionable and should not block a new value from potentially being submitted.
132+
result.success(null);
133+
return;
134+
}
120135
result.error(t);
121136
}
122137
},
@@ -152,6 +167,9 @@ public void onFailure(Throwable t) {
152167
* <p>The exposure compensation value set on the camera must be within the range of {@code
153168
* ExposureState#getExposureCompensationRange()} for the current {@code ExposureState} for the
154169
* call to succeed.
170+
*
171+
* <p>Will send a {@link GeneratedCameraXLibrary.Result} with a null result if operation was
172+
* canceled.
155173
*/
156174
public void setExposureCompensationIndex(
157175
@NonNull CameraControl cameraControl, @NonNull Long index, @NonNull Result<Long> result) {
@@ -166,6 +184,12 @@ public void onSuccess(Integer integerResult) {
166184
}
167185

168186
public void onFailure(Throwable t) {
187+
if (t instanceof CameraControl.OperationCanceledException) {
188+
// Operation was canceled due to camera being closed or a new request was submitted, which
189+
// is not actionable and should not block a new value from potentially being submitted.
190+
result.success(null);
191+
return;
192+
}
169193
result.error(t);
170194
}
171195
},

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

Lines changed: 28 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -83,14 +83,14 @@ private CameraStateType(final int index) {
8383
* <p>If you need to add another type to support a type S to use a LiveData<S> in this plugin,
8484
* ensure the following is done on the Dart side:
8585
*
86-
* <p>* In `../lib/src/live_data.dart`, add new cases for S in
86+
* <p>* In `camera_android_camerax/lib/src/live_data.dart`, add new cases for S in
8787
* `_LiveDataHostApiImpl#getValueFromInstances` to get the current value of type S from a
8888
* LiveData<S> instance and in `LiveDataFlutterApiImpl#create` to create the expected type of
8989
* LiveData<S> when requested.
9090
*
9191
* <p>On the native side, ensure the following is done:
9292
*
93-
* <p>* Update `LiveDataHostApiImpl#getValue` is updated to properly return identifiers for
93+
* <p>* Make sure `LiveDataHostApiImpl#getValue` is updated to properly return identifiers for
9494
* instances of type S. * Update `ObserverFlutterApiWrapper#onChanged` to properly handle
9595
* receiving calls with instances of type S if a LiveData<S> instance is observed.
9696
*/
@@ -146,6 +146,24 @@ private VideoResolutionFallbackRule(final int index) {
146146
}
147147
}
148148

149+
/**
150+
* The types of capture request options this plugin currently supports.
151+
*
152+
* <p>If you need to add another option to support, ensure the following is done on the Dart side:
153+
*
154+
* <p>* In `camera_android_camerax/lib/src/capture_request_options.dart`, add new cases for this
155+
* option in `_CaptureRequestOptionsHostApiImpl#createFromInstances` to create the expected Map
156+
* entry of option key index and value to send to the native side.
157+
*
158+
* <p>On the native side, ensure the following is done:
159+
*
160+
* <p>* Update `CaptureRequestOptionsHostApiImpl#create` to set the correct `CaptureRequest` key
161+
* with a valid value type for this option.
162+
*
163+
* <p>See https://developer.android.com/reference/android/hardware/camera2/CaptureRequest for the
164+
* sorts of capture request options that can be supported via CameraX's interoperability with
165+
* Camera2.
166+
*/
149167
public enum CaptureRequestKeySupportedType {
150168
CONTROL_AE_LOCK(0);
151169

@@ -3899,7 +3917,11 @@ public void create(@NonNull Long identifierArg, @NonNull Reply<Void> callback) {
38993917
public interface MeteringPointHostApi {
39003918

39013919
void create(
3902-
@NonNull Long identifier, @NonNull Double x, @NonNull Double y, @Nullable Double size);
3920+
@NonNull Long identifier,
3921+
@NonNull Double x,
3922+
@NonNull Double y,
3923+
@Nullable Double size,
3924+
@NonNull Long cameraInfoId);
39033925

39043926
@NonNull
39053927
Double getDefaultPointSize();
@@ -3927,12 +3949,14 @@ static void setup(
39273949
Double xArg = (Double) args.get(1);
39283950
Double yArg = (Double) args.get(2);
39293951
Double sizeArg = (Double) args.get(3);
3952+
Number cameraInfoIdArg = (Number) args.get(4);
39303953
try {
39313954
api.create(
39323955
(identifierArg == null) ? null : identifierArg.longValue(),
39333956
xArg,
39343957
yArg,
3935-
sizeArg);
3958+
sizeArg,
3959+
(cameraInfoIdArg == null) ? null : cameraInfoIdArg.longValue());
39363960
wrapped.add(0, null);
39373961
} catch (Throwable exception) {
39383962
ArrayList<Object> wrappedError = wrapError(exception);

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

Lines changed: 53 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -4,13 +4,20 @@
44

55
package io.flutter.plugins.camerax;
66

7+
import android.app.Activity;
8+
import android.content.Context;
9+
import android.os.Build;
10+
import android.view.Display;
11+
import android.view.WindowManager;
712
import androidx.annotation.NonNull;
813
import androidx.annotation.Nullable;
914
import androidx.annotation.VisibleForTesting;
15+
import androidx.camera.core.CameraInfo;
16+
import androidx.camera.core.DisplayOrientedMeteringPointFactory;
1017
import androidx.camera.core.MeteringPoint;
1118
import androidx.camera.core.MeteringPointFactory;
12-
import androidx.camera.core.SurfaceOrientedMeteringPointFactory;
1319
import io.flutter.plugins.camerax.GeneratedCameraXLibrary.MeteringPointHostApi;
20+
import java.util.Objects;
1421

1522
/**
1623
* Host API implementation for {@link MeteringPoint}.
@@ -25,29 +32,51 @@ public class MeteringPointHostApiImpl implements MeteringPointHostApi {
2532
/** Proxy for constructor and static methods of {@link MeteringPoint}. */
2633
@VisibleForTesting
2734
public static class MeteringPointProxy {
35+
Activity activity;
2836

2937
/**
3038
* Creates a surface oriented {@link MeteringPoint} with the specified x, y, and size.
3139
*
32-
* <p>A {@link SurfaceOrientedMeteringPointFactory} is used to construct the {@link
33-
* MeteringPoint} because underlying the camera preview that this plugin uses is a Flutter
34-
* texture that is backed by a {@link Surface} created by the Flutter Android embedding.
40+
* <p>A {@link DisplayOrientedMeteringPointFactory} is used to construct the {@link
41+
* MeteringPoint} because this factory handles the transformation of specified coordinates based
42+
* on camera information and the device orientation automatically.
3543
*/
3644
@NonNull
37-
public MeteringPoint create(@NonNull Double x, @NonNull Double y, @Nullable Double size) {
38-
SurfaceOrientedMeteringPointFactory factory = getSurfaceOrientedMeteringPointFactory(1f, 1f);
45+
public MeteringPoint create(
46+
@NonNull Double x,
47+
@NonNull Double y,
48+
@Nullable Double size,
49+
@NonNull CameraInfo cameraInfo) {
50+
Display display = null;
51+
52+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
53+
display = activity.getDisplay();
54+
} else {
55+
display = getDefaultDisplayForAndroidVersionBelowR(activity);
56+
}
57+
58+
DisplayOrientedMeteringPointFactory factory =
59+
getDisplayOrientedMeteringPointFactory(display, cameraInfo, 1f, 1f);
60+
3961
if (size == null) {
4062
return factory.createPoint(x.floatValue(), y.floatValue());
4163
} else {
4264
return factory.createPoint(x.floatValue(), y.floatValue(), size.floatValue());
4365
}
4466
}
4567

68+
@NonNull
69+
@SuppressWarnings("deprecation")
70+
private Display getDefaultDisplayForAndroidVersionBelowR(@NonNull Activity activity) {
71+
return ((WindowManager) activity.getSystemService(Context.WINDOW_SERVICE))
72+
.getDefaultDisplay();
73+
}
74+
4675
@VisibleForTesting
4776
@NonNull
48-
public SurfaceOrientedMeteringPointFactory getSurfaceOrientedMeteringPointFactory(
49-
float width, float height) {
50-
return new SurfaceOrientedMeteringPointFactory(width, height);
77+
public DisplayOrientedMeteringPointFactory getDisplayOrientedMeteringPointFactory(
78+
@NonNull Display display, @NonNull CameraInfo cameraInfo, float width, float height) {
79+
return new DisplayOrientedMeteringPointFactory(display, cameraInfo, width, height);
5180
}
5281

5382
/**
@@ -81,10 +110,23 @@ public MeteringPointHostApiImpl(@NonNull InstanceManager instanceManager) {
81110
this.proxy = proxy;
82111
}
83112

113+
public void setActivity(@NonNull Activity activity) {
114+
this.proxy.activity = activity;
115+
}
116+
84117
@Override
85118
public void create(
86-
@NonNull Long identifier, @NonNull Double x, @NonNull Double y, @Nullable Double size) {
87-
MeteringPoint meteringPoint = proxy.create(x, y, size);
119+
@NonNull Long identifier,
120+
@NonNull Double x,
121+
@NonNull Double y,
122+
@Nullable Double size,
123+
@NonNull Long cameraInfoId) {
124+
MeteringPoint meteringPoint =
125+
proxy.create(
126+
x,
127+
y,
128+
size,
129+
(CameraInfo) Objects.requireNonNull(instanceManager.getInstance(cameraInfoId)));
88130
instanceManager.addDartCreatedInstance(meteringPoint, identifier);
89131
}
90132

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

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,8 @@ public void onAttachedToActivity_setsActivityAsNeededAndPermissionsRegistry() {
9292
mock(SystemServicesHostApiImpl.class);
9393
final DeviceOrientationManagerHostApiImpl mockDeviceOrientationManagerHostApiImpl =
9494
mock(DeviceOrientationManagerHostApiImpl.class);
95+
final MeteringPointHostApiImpl mockMeteringPointHostApiImpl =
96+
mock(MeteringPointHostApiImpl.class);
9597
final ArgumentCaptor<PermissionsRegistry> permissionsRegistryCaptor =
9698
ArgumentCaptor.forClass(PermissionsRegistry.class);
9799

@@ -103,13 +105,15 @@ public void onAttachedToActivity_setsActivityAsNeededAndPermissionsRegistry() {
103105
plugin.liveDataHostApiImpl = mock(LiveDataHostApiImpl.class);
104106
plugin.systemServicesHostApiImpl = mockSystemServicesHostApiImpl;
105107
plugin.deviceOrientationManagerHostApiImpl = mockDeviceOrientationManagerHostApiImpl;
108+
plugin.meteringPointHostApiImpl = mockMeteringPointHostApiImpl;
106109

107110
plugin.onAttachedToEngine(flutterPluginBinding);
108111
plugin.onAttachedToActivity(activityPluginBinding);
109112

110113
// Check Activity references are set.
111114
verify(mockSystemServicesHostApiImpl).setActivity(mockActivity);
112115
verify(mockDeviceOrientationManagerHostApiImpl).setActivity(mockActivity);
116+
verify(mockMeteringPointHostApiImpl).setActivity(mockActivity);
113117

114118
// Check permissions registry reference is set.
115119
verify(mockSystemServicesHostApiImpl)
@@ -129,11 +133,14 @@ public void onAttachedToActivity_setsActivityAsNeededAndPermissionsRegistry() {
129133
mock(SystemServicesHostApiImpl.class);
130134
final DeviceOrientationManagerHostApiImpl mockDeviceOrientationManagerHostApiImpl =
131135
mock(DeviceOrientationManagerHostApiImpl.class);
136+
final MeteringPointHostApiImpl mockMeteringPointHostApiImpl =
137+
mock(MeteringPointHostApiImpl.class);
132138

133139
plugin.processCameraProviderHostApiImpl = mockProcessCameraProviderHostApiImpl;
134140
plugin.liveDataHostApiImpl = mockLiveDataHostApiImpl;
135141
plugin.systemServicesHostApiImpl = mockSystemServicesHostApiImpl;
136142
plugin.deviceOrientationManagerHostApiImpl = mockDeviceOrientationManagerHostApiImpl;
143+
plugin.meteringPointHostApiImpl = mockMeteringPointHostApiImpl;
137144

138145
plugin.onAttachedToEngine(flutterPluginBinding);
139146
plugin.onDetachedFromActivityForConfigChanges();
@@ -142,6 +149,7 @@ public void onAttachedToActivity_setsActivityAsNeededAndPermissionsRegistry() {
142149
verify(mockLiveDataHostApiImpl).setLifecycleOwner(null);
143150
verify(mockSystemServicesHostApiImpl).setActivity(null);
144151
verify(mockDeviceOrientationManagerHostApiImpl).setActivity(null);
152+
verify(mockMeteringPointHostApiImpl).setActivity(null);
145153
}
146154

147155
@Test
@@ -251,6 +259,8 @@ public void onReattachedToActivityForConfigChanges_setsActivityAndPermissionsReg
251259
mock(CameraControlHostApiImpl.class);
252260
final DeviceOrientationManagerHostApiImpl mockDeviceOrientationManagerHostApiImpl =
253261
mock(DeviceOrientationManagerHostApiImpl.class);
262+
final MeteringPointHostApiImpl mockMeteringPointHostApiImpl =
263+
mock(MeteringPointHostApiImpl.class);
254264
final ArgumentCaptor<PermissionsRegistry> permissionsRegistryCaptor =
255265
ArgumentCaptor.forClass(PermissionsRegistry.class);
256266

@@ -265,6 +275,7 @@ public void onReattachedToActivityForConfigChanges_setsActivityAndPermissionsReg
265275
plugin.imageAnalysisHostApiImpl = mockImageAnalysisHostApiImpl;
266276
plugin.cameraControlHostApiImpl = mockCameraControlHostApiImpl;
267277
plugin.deviceOrientationManagerHostApiImpl = mockDeviceOrientationManagerHostApiImpl;
278+
plugin.meteringPointHostApiImpl = mockMeteringPointHostApiImpl;
268279
plugin.liveDataHostApiImpl = mock(LiveDataHostApiImpl.class);
269280

270281
plugin.onAttachedToEngine(flutterPluginBinding);
@@ -273,6 +284,7 @@ public void onReattachedToActivityForConfigChanges_setsActivityAndPermissionsReg
273284
// Check Activity references are set.
274285
verify(mockSystemServicesHostApiImpl).setActivity(mockActivity);
275286
verify(mockDeviceOrientationManagerHostApiImpl).setActivity(mockActivity);
287+
verify(mockMeteringPointHostApiImpl).setActivity(mockActivity);
276288

277289
// Check Activity as Context references are set.
278290
verify(mockProcessCameraProviderHostApiImpl).setContext(mockActivity);
@@ -300,11 +312,14 @@ public void onDetachedFromActivity_removesReferencesToActivityPluginBindingAndAc
300312
final LiveDataHostApiImpl mockLiveDataHostApiImpl = mock(LiveDataHostApiImpl.class);
301313
final DeviceOrientationManagerHostApiImpl mockDeviceOrientationManagerHostApiImpl =
302314
mock(DeviceOrientationManagerHostApiImpl.class);
315+
final MeteringPointHostApiImpl mockMeteringPointHostApiImpl =
316+
mock(MeteringPointHostApiImpl.class);
303317

304318
plugin.processCameraProviderHostApiImpl = mockProcessCameraProviderHostApiImpl;
305319
plugin.liveDataHostApiImpl = mockLiveDataHostApiImpl;
306320
plugin.systemServicesHostApiImpl = mockSystemServicesHostApiImpl;
307321
plugin.deviceOrientationManagerHostApiImpl = mockDeviceOrientationManagerHostApiImpl;
322+
plugin.meteringPointHostApiImpl = mockMeteringPointHostApiImpl;
308323

309324
plugin.onAttachedToEngine(flutterPluginBinding);
310325
plugin.onDetachedFromActivityForConfigChanges();
@@ -313,6 +328,7 @@ public void onDetachedFromActivity_removesReferencesToActivityPluginBindingAndAc
313328
verify(mockLiveDataHostApiImpl).setLifecycleOwner(null);
314329
verify(mockSystemServicesHostApiImpl).setActivity(null);
315330
verify(mockDeviceOrientationManagerHostApiImpl).setActivity(null);
331+
verify(mockMeteringPointHostApiImpl).setActivity(null);
316332
}
317333

318334
@Test

0 commit comments

Comments
 (0)