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 BasicMessageChannel { - 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 { /** * Sends the specified message to the Flutter application, optionally expecting a reply. * + *

Any 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 BasicMessageChannel { } /** - * 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). + *

Overrides 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 BasicMessageChannel { public interface MessageHandler { /** - * Handles the specified message. + * Handles the specified message received from Flutter. + * + *

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.

+ * + *

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, Reply reply); } @@ -117,7 +135,7 @@ public final class BasicMessageChannel { 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 { public void onMessage(ByteBuffer message, final BinaryReply callback) { try { handler.onMessage(codec.decodeMessage(message), new Reply() { - 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); } diff --git a/shell/platform/android/io/flutter/plugin/common/BinaryMessenger.java b/shell/platform/android/io/flutter/plugin/common/BinaryMessenger.java index 1d91871c761..846cd1e0be5 100644 --- a/shell/platform/android/io/flutter/plugin/common/BinaryMessenger.java +++ b/shell/platform/android/io/flutter/plugin/common/BinaryMessenger.java @@ -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 + * BinaryMessages + * 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. + *

Any 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 AtomicReference 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); diff --git a/shell/platform/android/io/flutter/plugin/common/JSONMethodCodec.java b/shell/platform/android/io/flutter/plugin/common/JSONMethodCodec.java index 03b2203c2b3..353cc8d450f 100644 --- a/shell/platform/android/io/flutter/plugin/common/JSONMethodCodec.java +++ b/shell/platform/android/io/flutter/plugin/common/JSONMethodCodec.java @@ -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() { diff --git a/shell/platform/android/io/flutter/plugin/common/MethodChannel.java b/shell/platform/android/io/flutter/plugin/common/MethodChannel.java index ab04fbc6c10..e1e584413e6 100644 --- a/shell/platform/android/io/flutter/plugin/common/MethodChannel.java +++ b/shell/platform/android/io/flutter/plugin/common/MethodChannel.java @@ -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 + *

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 + * 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); } } }