Skip to content

Commit 19d19b5

Browse files
authored
[camerax] Implement image capture (flutter#3287)
[camerax] Implement image capture
1 parent 0f99306 commit 19d19b5

23 files changed

+1460
-47
lines changed

packages/camera/camera_android_camerax/CHANGELOG.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,5 +12,6 @@
1212
* Adds implementation of availableCameras().
1313
* Implements camera preview, createCamera, initializeCamera, onCameraError, onDeviceOrientationChanged, and onCameraInitialized.
1414
* Adds integration test to plugin.
15-
* Fixes instance manager hot restart behavior and fixes Java casting issue.
1615
* Bump CameraX version to 1.3.0-alpha04.
16+
* Fixes instance manager hot restart behavior and fixes Java casting issue.
17+
* Implements image capture.

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

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ public final class CameraAndroidCameraxPlugin implements FlutterPlugin, Activity
1818
private InstanceManager instanceManager;
1919
private FlutterPluginBinding pluginBinding;
2020
private ProcessCameraProviderHostApiImpl processCameraProviderHostApi;
21+
private ImageCaptureHostApiImpl imageCaptureHostApi;
2122
public SystemServicesHostApiImpl systemServicesHostApi;
2223

2324
/**
@@ -53,6 +54,8 @@ void setUp(BinaryMessenger binaryMessenger, Context context, TextureRegistry tex
5354
GeneratedCameraXLibrary.SystemServicesHostApi.setup(binaryMessenger, systemServicesHostApi);
5455
GeneratedCameraXLibrary.PreviewHostApi.setup(
5556
binaryMessenger, new PreviewHostApiImpl(binaryMessenger, instanceManager, textureRegistry));
57+
imageCaptureHostApi = new ImageCaptureHostApiImpl(binaryMessenger, instanceManager, context);
58+
GeneratedCameraXLibrary.ImageCaptureHostApi.setup(binaryMessenger, imageCaptureHostApi);
5659
}
5760

5861
@Override
@@ -107,5 +110,8 @@ public void updateContext(Context context) {
107110
if (processCameraProviderHostApi != null) {
108111
processCameraProviderHostApi.setContext(context);
109112
}
113+
if (imageCaptureHostApi != null) {
114+
processCameraProviderHostApi.setContext(context);
115+
}
110116
}
111117
}

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

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,10 @@
99
import android.view.Surface;
1010
import androidx.annotation.NonNull;
1111
import androidx.camera.core.CameraSelector;
12+
import androidx.camera.core.ImageCapture;
1213
import androidx.camera.core.Preview;
1314
import io.flutter.plugin.common.BinaryMessenger;
15+
import java.io.File;
1416

1517
/** Utility class used to create CameraX-related objects primarily for testing purposes. */
1618
public class CameraXProxy {
@@ -48,4 +50,15 @@ public SystemServicesFlutterApiImpl createSystemServicesFlutterApiImpl(
4850
@NonNull BinaryMessenger binaryMessenger) {
4951
return new SystemServicesFlutterApiImpl(binaryMessenger);
5052
}
53+
54+
public ImageCapture.Builder createImageCaptureBuilder() {
55+
return new ImageCapture.Builder();
56+
}
57+
58+
/**
59+
* Creates an {@link ImageCapture.OutputFileOptions} to configure where to save a captured image.
60+
*/
61+
public ImageCapture.OutputFileOptions createImageCaptureOutputFileOptions(@NonNull File file) {
62+
return new ImageCapture.OutputFileOptions.Builder(file).build();
63+
}
5164
}

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

Lines changed: 187 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -531,6 +531,9 @@ Long bindToLifecycle(
531531
@NonNull Long cameraSelectorIdentifier,
532532
@NonNull List<Long> useCaseIds);
533533

534+
@NonNull
535+
Boolean isBound(@NonNull Long identifier, @NonNull Long useCaseIdentifier);
536+
534537
void unbind(@NonNull Long identifier, @NonNull List<Long> useCaseIds);
535538

536539
void unbindAll(@NonNull Long identifier);
@@ -650,6 +653,40 @@ public void error(Throwable error) {
650653
channel.setMessageHandler(null);
651654
}
652655
}
656+
{
657+
BasicMessageChannel<Object> channel =
658+
new BasicMessageChannel<>(
659+
binaryMessenger,
660+
"dev.flutter.pigeon.ProcessCameraProviderHostApi.isBound",
661+
getCodec());
662+
if (api != null) {
663+
channel.setMessageHandler(
664+
(message, reply) -> {
665+
Map<String, Object> wrapped = new HashMap<>();
666+
try {
667+
ArrayList<Object> args = (ArrayList<Object>) message;
668+
Number identifierArg = (Number) args.get(0);
669+
if (identifierArg == null) {
670+
throw new NullPointerException("identifierArg unexpectedly null.");
671+
}
672+
Number useCaseIdentifierArg = (Number) args.get(1);
673+
if (useCaseIdentifierArg == null) {
674+
throw new NullPointerException("useCaseIdentifierArg unexpectedly null.");
675+
}
676+
Boolean output =
677+
api.isBound(
678+
(identifierArg == null) ? null : identifierArg.longValue(),
679+
(useCaseIdentifierArg == null) ? null : useCaseIdentifierArg.longValue());
680+
wrapped.put("result", output);
681+
} catch (Error | RuntimeException exception) {
682+
wrapped.put("error", wrapError(exception));
683+
}
684+
reply.reply(wrapped);
685+
});
686+
} else {
687+
channel.setMessageHandler(null);
688+
}
689+
}
653690
{
654691
BasicMessageChannel<Object> channel =
655692
new BasicMessageChannel<>(
@@ -1143,6 +1180,156 @@ static void setup(BinaryMessenger binaryMessenger, PreviewHostApi api) {
11431180
}
11441181
}
11451182

1183+
private static class ImageCaptureHostApiCodec extends StandardMessageCodec {
1184+
public static final ImageCaptureHostApiCodec INSTANCE = new ImageCaptureHostApiCodec();
1185+
1186+
private ImageCaptureHostApiCodec() {}
1187+
1188+
@Override
1189+
protected Object readValueOfType(byte type, ByteBuffer buffer) {
1190+
switch (type) {
1191+
case (byte) 128:
1192+
return ResolutionInfo.fromMap((Map<String, Object>) readValue(buffer));
1193+
1194+
default:
1195+
return super.readValueOfType(type, buffer);
1196+
}
1197+
}
1198+
1199+
@Override
1200+
protected void writeValue(ByteArrayOutputStream stream, Object value) {
1201+
if (value instanceof ResolutionInfo) {
1202+
stream.write(128);
1203+
writeValue(stream, ((ResolutionInfo) value).toMap());
1204+
} else {
1205+
super.writeValue(stream, value);
1206+
}
1207+
}
1208+
}
1209+
1210+
/** Generated interface from Pigeon that represents a handler of messages from Flutter. */
1211+
public interface ImageCaptureHostApi {
1212+
void create(
1213+
@NonNull Long identifier,
1214+
@Nullable Long flashMode,
1215+
@Nullable ResolutionInfo targetResolution);
1216+
1217+
void setFlashMode(@NonNull Long identifier, @NonNull Long flashMode);
1218+
1219+
void takePicture(@NonNull Long identifier, Result<String> result);
1220+
1221+
/** The codec used by ImageCaptureHostApi. */
1222+
static MessageCodec<Object> getCodec() {
1223+
return ImageCaptureHostApiCodec.INSTANCE;
1224+
}
1225+
1226+
/**
1227+
* Sets up an instance of `ImageCaptureHostApi` to handle messages through the
1228+
* `binaryMessenger`.
1229+
*/
1230+
static void setup(BinaryMessenger binaryMessenger, ImageCaptureHostApi api) {
1231+
{
1232+
BasicMessageChannel<Object> channel =
1233+
new BasicMessageChannel<>(
1234+
binaryMessenger, "dev.flutter.pigeon.ImageCaptureHostApi.create", getCodec());
1235+
if (api != null) {
1236+
channel.setMessageHandler(
1237+
(message, reply) -> {
1238+
Map<String, Object> wrapped = new HashMap<>();
1239+
try {
1240+
ArrayList<Object> args = (ArrayList<Object>) message;
1241+
Number identifierArg = (Number) args.get(0);
1242+
if (identifierArg == null) {
1243+
throw new NullPointerException("identifierArg unexpectedly null.");
1244+
}
1245+
Number flashModeArg = (Number) args.get(1);
1246+
ResolutionInfo targetResolutionArg = (ResolutionInfo) args.get(2);
1247+
api.create(
1248+
(identifierArg == null) ? null : identifierArg.longValue(),
1249+
(flashModeArg == null) ? null : flashModeArg.longValue(),
1250+
targetResolutionArg);
1251+
wrapped.put("result", null);
1252+
} catch (Error | RuntimeException exception) {
1253+
wrapped.put("error", wrapError(exception));
1254+
}
1255+
reply.reply(wrapped);
1256+
});
1257+
} else {
1258+
channel.setMessageHandler(null);
1259+
}
1260+
}
1261+
{
1262+
BasicMessageChannel<Object> channel =
1263+
new BasicMessageChannel<>(
1264+
binaryMessenger, "dev.flutter.pigeon.ImageCaptureHostApi.setFlashMode", getCodec());
1265+
if (api != null) {
1266+
channel.setMessageHandler(
1267+
(message, reply) -> {
1268+
Map<String, Object> wrapped = new HashMap<>();
1269+
try {
1270+
ArrayList<Object> args = (ArrayList<Object>) message;
1271+
Number identifierArg = (Number) args.get(0);
1272+
if (identifierArg == null) {
1273+
throw new NullPointerException("identifierArg unexpectedly null.");
1274+
}
1275+
Number flashModeArg = (Number) args.get(1);
1276+
if (flashModeArg == null) {
1277+
throw new NullPointerException("flashModeArg unexpectedly null.");
1278+
}
1279+
api.setFlashMode(
1280+
(identifierArg == null) ? null : identifierArg.longValue(),
1281+
(flashModeArg == null) ? null : flashModeArg.longValue());
1282+
wrapped.put("result", null);
1283+
} catch (Error | RuntimeException exception) {
1284+
wrapped.put("error", wrapError(exception));
1285+
}
1286+
reply.reply(wrapped);
1287+
});
1288+
} else {
1289+
channel.setMessageHandler(null);
1290+
}
1291+
}
1292+
{
1293+
BasicMessageChannel<Object> channel =
1294+
new BasicMessageChannel<>(
1295+
binaryMessenger, "dev.flutter.pigeon.ImageCaptureHostApi.takePicture", getCodec());
1296+
if (api != null) {
1297+
channel.setMessageHandler(
1298+
(message, reply) -> {
1299+
Map<String, Object> wrapped = new HashMap<>();
1300+
try {
1301+
ArrayList<Object> args = (ArrayList<Object>) message;
1302+
Number identifierArg = (Number) args.get(0);
1303+
if (identifierArg == null) {
1304+
throw new NullPointerException("identifierArg unexpectedly null.");
1305+
}
1306+
Result<String> resultCallback =
1307+
new Result<String>() {
1308+
public void success(String result) {
1309+
wrapped.put("result", result);
1310+
reply.reply(wrapped);
1311+
}
1312+
1313+
public void error(Throwable error) {
1314+
wrapped.put("error", wrapError(error));
1315+
reply.reply(wrapped);
1316+
}
1317+
};
1318+
1319+
api.takePicture(
1320+
(identifierArg == null) ? null : identifierArg.longValue(), resultCallback);
1321+
} catch (Error | RuntimeException exception) {
1322+
wrapped.put("error", wrapError(exception));
1323+
reply.reply(wrapped);
1324+
}
1325+
});
1326+
} else {
1327+
channel.setMessageHandler(null);
1328+
}
1329+
}
1330+
}
1331+
}
1332+
11461333
private static Map<String, Object> wrapError(Throwable exception) {
11471334
Map<String, Object> errorMap = new HashMap<>();
11481335
errorMap.put("message", exception.toString());
Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,121 @@
1+
// Copyright 2013 The Flutter Authors. All rights reserved.
2+
// Use of this source code is governed by a BSD-style license that can be
3+
// found in the LICENSE file.
4+
5+
package io.flutter.plugins.camerax;
6+
7+
import android.content.Context;
8+
import android.util.Size;
9+
import androidx.annotation.NonNull;
10+
import androidx.annotation.Nullable;
11+
import androidx.annotation.VisibleForTesting;
12+
import androidx.camera.core.ImageCapture;
13+
import androidx.camera.core.ImageCaptureException;
14+
import io.flutter.plugin.common.BinaryMessenger;
15+
import io.flutter.plugins.camerax.GeneratedCameraXLibrary.ImageCaptureHostApi;
16+
import java.io.File;
17+
import java.io.IOException;
18+
import java.util.Objects;
19+
import java.util.concurrent.Executors;
20+
21+
public class ImageCaptureHostApiImpl implements ImageCaptureHostApi {
22+
private final BinaryMessenger binaryMessenger;
23+
private final InstanceManager instanceManager;
24+
25+
private Context context;
26+
private SystemServicesFlutterApiImpl systemServicesFlutterApiImpl;
27+
28+
public static final String TEMPORARY_FILE_NAME = "CAP";
29+
public static final String JPG_FILE_TYPE = ".jpg";
30+
31+
@VisibleForTesting public CameraXProxy cameraXProxy = new CameraXProxy();
32+
33+
public ImageCaptureHostApiImpl(
34+
@NonNull BinaryMessenger binaryMessenger,
35+
@NonNull InstanceManager instanceManager,
36+
@NonNull Context context) {
37+
this.binaryMessenger = binaryMessenger;
38+
this.instanceManager = instanceManager;
39+
this.context = context;
40+
}
41+
42+
/**
43+
* Sets the context that the {@link ImageCapture} will use to find a location to save a captured
44+
* image.
45+
*/
46+
public void setContext(Context context) {
47+
this.context = context;
48+
}
49+
50+
/**
51+
* Creates an {@link ImageCapture} with the requested flash mode and target resolution if
52+
* specified.
53+
*/
54+
@Override
55+
public void create(
56+
@NonNull Long identifier,
57+
@Nullable Long flashMode,
58+
@Nullable GeneratedCameraXLibrary.ResolutionInfo targetResolution) {
59+
ImageCapture.Builder imageCaptureBuilder = cameraXProxy.createImageCaptureBuilder();
60+
if (flashMode != null) {
61+
// This sets the requested flash mode, but may fail silently.
62+
imageCaptureBuilder.setFlashMode(flashMode.intValue());
63+
}
64+
if (targetResolution != null) {
65+
imageCaptureBuilder.setTargetResolution(
66+
new Size(
67+
targetResolution.getWidth().intValue(), targetResolution.getHeight().intValue()));
68+
}
69+
ImageCapture imageCapture = imageCaptureBuilder.build();
70+
instanceManager.addDartCreatedInstance(imageCapture, identifier);
71+
}
72+
73+
/** Sets the flash mode of the {@link ImageCapture} instance with the specified identifier. */
74+
@Override
75+
public void setFlashMode(@NonNull Long identifier, @NonNull Long flashMode) {
76+
ImageCapture imageCapture =
77+
(ImageCapture) Objects.requireNonNull(instanceManager.getInstance(identifier));
78+
imageCapture.setFlashMode(flashMode.intValue());
79+
}
80+
81+
/** Captures a still image and uses the result to return its absolute path in memory. */
82+
@Override
83+
public void takePicture(
84+
@NonNull Long identifier, @NonNull GeneratedCameraXLibrary.Result<String> result) {
85+
ImageCapture imageCapture =
86+
(ImageCapture) Objects.requireNonNull(instanceManager.getInstance(identifier));
87+
final File outputDir = context.getCacheDir();
88+
File temporaryCaptureFile;
89+
try {
90+
temporaryCaptureFile = File.createTempFile(TEMPORARY_FILE_NAME, JPG_FILE_TYPE, outputDir);
91+
} catch (IOException | SecurityException e) {
92+
result.error(e);
93+
return;
94+
}
95+
96+
ImageCapture.OutputFileOptions outputFileOptions =
97+
cameraXProxy.createImageCaptureOutputFileOptions(temporaryCaptureFile);
98+
ImageCapture.OnImageSavedCallback onImageSavedCallback =
99+
createOnImageSavedCallback(temporaryCaptureFile, result);
100+
101+
imageCapture.takePicture(
102+
outputFileOptions, Executors.newSingleThreadExecutor(), onImageSavedCallback);
103+
}
104+
105+
/** Creates a callback used when saving a captured image. */
106+
@VisibleForTesting
107+
public ImageCapture.OnImageSavedCallback createOnImageSavedCallback(
108+
@NonNull File file, @NonNull GeneratedCameraXLibrary.Result<String> result) {
109+
return new ImageCapture.OnImageSavedCallback() {
110+
@Override
111+
public void onImageSaved(@NonNull ImageCapture.OutputFileResults outputFileResults) {
112+
result.success(file.getAbsolutePath());
113+
}
114+
115+
@Override
116+
public void onError(@NonNull ImageCaptureException exception) {
117+
result.error(exception);
118+
}
119+
};
120+
}
121+
}

0 commit comments

Comments
 (0)