diff --git a/shell/platform/android/io/flutter/plugin/common/BasicMessageChannel.java b/shell/platform/android/io/flutter/plugin/common/BasicMessageChannel.java index ae7a165598e..1b562a4a525 100644 --- a/shell/platform/android/io/flutter/plugin/common/BasicMessageChannel.java +++ b/shell/platform/android/io/flutter/plugin/common/BasicMessageChannel.java @@ -6,24 +6,25 @@ package io.flutter.plugin.common; import android.util.Log; import java.nio.ByteBuffer; + import io.flutter.plugin.common.BinaryMessenger.BinaryReply; import io.flutter.plugin.common.BinaryMessenger.BinaryMessageHandler; /** - * A named channel for communicating with the Flutter application using semi-structured messages. + * A named channel for communicating with the Flutter application using basic, asynchronous message passing. * - * Messages are encoded into binary before being sent, and binary messages received are decoded + *
Messages are encoded into binary before being sent, and binary messages received are decoded * into Java objects. The {@link MessageCodec} used must be compatible with the - * one used by the Flutter application. This can be achieved by creating a PlatformChannel + * one used by the Flutter application. This can be achieved by creating a + * BasicMessageChannel * counterpart of this channel on the Dart side. The static Java type of messages sent and received - * is Object, but only values supported by the specified {@link MessageCodec} can be used. + * is {@code Object}, but only values supported by the specified {@link MessageCodec} can be used.
* - * The channel supports basic message send/receive operations. All communication is asynchronous. - * - * Identically named channels may interfere with each other's communication. + *The logical identity of the channel is given by its name. Identically named channels will interfere + * with each other's communication.
*/ public final class BasicMessageChannelAny uncaught exception thrown by the reply callback will be caught and logged.
+ * * @param message the message, possibly null. * @param callback a {@link Reply} callback, possibly null. */ @@ -67,9 +70,13 @@ public final class BasicMessageChannelOverrides any existing handler registration for (the name of) this channel.
+ * + *If no handler has been registered, any incoming message on this channel will be handled silently + * by sending a null reply.
* * @param handler a {@link MessageHandler}, or null to deregister. */ @@ -84,10 +91,21 @@ public final class BasicMessageChannelHandler implementations must reply to all incoming messages, by submitting a single reply + * message to the given {@link Reply}. Failure to do so will result in lingering Flutter reply + * handlers. The reply may be submitted asynchronously.
+ * + *Any uncaught exception thrown by this method, or the preceding message decoding, will be + * caught by the channel implementation and logged, and a null reply message will be sent back + * to Flutter.
+ * + *Any uncaught exception thrown during encoding a reply message submitted to the {@link Reply} + * is treated similarly: the exception is logged, and a null reply is sent to Flutter.
* * @param message the message, possibly null. - * @param reply a {@link Reply} for providing a single message reply. + * @param reply a {@link Reply} for sending a single message reply back to Flutter. */ void onMessage(T message, ReplyAny uncaught exception thrown by the reply callback will be caught and logged.
+ * + * @param channel the name {@link String} of the logical channel used for the message. * @param message the message payload, a {@link ByteBuffer} with the message bytes between position * zero and current position, or null. * @param callback a {@link BinaryReply} callback invoked when the Flutter application responds to the @@ -36,11 +39,14 @@ public interface BinaryMessenger { * Registers a handler to be invoked when the Flutter application sends a message * to its host platform. * - * Registration overwrites any previous registration for the same channel name. - * Use a {@code null} handler to unregister. + *Registration overwrites any previous registration for the same channel name. + * Use a null handler to deregister.
+ * + *If no handler has been registered for a particular channel, any incoming message on + * that channel will be handled silently by sending a null reply.
* * @param channel the name {@link String} of the channel. - * @param handler a {@link BinaryMessageHandler} to be invoked on incoming messages. + * @param handler a {@link BinaryMessageHandler} to be invoked on incoming messages, or null. */ void setMessageHandler(String channel, BinaryMessageHandler handler); @@ -51,7 +57,14 @@ public interface BinaryMessenger { /** * Handles the specified message. * - * @param message the message {@link ByteBuffer} payload. + *Handler implementations must reply to all incoming messages, by submitting a single reply + * message to the given {@link BinaryReply}. Failure to do so will result in lingering Flutter reply + * handlers. The reply may be submitted asynchronously.
+ * + *Any uncaught exception thrown by this method will be caught by the messenger implementation and + * logged, and a null reply message will be sent back to Flutter.
+ * + * @param message the message {@link ByteBuffer} payload, possibly null. * @param reply A {@link BinaryReply} used for submitting a reply back to Flutter. */ void onMessage(ByteBuffer message, BinaryReply reply); diff --git a/shell/platform/android/io/flutter/plugin/common/EventChannel.java b/shell/platform/android/io/flutter/plugin/common/EventChannel.java index 0bd6e168b33..266e0c7f834 100644 --- a/shell/platform/android/io/flutter/plugin/common/EventChannel.java +++ b/shell/platform/android/io/flutter/plugin/common/EventChannel.java @@ -9,22 +9,24 @@ import io.flutter.plugin.common.BinaryMessenger.BinaryMessageHandler; import io.flutter.plugin.common.BinaryMessenger.BinaryReply; import java.nio.ByteBuffer; +import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicReference; /** * A named channel for communicating with the Flutter application using asynchronous * event streams. * - * Incoming requests for event stream setup are decoded from binary on receipt, and + *Incoming requests for event stream setup are decoded from binary on receipt, and * Java responses and events are encoded into binary before being transmitted back * to Flutter. The {@link MethodCodec} used must be compatible with the one used by - * the Flutter application. This can be achieved by creating a PlatformEventChannel - * counterpart of this channel on the Flutter side. The Java type of responses and - * events is Object, but only values supported by the specified {@link MethodCodec} - * can be used. + * the Flutter application. This can be achieved by creating an + * EventChannel + * counterpart of this channel on the Dart side. The Java type of stream configuration arguments, + * events, and error details is {@code Object}, but only values supported by the specified + * {@link MethodCodec} can be used.
* - * The identity of the channel is given by its name, so other uses of that name - * with may interfere with this channel's communication. + *The logical identity of the channel is given by its name. Identically named channels will interfere + * with each other's communication.
*/ public final class EventChannel { private static final String TAG = "EventChannel#"; @@ -64,7 +66,10 @@ public final class EventChannel { /** * Registers a stream handler on this channel. * - * Overrides any existing handler registration. + *Overrides any existing handler registration for (the name of) this channel.
+ * + *If no handler has been registered, any incoming stream setup requests will be handled + * silently by providing an empty stream.
* * @param handler a {@link StreamHandler}, or null to deregister. */ @@ -72,56 +77,75 @@ public final class EventChannel { messenger.setMessageHandler(name, handler == null ? null : new IncomingStreamRequestHandler(handler)); } - /** - * Event callback. Supports dual use: Producers of events to be sent to Flutter - * act as clients of this interface for sending events. Consumers of events sent - * from Flutter implement this interface for handling received events. - */ - public interface EventSink { - /** - * Consumes a successful event. - * - * @param event The event, possibly null. - */ - void success(Object event); - - /** - * Consumes an error event. - * - * @param errorCode An error code String. - * @param errorMessage A human-readable error message String, possibly null. - * @param errorDetails Error details, possibly null - */ - void error(String errorCode, String errorMessage, Object errorDetails); - - /** - * Consumes end of stream. No calls to {@link #success(Object)} or - * {@link #error(String, String, Object)} will be made following a call - * to this method. - */ - void endOfStream(); - } - /** * Handler of stream setup and tear-down requests. + * + *Implementations must be prepared to accept sequences of alternating calls to + * {@link #onListen(Object, EventSink)} and {@link #onCancel(Object)}. Implementations + * should ideally consume no resources when the last such call is not {@code onListen}. + * In typical situations, this means that the implementation should register itself + * with platform-specific event sources {@code onListen} and deregister again + * {@code onCancel}.
*/ public interface StreamHandler { /** * Handles a request to set up an event stream. * - * @param arguments Stream configuration arguments, possibly null. - * @param events An {@link EventSink} for emitting events to the Flutter receiver. + *Any uncaught exception thrown by this method, or the preceding arguments + * decoding, will be caught by the channel implementation and logged. An error result + * message will be sent back to Flutter.
+ * + *Any uncaught exception thrown during encoding an event or error submitted to the + * {@link EventSink} is treated similarly: the exception is logged, and an error event + * is sent to Flutter.
+ * + * @param arguments stream configuration arguments, possibly null. + * @param events an {@link EventSink} for emitting events to the Flutter receiver. */ void onListen(Object arguments, EventSink events); /** * Handles a request to tear down an event stream. * - * @param arguments Stream configuration arguments, possibly null. + *Any uncaught exception thrown by this method, or the preceding arguments + * decoding, will be caught by the channel implementation and logged. An error result + * result message will be sent back to Flutter.
+ * + * @param arguments stream configuration arguments, possibly null. */ void onCancel(Object arguments); } + /** + * Event callback. Supports dual use: Producers of events to be sent to Flutter + * act as clients of this interface for sending events. Consumers of events sent + * from Flutter implement this interface for handling received events (the latter + * facility has not been implemented yet). + */ + public interface EventSink { + /** + * Consumes a successful event. + * + * @param event the event, possibly null. + */ + void success(Object event); + + /** + * Consumes an error event. + * + * @param errorCode an error code String. + * @param errorMessage a human-readable error message String, possibly null. + * @param errorDetails error details, possibly null + */ + void error(String errorCode, String errorMessage, Object errorDetails); + + /** + * Consumes end of stream. Ensuing calls to {@link #success(Object)} or + * {@link #error(String, String, Object)}, if any, are ignored. + */ + void endOfStream(); + } + private final class IncomingStreamRequestHandler implements BinaryMessageHandler { private final StreamHandler handler; private final AtomicReferenceIncoming method calls are decoded from binary on receipt, and Java results are encoded * into binary before being transmitted back to Flutter. The {@link MethodCodec} used must be * compatible with the one used by the Flutter application. This can be achieved - * by creating a PlatformMethodChannel counterpart of this channel on the - * Flutter side. The Java type of method call arguments and results is Object, - * but only values supported by the specified {@link MethodCodec} can be used. + * by creating a + * MethodChannel + * counterpart of this channel on the Dart side. The Java type of method call arguments and results is + * {@code Object}, but only values supported by the specified {@link MethodCodec} can be used.
* - * The identity of the channel is given by its name, so other uses of that name - * with may interfere with this channel's communication. + *The logical identity of the channel is given by its name. Identically named channels will interfere + * with each other's communication.
*/ public final class MethodChannel { private static final String TAG = "MethodChannel#"; @@ -69,11 +70,13 @@ public final class MethodChannel { } /** - * Invokes a method on this channel. + * Invokes a method on this channel, optionally expecting a result. + * + *Any uncaught exception thrown by the result callback will be caught and logged.
* * @param method the name String of the method. * @param arguments the arguments for the invocation, possibly null. - * @param callback a {@link Result} callback for the invocation result. + * @param callback a {@link Result} callback for the invocation result, or null. */ public void invokeMethod(String method, Object arguments, Result callback) { messenger.send(name, codec.encodeMethodCall(new MethodCall(method, arguments)), @@ -83,7 +86,14 @@ public final class MethodChannel { /** * Registers a method call handler on this channel. * - * Overrides any existing handler registration. + *Overrides any existing handler registration for (the name of) this channel.
+ * + *If no handler has been registered, any incoming method call on this channel will be handled + * silently by sending a null reply. This results in a + * MissingPluginException + * on the Dart side, unless an + * OptionalMethodChannel + * is used.
* * @param handler a {@link MethodCallHandler}, or null to deregister. */ @@ -92,6 +102,30 @@ public final class MethodChannel { handler == null ? null : new IncomingMethodCallHandler(handler)); } + /** + * A handler of incoming method calls. + */ + public interface MethodCallHandler { + /** + * Handles the specified method call received from Flutter. + * + *Handler implementations must submit a result for all incoming calls, by making a single call + * on the given {@link Result} callback. Failure to do so will result in lingering Flutter result + * handlers. The result may be submitted asynchronously. Calls to unknown or unimplemented methods + * should be handled using {@link Result#notImplemented()}.
+ * + *Any uncaught exception thrown by this method, or the preceding method call decoding, will be + * caught by the channel implementation and logged, and an error result will be sent back to Flutter.
+ * + *Any uncaught exception thrown during encoding a result submitted to the {@link Result} + * is treated similarly: the exception is logged, and an error result is sent to Flutter.
+ * + * @param call A {@link MethodCall}. + * @param result A {@link Result} used for submitting the result of the call. + */ + void onMethodCall(MethodCall call, Result result); + } + /** * Method call result callback. Supports dual use: Implementations of methods * to be invoked by Flutter act as clients of this interface for sending results @@ -121,19 +155,6 @@ public final class MethodChannel { void notImplemented(); } - /** - * A handler of incoming method calls. - */ - public interface MethodCallHandler { - /** - * Handles the specified method call. - * - * @param call A {@link MethodCall}. - * @param result A {@link Result} used for submitting the result of the call. - */ - void onMethodCall(MethodCall call, Result result); - } - private final class IncomingResultHandler implements BinaryReply { private final Result callback; @@ -143,15 +164,19 @@ public final class MethodChannel { @Override public void reply(ByteBuffer reply) { - if (reply == null) { - callback.notImplemented(); - } else { - try { - final Object result = codec.decodeEnvelope(reply); - callback.success(result); - } catch (FlutterException e) { - callback.error(e.code, e.getMessage(), e.details); + try { + if (reply == null) { + callback.notImplemented(); + } else { + try { + final Object result = codec.decodeEnvelope(reply); + callback.success(result); + } catch (FlutterException e) { + callback.error(e.code, e.getMessage(), e.details); + } } + } catch (RuntimeException e) { + Log.e(TAG + name, "Failed to handle method call result", e); } } } @@ -165,42 +190,45 @@ public final class MethodChannel { @Override public void onMessage(ByteBuffer message, final BinaryReply reply) { - final MethodCall call = codec.decodeMethodCall(message); + MethodCall call; + try { + call = codec.decodeMethodCall(message); + } catch (RuntimeException e) { + Log.e(TAG + name, "Failed to decode method call", e); + reply.reply(codec.encodeErrorEnvelope("decode", e.getMessage(), null)); + return; + } try { handler.onMethodCall(call, new Result() { - private boolean done = false; - @Override public void success(Object result) { - checkDone(); - reply.reply(codec.encodeSuccessEnvelope(result)); - done = true; + try { + reply.reply(codec.encodeSuccessEnvelope(result)); + } catch (RuntimeException e) { + Log.e(TAG + name, "Failed to encode success result", e); + reply.reply(codec.encodeErrorEnvelope("encode", e.getMessage(), null)); + } } @Override public void error(String errorCode, String errorMessage, Object errorDetails) { - checkDone(); - reply.reply(codec.encodeErrorEnvelope( - errorCode, errorMessage, errorDetails)); - done = true; + try { + reply.reply(codec.encodeErrorEnvelope( + errorCode, errorMessage, errorDetails)); + } catch (RuntimeException e) { + Log.e(TAG + name, "Failed to encode error result", e); + reply.reply(codec.encodeErrorEnvelope("encode", e.getMessage(), null)); + } } @Override public void notImplemented() { - checkDone(); reply.reply(null); - done = true; - } - - private void checkDone() { - if (done) { - throw new IllegalStateException("Call result already provided"); - } } }); - } catch (Exception e) { + } catch (RuntimeException e) { Log.e(TAG + name, "Failed to handle method call", e); - reply.reply(codec.encodeErrorEnvelope("error", e.getMessage(), null)); + reply.reply(codec.encodeErrorEnvelope("uncaught", e.getMessage(), null)); } } } diff --git a/shell/platform/android/io/flutter/view/FlutterView.java b/shell/platform/android/io/flutter/view/FlutterView.java index 34b32c64968..8d268579bcb 100644 --- a/shell/platform/android/io/flutter/view/FlutterView.java +++ b/shell/platform/android/io/flutter/view/FlutterView.java @@ -45,6 +45,7 @@ import java.util.HashMap; import java.util.List; import java.util.Locale; import java.util.Map; +import java.util.concurrent.atomic.AtomicBoolean; /** * An Android view containing a Flutter app. @@ -607,8 +608,12 @@ public class FlutterView extends SurfaceView final ByteBuffer buffer = (message == null ? null : ByteBuffer.wrap(message)); handler.onMessage(buffer, new BinaryReply() { + private final AtomicBoolean done = new AtomicBoolean(false); @Override public void reply(ByteBuffer reply) { + if (done.getAndSet(true)) { + throw new IllegalStateException("Reply already submitted"); + } if (reply == null) { nativeInvokePlatformMessageEmptyResponseCallback(mNativePlatformView, replyId); @@ -637,7 +642,7 @@ public class FlutterView extends SurfaceView try { callback.reply(reply == null ? null : ByteBuffer.wrap(reply)); } catch (Exception ex) { - Log.e(TAG, "Uncaught exception in binary message handler reply", ex); + Log.e(TAG, "Uncaught exception in binary message reply handler", ex); } } }