diff --git a/packages/camera/camera_android/CHANGELOG.md b/packages/camera/camera_android/CHANGELOG.md index 3ffef761e2f..46bd41af128 100644 --- a/packages/camera/camera_android/CHANGELOG.md +++ b/packages/camera/camera_android/CHANGELOG.md @@ -1,3 +1,7 @@ +## 0.10.9+14 + +* Converts native to Dart platform calls to Pigeon. + ## 0.10.9+13 * Converts `getAvailableCameras` to Pigeon. diff --git a/packages/camera/camera_android/android/src/main/java/io/flutter/plugins/camera/CameraUtils.java b/packages/camera/camera_android/android/src/main/java/io/flutter/plugins/camera/CameraUtils.java index c9eedaa3b79..edd873d82b9 100644 --- a/packages/camera/camera_android/android/src/main/java/io/flutter/plugins/camera/CameraUtils.java +++ b/packages/camera/camera_android/android/src/main/java/io/flutter/plugins/camera/CameraUtils.java @@ -12,6 +12,8 @@ import android.hardware.camera2.CameraMetadata; import androidx.annotation.NonNull; import io.flutter.embedding.engine.systemchannels.PlatformChannel; +import io.flutter.plugins.camera.features.autofocus.FocusMode; +import io.flutter.plugins.camera.features.exposurelock.ExposureMode; import java.util.ArrayList; import java.util.List; @@ -143,4 +145,62 @@ public static List getAvailableCameras( } return cameras; } + + /** + * Converts a DeviceOrientation from the systemchannels package to a PlatformDeviceOrientation + * from Pigeon. + * + * @param orientation A DeviceOrientation. + * @return The corresponding PlatformDeviceOrientation. + */ + @NonNull + public static Messages.PlatformDeviceOrientation orientationToPigeon( + @NonNull PlatformChannel.DeviceOrientation orientation) { + switch (orientation) { + case PORTRAIT_UP: + return Messages.PlatformDeviceOrientation.PORTRAIT_UP; + case PORTRAIT_DOWN: + return Messages.PlatformDeviceOrientation.PORTRAIT_DOWN; + case LANDSCAPE_LEFT: + return Messages.PlatformDeviceOrientation.LANDSCAPE_LEFT; + case LANDSCAPE_RIGHT: + return Messages.PlatformDeviceOrientation.LANDSCAPE_RIGHT; + } + return Messages.PlatformDeviceOrientation.PORTRAIT_UP; + } + + /** + * Converts a FocusMode from the autofocus package to a PlatformFocusMode from Pigeon. + * + * @param focusMode A FocusMode. + * @return The corresponding PlatformFocusMode. + */ + @NonNull + public static Messages.PlatformFocusMode focusModeToPigeon(@NonNull FocusMode focusMode) { + switch (focusMode) { + case auto: + return Messages.PlatformFocusMode.AUTO; + case locked: + return Messages.PlatformFocusMode.LOCKED; + } + return Messages.PlatformFocusMode.AUTO; + } + + /** + * Converts an ExposureMode from the exposurelock package to a PlatformExposureMode from Pigeon. + * + * @param exposureMode An ExposureMode. + * @return The corresponding PlatformExposureMode. + */ + @NonNull + public static Messages.PlatformExposureMode exposureModeToPigeon( + @NonNull ExposureMode exposureMode) { + switch (exposureMode) { + case auto: + return Messages.PlatformExposureMode.AUTO; + case locked: + return Messages.PlatformExposureMode.LOCKED; + } + return Messages.PlatformExposureMode.AUTO; + } } diff --git a/packages/camera/camera_android/android/src/main/java/io/flutter/plugins/camera/DartMessenger.java b/packages/camera/camera_android/android/src/main/java/io/flutter/plugins/camera/DartMessenger.java index e2b2ef80820..819c57da043 100644 --- a/packages/camera/camera_android/android/src/main/java/io/flutter/plugins/camera/DartMessenger.java +++ b/packages/camera/camera_android/android/src/main/java/io/flutter/plugins/camera/DartMessenger.java @@ -5,69 +5,36 @@ package io.flutter.plugins.camera; import android.os.Handler; -import android.text.TextUtils; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import io.flutter.embedding.engine.systemchannels.PlatformChannel; -import io.flutter.plugin.common.BinaryMessenger; import io.flutter.plugin.common.MethodChannel; import io.flutter.plugins.camera.features.autofocus.FocusMode; import io.flutter.plugins.camera.features.exposurelock.ExposureMode; -import java.util.HashMap; -import java.util.Map; /** Utility class that facilitates communication to the Flutter client */ public class DartMessenger { @NonNull private final Handler handler; - @Nullable MethodChannel cameraChannel; - @Nullable MethodChannel deviceChannel; - - /** Specifies the different device related message types. */ - enum DeviceEventType { - /** Indicates the device's orientation has changed. */ - ORIENTATION_CHANGED("orientation_changed"); - final String method; - - DeviceEventType(String method) { - this.method = method; - } - } - - /** Specifies the different camera related message types. */ - enum CameraEventType { - /** Indicates that an error occurred while interacting with the camera. */ - ERROR("error"), - /** Indicates that the camera is closing. */ - CLOSING("camera_closing"), - /** Indicates that the camera is initialized. */ - INITIALIZED("initialized"); - - final String method; - - /** - * Converts the supplied method name to the matching {@link CameraEventType}. - * - * @param method name to be converted into a {@link CameraEventType}. - */ - CameraEventType(String method) { - this.method = method; - } - } + Messages.CameraGlobalEventApi globalEventApi; + Messages.CameraEventApi eventApi; /** * Creates a new instance of the {@link DartMessenger} class. * - * @param messenger is the {@link BinaryMessenger} that is used to communicate with Flutter. - * @param cameraId identifies the camera which is the source of the communication. * @param handler the handler used to manage the thread's message queue. This should always be a * handler managing the main thread since communication with Flutter should always happen on * the main thread. The handler is mainly supplied so it will be easier test this class. + * @param globalEventApi the API used to consume calls to dart that are not tied to a specific + * camera instance. + * @param eventApi the API used to consume calls to dart that are tied to this specific camera. */ - DartMessenger(BinaryMessenger messenger, long cameraId, @NonNull Handler handler) { - cameraChannel = - new MethodChannel(messenger, "plugins.flutter.io/camera_android/camera" + cameraId); - deviceChannel = new MethodChannel(messenger, "plugins.flutter.io/camera_android/fromPlatform"); + DartMessenger( + @NonNull Handler handler, + Messages.CameraGlobalEventApi globalEventApi, + Messages.CameraEventApi eventApi) { this.handler = handler; + this.globalEventApi = globalEventApi; + this.eventApi = eventApi; } /** @@ -77,13 +44,10 @@ enum CameraEventType { */ public void sendDeviceOrientationChangeEvent( @NonNull PlatformChannel.DeviceOrientation orientation) { - this.send( - DeviceEventType.ORIENTATION_CHANGED, - new HashMap() { - { - put("orientation", CameraUtils.serializeDeviceOrientation(orientation)); - } - }); + handler.post( + () -> + globalEventApi.deviceOrientationChanged( + CameraUtils.orientationToPigeon(orientation), new NoOpVoidResult())); } /** @@ -109,25 +73,30 @@ void sendCameraInitializedEvent( assert (focusMode != null); assert (exposurePointSupported != null); assert (focusPointSupported != null); - this.send( - CameraEventType.INITIALIZED, - new HashMap() { - { - put("previewWidth", previewWidth.doubleValue()); - put("previewHeight", previewHeight.doubleValue()); - put("exposureMode", exposureMode.toString()); - put("focusMode", focusMode.toString()); - put("exposurePointSupported", exposurePointSupported); - put("focusPointSupported", focusPointSupported); - } - }); + handler.post( + () -> + eventApi.initialized( + new Messages.PlatformCameraState.Builder() + .setPreviewSize( + new Messages.PlatformSize.Builder() + .setWidth(previewWidth.doubleValue()) + .setHeight(previewHeight.doubleValue()) + .build()) + .setExposurePointSupported(exposurePointSupported) + .setFocusPointSupported(focusPointSupported) + .setExposureMode(CameraUtils.exposureModeToPigeon(exposureMode)) + .setFocusMode(CameraUtils.focusModeToPigeon(focusMode)) + .build(), + new NoOpVoidResult())); } /** Sends a message to the Flutter client informing that the camera is closing. */ void sendCameraClosingEvent() { - send(CameraEventType.CLOSING); + handler.post(() -> eventApi.closed(new NoOpVoidResult())); } + // TODO(schectman): Make `description` non-null, see + // https://github.com/flutter/flutter/issues/156729 /** * Sends a message to the Flutter client informing that an error occurred while interacting with * the camera. @@ -135,43 +104,8 @@ void sendCameraClosingEvent() { * @param description contains details regarding the error that occurred. */ void sendCameraErrorEvent(@Nullable String description) { - this.send( - CameraEventType.ERROR, - new HashMap() { - { - if (!TextUtils.isEmpty(description)) put("description", description); - } - }); - } - - private void send(CameraEventType eventType) { - send(eventType, new HashMap<>()); - } - - private void send(CameraEventType eventType, Map args) { - if (cameraChannel == null) { - return; - } - - handler.post( - new Runnable() { - @Override - public void run() { - cameraChannel.invokeMethod(eventType.method, args); - } - }); - } - - private void send(DeviceEventType eventType) { - send(eventType, new HashMap<>()); - } - - private void send(DeviceEventType eventType, Map args) { - if (deviceChannel == null) { - return; - } - - handler.post(() -> deviceChannel.invokeMethod(eventType.method, args)); + String errorMessage = (description == null) ? "" : description; + handler.post(() -> eventApi.error(errorMessage, new NoOpVoidResult())); } /** diff --git a/packages/camera/camera_android/android/src/main/java/io/flutter/plugins/camera/Messages.java b/packages/camera/camera_android/android/src/main/java/io/flutter/plugins/camera/Messages.java index 2944bcd65eb..ef9fefe8e86 100644 --- a/packages/camera/camera_android/android/src/main/java/io/flutter/plugins/camera/Messages.java +++ b/packages/camera/camera_android/android/src/main/java/io/flutter/plugins/camera/Messages.java @@ -21,6 +21,7 @@ import java.lang.annotation.Target; import java.nio.ByteBuffer; import java.util.ArrayList; +import java.util.Collections; import java.util.List; import java.util.Objects; @@ -61,10 +62,17 @@ protected static ArrayList wrapError(@NonNull Throwable exception) { return errorList; } + @NonNull + protected static FlutterError createConnectionError(@NonNull String channelName) { + return new FlutterError( + "channel-error", "Unable to establish connection on channel: " + channelName + ".", ""); + } + @Target(METHOD) @Retention(CLASS) @interface CanIgnoreReturnValue {} + /** Pigeon equivalent of [CameraLensDirection]. */ public enum PlatformCameraLensDirection { FRONT(0), BACK(1), @@ -77,7 +85,49 @@ public enum PlatformCameraLensDirection { } } - /** Generated class from Pigeon that represents data sent in messages. */ + /** Pigeon equivalent of [DeviceOrientation]. */ + public enum PlatformDeviceOrientation { + PORTRAIT_UP(0), + PORTRAIT_DOWN(1), + LANDSCAPE_LEFT(2), + LANDSCAPE_RIGHT(3); + + final int index; + + PlatformDeviceOrientation(final int index) { + this.index = index; + } + } + + /** Pigeon equivalent of [ExposureMode]. */ + public enum PlatformExposureMode { + AUTO(0), + LOCKED(1); + + final int index; + + PlatformExposureMode(final int index) { + this.index = index; + } + } + + /** Pigeon equivalent of [FocusMode]. */ + public enum PlatformFocusMode { + AUTO(0), + LOCKED(1); + + final int index; + + PlatformFocusMode(final int index) { + this.index = index; + } + } + + /** + * Pigeon equivalent of [CameraDescription]. + * + *

