Clarify semantics of channel-related errors (#3607)

Mainly improvements to javadoc and error logging. A bit of refactoring.
This commit is contained in:
Mikkel Nygaard Ravn 2017-04-19 21:53:46 +02:00 committed by GitHub
parent 3ed4684a09
commit 4eed7d2732
6 changed files with 246 additions and 144 deletions

View File

@ -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
* <p>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
* <a href="https://docs.flutter.io/flutter/services/BasicMessageChannel-class.html">BasicMessageChannel</a>
* 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.</p>
*
* The channel supports basic message send/receive operations. All communication is asynchronous.
*
* Identically named channels may interfere with each other's communication.
* <p>The logical identity of the channel is given by its name. Identically named channels will interfere
* with each other's communication.</p>
*/
public final class BasicMessageChannel<T> {
private static final String TAG = "FlutterMessageChannel#";
private static final String TAG = "BasicMessageChannel#";
private final BinaryMessenger messenger;
private final String name;
@ -58,6 +59,8 @@ public final class BasicMessageChannel<T> {
/**
* Sends the specified message to the Flutter application, optionally expecting a reply.
*
* <p>Any uncaught exception thrown by the reply callback will be caught and logged.</p>
*
* @param message the message, possibly null.
* @param callback a {@link Reply} callback, possibly null.
*/
@ -67,9 +70,13 @@ public final class BasicMessageChannel<T> {
}
/**
* Registers a message handler on this channel.
* Registers a message handler on this channel for receiving messages sent from the Flutter
* application.
*
* Overrides any existing handler registration (for messages, method calls, or streams).
* <p>Overrides any existing handler registration for (the name of) this channel.</p>
*
* <p>If no handler has been registered, any incoming message on this channel will be handled silently
* by sending a null reply.</p>
*
* @param handler a {@link MessageHandler}, or null to deregister.
*/
@ -84,10 +91,21 @@ public final class BasicMessageChannel<T> {
public interface MessageHandler<T> {
/**
* Handles the specified message.
* Handles the specified message received from Flutter.
*
* <p>Handler 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.</p>
*
* <p>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.</p>
*
* <p>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.</p>
*
* @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, Reply<T> reply);
}
@ -117,7 +135,7 @@ public final class BasicMessageChannel<T> {
public void reply(ByteBuffer reply) {
try {
callback.reply(codec.decodeMessage(reply));
} catch (Exception e) {
} catch (RuntimeException e) {
Log.e(TAG + name, "Failed to handle message reply", e);
}
}
@ -134,18 +152,17 @@ public final class BasicMessageChannel<T> {
public void onMessage(ByteBuffer message, final BinaryReply callback) {
try {
handler.onMessage(codec.decodeMessage(message), new Reply<T>() {
private boolean done = false;
@Override
public void reply(T reply) {
if (done) {
throw new IllegalStateException("Call result already provided");
try {
callback.reply(codec.encodeMessage(reply));
} catch (RuntimeException e) {
Log.e(TAG + name, "Failed to encode reply", e);
callback.reply(null);
}
callback.reply(codec.encodeMessage(reply));
done = true;
}
});
} catch (Exception e) {
} catch (RuntimeException e) {
Log.e(TAG + name, "Failed to handle message", e);
callback.reply(null);
}

View File

@ -4,7 +4,9 @@ import java.nio.ByteBuffer;
/**
* Facility for communicating with Flutter using asynchronous message passing with binary messages.
* The Flutter Dart code can use {@code BinaryMessages} to participate.
* The Flutter Dart code should use
* <a href="https://docs.flutter.io/flutter/services/BinaryMessages-class.html">BinaryMessages</a>
* to participate.
*
* @see BasicMessageChannel , which supports message passing with Strings and semi-structured messages.
* @see MethodChannel , which supports communication using asynchronous method invocation.
@ -12,19 +14,20 @@ import java.nio.ByteBuffer;
*/
public interface BinaryMessenger {
/**
* Sends a binary message to the Flutter application. The Flutter Dart code can register a
* platform message handler with {@code BinaryMessages} that will receive these messages.
* Sends a binary message to the Flutter application.
*
* @param channel the name {@link String} of the channel that will receive this message.
* @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.
*/
void send(String channel, ByteBuffer message);
/**
* Sends a binary message to the Flutter application,
* Sends a binary message to the Flutter application, optionally expecting a reply.
*
* @param channel the name {@link String} of the channel that will receive this message.
* <p>Any uncaught exception thrown by the reply callback will be caught and logged.</p>
*
* @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.
* <p>Registration overwrites any previous registration for the same channel name.
* Use a null handler to deregister.</p>
*
* <p>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.</p>
*
* @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.
* <p>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.</p>
*
* <p>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.</p>
*
* @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);

View File

@ -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
* <p>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
* <a href="https://docs.flutter.io/flutter/services/EventChannel-class.html">EventChannel</a>
* 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.</p>
*
* The identity of the channel is given by its name, so other uses of that name
* with may interfere with this channel's communication.
* <p>The logical identity of the channel is given by its name. Identically named channels will interfere
* with each other's communication.</p>
*/
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.
* <p>Overrides any existing handler registration for (the name of) this channel.</p>
*
* <p>If no handler has been registered, any incoming stream setup requests will be handled
* silently by providing an empty stream.</p>
*
* @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.
*
* <p>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}.</p>
*/
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.
* <p>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.</p>
*
* <p>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.</p>
*
* @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.
* <p>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.</p>
*
* @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 AtomicReference<EventSink> activeSink = new AtomicReference<>(null);
@ -132,13 +156,18 @@ public final class EventChannel {
@Override
public void onMessage(ByteBuffer message, final BinaryReply reply) {
final MethodCall call = codec.decodeMethodCall(message);
if (call.method.equals("listen")) {
onListen(call.arguments, reply);
} else if (call.method.equals("cancel")) {
onCancel(call.arguments, reply);
} else {
reply.reply(null);
try {
final MethodCall call = codec.decodeMethodCall(message);
if (call.method.equals("listen")) {
onListen(call.arguments, reply);
} else if (call.method.equals("cancel")) {
onCancel(call.arguments, reply);
} else {
reply.reply(null);
}
} catch (RuntimeException e) {
Log.e(TAG + name, "Failed to decode event stream lifecycle call", e);
reply.reply(codec.encodeErrorEnvelope("decode", e.getMessage(), null));
}
}
@ -148,10 +177,10 @@ public final class EventChannel {
try {
handler.onListen(arguments, eventSink);
callback.reply(codec.encodeSuccessEnvelope(null));
} catch (Exception e) {
} catch (RuntimeException e) {
activeSink.set(null);
Log.e(TAG + name, "Failed to open event stream", e);
callback.reply(codec.encodeErrorEnvelope("error", e.getMessage(), null));
callback.reply(codec.encodeErrorEnvelope("uncaught", e.getMessage(), null));
}
} else {
callback.reply(codec.encodeErrorEnvelope("error", "Stream already active", null));
@ -164,9 +193,9 @@ public final class EventChannel {
try {
handler.onCancel(arguments);
callback.reply(codec.encodeSuccessEnvelope(null));
} catch (Exception e) {
} catch (RuntimeException e) {
Log.e(TAG + name, "Failed to close event stream", e);
callback.reply(codec.encodeErrorEnvelope("error", e.getMessage(), null));
callback.reply(codec.encodeErrorEnvelope("uncaught", e.getMessage(), null));
}
} else {
callback.reply(codec.encodeErrorEnvelope("error", "No active stream to cancel", null));
@ -174,30 +203,39 @@ public final class EventChannel {
}
private final class EventSinkImplementation implements EventSink {
final AtomicBoolean hasEnded = new AtomicBoolean(false);
@Override
public void success(Object event) {
if (activeSink.get() != this) {
if (hasEnded.get() || activeSink.get() != this) {
return;
}
EventChannel.this.messenger.send(
name,
codec.encodeSuccessEnvelope(event));
try {
EventChannel.this.messenger.send(name, codec.encodeSuccessEnvelope(event));
} catch (RuntimeException e) {
Log.e(TAG + name, "Failed to encode event", e);
EventChannel.this.messenger.send(name, codec.encodeErrorEnvelope("encode", e.getMessage(), null));
}
}
@Override
public void error(String errorCode, String errorMessage,
Object errorDetails) {
if (activeSink.get() != this) {
public void error(String errorCode, String errorMessage, Object errorDetails) {
if (hasEnded.get() || activeSink.get() != this) {
return;
}
EventChannel.this.messenger.send(
name,
codec.encodeErrorEnvelope(errorCode, errorMessage, errorDetails));
try {
EventChannel.this.messenger.send(
name,
codec.encodeErrorEnvelope(errorCode, errorMessage, errorDetails));
} catch (RuntimeException e) {
Log.e(TAG + name, "Failed to encode error", e);
EventChannel.this.messenger.send(name, codec.encodeErrorEnvelope("encode", e.getMessage(), null));
}
}
@Override
public void endOfStream() {
if (activeSink.get() != this) {
if (hasEnded.getAndSet(true) || activeSink.get() != this) {
return;
}
EventChannel.this.messenger.send(name, null);

View File

@ -11,6 +11,7 @@ import org.json.JSONObject;
* {@link JSONMessageCodec}.
*/
public final class JSONMethodCodec implements MethodCodec {
// This codec must match the Dart codec of the same name in package flutter/services.
public static final JSONMethodCodec INSTANCE = new JSONMethodCodec();
private JSONMethodCodec() {

View File

@ -13,15 +13,16 @@ import java.nio.ByteBuffer;
* A named channel for communicating with the Flutter application using asynchronous
* method calls.
*
* Incoming method calls are decoded from binary on receipt, and Java results are encoded
* <p>Incoming 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
* <a href="https://docs.flutter.io/flutter/services/MethodChannel-class.html">MethodChannel</a>
* 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.</p>
*
* The identity of the channel is given by its name, so other uses of that name
* with may interfere with this channel's communication.
* <p>The logical identity of the channel is given by its name. Identically named channels will interfere
* with each other's communication.</p>
*/
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.
*
* <p>Any uncaught exception thrown by the result callback will be caught and logged.</p>
*
* @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.
* <p>Overrides any existing handler registration for (the name of) this channel.</p>
*
* <p>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
* <a href="https://docs.flutter.io/flutter/services/MissingPluginException-class.html">MissingPluginException</a>
* on the Dart side, unless an
* <a href="https://docs.flutter.io/flutter/services/OptionalMethodChannel-class.html">OptionalMethodChannel</a>
* is used.</p>
*
* @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.
*
* <p>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()}.</p>
*
* <p>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.</p>
*
* <p>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.</p>
*
* @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));
}
}
}

View File

@ -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);
}
}
}