[camerax] Wrap methods necessary for preview implementation (#7046)
* Add code needed from proof of concept
* Add test files, delete unecessary method
* Add tests, remove unecessary code
* Fix analyze
* Update changelog
* Cleanup:
* Cleanup and add switch
* Finish todo
* Add onCameraError
* Fix pigeon file
* Add method for releasing flutter texture and cleanup surface logic
* Add test for release method
* Add dart test
* Update changelog
* Modify flutter api names to avoid stack overflow
* Cleanup
* Fix tests
* Delete space
* Address review 1
* Update switch
* Add annotations and constants in tests
* Reset verification behavior
diff --git a/packages/camera/camera_android_camerax/CHANGELOG.md b/packages/camera/camera_android_camerax/CHANGELOG.md
index d5355c6..080240a 100644
--- a/packages/camera/camera_android_camerax/CHANGELOG.md
+++ b/packages/camera/camera_android_camerax/CHANGELOG.md
@@ -8,3 +8,4 @@
* Adds Camera and UseCase classes, along with methods for binding UseCases to a lifecycle with the ProcessCameraProvider.
* Bump CameraX version to 1.3.0-alpha03 and Kotlin version to 1.8.0.
* Changes instance manager to allow the separate creation of identical objects.
+* Adds Preview and Surface classes, along with other methods needed to implement camera preview.
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 c35394f..b61e7ac 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
@@ -49,6 +49,8 @@
binaryMessenger, processCameraProviderHostApi);
systemServicesHostApi = new SystemServicesHostApiImpl(binaryMessenger, instanceManager);
GeneratedCameraXLibrary.SystemServicesHostApi.setup(binaryMessenger, systemServicesHostApi);
+ GeneratedCameraXLibrary.PreviewHostApi.setup(
+ binaryMessenger, new PreviewHostApiImpl(binaryMessenger, instanceManager, textureRegistry));
}
@Override
diff --git a/packages/camera/camera_android_camerax/android/src/main/java/io/flutter/plugins/camerax/CameraXProxy.java b/packages/camera/camera_android_camerax/android/src/main/java/io/flutter/plugins/camerax/CameraXProxy.java
index 83c43a9..4a3d277 100644
--- a/packages/camera/camera_android_camerax/android/src/main/java/io/flutter/plugins/camerax/CameraXProxy.java
+++ b/packages/camera/camera_android_camerax/android/src/main/java/io/flutter/plugins/camerax/CameraXProxy.java
@@ -5,8 +5,14 @@
package io.flutter.plugins.camerax;
import android.app.Activity;
+import android.graphics.SurfaceTexture;
+import android.view.Surface;
+import androidx.annotation.NonNull;
import androidx.camera.core.CameraSelector;
+import androidx.camera.core.Preview;
+import io.flutter.plugin.common.BinaryMessenger;
+/** Utility class used to create CameraX-related objects primarily for testing purposes. */
public class CameraXProxy {
public CameraSelector.Builder createCameraSelectorBuilder() {
return new CameraSelector.Builder();
@@ -17,10 +23,29 @@
}
public DeviceOrientationManager createDeviceOrientationManager(
- Activity activity,
- Boolean isFrontFacing,
- int sensorOrientation,
- DeviceOrientationManager.DeviceOrientationChangeCallback callback) {
+ @NonNull Activity activity,
+ @NonNull Boolean isFrontFacing,
+ @NonNull int sensorOrientation,
+ @NonNull DeviceOrientationManager.DeviceOrientationChangeCallback callback) {
return new DeviceOrientationManager(activity, isFrontFacing, sensorOrientation, callback);
}
+
+ public Preview.Builder createPreviewBuilder() {
+ return new Preview.Builder();
+ }
+
+ public Surface createSurface(@NonNull SurfaceTexture surfaceTexture) {
+ return new Surface(surfaceTexture);
+ }
+
+ /**
+ * Creates an instance of the {@code SystemServicesFlutterApiImpl}.
+ *
+ * <p>Included in this class to utilize the callback methods it provides, e.g. {@code
+ * onCameraError(String)}.
+ */
+ public SystemServicesFlutterApiImpl createSystemServicesFlutterApiImpl(
+ @NonNull BinaryMessenger binaryMessenger) {
+ return new SystemServicesFlutterApiImpl(binaryMessenger);
+ }
}
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 528870c..1e61ea6 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
@@ -26,6 +26,82 @@
public class GeneratedCameraXLibrary {
/** Generated class from Pigeon that represents data sent in messages. */
+ public static class ResolutionInfo {
+ private @NonNull Long width;
+
+ public @NonNull Long getWidth() {
+ return width;
+ }
+
+ public void setWidth(@NonNull Long setterArg) {
+ if (setterArg == null) {
+ throw new IllegalStateException("Nonnull field \"width\" is null.");
+ }
+ this.width = setterArg;
+ }
+
+ private @NonNull Long height;
+
+ public @NonNull Long getHeight() {
+ return height;
+ }
+
+ public void setHeight(@NonNull Long setterArg) {
+ if (setterArg == null) {
+ throw new IllegalStateException("Nonnull field \"height\" is null.");
+ }
+ this.height = setterArg;
+ }
+
+ /** Constructor is private to enforce null safety; use Builder. */
+ private ResolutionInfo() {}
+
+ public static final class Builder {
+ private @Nullable Long width;
+
+ public @NonNull Builder setWidth(@NonNull Long setterArg) {
+ this.width = setterArg;
+ return this;
+ }
+
+ private @Nullable Long height;
+
+ public @NonNull Builder setHeight(@NonNull Long setterArg) {
+ this.height = setterArg;
+ return this;
+ }
+
+ public @NonNull ResolutionInfo build() {
+ ResolutionInfo pigeonReturn = new ResolutionInfo();
+ pigeonReturn.setWidth(width);
+ pigeonReturn.setHeight(height);
+ return pigeonReturn;
+ }
+ }
+
+ @NonNull
+ Map<String, Object> toMap() {
+ Map<String, Object> toMapResult = new HashMap<>();
+ toMapResult.put("width", width);
+ toMapResult.put("height", height);
+ return toMapResult;
+ }
+
+ static @NonNull ResolutionInfo fromMap(@NonNull Map<String, Object> map) {
+ ResolutionInfo pigeonResult = new ResolutionInfo();
+ Object width = map.get("width");
+ pigeonResult.setWidth(
+ (width == null) ? null : ((width instanceof Integer) ? (Integer) width : (Long) width));
+ Object height = map.get("height");
+ pigeonResult.setHeight(
+ (height == null)
+ ? null
+ : ((height instanceof Integer) ? (Integer) height : (Long) height));
+ return pigeonResult;
+ }
+ }
+
+ /** Generated class from Pigeon that represents data sent in messages. */
public static class CameraPermissionsErrorData {
private @NonNull String errorCode;
@@ -843,6 +919,185 @@
callback.reply(null);
});
}
+
+ public void onCameraError(@NonNull String errorDescriptionArg, Reply<Void> callback) {
+ BasicMessageChannel<Object> channel =
+ new BasicMessageChannel<>(
+ binaryMessenger,
+ "dev.flutter.pigeon.SystemServicesFlutterApi.onCameraError",
+ getCodec());
+ channel.send(
+ new ArrayList<Object>(Arrays.asList(errorDescriptionArg)),
+ channelReply -> {
+ callback.reply(null);
+ });
+ }
+ }
+
+ private static class PreviewHostApiCodec extends StandardMessageCodec {
+ public static final PreviewHostApiCodec INSTANCE = new PreviewHostApiCodec();
+
+ private PreviewHostApiCodec() {}
+
+ @Override
+ protected Object readValueOfType(byte type, ByteBuffer buffer) {
+ switch (type) {
+ case (byte) 128:
+ return ResolutionInfo.fromMap((Map<String, Object>) readValue(buffer));
+
+ case (byte) 129:
+ return ResolutionInfo.fromMap((Map<String, Object>) readValue(buffer));
+
+ default:
+ return super.readValueOfType(type, buffer);
+ }
+ }
+
+ @Override
+ protected void writeValue(ByteArrayOutputStream stream, Object value) {
+ if (value instanceof ResolutionInfo) {
+ stream.write(128);
+ writeValue(stream, ((ResolutionInfo) value).toMap());
+ } else if (value instanceof ResolutionInfo) {
+ stream.write(129);
+ writeValue(stream, ((ResolutionInfo) value).toMap());
+ } else {
+ super.writeValue(stream, value);
+ }
+ }
+ }
+
+ /** Generated interface from Pigeon that represents a handler of messages from Flutter. */
+ public interface PreviewHostApi {
+ void create(
+ @NonNull Long identifier,
+ @Nullable Long rotation,
+ @Nullable ResolutionInfo targetResolution);
+
+ @NonNull
+ Long setSurfaceProvider(@NonNull Long identifier);
+
+ void releaseFlutterSurfaceTexture();
+
+ @NonNull
+ ResolutionInfo getResolutionInfo(@NonNull Long identifier);
+
+ /** The codec used by PreviewHostApi. */
+ static MessageCodec<Object> getCodec() {
+ return PreviewHostApiCodec.INSTANCE;
+ }
+
+ /** Sets up an instance of `PreviewHostApi` to handle messages through the `binaryMessenger`. */
+ static void setup(BinaryMessenger binaryMessenger, PreviewHostApi api) {
+ {
+ BasicMessageChannel<Object> channel =
+ new BasicMessageChannel<>(
+ binaryMessenger, "dev.flutter.pigeon.PreviewHostApi.create", getCodec());
+ if (api != null) {
+ channel.setMessageHandler(
+ (message, reply) -> {
+ Map<String, Object> wrapped = new HashMap<>();
+ try {
+ ArrayList<Object> args = (ArrayList<Object>) message;
+ Number identifierArg = (Number) args.get(0);
+ if (identifierArg == null) {
+ throw new NullPointerException("identifierArg unexpectedly null.");
+ }
+ Number rotationArg = (Number) args.get(1);
+ ResolutionInfo targetResolutionArg = (ResolutionInfo) args.get(2);
+ api.create(
+ (identifierArg == null) ? null : identifierArg.longValue(),
+ (rotationArg == null) ? null : rotationArg.longValue(),
+ targetResolutionArg);
+ wrapped.put("result", null);
+ } catch (Error | RuntimeException exception) {
+ wrapped.put("error", wrapError(exception));
+ }
+ reply.reply(wrapped);
+ });
+ } else {
+ channel.setMessageHandler(null);
+ }
+ }
+ {
+ BasicMessageChannel<Object> channel =
+ new BasicMessageChannel<>(
+ binaryMessenger,
+ "dev.flutter.pigeon.PreviewHostApi.setSurfaceProvider",
+ getCodec());
+ if (api != null) {
+ channel.setMessageHandler(
+ (message, reply) -> {
+ Map<String, Object> wrapped = new HashMap<>();
+ try {
+ ArrayList<Object> args = (ArrayList<Object>) message;
+ Number identifierArg = (Number) args.get(0);
+ if (identifierArg == null) {
+ throw new NullPointerException("identifierArg unexpectedly null.");
+ }
+ Long output =
+ api.setSurfaceProvider(
+ (identifierArg == null) ? null : identifierArg.longValue());
+ wrapped.put("result", output);
+ } catch (Error | RuntimeException exception) {
+ wrapped.put("error", wrapError(exception));
+ }
+ reply.reply(wrapped);
+ });
+ } else {
+ channel.setMessageHandler(null);
+ }
+ }
+ {
+ BasicMessageChannel<Object> channel =
+ new BasicMessageChannel<>(
+ binaryMessenger,
+ "dev.flutter.pigeon.PreviewHostApi.releaseFlutterSurfaceTexture",
+ getCodec());
+ if (api != null) {
+ channel.setMessageHandler(
+ (message, reply) -> {
+ Map<String, Object> wrapped = new HashMap<>();
+ try {
+ api.releaseFlutterSurfaceTexture();
+ wrapped.put("result", null);
+ } catch (Error | RuntimeException exception) {
+ wrapped.put("error", wrapError(exception));
+ }
+ reply.reply(wrapped);
+ });
+ } else {
+ channel.setMessageHandler(null);
+ }
+ }
+ {
+ BasicMessageChannel<Object> channel =
+ new BasicMessageChannel<>(
+ binaryMessenger, "dev.flutter.pigeon.PreviewHostApi.getResolutionInfo", getCodec());
+ if (api != null) {
+ channel.setMessageHandler(
+ (message, reply) -> {
+ Map<String, Object> wrapped = new HashMap<>();
+ try {
+ ArrayList<Object> args = (ArrayList<Object>) message;
+ Number identifierArg = (Number) args.get(0);
+ if (identifierArg == null) {
+ throw new NullPointerException("identifierArg unexpectedly null.");
+ }
+ ResolutionInfo output =
+ api.getResolutionInfo(
+ (identifierArg == null) ? null : identifierArg.longValue());
+ wrapped.put("result", output);
+ } catch (Error | RuntimeException exception) {
+ wrapped.put("error", wrapError(exception));
+ }
+ reply.reply(wrapped);
+ });
+ } else {
+ channel.setMessageHandler(null);
+ }
+ }
+ }
}
private static Map<String, Object> wrapError(Throwable exception) {
diff --git a/packages/camera/camera_android_camerax/android/src/main/java/io/flutter/plugins/camerax/PreviewHostApiImpl.java b/packages/camera/camera_android_camerax/android/src/main/java/io/flutter/plugins/camerax/PreviewHostApiImpl.java
new file mode 100644
index 0000000..838f0b3
--- /dev/null
+++ b/packages/camera/camera_android_camerax/android/src/main/java/io/flutter/plugins/camerax/PreviewHostApiImpl.java
@@ -0,0 +1,149 @@
+// Copyright 2013 The Flutter Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+package io.flutter.plugins.camerax;
+
+import android.graphics.SurfaceTexture;
+import android.util.Size;
+import android.view.Surface;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.annotation.VisibleForTesting;
+import androidx.camera.core.Preview;
+import androidx.camera.core.SurfaceRequest;
+import io.flutter.plugin.common.BinaryMessenger;
+import io.flutter.plugins.camerax.GeneratedCameraXLibrary.PreviewHostApi;
+import io.flutter.view.TextureRegistry;
+import java.util.Objects;
+import java.util.concurrent.Executors;
+
+public class PreviewHostApiImpl implements PreviewHostApi {
+ private final BinaryMessenger binaryMessenger;
+ private final InstanceManager instanceManager;
+ private final TextureRegistry textureRegistry;
+
+ @VisibleForTesting public CameraXProxy cameraXProxy = new CameraXProxy();
+ @VisibleForTesting public TextureRegistry.SurfaceTextureEntry flutterSurfaceTexture;
+
+ public PreviewHostApiImpl(
+ @NonNull BinaryMessenger binaryMessenger,
+ @NonNull InstanceManager instanceManager,
+ @NonNull TextureRegistry textureRegistry) {
+ this.binaryMessenger = binaryMessenger;
+ this.instanceManager = instanceManager;
+ this.textureRegistry = textureRegistry;
+ }
+
+ /** Creates a {@link Preview} with the target rotation and resolution if specified. */
+ @Override
+ public void create(
+ @NonNull Long identifier,
+ @Nullable Long rotation,
+ @Nullable GeneratedCameraXLibrary.ResolutionInfo targetResolution) {
+ Preview.Builder previewBuilder = cameraXProxy.createPreviewBuilder();
+ if (rotation != null) {
+ previewBuilder.setTargetRotation(rotation.intValue());
+ }
+ if (targetResolution != null) {
+ previewBuilder.setTargetResolution(
+ new Size(
+ targetResolution.getWidth().intValue(), targetResolution.getHeight().intValue()));
+ }
+ Preview preview = previewBuilder.build();
+ instanceManager.addDartCreatedInstance(preview, identifier);
+ }
+
+ /**
+ * Sets the {@link Preview.SurfaceProvider} that will be used to provide a {@code Surface} backed
+ * by a Flutter {@link TextureRegistry.SurfaceTextureEntry} used to build the {@link Preview}.
+ */
+ @Override
+ public Long setSurfaceProvider(@NonNull Long identifier) {
+ Preview preview = (Preview) Objects.requireNonNull(instanceManager.getInstance(identifier));
+ flutterSurfaceTexture = textureRegistry.createSurfaceTexture();
+ SurfaceTexture surfaceTexture = flutterSurfaceTexture.surfaceTexture();
+ Preview.SurfaceProvider surfaceProvider = createSurfaceProvider(surfaceTexture);
+ preview.setSurfaceProvider(surfaceProvider);
+
+ return flutterSurfaceTexture.id();
+ }
+
+ /**
+ * Creates a {@link Preview.SurfaceProvider} that specifies how to provide a {@link Surface} to a
+ * {@code Preview} that is backed by a Flutter {@link TextureRegistry.SurfaceTextureEntry}.
+ */
+ @VisibleForTesting
+ public Preview.SurfaceProvider createSurfaceProvider(@NonNull SurfaceTexture surfaceTexture) {
+ return new Preview.SurfaceProvider() {
+ @Override
+ public void onSurfaceRequested(SurfaceRequest request) {
+ surfaceTexture.setDefaultBufferSize(
+ request.getResolution().getWidth(), request.getResolution().getHeight());
+ Surface flutterSurface = cameraXProxy.createSurface(surfaceTexture);
+ request.provideSurface(
+ flutterSurface,
+ Executors.newSingleThreadExecutor(),
+ (result) -> {
+ // See https://developer.android.com/reference/androidx/camera/core/SurfaceRequest.Result for documentation.
+ // Always attempt a release.
+ flutterSurface.release();
+ int resultCode = result.getResultCode();
+ switch (resultCode) {
+ case SurfaceRequest.Result.RESULT_REQUEST_CANCELLED:
+ case SurfaceRequest.Result.RESULT_WILL_NOT_PROVIDE_SURFACE:
+ case SurfaceRequest.Result.RESULT_SURFACE_ALREADY_PROVIDED:
+ case SurfaceRequest.Result.RESULT_SURFACE_USED_SUCCESSFULLY:
+ // Only need to release, do nothing.
+ break;
+ case SurfaceRequest.Result.RESULT_INVALID_SURFACE: // Intentional fall through.
+ default:
+ // Release and send error.
+ SystemServicesFlutterApiImpl systemServicesFlutterApi =
+ cameraXProxy.createSystemServicesFlutterApiImpl(binaryMessenger);
+ systemServicesFlutterApi.sendCameraError(
+ getProvideSurfaceErrorDescription(resultCode), reply -> {});
+ break;
+ }
+ });
+ };
+ };
+ }
+
+ /**
+ * Returns an error description for each {@link SurfaceRequest.Result} that represents an error
+ * with providing a surface.
+ */
+ private String getProvideSurfaceErrorDescription(@Nullable int resultCode) {
+ switch (resultCode) {
+ case SurfaceRequest.Result.RESULT_INVALID_SURFACE:
+ return resultCode + ": Provided surface could not be used by the camera.";
+ default:
+ return resultCode + ": Attempt to provide a surface resulted with unrecognizable code.";
+ }
+ }
+
+ /**
+ * Releases the Flutter {@link TextureRegistry.SurfaceTextureEntry} if used to provide a surface
+ * for a {@link Preview}.
+ */
+ @Override
+ public void releaseFlutterSurfaceTexture() {
+ if (flutterSurfaceTexture != null) {
+ flutterSurfaceTexture.release();
+ }
+ }
+
+ /** Returns the resolution information for the specified {@link Preview}. */
+ @Override
+ public GeneratedCameraXLibrary.ResolutionInfo getResolutionInfo(@NonNull Long identifier) {
+ Preview preview = (Preview) Objects.requireNonNull(instanceManager.getInstance(identifier));
+ Size resolution = preview.getResolutionInfo().getResolution();
+
+ GeneratedCameraXLibrary.ResolutionInfo.Builder resolutionInfo =
+ new GeneratedCameraXLibrary.ResolutionInfo.Builder()
+ .setWidth(Long.valueOf(resolution.getWidth()))
+ .setHeight(Long.valueOf(resolution.getHeight()));
+ return resolutionInfo.build();
+ }
+}
diff --git a/packages/camera/camera_android_camerax/android/src/main/java/io/flutter/plugins/camerax/SystemServicesFlutterApiImpl.java b/packages/camera/camera_android_camerax/android/src/main/java/io/flutter/plugins/camerax/SystemServicesFlutterApiImpl.java
index 1e9f33b..6315897 100644
--- a/packages/camera/camera_android_camerax/android/src/main/java/io/flutter/plugins/camerax/SystemServicesFlutterApiImpl.java
+++ b/packages/camera/camera_android_camerax/android/src/main/java/io/flutter/plugins/camerax/SystemServicesFlutterApiImpl.java
@@ -4,19 +4,21 @@
package io.flutter.plugins.camerax;
+import androidx.annotation.NonNull;
import io.flutter.plugin.common.BinaryMessenger;
import io.flutter.plugins.camerax.GeneratedCameraXLibrary.SystemServicesFlutterApi;
public class SystemServicesFlutterApiImpl extends SystemServicesFlutterApi {
- public SystemServicesFlutterApiImpl(
- BinaryMessenger binaryMessenger, InstanceManager instanceManager) {
+ public SystemServicesFlutterApiImpl(@NonNull BinaryMessenger binaryMessenger) {
super(binaryMessenger);
- this.instanceManager = instanceManager;
}
- private final InstanceManager instanceManager;
-
- public void onDeviceOrientationChanged(String orientation, Reply<Void> reply) {
+ public void sendDeviceOrientationChangedEvent(
+ @NonNull String orientation, @NonNull Reply<Void> reply) {
super.onDeviceOrientationChanged(orientation, reply);
}
+
+ public void sendCameraError(@NonNull String errorDescription, @NonNull Reply<Void> reply) {
+ super.onCameraError(errorDescription, reply);
+ }
}
diff --git a/packages/camera/camera_android_camerax/android/src/main/java/io/flutter/plugins/camerax/SystemServicesHostApiImpl.java b/packages/camera/camera_android_camerax/android/src/main/java/io/flutter/plugins/camerax/SystemServicesHostApiImpl.java
index e8eb715..a698581 100644
--- a/packages/camera/camera_android_camerax/android/src/main/java/io/flutter/plugins/camerax/SystemServicesHostApiImpl.java
+++ b/packages/camera/camera_android_camerax/android/src/main/java/io/flutter/plugins/camerax/SystemServicesHostApiImpl.java
@@ -28,8 +28,7 @@
BinaryMessenger binaryMessenger, InstanceManager instanceManager) {
this.binaryMessenger = binaryMessenger;
this.instanceManager = instanceManager;
- this.systemServicesFlutterApi =
- new SystemServicesFlutterApiImpl(binaryMessenger, instanceManager);
+ this.systemServicesFlutterApi = new SystemServicesFlutterApiImpl(binaryMessenger);
}
public void setActivity(Activity activity) {
@@ -86,7 +85,7 @@
isFrontFacing,
sensorOrientation.intValue(),
(DeviceOrientation newOrientation) -> {
- systemServicesFlutterApi.onDeviceOrientationChanged(
+ systemServicesFlutterApi.sendDeviceOrientationChangedEvent(
serializeDeviceOrientation(newOrientation), reply -> {});
});
deviceOrientationManager.start();
diff --git a/packages/camera/camera_android_camerax/android/src/test/java/io/flutter/plugins/camerax/PreviewTest.java b/packages/camera/camera_android_camerax/android/src/test/java/io/flutter/plugins/camerax/PreviewTest.java
new file mode 100644
index 0000000..9cb4e91
--- /dev/null
+++ b/packages/camera/camera_android_camerax/android/src/test/java/io/flutter/plugins/camerax/PreviewTest.java
@@ -0,0 +1,221 @@
+// Copyright 2013 The Flutter Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+package io.flutter.plugins.camerax;
+
+import static org.junit.Assert.assertEquals;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.anyString;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.reset;
+import static org.mockito.Mockito.spy;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import android.graphics.SurfaceTexture;
+import android.util.Size;
+import android.view.Surface;
+import androidx.camera.core.Preview;
+import androidx.camera.core.SurfaceRequest;
+import androidx.core.util.Consumer;
+import io.flutter.plugin.common.BinaryMessenger;
+import io.flutter.plugins.camerax.GeneratedCameraXLibrary.ResolutionInfo;
+import io.flutter.plugins.camerax.GeneratedCameraXLibrary.SystemServicesFlutterApi.Reply;
+import io.flutter.view.TextureRegistry;
+import java.util.concurrent.Executor;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.ArgumentCaptor;
+import org.mockito.Mock;
+import org.mockito.junit.MockitoJUnit;
+import org.mockito.junit.MockitoRule;
+import org.robolectric.RobolectricTestRunner;
+
+@RunWith(RobolectricTestRunner.class)
+public class PreviewTest {
+ @Rule public MockitoRule mockitoRule = MockitoJUnit.rule();
+
+ @Mock public Preview mockPreview;
+ @Mock public BinaryMessenger mockBinaryMessenger;
+ @Mock public TextureRegistry mockTextureRegistry;
+ @Mock public CameraXProxy mockCameraXProxy;
+
+ InstanceManager testInstanceManager;
+
+ @Before
+ public void setUp() {
+ testInstanceManager = spy(InstanceManager.open(identifier -> {}));
+ }
+
+ @After
+ public void tearDown() {
+ testInstanceManager.close();
+ }
+
+ @Test
+ public void create_createsPreviewWithCorrectConfiguration() {
+ final PreviewHostApiImpl previewHostApi =
+ new PreviewHostApiImpl(mockBinaryMessenger, testInstanceManager, mockTextureRegistry);
+ final Preview.Builder mockPreviewBuilder = mock(Preview.Builder.class);
+ final int targetRotation = 90;
+ final int targetResolutionWidth = 10;
+ final int targetResolutionHeight = 50;
+ final Long previewIdentifier = 3L;
+ final GeneratedCameraXLibrary.ResolutionInfo resolutionInfo =
+ new GeneratedCameraXLibrary.ResolutionInfo.Builder()
+ .setWidth(Long.valueOf(targetResolutionWidth))
+ .setHeight(Long.valueOf(targetResolutionHeight))
+ .build();
+
+ previewHostApi.cameraXProxy = mockCameraXProxy;
+ when(mockCameraXProxy.createPreviewBuilder()).thenReturn(mockPreviewBuilder);
+ when(mockPreviewBuilder.build()).thenReturn(mockPreview);
+
+ final ArgumentCaptor<Size> sizeCaptor = ArgumentCaptor.forClass(Size.class);
+
+ previewHostApi.create(previewIdentifier, Long.valueOf(targetRotation), resolutionInfo);
+
+ verify(mockPreviewBuilder).setTargetRotation(targetRotation);
+ verify(mockPreviewBuilder).setTargetResolution(sizeCaptor.capture());
+ assertEquals(sizeCaptor.getValue().getWidth(), targetResolutionWidth);
+ assertEquals(sizeCaptor.getValue().getHeight(), targetResolutionHeight);
+ verify(mockPreviewBuilder).build();
+ verify(testInstanceManager).addDartCreatedInstance(mockPreview, previewIdentifier);
+ }
+
+ @Test
+ public void setSurfaceProviderTest_createsSurfaceProviderAndReturnsTextureEntryId() {
+ final PreviewHostApiImpl previewHostApi =
+ spy(new PreviewHostApiImpl(mockBinaryMessenger, testInstanceManager, mockTextureRegistry));
+ final TextureRegistry.SurfaceTextureEntry mockSurfaceTextureEntry =
+ mock(TextureRegistry.SurfaceTextureEntry.class);
+ final SurfaceTexture mockSurfaceTexture = mock(SurfaceTexture.class);
+ final Long previewIdentifier = 5L;
+ final Long surfaceTextureEntryId = 120L;
+
+ previewHostApi.cameraXProxy = mockCameraXProxy;
+ testInstanceManager.addDartCreatedInstance(mockPreview, previewIdentifier);
+
+ when(mockTextureRegistry.createSurfaceTexture()).thenReturn(mockSurfaceTextureEntry);
+ when(mockSurfaceTextureEntry.surfaceTexture()).thenReturn(mockSurfaceTexture);
+ when(mockSurfaceTextureEntry.id()).thenReturn(surfaceTextureEntryId);
+
+ final ArgumentCaptor<Preview.SurfaceProvider> surfaceProviderCaptor =
+ ArgumentCaptor.forClass(Preview.SurfaceProvider.class);
+ final ArgumentCaptor<Surface> surfaceCaptor = ArgumentCaptor.forClass(Surface.class);
+ final ArgumentCaptor<Consumer> consumerCaptor = ArgumentCaptor.forClass(Consumer.class);
+
+ // Test that surface provider was set and the surface texture ID was returned.
+ assertEquals(previewHostApi.setSurfaceProvider(previewIdentifier), surfaceTextureEntryId);
+ verify(mockPreview).setSurfaceProvider(surfaceProviderCaptor.capture());
+ verify(previewHostApi).createSurfaceProvider(mockSurfaceTexture);
+ }
+
+ @Test
+ public void createSurfaceProvider_createsExpectedPreviewSurfaceProvider() {
+ final PreviewHostApiImpl previewHostApi =
+ new PreviewHostApiImpl(mockBinaryMessenger, testInstanceManager, mockTextureRegistry);
+ final SurfaceTexture mockSurfaceTexture = mock(SurfaceTexture.class);
+ final Surface mockSurface = mock(Surface.class);
+ final SurfaceRequest mockSurfaceRequest = mock(SurfaceRequest.class);
+ final SurfaceRequest.Result mockSurfaceRequestResult = mock(SurfaceRequest.Result.class);
+ final SystemServicesFlutterApiImpl mockSystemServicesFlutterApi =
+ mock(SystemServicesFlutterApiImpl.class);
+ final int resolutionWidth = 200;
+ final int resolutionHeight = 500;
+
+ previewHostApi.cameraXProxy = mockCameraXProxy;
+ when(mockCameraXProxy.createSurface(mockSurfaceTexture)).thenReturn(mockSurface);
+ when(mockSurfaceRequest.getResolution())
+ .thenReturn(new Size(resolutionWidth, resolutionHeight));
+ when(mockCameraXProxy.createSystemServicesFlutterApiImpl(mockBinaryMessenger))
+ .thenReturn(mockSystemServicesFlutterApi);
+
+ final ArgumentCaptor<Surface> surfaceCaptor = ArgumentCaptor.forClass(Surface.class);
+ final ArgumentCaptor<Consumer> consumerCaptor = ArgumentCaptor.forClass(Consumer.class);
+
+ Preview.SurfaceProvider previewSurfaceProvider =
+ previewHostApi.createSurfaceProvider(mockSurfaceTexture);
+ previewSurfaceProvider.onSurfaceRequested(mockSurfaceRequest);
+
+ verify(mockSurfaceTexture).setDefaultBufferSize(resolutionWidth, resolutionHeight);
+ verify(mockSurfaceRequest)
+ .provideSurface(surfaceCaptor.capture(), any(Executor.class), consumerCaptor.capture());
+
+ // Test that the surface derived from the surface texture entry will be provided to the surface request.
+ assertEquals(surfaceCaptor.getValue(), mockSurface);
+
+ // Test that the Consumer used to handle surface request result releases Flutter surface texture appropriately
+ // and sends camera errors appropriately.
+ Consumer<SurfaceRequest.Result> capturedConsumer = consumerCaptor.getValue();
+
+ // Case where Surface should be released.
+ when(mockSurfaceRequestResult.getResultCode())
+ .thenReturn(SurfaceRequest.Result.RESULT_REQUEST_CANCELLED);
+ capturedConsumer.accept(mockSurfaceRequestResult);
+ verify(mockSurface).release();
+ reset(mockSurface);
+
+ when(mockSurfaceRequestResult.getResultCode())
+ .thenReturn(SurfaceRequest.Result.RESULT_REQUEST_CANCELLED);
+ capturedConsumer.accept(mockSurfaceRequestResult);
+ verify(mockSurface).release();
+ reset(mockSurface);
+
+ when(mockSurfaceRequestResult.getResultCode())
+ .thenReturn(SurfaceRequest.Result.RESULT_WILL_NOT_PROVIDE_SURFACE);
+ capturedConsumer.accept(mockSurfaceRequestResult);
+ verify(mockSurface).release();
+ reset(mockSurface);
+
+ when(mockSurfaceRequestResult.getResultCode())
+ .thenReturn(SurfaceRequest.Result.RESULT_SURFACE_USED_SUCCESSFULLY);
+ capturedConsumer.accept(mockSurfaceRequestResult);
+ verify(mockSurface).release();
+ reset(mockSurface);
+
+ // Case where error must be sent.
+ when(mockSurfaceRequestResult.getResultCode())
+ .thenReturn(SurfaceRequest.Result.RESULT_INVALID_SURFACE);
+ capturedConsumer.accept(mockSurfaceRequestResult);
+ verify(mockSurface).release();
+ verify(mockSystemServicesFlutterApi).sendCameraError(anyString(), any(Reply.class));
+ }
+
+ @Test
+ public void releaseFlutterSurfaceTexture_makesCallToReleaseFlutterSurfaceTexture() {
+ final PreviewHostApiImpl previewHostApi =
+ new PreviewHostApiImpl(mockBinaryMessenger, testInstanceManager, mockTextureRegistry);
+ final TextureRegistry.SurfaceTextureEntry mockSurfaceTextureEntry =
+ mock(TextureRegistry.SurfaceTextureEntry.class);
+
+ previewHostApi.flutterSurfaceTexture = mockSurfaceTextureEntry;
+
+ previewHostApi.releaseFlutterSurfaceTexture();
+ verify(mockSurfaceTextureEntry).release();
+ }
+
+ @Test
+ public void getResolutionInfo_makesCallToRetrievePreviewResolutionInfo() {
+ final PreviewHostApiImpl previewHostApi =
+ new PreviewHostApiImpl(mockBinaryMessenger, testInstanceManager, mockTextureRegistry);
+ final androidx.camera.core.ResolutionInfo mockResolutionInfo =
+ mock(androidx.camera.core.ResolutionInfo.class);
+ final Long previewIdentifier = 23L;
+ final int resolutionWidth = 500;
+ final int resolutionHeight = 200;
+
+ testInstanceManager.addDartCreatedInstance(mockPreview, previewIdentifier);
+ when(mockPreview.getResolutionInfo()).thenReturn(mockResolutionInfo);
+ when(mockResolutionInfo.getResolution())
+ .thenReturn(new Size(resolutionWidth, resolutionHeight));
+
+ ResolutionInfo resolutionInfo = previewHostApi.getResolutionInfo(previewIdentifier);
+ assertEquals(resolutionInfo.getWidth(), Long.valueOf(resolutionWidth));
+ assertEquals(resolutionInfo.getHeight(), Long.valueOf(resolutionHeight));
+ }
+}
diff --git a/packages/camera/camera_android_camerax/android/src/test/java/io/flutter/plugins/camerax/SystemServicesTest.java b/packages/camera/camera_android_camerax/android/src/test/java/io/flutter/plugins/camerax/SystemServicesTest.java
index d90c263..eb36c45 100644
--- a/packages/camera/camera_android_camerax/android/src/test/java/io/flutter/plugins/camerax/SystemServicesTest.java
+++ b/packages/camera/camera_android_camerax/android/src/test/java/io/flutter/plugins/camerax/SystemServicesTest.java
@@ -129,7 +129,8 @@
deviceOrientationChangeCallback.onChange(DeviceOrientation.PORTRAIT_DOWN);
verify(systemServicesFlutterApi)
- .onDeviceOrientationChanged(eq("PORTRAIT_DOWN"), any(Reply.class));
+ .sendDeviceOrientationChangedEvent(
+ eq(DeviceOrientation.PORTRAIT_DOWN.toString()), any(Reply.class));
// Test that the DeviceOrientationManager starts listening for device orientation changes.
verify(mockDeviceOrientationManager).start();
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 6d88699..1d315e5 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
@@ -10,6 +10,31 @@
import 'package:flutter/foundation.dart' show WriteBuffer, ReadBuffer;
import 'package:flutter/services.dart';
+class ResolutionInfo {
+ ResolutionInfo({
+ required this.width,
+ required this.height,
+ });
+
+ int width;
+ int height;
+
+ Object encode() {
+ final Map<Object?, Object?> pigeonMap = <Object?, Object?>{};
+ pigeonMap['width'] = width;
+ pigeonMap['height'] = height;
+ return pigeonMap;
+ }
+
+ static ResolutionInfo decode(Object message) {
+ final Map<Object?, Object?> pigeonMap = message as Map<Object?, Object?>;
+ return ResolutionInfo(
+ width: pigeonMap['width']! as int,
+ height: pigeonMap['height']! as int,
+ );
+ }
+}
+
class CameraPermissionsErrorData {
CameraPermissionsErrorData({
required this.errorCode,
@@ -634,6 +659,7 @@
static const MessageCodec<Object?> codec = _SystemServicesFlutterApiCodec();
void onDeviceOrientationChanged(String orientation);
+ void onCameraError(String errorDescription);
static void setup(SystemServicesFlutterApi? api,
{BinaryMessenger? binaryMessenger}) {
{
@@ -656,5 +682,174 @@
});
}
}
+ {
+ final BasicMessageChannel<Object?> channel = BasicMessageChannel<Object?>(
+ 'dev.flutter.pigeon.SystemServicesFlutterApi.onCameraError', codec,
+ binaryMessenger: binaryMessenger);
+ if (api == null) {
+ channel.setMessageHandler(null);
+ } else {
+ channel.setMessageHandler((Object? message) async {
+ assert(message != null,
+ 'Argument for dev.flutter.pigeon.SystemServicesFlutterApi.onCameraError was null.');
+ final List<Object?> args = (message as List<Object?>?)!;
+ final String? arg_errorDescription = (args[0] as String?);
+ assert(arg_errorDescription != null,
+ 'Argument for dev.flutter.pigeon.SystemServicesFlutterApi.onCameraError was null, expected non-null String.');
+ api.onCameraError(arg_errorDescription!);
+ return;
+ });
+ }
+ }
+ }
+}
+
+class _PreviewHostApiCodec extends StandardMessageCodec {
+ const _PreviewHostApiCodec();
+ @override
+ void writeValue(WriteBuffer buffer, Object? value) {
+ if (value is ResolutionInfo) {
+ buffer.putUint8(128);
+ writeValue(buffer, value.encode());
+ } else if (value is ResolutionInfo) {
+ buffer.putUint8(129);
+ writeValue(buffer, value.encode());
+ } else {
+ super.writeValue(buffer, value);
+ }
+ }
+
+ @override
+ Object? readValueOfType(int type, ReadBuffer buffer) {
+ switch (type) {
+ case 128:
+ return ResolutionInfo.decode(readValue(buffer)!);
+
+ case 129:
+ return ResolutionInfo.decode(readValue(buffer)!);
+
+ default:
+ return super.readValueOfType(type, buffer);
+ }
+ }
+}
+
+class PreviewHostApi {
+ /// Constructor for [PreviewHostApi]. The [binaryMessenger] named argument is
+ /// available for dependency injection. If it is left null, the default
+ /// BinaryMessenger will be used which routes to the host platform.
+ PreviewHostApi({BinaryMessenger? binaryMessenger})
+ : _binaryMessenger = binaryMessenger;
+
+ final BinaryMessenger? _binaryMessenger;
+
+ static const MessageCodec<Object?> codec = _PreviewHostApiCodec();
+
+ Future<void> create(int arg_identifier, int? arg_rotation,
+ ResolutionInfo? arg_targetResolution) async {
+ final BasicMessageChannel<Object?> channel = BasicMessageChannel<Object?>(
+ 'dev.flutter.pigeon.PreviewHostApi.create', codec,
+ binaryMessenger: _binaryMessenger);
+ final Map<Object?, Object?>? replyMap = await channel
+ .send(<Object?>[arg_identifier, arg_rotation, arg_targetResolution])
+ as Map<Object?, Object?>?;
+ if (replyMap == null) {
+ throw PlatformException(
+ code: 'channel-error',
+ message: 'Unable to establish connection on channel.',
+ );
+ } else if (replyMap['error'] != null) {
+ final Map<Object?, Object?> error =
+ (replyMap['error'] as Map<Object?, Object?>?)!;
+ throw PlatformException(
+ code: (error['code'] as String?)!,
+ message: error['message'] as String?,
+ details: error['details'],
+ );
+ } else {
+ return;
+ }
+ }
+
+ Future<int> setSurfaceProvider(int arg_identifier) async {
+ final BasicMessageChannel<Object?> channel = BasicMessageChannel<Object?>(
+ 'dev.flutter.pigeon.PreviewHostApi.setSurfaceProvider', codec,
+ binaryMessenger: _binaryMessenger);
+ final Map<Object?, Object?>? replyMap =
+ await channel.send(<Object?>[arg_identifier]) as Map<Object?, Object?>?;
+ if (replyMap == null) {
+ throw PlatformException(
+ code: 'channel-error',
+ message: 'Unable to establish connection on channel.',
+ );
+ } else if (replyMap['error'] != null) {
+ final Map<Object?, Object?> error =
+ (replyMap['error'] as Map<Object?, Object?>?)!;
+ throw PlatformException(
+ code: (error['code'] as String?)!,
+ message: error['message'] as String?,
+ details: error['details'],
+ );
+ } else if (replyMap['result'] == null) {
+ throw PlatformException(
+ code: 'null-error',
+ message: 'Host platform returned null value for non-null return value.',
+ );
+ } else {
+ return (replyMap['result'] as int?)!;
+ }
+ }
+
+ Future<void> releaseFlutterSurfaceTexture() async {
+ final BasicMessageChannel<Object?> channel = BasicMessageChannel<Object?>(
+ 'dev.flutter.pigeon.PreviewHostApi.releaseFlutterSurfaceTexture', codec,
+ binaryMessenger: _binaryMessenger);
+ final Map<Object?, Object?>? replyMap =
+ await channel.send(null) as Map<Object?, Object?>?;
+ if (replyMap == null) {
+ throw PlatformException(
+ code: 'channel-error',
+ message: 'Unable to establish connection on channel.',
+ );
+ } else if (replyMap['error'] != null) {
+ final Map<Object?, Object?> error =
+ (replyMap['error'] as Map<Object?, Object?>?)!;
+ throw PlatformException(
+ code: (error['code'] as String?)!,
+ message: error['message'] as String?,
+ details: error['details'],
+ );
+ } else {
+ return;
+ }
+ }
+
+ Future<ResolutionInfo> getResolutionInfo(int arg_identifier) async {
+ final BasicMessageChannel<Object?> channel = BasicMessageChannel<Object?>(
+ 'dev.flutter.pigeon.PreviewHostApi.getResolutionInfo', codec,
+ binaryMessenger: _binaryMessenger);
+ final Map<Object?, Object?>? replyMap =
+ await channel.send(<Object?>[arg_identifier]) as Map<Object?, Object?>?;
+ if (replyMap == null) {
+ throw PlatformException(
+ code: 'channel-error',
+ message: 'Unable to establish connection on channel.',
+ );
+ } else if (replyMap['error'] != null) {
+ final Map<Object?, Object?> error =
+ (replyMap['error'] as Map<Object?, Object?>?)!;
+ throw PlatformException(
+ code: (error['code'] as String?)!,
+ message: error['message'] as String?,
+ details: error['details'],
+ );
+ } else if (replyMap['result'] == null) {
+ throw PlatformException(
+ code: 'null-error',
+ message: 'Host platform returned null value for non-null return value.',
+ );
+ } else {
+ return (replyMap['result'] as ResolutionInfo?)!;
+ }
}
}
diff --git a/packages/camera/camera_android_camerax/lib/src/preview.dart b/packages/camera/camera_android_camerax/lib/src/preview.dart
new file mode 100644
index 0000000..602bcb3
--- /dev/null
+++ b/packages/camera/camera_android_camerax/lib/src/preview.dart
@@ -0,0 +1,126 @@
+// Copyright 2013 The Flutter Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+import 'package:flutter/services.dart' show BinaryMessenger;
+
+import 'camerax_library.g.dart';
+import 'instance_manager.dart';
+import 'java_object.dart';
+import 'use_case.dart';
+
+/// Use case that provides a camera preview stream for display.
+///
+/// See https://developer.android.com/reference/androidx/camera/core/Preview.
+class Preview extends UseCase {
+ /// Creates a [Preview].
+ Preview(
+ {BinaryMessenger? binaryMessenger,
+ InstanceManager? instanceManager,
+ this.targetRotation,
+ this.targetResolution})
+ : super.detached(
+ binaryMessenger: binaryMessenger,
+ instanceManager: instanceManager) {
+ _api = PreviewHostApiImpl(
+ binaryMessenger: binaryMessenger, instanceManager: instanceManager);
+ _api.createFromInstance(this, targetRotation, targetResolution);
+ }
+
+ /// Constructs a [Preview] that is not automatically attached to a native object.
+ Preview.detached(
+ {BinaryMessenger? binaryMessenger,
+ InstanceManager? instanceManager,
+ this.targetRotation,
+ this.targetResolution})
+ : super.detached(
+ binaryMessenger: binaryMessenger,
+ instanceManager: instanceManager) {
+ _api = PreviewHostApiImpl(
+ binaryMessenger: binaryMessenger, instanceManager: instanceManager);
+ }
+
+ late final PreviewHostApiImpl _api;
+
+ /// Target rotation of the camera used for the preview stream.
+ final int? targetRotation;
+
+ /// Target resolution of the camera preview stream.
+ final ResolutionInfo? targetResolution;
+
+ /// Sets the surface provider for the preview stream.
+ ///
+ /// Returns the ID of the FlutterSurfaceTextureEntry used on the native end
+ /// used to display the preview stream on a [Texture] of the same ID.
+ Future<int> setSurfaceProvider() {
+ return _api.setSurfaceProviderFromInstance(this);
+ }
+
+ /// Releases Flutter surface texture used to provide a surface for the preview
+ /// stream.
+ void releaseFlutterSurfaceTexture() {
+ _api.releaseFlutterSurfaceTextureFromInstance();
+ }
+
+ /// Retrieves the selected resolution information of this [Preview].
+ Future<ResolutionInfo> getResolutionInfo() {
+ return _api.getResolutionInfoFromInstance(this);
+ }
+}
+
+/// Host API implementation of [Preview].
+class PreviewHostApiImpl extends PreviewHostApi {
+ /// Constructs a [PreviewHostApiImpl].
+ PreviewHostApiImpl({this.binaryMessenger, InstanceManager? instanceManager}) {
+ this.instanceManager = instanceManager ?? JavaObject.globalInstanceManager;
+ }
+
+ /// Receives binary data across the Flutter platform barrier.
+ ///
+ /// If it is null, the default BinaryMessenger will be used which routes to
+ /// the host platform.
+ final BinaryMessenger? binaryMessenger;
+
+ /// Maintains instances stored to communicate with native language objects.
+ late final InstanceManager instanceManager;
+
+ /// Creates a [Preview] with the target rotation provided if specified.
+ void createFromInstance(
+ Preview instance, int? targetRotation, ResolutionInfo? targetResolution) {
+ final int identifier = instanceManager.addDartCreatedInstance(instance,
+ onCopy: (Preview original) {
+ return Preview.detached(
+ binaryMessenger: binaryMessenger,
+ instanceManager: instanceManager,
+ targetRotation: original.targetRotation);
+ });
+ create(identifier, targetRotation, targetResolution);
+ }
+
+ /// Sets the surface provider of the specified [Preview] instance and returns
+ /// the ID corresponding to the surface it will provide.
+ Future<int> setSurfaceProviderFromInstance(Preview instance) async {
+ final int? identifier = instanceManager.getIdentifier(instance);
+ assert(identifier != null,
+ 'No Preview has the identifer of that requested to set the surface provider on.');
+
+ final int surfaceTextureEntryId = await setSurfaceProvider(identifier!);
+ return surfaceTextureEntryId;
+ }
+
+ /// Releases Flutter surface texture used to provide a surface for the preview
+ /// stream if a surface provider was set for a [Preview] instance.
+ void releaseFlutterSurfaceTextureFromInstance() {
+ releaseFlutterSurfaceTexture();
+ }
+
+ /// Gets the resolution information of the specified [Preview] instance.
+ Future<ResolutionInfo> getResolutionInfoFromInstance(Preview instance) async {
+ final int? identifier = instanceManager.getIdentifier(instance);
+ assert(identifier != null,
+ 'No Preview has the identifer of that requested to get the resolution information for.');
+
+ final ResolutionInfo resolutionInfo = await getResolutionInfo(identifier!);
+ return resolutionInfo;
+ }
+}
diff --git a/packages/camera/camera_android_camerax/lib/src/surface.dart b/packages/camera/camera_android_camerax/lib/src/surface.dart
new file mode 100644
index 0000000..ea8cf8c
--- /dev/null
+++ b/packages/camera/camera_android_camerax/lib/src/surface.dart
@@ -0,0 +1,34 @@
+// Copyright 2013 The Flutter Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+import 'java_object.dart';
+
+/// Handle onto the raw buffer managed by screen compositor.
+///
+/// See https://developer.android.com/reference/android/view/Surface.html.
+class Surface extends JavaObject {
+ /// Creates a detached [UseCase].
+ Surface.detached({super.binaryMessenger, super.instanceManager})
+ : super.detached();
+
+ /// Rotation constant to signify the natural orientation.
+ ///
+ /// See https://developer.android.com/reference/android/view/Surface.html#ROTATION_0.
+ static const int ROTATION_0 = 0;
+
+ /// Rotation constant to signify a 90 degrees rotation.
+ ///
+ /// See https://developer.android.com/reference/android/view/Surface.html#ROTATION_90.
+ static const int ROTATION_90 = 1;
+
+ /// Rotation constant to signify a 180 degrees rotation.
+ ///
+ /// See https://developer.android.com/reference/android/view/Surface.html#ROTATION_180.
+ static const int ROTATION_180 = 2;
+
+ /// Rotation constant to signify a 270 degrees rotation.
+ ///
+ /// See https://developer.android.com/reference/android/view/Surface.html#ROTATION_270.
+ static const int ROTATION_270 = 3;
+}
diff --git a/packages/camera/camera_android_camerax/lib/src/system_services.dart b/packages/camera/camera_android_camerax/lib/src/system_services.dart
index bc6477e..4ca90e2 100644
--- a/packages/camera/camera_android_camerax/lib/src/system_services.dart
+++ b/packages/camera/camera_android_camerax/lib/src/system_services.dart
@@ -16,7 +16,7 @@
// ignore_for_file: avoid_classes_with_only_static_members
/// Utility class that offers access to Android system services needed for
-/// camera usage.
+/// camera usage and other informational streams.
class SystemServices {
/// Stream that emits the device orientation whenever it is changed.
///
@@ -26,6 +26,10 @@
deviceOrientationChangedStreamController =
StreamController<DeviceOrientationChangedEvent>.broadcast();
+ /// Stream that emits the errors caused by camera usage on the native side.
+ static final StreamController<String> cameraErrorStreamController =
+ StreamController<String>.broadcast();
+
/// Requests permission to access the camera and audio if specified.
static Future<void> requestCameraPermissions(bool enableAudio,
{BinaryMessenger? binaryMessenger}) {
@@ -134,4 +138,12 @@
'"$orientation" is not a valid DeviceOrientation value');
}
}
+
+ /// Callback method for any errors caused by camera usage on the Java side.
+ @override
+ void onCameraError(String errorDescription) {
+ // TODO(camsim99): Use this to implement onCameraError method in plugin.
+ // See https://github.com/flutter/flutter/issues/119571 for context.
+ SystemServices.cameraErrorStreamController.add(errorDescription);
+ }
}
diff --git a/packages/camera/camera_android_camerax/pigeons/camerax_library.dart b/packages/camera/camera_android_camerax/pigeons/camerax_library.dart
index 7fce6ce..4172cd7 100644
--- a/packages/camera/camera_android_camerax/pigeons/camerax_library.dart
+++ b/packages/camera/camera_android_camerax/pigeons/camerax_library.dart
@@ -26,6 +26,16 @@
),
),
)
+class ResolutionInfo {
+ ResolutionInfo({
+ required this.width,
+ required this.height,
+ });
+
+ int width;
+ int height;
+}
+
class CameraPermissionsErrorData {
CameraPermissionsErrorData({
required this.errorCode,
@@ -107,4 +117,17 @@
@FlutterApi()
abstract class SystemServicesFlutterApi {
void onDeviceOrientationChanged(String orientation);
+
+ void onCameraError(String errorDescription);
+}
+
+@HostApi(dartHostTestHandler: 'TestPreviewHostApi')
+abstract class PreviewHostApi {
+ void create(int identifier, int? rotation, ResolutionInfo? targetResolution);
+
+ int setSurfaceProvider(int identifier);
+
+ void releaseFlutterSurfaceTexture();
+
+ ResolutionInfo getResolutionInfo(int identifier);
}
diff --git a/packages/camera/camera_android_camerax/test/preview_test.dart b/packages/camera/camera_android_camerax/test/preview_test.dart
new file mode 100644
index 0000000..36b56f0
--- /dev/null
+++ b/packages/camera/camera_android_camerax/test/preview_test.dart
@@ -0,0 +1,138 @@
+// Copyright 2013 The Flutter Authors. All rights reserved.
+// 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/camerax_library.g.dart';
+import 'package:camera_android_camerax/src/instance_manager.dart';
+import 'package:camera_android_camerax/src/preview.dart';
+import 'package:flutter_test/flutter_test.dart';
+import 'package:mockito/annotations.dart';
+import 'package:mockito/mockito.dart';
+
+import 'preview_test.mocks.dart';
+import 'test_camerax_library.g.dart';
+
+@GenerateMocks(<Type>[TestPreviewHostApi])
+void main() {
+ TestWidgetsFlutterBinding.ensureInitialized();
+
+ group('Preview', () {
+ tearDown(() => TestPreviewHostApi.setup(null));
+
+ test('detached create does not call create on the Java side', () async {
+ final MockTestPreviewHostApi mockApi = MockTestPreviewHostApi();
+ TestPreviewHostApi.setup(mockApi);
+
+ final InstanceManager instanceManager = InstanceManager(
+ onWeakReferenceRemoved: (_) {},
+ );
+ Preview.detached(
+ instanceManager: instanceManager,
+ targetRotation: 90,
+ targetResolution: ResolutionInfo(width: 50, height: 10),
+ );
+
+ verifyNever(mockApi.create(argThat(isA<int>()), argThat(isA<int>()),
+ argThat(isA<ResolutionInfo>())));
+ });
+
+ test('create calls create on the Java side', () async {
+ final MockTestPreviewHostApi mockApi = MockTestPreviewHostApi();
+ TestPreviewHostApi.setup(mockApi);
+
+ final InstanceManager instanceManager = InstanceManager(
+ onWeakReferenceRemoved: (_) {},
+ );
+ const int targetRotation = 90;
+ const int targetResolutionWidth = 10;
+ const int targetResolutionHeight = 50;
+ Preview(
+ instanceManager: instanceManager,
+ targetRotation: targetRotation,
+ targetResolution: ResolutionInfo(
+ width: targetResolutionWidth, height: targetResolutionHeight),
+ );
+
+ final VerificationResult createVerification = verify(mockApi.create(
+ argThat(isA<int>()), argThat(equals(targetRotation)), captureAny));
+ final ResolutionInfo capturedResolutionInfo =
+ createVerification.captured.single as ResolutionInfo;
+ expect(capturedResolutionInfo.width, equals(targetResolutionWidth));
+ expect(capturedResolutionInfo.height, equals(targetResolutionHeight));
+ });
+
+ test(
+ 'setSurfaceProvider makes call to set surface provider for preview instance',
+ () async {
+ final MockTestPreviewHostApi mockApi = MockTestPreviewHostApi();
+ TestPreviewHostApi.setup(mockApi);
+
+ final InstanceManager instanceManager = InstanceManager(
+ onWeakReferenceRemoved: (_) {},
+ );
+ const int textureId = 8;
+ final Preview preview = Preview.detached(
+ instanceManager: instanceManager,
+ );
+ instanceManager.addHostCreatedInstance(
+ preview,
+ 0,
+ onCopy: (_) => Preview.detached(),
+ );
+
+ when(mockApi.setSurfaceProvider(instanceManager.getIdentifier(preview)))
+ .thenReturn(textureId);
+ expect(await preview.setSurfaceProvider(), equals(textureId));
+
+ verify(
+ mockApi.setSurfaceProvider(instanceManager.getIdentifier(preview)));
+ });
+
+ test(
+ 'releaseFlutterSurfaceTexture makes call to relase flutter surface texture entry',
+ () async {
+ final MockTestPreviewHostApi mockApi = MockTestPreviewHostApi();
+ TestPreviewHostApi.setup(mockApi);
+
+ final Preview preview = Preview.detached();
+
+ preview.releaseFlutterSurfaceTexture();
+
+ verify(mockApi.releaseFlutterSurfaceTexture());
+ });
+
+ test(
+ 'getResolutionInfo makes call to get resolution information for preview instance',
+ () async {
+ final MockTestPreviewHostApi mockApi = MockTestPreviewHostApi();
+ TestPreviewHostApi.setup(mockApi);
+
+ final InstanceManager instanceManager = InstanceManager(
+ onWeakReferenceRemoved: (_) {},
+ );
+ final Preview preview = Preview.detached(
+ instanceManager: instanceManager,
+ );
+ const int resolutionWidth = 10;
+ const int resolutionHeight = 60;
+ final ResolutionInfo testResolutionInfo =
+ ResolutionInfo(width: resolutionWidth, height: resolutionHeight);
+
+ instanceManager.addHostCreatedInstance(
+ preview,
+ 0,
+ onCopy: (_) => Preview.detached(),
+ );
+
+ when(mockApi.getResolutionInfo(instanceManager.getIdentifier(preview)))
+ .thenReturn(testResolutionInfo);
+
+ final ResolutionInfo previewResolutionInfo =
+ await preview.getResolutionInfo();
+ expect(previewResolutionInfo.width, equals(resolutionWidth));
+ expect(previewResolutionInfo.height, equals(resolutionHeight));
+
+ verify(mockApi.getResolutionInfo(instanceManager.getIdentifier(preview)));
+ });
+ });
+}
diff --git a/packages/camera/camera_android_camerax/test/preview_test.mocks.dart b/packages/camera/camera_android_camerax/test/preview_test.mocks.dart
new file mode 100644
index 0000000..60fa152
--- /dev/null
+++ b/packages/camera/camera_android_camerax/test/preview_test.mocks.dart
@@ -0,0 +1,89 @@
+// Mocks generated by Mockito 5.3.2 from annotations
+// in camera_android_camerax/test/preview_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 _i2;
+import 'package:mockito/mockito.dart' as _i1;
+
+import 'test_camerax_library.g.dart' as _i3;
+
+// ignore_for_file: type=lint
+// ignore_for_file: avoid_redundant_argument_values
+// ignore_for_file: avoid_setters_without_getters
+// ignore_for_file: comment_references
+// ignore_for_file: implementation_imports
+// ignore_for_file: invalid_use_of_visible_for_testing_member
+// ignore_for_file: prefer_const_constructors
+// ignore_for_file: unnecessary_parenthesis
+// ignore_for_file: camel_case_types
+// ignore_for_file: subtype_of_sealed_class
+
+class _FakeResolutionInfo_0 extends _i1.SmartFake
+ implements _i2.ResolutionInfo {
+ _FakeResolutionInfo_0(
+ Object parent,
+ Invocation parentInvocation,
+ ) : super(
+ parent,
+ parentInvocation,
+ );
+}
+
+/// A class which mocks [TestPreviewHostApi].
+///
+/// See the documentation for Mockito's code generation for more information.
+class MockTestPreviewHostApi extends _i1.Mock
+ implements _i3.TestPreviewHostApi {
+ MockTestPreviewHostApi() {
+ _i1.throwOnMissingStub(this);
+ }
+
+ @override
+ void create(
+ int? identifier,
+ int? rotation,
+ _i2.ResolutionInfo? targetResolution,
+ ) =>
+ super.noSuchMethod(
+ Invocation.method(
+ #create,
+ [
+ identifier,
+ rotation,
+ targetResolution,
+ ],
+ ),
+ returnValueForMissingStub: null,
+ );
+ @override
+ int setSurfaceProvider(int? identifier) => (super.noSuchMethod(
+ Invocation.method(
+ #setSurfaceProvider,
+ [identifier],
+ ),
+ returnValue: 0,
+ ) as int);
+ @override
+ void releaseFlutterSurfaceTexture() => super.noSuchMethod(
+ Invocation.method(
+ #releaseFlutterSurfaceTexture,
+ [],
+ ),
+ returnValueForMissingStub: null,
+ );
+ @override
+ _i2.ResolutionInfo getResolutionInfo(int? identifier) => (super.noSuchMethod(
+ Invocation.method(
+ #getResolutionInfo,
+ [identifier],
+ ),
+ returnValue: _FakeResolutionInfo_0(
+ this,
+ Invocation.method(
+ #getResolutionInfo,
+ [identifier],
+ ),
+ ),
+ ) as _i2.ResolutionInfo);
+}
diff --git a/packages/camera/camera_android_camerax/test/system_services_test.dart b/packages/camera/camera_android_camerax/test/system_services_test.dart
index 2d2cea6..38037ea 100644
--- a/packages/camera/camera_android_camerax/test/system_services_test.dart
+++ b/packages/camera/camera_android_camerax/test/system_services_test.dart
@@ -97,5 +97,14 @@
'message',
'"FAKE_ORIENTATION" is not a valid DeviceOrientation value')));
});
+
+ test('onCameraError adds new error to stream', () {
+ const String testErrorDescription = 'Test error description!';
+ SystemServices.cameraErrorStreamController.stream
+ .listen((String errorDescription) {
+ expect(errorDescription, equals(testErrorDescription));
+ });
+ SystemServicesFlutterApiImpl().onCameraError(testErrorDescription);
+ });
});
}
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 55f2c5e..3f0e9c2 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
@@ -356,3 +356,120 @@
}
}
}
+
+class _TestPreviewHostApiCodec extends StandardMessageCodec {
+ const _TestPreviewHostApiCodec();
+ @override
+ void writeValue(WriteBuffer buffer, Object? value) {
+ if (value is ResolutionInfo) {
+ buffer.putUint8(128);
+ writeValue(buffer, value.encode());
+ } else if (value is ResolutionInfo) {
+ buffer.putUint8(129);
+ writeValue(buffer, value.encode());
+ } else {
+ super.writeValue(buffer, value);
+ }
+ }
+
+ @override
+ Object? readValueOfType(int type, ReadBuffer buffer) {
+ switch (type) {
+ case 128:
+ return ResolutionInfo.decode(readValue(buffer)!);
+
+ case 129:
+ return ResolutionInfo.decode(readValue(buffer)!);
+
+ default:
+ return super.readValueOfType(type, buffer);
+ }
+ }
+}
+
+abstract class TestPreviewHostApi {
+ static const MessageCodec<Object?> codec = _TestPreviewHostApiCodec();
+
+ void create(int identifier, int? rotation, ResolutionInfo? targetResolution);
+ int setSurfaceProvider(int identifier);
+ void releaseFlutterSurfaceTexture();
+ ResolutionInfo getResolutionInfo(int identifier);
+ static void setup(TestPreviewHostApi? api,
+ {BinaryMessenger? binaryMessenger}) {
+ {
+ final BasicMessageChannel<Object?> channel = BasicMessageChannel<Object?>(
+ 'dev.flutter.pigeon.PreviewHostApi.create', codec,
+ binaryMessenger: binaryMessenger);
+ if (api == null) {
+ channel.setMockMessageHandler(null);
+ } else {
+ channel.setMockMessageHandler((Object? message) async {
+ assert(message != null,
+ 'Argument for dev.flutter.pigeon.PreviewHostApi.create was null.');
+ final List<Object?> args = (message as List<Object?>?)!;
+ final int? arg_identifier = (args[0] as int?);
+ assert(arg_identifier != null,
+ 'Argument for dev.flutter.pigeon.PreviewHostApi.create was null, expected non-null int.');
+ final int? arg_rotation = (args[1] as int?);
+ final ResolutionInfo? arg_targetResolution =
+ (args[2] as ResolutionInfo?);
+ api.create(arg_identifier!, arg_rotation, arg_targetResolution);
+ return <Object?, Object?>{};
+ });
+ }
+ }
+ {
+ final BasicMessageChannel<Object?> channel = BasicMessageChannel<Object?>(
+ 'dev.flutter.pigeon.PreviewHostApi.setSurfaceProvider', codec,
+ binaryMessenger: binaryMessenger);
+ if (api == null) {
+ channel.setMockMessageHandler(null);
+ } else {
+ channel.setMockMessageHandler((Object? message) async {
+ assert(message != null,
+ 'Argument for dev.flutter.pigeon.PreviewHostApi.setSurfaceProvider was null.');
+ final List<Object?> args = (message as List<Object?>?)!;
+ final int? arg_identifier = (args[0] as int?);
+ assert(arg_identifier != null,
+ 'Argument for dev.flutter.pigeon.PreviewHostApi.setSurfaceProvider was null, expected non-null int.');
+ final int output = api.setSurfaceProvider(arg_identifier!);
+ return <Object?, Object?>{'result': output};
+ });
+ }
+ }
+ {
+ final BasicMessageChannel<Object?> channel = BasicMessageChannel<Object?>(
+ 'dev.flutter.pigeon.PreviewHostApi.releaseFlutterSurfaceTexture',
+ codec,
+ binaryMessenger: binaryMessenger);
+ if (api == null) {
+ channel.setMockMessageHandler(null);
+ } else {
+ channel.setMockMessageHandler((Object? message) async {
+ // ignore message
+ api.releaseFlutterSurfaceTexture();
+ return <Object?, Object?>{};
+ });
+ }
+ }
+ {
+ final BasicMessageChannel<Object?> channel = BasicMessageChannel<Object?>(
+ 'dev.flutter.pigeon.PreviewHostApi.getResolutionInfo', codec,
+ binaryMessenger: binaryMessenger);
+ if (api == null) {
+ channel.setMockMessageHandler(null);
+ } else {
+ channel.setMockMessageHandler((Object? message) async {
+ assert(message != null,
+ 'Argument for dev.flutter.pigeon.PreviewHostApi.getResolutionInfo was null.');
+ final List<Object?> args = (message as List<Object?>?)!;
+ final int? arg_identifier = (args[0] as int?);
+ assert(arg_identifier != null,
+ 'Argument for dev.flutter.pigeon.PreviewHostApi.getResolutionInfo was null, expected non-null int.');
+ final ResolutionInfo output = api.getResolutionInfo(arg_identifier!);
+ return <Object?, Object?>{'result': output};
+ });
+ }
+ }
+ }
+}