Generated class from Pigeon that represents data sent in messages. + */ public static final class PlatformCameraDescription { private @NonNull String name; @@ -196,6 +246,278 @@ ArrayList toList() { } } + /** + * Data needed for [CameraInitializedEvent]. + * + *

Generated class from Pigeon that represents data sent in messages. + */ + public static final class PlatformCameraState { + private @NonNull PlatformSize previewSize; + + public @NonNull PlatformSize getPreviewSize() { + return previewSize; + } + + public void setPreviewSize(@NonNull PlatformSize setterArg) { + if (setterArg == null) { + throw new IllegalStateException("Nonnull field \"previewSize\" is null."); + } + this.previewSize = setterArg; + } + + private @NonNull PlatformExposureMode exposureMode; + + public @NonNull PlatformExposureMode getExposureMode() { + return exposureMode; + } + + public void setExposureMode(@NonNull PlatformExposureMode setterArg) { + if (setterArg == null) { + throw new IllegalStateException("Nonnull field \"exposureMode\" is null."); + } + this.exposureMode = setterArg; + } + + private @NonNull PlatformFocusMode focusMode; + + public @NonNull PlatformFocusMode getFocusMode() { + return focusMode; + } + + public void setFocusMode(@NonNull PlatformFocusMode setterArg) { + if (setterArg == null) { + throw new IllegalStateException("Nonnull field \"focusMode\" is null."); + } + this.focusMode = setterArg; + } + + private @NonNull Boolean exposurePointSupported; + + public @NonNull Boolean getExposurePointSupported() { + return exposurePointSupported; + } + + public void setExposurePointSupported(@NonNull Boolean setterArg) { + if (setterArg == null) { + throw new IllegalStateException("Nonnull field \"exposurePointSupported\" is null."); + } + this.exposurePointSupported = setterArg; + } + + private @NonNull Boolean focusPointSupported; + + public @NonNull Boolean getFocusPointSupported() { + return focusPointSupported; + } + + public void setFocusPointSupported(@NonNull Boolean setterArg) { + if (setterArg == null) { + throw new IllegalStateException("Nonnull field \"focusPointSupported\" is null."); + } + this.focusPointSupported = setterArg; + } + + /** Constructor is non-public to enforce null safety; use Builder. */ + PlatformCameraState() {} + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + PlatformCameraState that = (PlatformCameraState) o; + return previewSize.equals(that.previewSize) + && exposureMode.equals(that.exposureMode) + && focusMode.equals(that.focusMode) + && exposurePointSupported.equals(that.exposurePointSupported) + && focusPointSupported.equals(that.focusPointSupported); + } + + @Override + public int hashCode() { + return Objects.hash( + previewSize, exposureMode, focusMode, exposurePointSupported, focusPointSupported); + } + + public static final class Builder { + + private @Nullable PlatformSize previewSize; + + @CanIgnoreReturnValue + public @NonNull Builder setPreviewSize(@NonNull PlatformSize setterArg) { + this.previewSize = setterArg; + return this; + } + + private @Nullable PlatformExposureMode exposureMode; + + @CanIgnoreReturnValue + public @NonNull Builder setExposureMode(@NonNull PlatformExposureMode setterArg) { + this.exposureMode = setterArg; + return this; + } + + private @Nullable PlatformFocusMode focusMode; + + @CanIgnoreReturnValue + public @NonNull Builder setFocusMode(@NonNull PlatformFocusMode setterArg) { + this.focusMode = setterArg; + return this; + } + + private @Nullable Boolean exposurePointSupported; + + @CanIgnoreReturnValue + public @NonNull Builder setExposurePointSupported(@NonNull Boolean setterArg) { + this.exposurePointSupported = setterArg; + return this; + } + + private @Nullable Boolean focusPointSupported; + + @CanIgnoreReturnValue + public @NonNull Builder setFocusPointSupported(@NonNull Boolean setterArg) { + this.focusPointSupported = setterArg; + return this; + } + + public @NonNull PlatformCameraState build() { + PlatformCameraState pigeonReturn = new PlatformCameraState(); + pigeonReturn.setPreviewSize(previewSize); + pigeonReturn.setExposureMode(exposureMode); + pigeonReturn.setFocusMode(focusMode); + pigeonReturn.setExposurePointSupported(exposurePointSupported); + pigeonReturn.setFocusPointSupported(focusPointSupported); + return pigeonReturn; + } + } + + @NonNull + ArrayList toList() { + ArrayList toListResult = new ArrayList<>(5); + toListResult.add(previewSize); + toListResult.add(exposureMode); + toListResult.add(focusMode); + toListResult.add(exposurePointSupported); + toListResult.add(focusPointSupported); + return toListResult; + } + + static @NonNull PlatformCameraState fromList(@NonNull ArrayList pigeonVar_list) { + PlatformCameraState pigeonResult = new PlatformCameraState(); + Object previewSize = pigeonVar_list.get(0); + pigeonResult.setPreviewSize((PlatformSize) previewSize); + Object exposureMode = pigeonVar_list.get(1); + pigeonResult.setExposureMode((PlatformExposureMode) exposureMode); + Object focusMode = pigeonVar_list.get(2); + pigeonResult.setFocusMode((PlatformFocusMode) focusMode); + Object exposurePointSupported = pigeonVar_list.get(3); + pigeonResult.setExposurePointSupported((Boolean) exposurePointSupported); + Object focusPointSupported = pigeonVar_list.get(4); + pigeonResult.setFocusPointSupported((Boolean) focusPointSupported); + return pigeonResult; + } + } + + /** + * Pigeon equivalent of [Size]. + * + *

Generated class from Pigeon that represents data sent in messages. + */ + public static final class PlatformSize { + private @NonNull Double width; + + public @NonNull Double getWidth() { + return width; + } + + public void setWidth(@NonNull Double setterArg) { + if (setterArg == null) { + throw new IllegalStateException("Nonnull field \"width\" is null."); + } + this.width = setterArg; + } + + private @NonNull Double height; + + public @NonNull Double getHeight() { + return height; + } + + public void setHeight(@NonNull Double setterArg) { + if (setterArg == null) { + throw new IllegalStateException("Nonnull field \"height\" is null."); + } + this.height = setterArg; + } + + /** Constructor is non-public to enforce null safety; use Builder. */ + PlatformSize() {} + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + PlatformSize that = (PlatformSize) o; + return width.equals(that.width) && height.equals(that.height); + } + + @Override + public int hashCode() { + return Objects.hash(width, height); + } + + public static final class Builder { + + private @Nullable Double width; + + @CanIgnoreReturnValue + public @NonNull Builder setWidth(@NonNull Double setterArg) { + this.width = setterArg; + return this; + } + + private @Nullable Double height; + + @CanIgnoreReturnValue + public @NonNull Builder setHeight(@NonNull Double setterArg) { + this.height = setterArg; + return this; + } + + public @NonNull PlatformSize build() { + PlatformSize pigeonReturn = new PlatformSize(); + pigeonReturn.setWidth(width); + pigeonReturn.setHeight(height); + return pigeonReturn; + } + } + + @NonNull + ArrayList toList() { + ArrayList toListResult = new ArrayList<>(2); + toListResult.add(width); + toListResult.add(height); + return toListResult; + } + + static @NonNull PlatformSize fromList(@NonNull ArrayList pigeonVar_list) { + PlatformSize pigeonResult = new PlatformSize(); + Object width = pigeonVar_list.get(0); + pigeonResult.setWidth((Double) width); + Object height = pigeonVar_list.get(1); + pigeonResult.setHeight((Double) height); + return pigeonResult; + } + } + private static class PigeonCodec extends StandardMessageCodec { public static final PigeonCodec INSTANCE = new PigeonCodec(); @@ -212,7 +534,28 @@ protected Object readValueOfType(byte type, @NonNull ByteBuffer buffer) { : PlatformCameraLensDirection.values()[((Long) value).intValue()]; } case (byte) 130: + { + Object value = readValue(buffer); + return value == null + ? null + : PlatformDeviceOrientation.values()[((Long) value).intValue()]; + } + case (byte) 131: + { + Object value = readValue(buffer); + return value == null ? null : PlatformExposureMode.values()[((Long) value).intValue()]; + } + case (byte) 132: + { + Object value = readValue(buffer); + return value == null ? null : PlatformFocusMode.values()[((Long) value).intValue()]; + } + case (byte) 133: return PlatformCameraDescription.fromList((ArrayList) readValue(buffer)); + case (byte) 134: + return PlatformCameraState.fromList((ArrayList) readValue(buffer)); + case (byte) 135: + return PlatformSize.fromList((ArrayList) readValue(buffer)); default: return super.readValueOfType(type, buffer); } @@ -223,18 +566,61 @@ protected void writeValue(@NonNull ByteArrayOutputStream stream, Object value) { if (value instanceof PlatformCameraLensDirection) { stream.write(129); writeValue(stream, value == null ? null : ((PlatformCameraLensDirection) value).index); - } else if (value instanceof PlatformCameraDescription) { + } else if (value instanceof PlatformDeviceOrientation) { stream.write(130); + writeValue(stream, value == null ? null : ((PlatformDeviceOrientation) value).index); + } else if (value instanceof PlatformExposureMode) { + stream.write(131); + writeValue(stream, value == null ? null : ((PlatformExposureMode) value).index); + } else if (value instanceof PlatformFocusMode) { + stream.write(132); + writeValue(stream, value == null ? null : ((PlatformFocusMode) value).index); + } else if (value instanceof PlatformCameraDescription) { + stream.write(133); writeValue(stream, ((PlatformCameraDescription) value).toList()); + } else if (value instanceof PlatformCameraState) { + stream.write(134); + writeValue(stream, ((PlatformCameraState) value).toList()); + } else if (value instanceof PlatformSize) { + stream.write(135); + writeValue(stream, ((PlatformSize) value).toList()); } else { super.writeValue(stream, value); } } } - /** Generated interface from Pigeon that represents a handler of messages from Flutter. */ - public interface CameraApi { + /** Asynchronous error handling return type for non-nullable API method returns. */ + public interface Result { + /** Success case callback method for handling returns. */ + void success(@NonNull T result); + /** Failure case callback method for handling errors. */ + void error(@NonNull Throwable error); + } + /** Asynchronous error handling return type for nullable API method returns. */ + public interface NullableResult { + /** Success case callback method for handling returns. */ + void success(@Nullable T result); + + /** Failure case callback method for handling errors. */ + void error(@NonNull Throwable error); + } + /** Asynchronous error handling return type for void API method returns. */ + public interface VoidResult { + /** Success case callback method for handling returns. */ + void success(); + + /** Failure case callback method for handling errors. */ + void error(@NonNull Throwable error); + } + /** + * Handles calls from Dart to the native side. + * + *

Generated interface from Pigeon that represents a handler of messages from Flutter. + */ + public interface CameraApi { + /** Returns the list of available cameras. */ @NonNull List getAvailableCameras(); @@ -277,4 +663,147 @@ static void setUp( } } } + /** + * Handles calls from native side to Dart that are not camera-specific. + * + *

Generated class from Pigeon that represents Flutter messages that can be called from Java. + */ + public static class CameraGlobalEventApi { + private final @NonNull BinaryMessenger binaryMessenger; + private final String messageChannelSuffix; + + public CameraGlobalEventApi(@NonNull BinaryMessenger argBinaryMessenger) { + this(argBinaryMessenger, ""); + } + + public CameraGlobalEventApi( + @NonNull BinaryMessenger argBinaryMessenger, @NonNull String messageChannelSuffix) { + this.binaryMessenger = argBinaryMessenger; + this.messageChannelSuffix = messageChannelSuffix.isEmpty() ? "" : "." + messageChannelSuffix; + } + + /** Public interface for sending reply. The codec used by CameraGlobalEventApi. */ + static @NonNull MessageCodec getCodec() { + return PigeonCodec.INSTANCE; + } + /** Called when the device's physical orientation changes. */ + public void deviceOrientationChanged( + @NonNull PlatformDeviceOrientation orientationArg, @NonNull VoidResult result) { + final String channelName = + "dev.flutter.pigeon.camera_android.CameraGlobalEventApi.deviceOrientationChanged" + + messageChannelSuffix; + BasicMessageChannel channel = + new BasicMessageChannel<>(binaryMessenger, channelName, getCodec()); + channel.send( + new ArrayList<>(Collections.singletonList(orientationArg)), + channelReply -> { + if (channelReply instanceof List) { + List listReply = (List) channelReply; + if (listReply.size() > 1) { + result.error( + new FlutterError( + (String) listReply.get(0), (String) listReply.get(1), listReply.get(2))); + } else { + result.success(); + } + } else { + result.error(createConnectionError(channelName)); + } + }); + } + } + /** + * Handles device-specific calls from native side to Dart. + * + *

Generated class from Pigeon that represents Flutter messages that can be called from Java. + */ + public static class CameraEventApi { + private final @NonNull BinaryMessenger binaryMessenger; + private final String messageChannelSuffix; + + public CameraEventApi(@NonNull BinaryMessenger argBinaryMessenger) { + this(argBinaryMessenger, ""); + } + + public CameraEventApi( + @NonNull BinaryMessenger argBinaryMessenger, @NonNull String messageChannelSuffix) { + this.binaryMessenger = argBinaryMessenger; + this.messageChannelSuffix = messageChannelSuffix.isEmpty() ? "" : "." + messageChannelSuffix; + } + + /** Public interface for sending reply. The codec used by CameraEventApi. */ + static @NonNull MessageCodec getCodec() { + return PigeonCodec.INSTANCE; + } + /** Called when the camera is initialized. */ + public void initialized( + @NonNull PlatformCameraState initialStateArg, @NonNull VoidResult result) { + final String channelName = + "dev.flutter.pigeon.camera_android.CameraEventApi.initialized" + messageChannelSuffix; + BasicMessageChannel channel = + new BasicMessageChannel<>(binaryMessenger, channelName, getCodec()); + channel.send( + new ArrayList<>(Collections.singletonList(initialStateArg)), + channelReply -> { + if (channelReply instanceof List) { + List listReply = (List) channelReply; + if (listReply.size() > 1) { + result.error( + new FlutterError( + (String) listReply.get(0), (String) listReply.get(1), listReply.get(2))); + } else { + result.success(); + } + } else { + result.error(createConnectionError(channelName)); + } + }); + } + /** Called when an error occurs in the camera. */ + public void error(@NonNull String messageArg, @NonNull VoidResult result) { + final String channelName = + "dev.flutter.pigeon.camera_android.CameraEventApi.error" + messageChannelSuffix; + BasicMessageChannel channel = + new BasicMessageChannel<>(binaryMessenger, channelName, getCodec()); + channel.send( + new ArrayList<>(Collections.singletonList(messageArg)), + channelReply -> { + if (channelReply instanceof List) { + List listReply = (List) channelReply; + if (listReply.size() > 1) { + result.error( + new FlutterError( + (String) listReply.get(0), (String) listReply.get(1), listReply.get(2))); + } else { + result.success(); + } + } else { + result.error(createConnectionError(channelName)); + } + }); + } + /** Called when the camera closes. */ + public void closed(@NonNull VoidResult result) { + final String channelName = + "dev.flutter.pigeon.camera_android.CameraEventApi.closed" + messageChannelSuffix; + BasicMessageChannel channel = + new BasicMessageChannel<>(binaryMessenger, channelName, getCodec()); + channel.send( + null, + channelReply -> { + if (channelReply instanceof List) { + List listReply = (List) channelReply; + if (listReply.size() > 1) { + result.error( + new FlutterError( + (String) listReply.get(0), (String) listReply.get(1), listReply.get(2))); + } else { + result.success(); + } + } else { + result.error(createConnectionError(channelName)); + } + }); + } + } } diff --git a/packages/camera/camera_android/android/src/main/java/io/flutter/plugins/camera/MethodCallHandlerImpl.java b/packages/camera/camera_android/android/src/main/java/io/flutter/plugins/camera/MethodCallHandlerImpl.java index ba80112a734..a02872bd490 100644 --- a/packages/camera/camera_android/android/src/main/java/io/flutter/plugins/camera/MethodCallHandlerImpl.java +++ b/packages/camera/camera_android/android/src/main/java/io/flutter/plugins/camera/MethodCallHandlerImpl.java @@ -391,9 +391,12 @@ private void instantiateCamera(MethodCall call, Result result) throws CameraAcce TextureRegistry.SurfaceTextureEntry flutterSurfaceTexture = textureRegistry.createSurfaceTexture(); + long cameraId = flutterSurfaceTexture.id(); DartMessenger dartMessenger = new DartMessenger( - messenger, flutterSurfaceTexture.id(), new Handler(Looper.getMainLooper())); + new Handler(Looper.getMainLooper()), + new Messages.CameraGlobalEventApi(messenger), + new Messages.CameraEventApi(messenger, String.valueOf(cameraId))); CameraProperties cameraProperties = new CameraPropertiesImpl(cameraName, CameraUtils.getCameraManager(activity)); ResolutionPreset resolutionPreset = ResolutionPreset.valueOf(preset); diff --git a/packages/camera/camera_android/android/src/main/java/io/flutter/plugins/camera/NoOpVoidResult.java b/packages/camera/camera_android/android/src/main/java/io/flutter/plugins/camera/NoOpVoidResult.java new file mode 100644 index 00000000000..2ce29e95851 --- /dev/null +++ b/packages/camera/camera_android/android/src/main/java/io/flutter/plugins/camera/NoOpVoidResult.java @@ -0,0 +1,20 @@ +// 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.camera; + +import androidx.annotation.NonNull; + +/** + * A convenience class for results of a Pigeon Flutter API method call that perform no action. + * + *

Longer term, any call using this is likely a good candidate to migrate to event channels. + */ +public class NoOpVoidResult implements Messages.VoidResult { + @Override + public void success() {} + + @Override + public void error(@NonNull Throwable error) {} +} diff --git a/packages/camera/camera_android/android/src/test/java/io/flutter/plugins/camera/DartMessengerTest.java b/packages/camera/camera_android/android/src/test/java/io/flutter/plugins/camera/DartMessengerTest.java index 0a2fc43d03c..a053d937b91 100644 --- a/packages/camera/camera_android/android/src/test/java/io/flutter/plugins/camera/DartMessengerTest.java +++ b/packages/camera/camera_android/android/src/test/java/io/flutter/plugins/camera/DartMessengerTest.java @@ -4,132 +4,116 @@ package io.flutter.plugins.camera; -import static junit.framework.TestCase.assertNull; import static org.junit.Assert.assertEquals; import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.doAnswer; import static org.mockito.Mockito.mock; import android.os.Handler; -import androidx.annotation.NonNull; import io.flutter.embedding.engine.systemchannels.PlatformChannel; -import io.flutter.plugin.common.BinaryMessenger; -import io.flutter.plugin.common.MethodCall; -import io.flutter.plugin.common.StandardMethodCodec; import io.flutter.plugins.camera.features.autofocus.FocusMode; import io.flutter.plugins.camera.features.exposurelock.ExposureMode; -import java.nio.ByteBuffer; import java.util.ArrayList; import java.util.List; import org.junit.Before; import org.junit.Test; import org.mockito.invocation.InvocationOnMock; -import org.mockito.stubbing.Answer; public class DartMessengerTest { - /** A {@link BinaryMessenger} implementation that does nothing but save its messages. */ - private static class FakeBinaryMessenger implements BinaryMessenger { - private final List sentMessages = new ArrayList<>(); - @Override - public void send(@NonNull String channel, ByteBuffer message) { - sentMessages.add(message); - } - - @Override - public void send(@NonNull String channel, ByteBuffer message, BinaryReply callback) { - send(channel, message); - } - - @Override - public void setMessageHandler(@NonNull String channel, BinaryMessageHandler handler) {} - - List getMessages() { - return new ArrayList<>(sentMessages); - } - } - - private Handler mockHandler; private DartMessenger dartMessenger; - private FakeBinaryMessenger fakeBinaryMessenger; + private Messages.CameraGlobalEventApi mockGlobalEventApi; + private Messages.CameraEventApi mockEventApi; @Before public void setUp() { - mockHandler = mock(Handler.class); - fakeBinaryMessenger = new FakeBinaryMessenger(); - dartMessenger = new DartMessenger(fakeBinaryMessenger, 0, mockHandler); + Handler mockHandler = mock(Handler.class); + doAnswer( + (InvocationOnMock invocation) -> { + Runnable r = invocation.getArgument(0); + if (r != null) { + r.run(); + } + return true; + }) + .when(mockHandler) + .post(any(Runnable.class)); + mockGlobalEventApi = mock(Messages.CameraGlobalEventApi.class); + mockEventApi = mock(Messages.CameraEventApi.class); + dartMessenger = new DartMessenger(mockHandler, mockGlobalEventApi, mockEventApi); } @Test public void sendCameraErrorEvent_includesErrorDescriptions() { - doAnswer(createPostHandlerAnswer()).when(mockHandler).post(any(Runnable.class)); + final List errorsList = new ArrayList<>(); + doAnswer( + (InvocationOnMock invocation) -> { + String description = invocation.getArgument(0); + errorsList.add(description); + return null; + }) + .when(mockEventApi) + .error(any(), any()); dartMessenger.sendCameraErrorEvent("error description"); - List sentMessages = fakeBinaryMessenger.getMessages(); - assertEquals(1, sentMessages.size()); - MethodCall call = decodeSentMessage(sentMessages.get(0)); - assertEquals("error", call.method); - assertEquals("error description", call.argument("description")); + assertEquals(1, errorsList.size()); + assertEquals("error description", errorsList.get(0)); } @Test public void sendCameraInitializedEvent_includesPreviewSize() { - doAnswer(createPostHandlerAnswer()).when(mockHandler).post(any(Runnable.class)); + final List statesList = new ArrayList<>(); + doAnswer( + (InvocationOnMock invocation) -> { + Messages.PlatformCameraState state = invocation.getArgument(0); + statesList.add(state); + return null; + }) + .when(mockEventApi) + .initialized(any(), any()); dartMessenger.sendCameraInitializedEvent(0, 0, ExposureMode.auto, FocusMode.auto, true, true); - List sentMessages = fakeBinaryMessenger.getMessages(); - assertEquals(1, sentMessages.size()); - MethodCall call = decodeSentMessage(sentMessages.get(0)); - assertEquals("initialized", call.method); - assertEquals(0, (double) call.argument("previewWidth"), 0); - assertEquals(0, (double) call.argument("previewHeight"), 0); - assertEquals("ExposureMode auto", call.argument("exposureMode"), "auto"); - assertEquals("FocusMode continuous", call.argument("focusMode"), "auto"); - assertEquals("exposurePointSupported", call.argument("exposurePointSupported"), true); - assertEquals("focusPointSupported", call.argument("focusPointSupported"), true); + assertEquals(1, statesList.size()); + Messages.PlatformCameraState state = statesList.get(0); + assertEquals(0, state.getPreviewSize().getWidth(), 0); + assertEquals(0, state.getPreviewSize().getHeight(), 0); + assertEquals("ExposureMode auto", Messages.PlatformExposureMode.AUTO, state.getExposureMode()); + assertEquals("FocusMode continuous", Messages.PlatformFocusMode.AUTO, state.getFocusMode()); + assertEquals("exposurePointSupported", true, state.getExposurePointSupported()); + assertEquals("focusPointSupported", true, state.getFocusPointSupported()); } @Test public void sendCameraClosingEvent() { - doAnswer(createPostHandlerAnswer()).when(mockHandler).post(any(Runnable.class)); + final List calls = new ArrayList<>(); + doAnswer( + (InvocationOnMock invocation) -> { + calls.add(1); + return null; + }) + .when(mockEventApi) + .closed(any()); dartMessenger.sendCameraClosingEvent(); - List sentMessages = fakeBinaryMessenger.getMessages(); - assertEquals(1, sentMessages.size()); - MethodCall call = decodeSentMessage(sentMessages.get(0)); - assertEquals("camera_closing", call.method); - assertNull(call.argument("description")); + assertEquals(1, calls.size()); + assertEquals(1, calls.get(0).intValue()); } @Test public void sendDeviceOrientationChangedEvent() { - doAnswer(createPostHandlerAnswer()).when(mockHandler).post(any(Runnable.class)); + final List eventsList = new ArrayList<>(); + doAnswer( + (InvocationOnMock invocation) -> { + Messages.PlatformDeviceOrientation orientation = invocation.getArgument(0); + eventsList.add(orientation); + return null; + }) + .when(mockGlobalEventApi) + .deviceOrientationChanged(any(), any()); dartMessenger.sendDeviceOrientationChangeEvent(PlatformChannel.DeviceOrientation.PORTRAIT_UP); - List sentMessages = fakeBinaryMessenger.getMessages(); - assertEquals(1, sentMessages.size()); - MethodCall call = decodeSentMessage(sentMessages.get(0)); - assertEquals("orientation_changed", call.method); - assertEquals(call.argument("orientation"), "portraitUp"); - } - - private static Answer createPostHandlerAnswer() { - return new Answer() { - @Override - public Boolean answer(InvocationOnMock invocation) throws Throwable { - Runnable runnable = invocation.getArgument(0, Runnable.class); - if (runnable != null) { - runnable.run(); - } - return true; - } - }; - } - - private MethodCall decodeSentMessage(ByteBuffer sentMessage) { - sentMessage.position(0); - - return StandardMethodCodec.INSTANCE.decodeMethodCall(sentMessage); + assertEquals(1, eventsList.size()); + assertEquals(Messages.PlatformDeviceOrientation.PORTRAIT_UP, eventsList.get(0)); } } diff --git a/packages/camera/camera_android/lib/src/android_camera.dart b/packages/camera/camera_android/lib/src/android_camera.dart index b5566090a07..95f339779ea 100644 --- a/packages/camera/camera_android/lib/src/android_camera.dart +++ b/packages/camera/camera_android/lib/src/android_camera.dart @@ -29,8 +29,6 @@ class AndroidCamera extends CameraPlatform { CameraPlatform.instance = AndroidCamera(); } - final Map _channels = {}; - final CameraApi _hostApi; /// The name of the channel that device events from the platform side are @@ -50,20 +48,15 @@ class AndroidCamera extends CameraPlatform { final StreamController cameraEventStreamController = StreamController.broadcast(); - /// The controller we need to broadcast the different events coming - /// from handleMethodCall, specific to general device events. - /// - /// It is a `broadcast` because multiple controllers will connect to - /// different stream views of this Controller. - late final StreamController _deviceEventStreamController = - _createDeviceEventStreamController(); + /// Handler for device-level callbacks from the native side. + @visibleForTesting + late final HostDeviceMessageHandler hostHandler = HostDeviceMessageHandler(); - StreamController _createDeviceEventStreamController() { - // Set up the method handler lazily. - const MethodChannel channel = MethodChannel(deviceEventChannelName); - channel.setMethodCallHandler(_handleDeviceMethodCall); - return StreamController.broadcast(); - } + /// Map of camera IDs to camera-level callback handlers listening to their + /// respective platform channels. + @visibleForTesting + final Map hostCameraHandlers = + {}; // The stream to receive frames from the native code. StreamSubscription? _platformImageStreamSubscription; @@ -138,13 +131,8 @@ class AndroidCamera extends CameraPlatform { int cameraId, { ImageFormatGroup imageFormatGroup = ImageFormatGroup.unknown, }) { - _channels.putIfAbsent(cameraId, () { - final MethodChannel channel = - MethodChannel('plugins.flutter.io/camera_android/camera$cameraId'); - channel.setMethodCallHandler( - (MethodCall call) => handleCameraMethodCall(call, cameraId)); - return channel; - }); + hostCameraHandlers.putIfAbsent(cameraId, + () => HostCameraMessageHandler(cameraId, cameraEventStreamController)); final Completer completer = Completer(); @@ -180,11 +168,9 @@ class AndroidCamera extends CameraPlatform { @override Future dispose(int cameraId) async { - if (_channels.containsKey(cameraId)) { - final MethodChannel? cameraChannel = _channels[cameraId]; - cameraChannel?.setMethodCallHandler(null); - _channels.remove(cameraId); - } + final HostCameraMessageHandler? handler = + hostCameraHandlers.remove(cameraId); + handler?.dispose(); await _channel.invokeMethod( 'dispose', @@ -219,7 +205,7 @@ class AndroidCamera extends CameraPlatform { @override Stream onDeviceOrientationChanged() { - return _deviceEventStreamController.stream + return hostHandler.deviceEventStreamController.stream .whereType(); } @@ -585,64 +571,63 @@ class AndroidCamera extends CameraPlatform { // ignore: dead_code return 'max'; } +} - /// Converts messages received from the native platform into device events. - Future _handleDeviceMethodCall(MethodCall call) async { - switch (call.method) { - case 'orientation_changed': - final Map arguments = _getArgumentDictionary(call); - _deviceEventStreamController.add(DeviceOrientationChangedEvent( - deserializeDeviceOrientation(arguments['orientation']! as String))); - default: - throw MissingPluginException(); - } +/// Handles callbacks from the platform host that are not camera-specific. +@visibleForTesting +class HostDeviceMessageHandler implements CameraGlobalEventApi { + /// Creates a new handler and registers it to listen to the global event platform channel. + HostDeviceMessageHandler() { + CameraGlobalEventApi.setUp(this); } - /// Converts messages received from the native platform into camera events. - /// - /// This is only exposed for test purposes. It shouldn't be used by clients of - /// the plugin as it may break or change at any time. - @visibleForTesting - Future handleCameraMethodCall(MethodCall call, int cameraId) async { - switch (call.method) { - case 'initialized': - final Map arguments = _getArgumentDictionary(call); - cameraEventStreamController.add(CameraInitializedEvent( - cameraId, - arguments['previewWidth']! as double, - arguments['previewHeight']! as double, - deserializeExposureMode(arguments['exposureMode']! as String), - arguments['exposurePointSupported']! as bool, - deserializeFocusMode(arguments['focusMode']! as String), - arguments['focusPointSupported']! as bool, - )); - case 'resolution_changed': - final Map arguments = _getArgumentDictionary(call); - cameraEventStreamController.add(CameraResolutionChangedEvent( - cameraId, - arguments['captureWidth']! as double, - arguments['captureHeight']! as double, - )); - case 'camera_closing': - cameraEventStreamController.add(CameraClosingEvent( - cameraId, - )); - case 'error': - final Map arguments = _getArgumentDictionary(call); - cameraEventStreamController.add(CameraErrorEvent( - cameraId, - arguments['description']! as String, - )); - default: - throw MissingPluginException(); - } + /// The controller that broadcasts device events coming from the host platform. + final StreamController deviceEventStreamController = + StreamController.broadcast(); + @override + void deviceOrientationChanged(PlatformDeviceOrientation orientation) { + deviceEventStreamController.add(DeviceOrientationChangedEvent( + deviceOrientationFromPlatform(orientation))); } +} - /// Returns the arguments of [call] as typed string-keyed Map. - /// - /// This does not do any type validation, so is only safe to call if the - /// arguments are known to be a map. - Map _getArgumentDictionary(MethodCall call) { - return (call.arguments as Map).cast(); +/// Handles camera-specific callbacks from the platform host. +@visibleForTesting +class HostCameraMessageHandler implements CameraEventApi { + /// Creates a new handler and registers it to listen to its camera's platform channel. + HostCameraMessageHandler(this.cameraId, this.cameraEventStreamController) { + CameraEventApi.setUp(this, messageChannelSuffix: '$cameraId'); + } + + /// Removes this handler from its platform channel. + void dispose() { + CameraEventApi.setUp(null, messageChannelSuffix: '$cameraId'); + } + + /// The ID of the camera for which this handler listens for events. + final int cameraId; + + /// The controller which broadcasts camera events from the host platform. + final StreamController cameraEventStreamController; + @override + void error(String message) { + cameraEventStreamController.add(CameraErrorEvent(cameraId, message)); + } + + @override + void initialized(PlatformCameraState initialState) { + cameraEventStreamController.add(CameraInitializedEvent( + cameraId, + initialState.previewSize.width, + initialState.previewSize.height, + exposureModeFromPlatform(initialState.exposureMode), + initialState.exposurePointSupported, + focusModeFromPlatform(initialState.focusMode), + initialState.focusPointSupported)); + } + + @override + void closed() { + cameraEventStreamController.add(CameraClosingEvent(cameraId)); } } diff --git a/packages/camera/camera_android/lib/src/messages.g.dart b/packages/camera/camera_android/lib/src/messages.g.dart index 9ee641509c4..87f810d67bb 100644 --- a/packages/camera/camera_android/lib/src/messages.g.dart +++ b/packages/camera/camera_android/lib/src/messages.g.dart @@ -18,12 +18,45 @@ PlatformException _createConnectionError(String channelName) { ); } +List wrapResponse( + {Object? result, PlatformException? error, bool empty = false}) { + if (empty) { + return []; + } + if (error == null) { + return [result]; + } + return [error.code, error.message, error.details]; +} + +/// Pigeon equivalent of [CameraLensDirection]. enum PlatformCameraLensDirection { front, back, external, } +/// Pigeon equivalent of [DeviceOrientation]. +enum PlatformDeviceOrientation { + portraitUp, + portraitDown, + landscapeLeft, + landscapeRight, +} + +/// Pigeon equivalent of [ExposureMode]. +enum PlatformExposureMode { + auto, + locked, +} + +/// Pigeon equivalent of [FocusMode]. +enum PlatformFocusMode { + auto, + locked, +} + +/// Pigeon equivalent of [CameraDescription]. class PlatformCameraDescription { PlatformCameraDescription({ required this.name, @@ -55,6 +88,75 @@ class PlatformCameraDescription { } } +/// Data needed for [CameraInitializedEvent]. +class PlatformCameraState { + PlatformCameraState({ + required this.previewSize, + required this.exposureMode, + required this.focusMode, + required this.exposurePointSupported, + required this.focusPointSupported, + }); + + PlatformSize previewSize; + + PlatformExposureMode exposureMode; + + PlatformFocusMode focusMode; + + bool exposurePointSupported; + + bool focusPointSupported; + + Object encode() { + return [ + previewSize, + exposureMode, + focusMode, + exposurePointSupported, + focusPointSupported, + ]; + } + + static PlatformCameraState decode(Object result) { + result as List; + return PlatformCameraState( + previewSize: result[0]! as PlatformSize, + exposureMode: result[1]! as PlatformExposureMode, + focusMode: result[2]! as PlatformFocusMode, + exposurePointSupported: result[3]! as bool, + focusPointSupported: result[4]! as bool, + ); + } +} + +/// Pigeon equivalent of [Size]. +class PlatformSize { + PlatformSize({ + required this.width, + required this.height, + }); + + double width; + + double height; + + Object encode() { + return [ + width, + height, + ]; + } + + static PlatformSize decode(Object result) { + result as List; + return PlatformSize( + width: result[0]! as double, + height: result[1]! as double, + ); + } +} + class _PigeonCodec extends StandardMessageCodec { const _PigeonCodec(); @override @@ -65,8 +167,23 @@ class _PigeonCodec extends StandardMessageCodec { } else if (value is PlatformCameraLensDirection) { buffer.putUint8(129); writeValue(buffer, value.index); - } else if (value is PlatformCameraDescription) { + } else if (value is PlatformDeviceOrientation) { buffer.putUint8(130); + writeValue(buffer, value.index); + } else if (value is PlatformExposureMode) { + buffer.putUint8(131); + writeValue(buffer, value.index); + } else if (value is PlatformFocusMode) { + buffer.putUint8(132); + writeValue(buffer, value.index); + } else if (value is PlatformCameraDescription) { + buffer.putUint8(133); + writeValue(buffer, value.encode()); + } else if (value is PlatformCameraState) { + buffer.putUint8(134); + writeValue(buffer, value.encode()); + } else if (value is PlatformSize) { + buffer.putUint8(135); writeValue(buffer, value.encode()); } else { super.writeValue(buffer, value); @@ -80,13 +197,27 @@ class _PigeonCodec extends StandardMessageCodec { final int? value = readValue(buffer) as int?; return value == null ? null : PlatformCameraLensDirection.values[value]; case 130: + final int? value = readValue(buffer) as int?; + return value == null ? null : PlatformDeviceOrientation.values[value]; + case 131: + final int? value = readValue(buffer) as int?; + return value == null ? null : PlatformExposureMode.values[value]; + case 132: + final int? value = readValue(buffer) as int?; + return value == null ? null : PlatformFocusMode.values[value]; + case 133: return PlatformCameraDescription.decode(readValue(buffer)!); + case 134: + return PlatformCameraState.decode(readValue(buffer)!); + case 135: + return PlatformSize.decode(readValue(buffer)!); default: return super.readValueOfType(type, buffer); } } } +/// Handles calls from Dart to the native side. class CameraApi { /// Constructor for [CameraApi]. The [binaryMessenger] named argument is /// available for dependency injection. If it is left null, the default @@ -102,6 +233,7 @@ class CameraApi { final String pigeonVar_messageChannelSuffix; + /// Returns the list of available cameras. Future> getAvailableCameras() async { final String pigeonVar_channelName = 'dev.flutter.pigeon.camera_android.CameraApi.getAvailableCameras$pigeonVar_messageChannelSuffix'; @@ -132,3 +264,155 @@ class CameraApi { } } } + +/// Handles calls from native side to Dart that are not camera-specific. +abstract class CameraGlobalEventApi { + static const MessageCodec pigeonChannelCodec = _PigeonCodec(); + + /// Called when the device's physical orientation changes. + void deviceOrientationChanged(PlatformDeviceOrientation orientation); + + static void setUp( + CameraGlobalEventApi? api, { + BinaryMessenger? binaryMessenger, + String messageChannelSuffix = '', + }) { + messageChannelSuffix = + messageChannelSuffix.isNotEmpty ? '.$messageChannelSuffix' : ''; + { + final BasicMessageChannel< + Object?> pigeonVar_channel = BasicMessageChannel< + Object?>( + 'dev.flutter.pigeon.camera_android.CameraGlobalEventApi.deviceOrientationChanged$messageChannelSuffix', + pigeonChannelCodec, + binaryMessenger: binaryMessenger); + if (api == null) { + pigeonVar_channel.setMessageHandler(null); + } else { + pigeonVar_channel.setMessageHandler((Object? message) async { + assert(message != null, + 'Argument for dev.flutter.pigeon.camera_android.CameraGlobalEventApi.deviceOrientationChanged was null.'); + final List args = (message as List?)!; + final PlatformDeviceOrientation? arg_orientation = + (args[0] as PlatformDeviceOrientation?); + assert(arg_orientation != null, + 'Argument for dev.flutter.pigeon.camera_android.CameraGlobalEventApi.deviceOrientationChanged was null, expected non-null PlatformDeviceOrientation.'); + try { + api.deviceOrientationChanged(arg_orientation!); + return wrapResponse(empty: true); + } on PlatformException catch (e) { + return wrapResponse(error: e); + } catch (e) { + return wrapResponse( + error: PlatformException(code: 'error', message: e.toString())); + } + }); + } + } + } +} + +/// Handles device-specific calls from native side to Dart. +abstract class CameraEventApi { + static const MessageCodec pigeonChannelCodec = _PigeonCodec(); + + /// Called when the camera is initialized. + void initialized(PlatformCameraState initialState); + + /// Called when an error occurs in the camera. + void error(String message); + + /// Called when the camera closes. + void closed(); + + static void setUp( + CameraEventApi? api, { + BinaryMessenger? binaryMessenger, + String messageChannelSuffix = '', + }) { + messageChannelSuffix = + messageChannelSuffix.isNotEmpty ? '.$messageChannelSuffix' : ''; + { + final BasicMessageChannel< + Object?> pigeonVar_channel = BasicMessageChannel< + Object?>( + 'dev.flutter.pigeon.camera_android.CameraEventApi.initialized$messageChannelSuffix', + pigeonChannelCodec, + binaryMessenger: binaryMessenger); + if (api == null) { + pigeonVar_channel.setMessageHandler(null); + } else { + pigeonVar_channel.setMessageHandler((Object? message) async { + assert(message != null, + 'Argument for dev.flutter.pigeon.camera_android.CameraEventApi.initialized was null.'); + final List args = (message as List?)!; + final PlatformCameraState? arg_initialState = + (args[0] as PlatformCameraState?); + assert(arg_initialState != null, + 'Argument for dev.flutter.pigeon.camera_android.CameraEventApi.initialized was null, expected non-null PlatformCameraState.'); + try { + api.initialized(arg_initialState!); + return wrapResponse(empty: true); + } on PlatformException catch (e) { + return wrapResponse(error: e); + } catch (e) { + return wrapResponse( + error: PlatformException(code: 'error', message: e.toString())); + } + }); + } + } + { + final BasicMessageChannel< + Object?> pigeonVar_channel = BasicMessageChannel< + Object?>( + 'dev.flutter.pigeon.camera_android.CameraEventApi.error$messageChannelSuffix', + pigeonChannelCodec, + binaryMessenger: binaryMessenger); + if (api == null) { + pigeonVar_channel.setMessageHandler(null); + } else { + pigeonVar_channel.setMessageHandler((Object? message) async { + assert(message != null, + 'Argument for dev.flutter.pigeon.camera_android.CameraEventApi.error was null.'); + final List args = (message as List?)!; + final String? arg_message = (args[0] as String?); + assert(arg_message != null, + 'Argument for dev.flutter.pigeon.camera_android.CameraEventApi.error was null, expected non-null String.'); + try { + api.error(arg_message!); + return wrapResponse(empty: true); + } on PlatformException catch (e) { + return wrapResponse(error: e); + } catch (e) { + return wrapResponse( + error: PlatformException(code: 'error', message: e.toString())); + } + }); + } + } + { + final BasicMessageChannel< + Object?> pigeonVar_channel = BasicMessageChannel< + Object?>( + 'dev.flutter.pigeon.camera_android.CameraEventApi.closed$messageChannelSuffix', + pigeonChannelCodec, + binaryMessenger: binaryMessenger); + if (api == null) { + pigeonVar_channel.setMessageHandler(null); + } else { + pigeonVar_channel.setMessageHandler((Object? message) async { + try { + api.closed(); + return wrapResponse(empty: true); + } on PlatformException catch (e) { + return wrapResponse(error: e); + } catch (e) { + return wrapResponse( + error: PlatformException(code: 'error', message: e.toString())); + } + }); + } + } + } +} diff --git a/packages/camera/camera_android/lib/src/utils.dart b/packages/camera/camera_android/lib/src/utils.dart index 9af1d97d6ca..0a8742fbf01 100644 --- a/packages/camera/camera_android/lib/src/utils.dart +++ b/packages/camera/camera_android/lib/src/utils.dart @@ -17,6 +17,32 @@ CameraLensDirection cameraLensDirectionFromPlatform( }; } +/// Converts a [PlatformDeviceOrientation] to [DeviceOrientation]. +DeviceOrientation deviceOrientationFromPlatform( + PlatformDeviceOrientation orientation) => + switch (orientation) { + PlatformDeviceOrientation.portraitUp => DeviceOrientation.portraitUp, + PlatformDeviceOrientation.portraitDown => DeviceOrientation.portraitDown, + PlatformDeviceOrientation.landscapeLeft => + DeviceOrientation.landscapeLeft, + PlatformDeviceOrientation.landscapeRight => + DeviceOrientation.landscapeRight, + }; + +/// Converts a [PlatformExposureMode] to [ExposureMode]. +ExposureMode exposureModeFromPlatform(PlatformExposureMode exposureMode) => + switch (exposureMode) { + PlatformExposureMode.auto => ExposureMode.auto, + PlatformExposureMode.locked => ExposureMode.locked, + }; + +/// Converts a [PlatformFocusMode] to [FocusMode]. +FocusMode focusModeFromPlatform(PlatformFocusMode focusMode) => + switch (focusMode) { + PlatformFocusMode.auto => FocusMode.auto, + PlatformFocusMode.locked => FocusMode.locked, + }; + /// Returns the device orientation as a String. String serializeDeviceOrientation(DeviceOrientation orientation) { switch (orientation) { @@ -37,19 +63,3 @@ String serializeDeviceOrientation(DeviceOrientation orientation) { // ignore: dead_code return 'portraitUp'; } - -/// Returns the device orientation for a given String. -DeviceOrientation deserializeDeviceOrientation(String str) { - switch (str) { - case 'portraitUp': - return DeviceOrientation.portraitUp; - case 'portraitDown': - return DeviceOrientation.portraitDown; - case 'landscapeRight': - return DeviceOrientation.landscapeRight; - case 'landscapeLeft': - return DeviceOrientation.landscapeLeft; - default: - throw ArgumentError('"$str" is not a valid DeviceOrientation value'); - } -} diff --git a/packages/camera/camera_android/pigeons/messages.dart b/packages/camera/camera_android/pigeons/messages.dart index e66f62c0232..5e0e6383c32 100644 --- a/packages/camera/camera_android/pigeons/messages.dart +++ b/packages/camera/camera_android/pigeons/messages.dart @@ -1,7 +1,6 @@ // 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_platform_interface/camera_platform_interface.dart'; import 'package:pigeon/pigeon.dart'; @ConfigurePigeon(PigeonOptions( @@ -29,8 +28,71 @@ class PlatformCameraDescription { final int sensorOrientation; } +/// Pigeon equivalent of [DeviceOrientation]. +enum PlatformDeviceOrientation { + portraitUp, + portraitDown, + landscapeLeft, + landscapeRight, +} + +/// Pigeon equivalent of [ExposureMode]. +enum PlatformExposureMode { + auto, + locked, +} + +/// Pigeon equivalent of [FocusMode]. +enum PlatformFocusMode { + auto, + locked, +} + +/// Data needed for [CameraInitializedEvent]. +class PlatformCameraState { + PlatformCameraState( + {required this.previewSize, + required this.exposureMode, + required this.focusMode, + required this.exposurePointSupported, + required this.focusPointSupported}); + final PlatformSize previewSize; + final PlatformExposureMode exposureMode; + final PlatformFocusMode focusMode; + final bool exposurePointSupported; + final bool focusPointSupported; +} + +/// Pigeon equivalent of [Size]. +class PlatformSize { + PlatformSize({required this.width, required this.height}); + final double width; + final double height; +} + +/// Handles calls from Dart to the native side. @HostApi() abstract class CameraApi { /// Returns the list of available cameras. List getAvailableCameras(); } + +/// Handles calls from native side to Dart that are not camera-specific. +@FlutterApi() +abstract class CameraGlobalEventApi { + /// Called when the device's physical orientation changes. + void deviceOrientationChanged(PlatformDeviceOrientation orientation); +} + +/// Handles device-specific calls from native side to Dart. +@FlutterApi() +abstract class CameraEventApi { + /// Called when the camera is initialized. + void initialized(PlatformCameraState initialState); + + /// Called when an error occurs in the camera. + void error(String message); + + /// Called when the camera closes. + void closed(); +} diff --git a/packages/camera/camera_android/pubspec.yaml b/packages/camera/camera_android/pubspec.yaml index 5d3e369ad47..7c3cb83a46a 100644 --- a/packages/camera/camera_android/pubspec.yaml +++ b/packages/camera/camera_android/pubspec.yaml @@ -3,7 +3,7 @@ description: Android implementation of the camera plugin. repository: https://github.com/flutter/packages/tree/main/packages/camera/camera_android issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+camera%22 -version: 0.10.9+13 +version: 0.10.9+14 environment: sdk: ^3.5.0 diff --git a/packages/camera/camera_android/test/android_camera_test.dart b/packages/camera/camera_android/test/android_camera_test.dart index 17b8904f09b..4024992ec57 100644 --- a/packages/camera/camera_android/test/android_camera_test.dart +++ b/packages/camera/camera_android/test/android_camera_test.dart @@ -362,17 +362,22 @@ void main() { StreamQueue(eventStream); // Emit test events + final PlatformSize previewSize = PlatformSize(width: 3840, height: 2160); final CameraInitializedEvent event = CameraInitializedEvent( cameraId, - 3840, - 2160, + previewSize.width, + previewSize.height, ExposureMode.auto, true, FocusMode.auto, true, ); - await camera.handleCameraMethodCall( - MethodCall('initialized', event.toJson()), cameraId); + camera.hostCameraHandlers[cameraId]!.initialized(PlatformCameraState( + previewSize: previewSize, + exposureMode: PlatformExposureMode.auto, + focusMode: PlatformFocusMode.auto, + exposurePointSupported: true, + focusPointSupported: true)); // Assert expect(await streamQueue.next, event); @@ -381,37 +386,6 @@ void main() { await streamQueue.cancel(); }); - test('Should receive resolution changes', () async { - // Act - final Stream resolutionStream = - camera.onCameraResolutionChanged(cameraId); - final StreamQueue streamQueue = - StreamQueue(resolutionStream); - - // Emit test events - final CameraResolutionChangedEvent fhdEvent = - CameraResolutionChangedEvent(cameraId, 1920, 1080); - final CameraResolutionChangedEvent uhdEvent = - CameraResolutionChangedEvent(cameraId, 3840, 2160); - await camera.handleCameraMethodCall( - MethodCall('resolution_changed', fhdEvent.toJson()), cameraId); - await camera.handleCameraMethodCall( - MethodCall('resolution_changed', uhdEvent.toJson()), cameraId); - await camera.handleCameraMethodCall( - MethodCall('resolution_changed', fhdEvent.toJson()), cameraId); - await camera.handleCameraMethodCall( - MethodCall('resolution_changed', uhdEvent.toJson()), cameraId); - - // Assert - expect(await streamQueue.next, fhdEvent); - expect(await streamQueue.next, uhdEvent); - expect(await streamQueue.next, fhdEvent); - expect(await streamQueue.next, uhdEvent); - - // Clean up - await streamQueue.cancel(); - }); - test('Should receive camera closing events', () async { // Act final Stream eventStream = @@ -421,12 +395,9 @@ void main() { // Emit test events final CameraClosingEvent event = CameraClosingEvent(cameraId); - await camera.handleCameraMethodCall( - MethodCall('camera_closing', event.toJson()), cameraId); - await camera.handleCameraMethodCall( - MethodCall('camera_closing', event.toJson()), cameraId); - await camera.handleCameraMethodCall( - MethodCall('camera_closing', event.toJson()), cameraId); + for (int i = 0; i < 3; i++) { + camera.hostCameraHandlers[cameraId]!.closed(); + } // Assert expect(await streamQueue.next, event); @@ -447,12 +418,9 @@ void main() { // Emit test events final CameraErrorEvent event = CameraErrorEvent(cameraId, 'Error Description'); - await camera.handleCameraMethodCall( - MethodCall('error', event.toJson()), cameraId); - await camera.handleCameraMethodCall( - MethodCall('error', event.toJson()), cameraId); - await camera.handleCameraMethodCall( - MethodCall('error', event.toJson()), cameraId); + for (int i = 0; i < 3; i++) { + camera.hostCameraHandlers[cameraId]!.error('Error Description'); + } // Assert expect(await streamQueue.next, event); @@ -474,12 +442,8 @@ void main() { const DeviceOrientationChangedEvent event = DeviceOrientationChangedEvent(DeviceOrientation.portraitUp); for (int i = 0; i < 3; i++) { - await TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger - .handlePlatformMessage( - AndroidCamera.deviceEventChannelName, - const StandardMethodCodec().encodeMethodCall( - MethodCall('orientation_changed', event.toJson())), - null); + camera.hostHandler + .deviceOrientationChanged(PlatformDeviceOrientation.portraitUp); } // Assert @@ -958,16 +922,6 @@ void main() { expect((widget as Texture).textureId, cameraId); }); - test('Should throw MissingPluginException when handling unknown method', - () { - final AndroidCamera camera = AndroidCamera(); - - expect( - () => camera.handleCameraMethodCall( - const MethodCall('unknown_method'), 1), - throwsA(isA())); - }); - test('Should get the max zoom level', () async { // Arrange final MethodChannelMock channel = MethodChannelMock( diff --git a/packages/camera/camera_android/test/utils_test.dart b/packages/camera/camera_android/test/utils_test.dart index 81032101f18..864b83d7e1d 100644 --- a/packages/camera/camera_android/test/utils_test.dart +++ b/packages/camera/camera_android/test/utils_test.dart @@ -38,15 +38,33 @@ void main() { 'landscapeLeft'); }); - test('deserializeDeviceOrientation() should deserialize correctly', () { - expect(deserializeDeviceOrientation('portraitUp'), + test('deviceOrientationFromPlatform() should convert correctly', () { + expect( + deviceOrientationFromPlatform(PlatformDeviceOrientation.portraitUp), DeviceOrientation.portraitUp); - expect(deserializeDeviceOrientation('portraitDown'), + expect( + deviceOrientationFromPlatform(PlatformDeviceOrientation.portraitDown), DeviceOrientation.portraitDown); - expect(deserializeDeviceOrientation('landscapeRight'), + expect( + deviceOrientationFromPlatform( + PlatformDeviceOrientation.landscapeRight), DeviceOrientation.landscapeRight); - expect(deserializeDeviceOrientation('landscapeLeft'), + expect( + deviceOrientationFromPlatform( + PlatformDeviceOrientation.landscapeLeft), DeviceOrientation.landscapeLeft); }); + + test('exposureModeFromPlatform() should convert correctly', () { + expect(exposureModeFromPlatform(PlatformExposureMode.auto), + ExposureMode.auto); + expect(exposureModeFromPlatform(PlatformExposureMode.locked), + ExposureMode.locked); + }); + + test('focusModeFromPlatform() should convert correctly', () { + expect(focusModeFromPlatform(PlatformFocusMode.auto), FocusMode.auto); + expect(focusModeFromPlatform(PlatformFocusMode.locked), FocusMode.locked); + }); }); }