diff --git a/shell/platform/android/io/flutter/plugin/common/BasicMessageChannel.java b/shell/platform/android/io/flutter/plugin/common/BasicMessageChannel.java index 1b562a4a525..3e7a285793a 100644 --- a/shell/platform/android/io/flutter/plugin/common/BasicMessageChannel.java +++ b/shell/platform/android/io/flutter/plugin/common/BasicMessageChannel.java @@ -154,12 +154,7 @@ public final class BasicMessageChannel { handler.onMessage(codec.decodeMessage(message), new Reply() { @Override public void reply(T reply) { - 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)); } }); } catch (RuntimeException e) { diff --git a/shell/platform/android/io/flutter/plugin/common/BinaryMessenger.java b/shell/platform/android/io/flutter/plugin/common/BinaryMessenger.java index 846cd1e0be5..9257bb83124 100644 --- a/shell/platform/android/io/flutter/plugin/common/BinaryMessenger.java +++ b/shell/platform/android/io/flutter/plugin/common/BinaryMessenger.java @@ -17,8 +17,8 @@ public interface BinaryMessenger { * Sends a binary message to the Flutter application. * * @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 message the message payload, a direct-allocated {@link ByteBuffer} with the message bytes + * between position zero and current position, or null. */ void send(String channel, ByteBuffer message); @@ -28,8 +28,8 @@ public interface BinaryMessenger { *

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 message the message payload, a direct-allocated {@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 * message, possibly null. */ @@ -79,8 +79,8 @@ public interface BinaryMessenger { /** * Handles the specified reply. * - * @param reply the reply payload, a {@link ByteBuffer} or null. Senders of outgoing - * replies must place the reply bytes between position zero and current position. + * @param reply the reply payload, a direct-allocated {@link ByteBuffer} or null. Senders of + * outgoing replies must place the reply bytes between position zero and current position. * Reply receivers can read from the buffer directly. */ void reply(ByteBuffer reply); diff --git a/shell/platform/android/io/flutter/plugin/common/EventChannel.java b/shell/platform/android/io/flutter/plugin/common/EventChannel.java index 266e0c7f834..c944233728b 100644 --- a/shell/platform/android/io/flutter/plugin/common/EventChannel.java +++ b/shell/platform/android/io/flutter/plugin/common/EventChannel.java @@ -91,13 +91,8 @@ public final class EventChannel { /** * Handles a request to set up an event stream. * - *

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.

+ *

Any uncaught exception thrown by this method will be caught by the channel + * implementation and logged. An error result message will be sent back to Flutter.

* * @param arguments stream configuration arguments, possibly null. * @param events an {@link EventSink} for emitting events to the Flutter receiver. @@ -107,9 +102,8 @@ public final class EventChannel { /** * Handles a request to tear down an event stream. * - *

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.

+ *

Any uncaught exception thrown by this method will be caught by the channel + * implementation and logged. An error result message will be sent back to Flutter.

* * @param arguments stream configuration arguments, possibly null. */ @@ -156,18 +150,13 @@ public final class EventChannel { @Override public void onMessage(ByteBuffer message, final BinaryReply reply) { - 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)); + 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); } } @@ -180,7 +169,7 @@ public final class EventChannel { } catch (RuntimeException e) { activeSink.set(null); Log.e(TAG + name, "Failed to open event stream", e); - callback.reply(codec.encodeErrorEnvelope("uncaught", e.getMessage(), null)); + callback.reply(codec.encodeErrorEnvelope("error", e.getMessage(), null)); } } else { callback.reply(codec.encodeErrorEnvelope("error", "Stream already active", null)); @@ -195,7 +184,7 @@ public final class EventChannel { callback.reply(codec.encodeSuccessEnvelope(null)); } catch (RuntimeException e) { Log.e(TAG + name, "Failed to close event stream", e); - callback.reply(codec.encodeErrorEnvelope("uncaught", e.getMessage(), null)); + callback.reply(codec.encodeErrorEnvelope("error", e.getMessage(), null)); } } else { callback.reply(codec.encodeErrorEnvelope("error", "No active stream to cancel", null)); @@ -210,12 +199,7 @@ public final class EventChannel { if (hasEnded.get() || activeSink.get() != this) { return; } - 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)); - } + EventChannel.this.messenger.send(name, codec.encodeSuccessEnvelope(event)); } @Override @@ -223,14 +207,9 @@ public final class EventChannel { if (hasEnded.get() || activeSink.get() != this) { return; } - 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)); - } + EventChannel.this.messenger.send( + name, + codec.encodeErrorEnvelope(errorCode, errorMessage, errorDetails)); } @Override diff --git a/shell/platform/android/io/flutter/plugin/common/JSONMessageCodec.java b/shell/platform/android/io/flutter/plugin/common/JSONMessageCodec.java index d534b209ef6..ab284c9753b 100644 --- a/shell/platform/android/io/flutter/plugin/common/JSONMessageCodec.java +++ b/shell/platform/android/io/flutter/plugin/common/JSONMessageCodec.java @@ -26,7 +26,12 @@ public final class JSONMessageCodec implements MessageCodec { if (message == null) { return null; } - return StringCodec.INSTANCE.encodeMessage(JSONUtil.wrap(message).toString()); + final Object wrapped = JSONUtil.wrap(message); + if (wrapped instanceof String) { + return StringCodec.INSTANCE.encodeMessage(JSONObject.quote((String) wrapped)); + } else { + return StringCodec.INSTANCE.encodeMessage(wrapped.toString()); + } } @Override diff --git a/shell/platform/android/io/flutter/plugin/common/JSONMethodCodec.java b/shell/platform/android/io/flutter/plugin/common/JSONMethodCodec.java index 353cc8d450f..263ab2cff39 100644 --- a/shell/platform/android/io/flutter/plugin/common/JSONMethodCodec.java +++ b/shell/platform/android/io/flutter/plugin/common/JSONMethodCodec.java @@ -36,7 +36,7 @@ public final class JSONMethodCodec implements MethodCodec { if (json instanceof JSONObject) { final JSONObject map = (JSONObject) json; final Object method = map.get("method"); - final Object arguments = map.get("args"); + final Object arguments = unwrapNull(map.opt("args")); if (method instanceof String) { return new MethodCall((String) method, arguments); } @@ -58,7 +58,7 @@ public final class JSONMethodCodec implements MethodCodec { Object errorDetails) { return JSONMessageCodec.INSTANCE.encodeMessage(new JSONArray() .put(errorCode) - .put(errorMessage) + .put(JSONUtil.wrap(errorMessage)) .put(JSONUtil.wrap(errorDetails))); } @@ -69,20 +69,24 @@ public final class JSONMethodCodec implements MethodCodec { if (json instanceof JSONArray) { final JSONArray array = (JSONArray) json; if (array.length() == 1) { - return array.get(0); + return unwrapNull(array.opt(0)); } if (array.length() == 3) { final Object code = array.get(0); - final Object message = array.get(1); - final Object details = array.get(2); + final Object message = unwrapNull(array.opt(1)); + final Object details = unwrapNull(array.opt(2)); if (code instanceof String && (message == null || message instanceof String)) { throw new FlutterException((String) code, (String) message, details); } } } - throw new IllegalArgumentException("Invalid method call: " + json); + throw new IllegalArgumentException("Invalid envelope: " + json); } catch (JSONException e) { throw new IllegalArgumentException("Invalid JSON", e); } } + + Object unwrapNull(Object value) { + return (value == JSONObject.NULL) ? null : value; + } } diff --git a/shell/platform/android/io/flutter/plugin/common/MethodChannel.java b/shell/platform/android/io/flutter/plugin/common/MethodChannel.java index e1e584413e6..ebfa421b713 100644 --- a/shell/platform/android/io/flutter/plugin/common/MethodChannel.java +++ b/shell/platform/android/io/flutter/plugin/common/MethodChannel.java @@ -114,11 +114,8 @@ public final class MethodChannel { * 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.

+ *

Any uncaught exception thrown by this method will be caught by the channel implementation and + * logged, and an error result will be sent back to Flutter.

* * @param call A {@link MethodCall}. * @param result A {@link Result} used for submitting the result of the call. @@ -169,8 +166,7 @@ public final class MethodChannel { callback.notImplemented(); } else { try { - final Object result = codec.decodeEnvelope(reply); - callback.success(result); + callback.success(codec.decodeEnvelope(reply)); } catch (FlutterException e) { callback.error(e.code, e.getMessage(), e.details); } @@ -190,35 +186,17 @@ public final class MethodChannel { @Override public void onMessage(ByteBuffer message, final BinaryReply reply) { - 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; - } + final MethodCall call = codec.decodeMethodCall(message); try { handler.onMethodCall(call, new Result() { @Override public void success(Object result) { - 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)); - } + reply.reply(codec.encodeSuccessEnvelope(result)); } @Override public void error(String errorCode, String errorMessage, Object errorDetails) { - 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)); - } + reply.reply(codec.encodeErrorEnvelope(errorCode, errorMessage, errorDetails)); } @Override @@ -228,7 +206,7 @@ public final class MethodChannel { }); } catch (RuntimeException e) { Log.e(TAG + name, "Failed to handle method call", e); - reply.reply(codec.encodeErrorEnvelope("uncaught", e.getMessage(), null)); + reply.reply(codec.encodeErrorEnvelope("error", e.getMessage(), null)); } } } diff --git a/shell/platform/darwin/ios/framework/Source/FlutterCodecs.mm b/shell/platform/darwin/ios/framework/Source/FlutterCodecs.mm index e37395d6925..04b60790240 100644 --- a/shell/platform/darwin/ios/framework/Source/FlutterCodecs.mm +++ b/shell/platform/darwin/ios/framework/Source/FlutterCodecs.mm @@ -57,7 +57,17 @@ - (NSData*)encode:(id)message { if (message == nil) return nil; - NSData* encoding = [NSJSONSerialization dataWithJSONObject:message options:0 error:nil]; + NSData* encoding; + if ([message isKindOfClass:[NSArray class]] || [message isKindOfClass:[NSDictionary class]]) { + encoding = [NSJSONSerialization dataWithJSONObject:message options:0 error:nil]; + } else { + // NSJSONSerialization does not support top-level simple values. + // We encode as singleton array, then extract the relevant bytes. + encoding = [NSJSONSerialization dataWithJSONObject:@[message] options:0 error:nil]; + if (encoding) { + encoding = [encoding subdataWithRange:NSMakeRange(1, encoding.length - 2)]; + } + } NSAssert(encoding, @"Invalid JSON message, encoding failed"); return encoding; } @@ -65,9 +75,28 @@ - (id)decode:(NSData*)message { if (message == nil) return nil; - id decoded = [NSJSONSerialization JSONObjectWithData:message options:0 error:nil]; + BOOL isSimpleValue = NO; + id decoded = nil; + if (0 < message.length) { + UInt8 first; + [message getBytes:&first length:1]; + isSimpleValue = first != '{' && first != '['; + if (isSimpleValue) { + // NSJSONSerialization does not support top-level simple values. + // We expand encoding to singleton array, then decode that and extract + // the single entry. + UInt8 begin = '['; + UInt8 end = ']'; + NSMutableData* expandedMessage = [NSMutableData dataWithLength:message.length + 2]; + [expandedMessage replaceBytesInRange:NSMakeRange(0, 1) withBytes:&begin]; + [expandedMessage replaceBytesInRange:NSMakeRange(1, message.length) withBytes:message.bytes]; + [expandedMessage replaceBytesInRange:NSMakeRange(message.length + 1, 1) withBytes:&end]; + message = expandedMessage; + } + decoded = [NSJSONSerialization JSONObjectWithData:message options:0 error:nil]; + } NSAssert(decoded, @"Invalid JSON message, decoding failed"); - return decoded; + return isSimpleValue ? ((NSArray*) decoded)[0] : decoded; } @end @@ -83,27 +112,26 @@ - (NSData*)encodeMethodCall:(FlutterMethodCall*)call { return [[FlutterJSONMessageCodec sharedInstance] encode:@{ @"method" : call.method, - @"args" : (call.arguments == nil ? [NSNull null] : call.arguments), + @"args" : [self wrapNil:call.arguments], }]; } - (NSData*)encodeSuccessEnvelope:(id)result { - return - [[FlutterJSONMessageCodec sharedInstance] encode:@[ result == nil ? [NSNull null] : result ]]; + return [[FlutterJSONMessageCodec sharedInstance] encode:@[[self wrapNil:result]]]; } - (NSData*)encodeErrorEnvelope:(FlutterError*)error { return [[FlutterJSONMessageCodec sharedInstance] encode:@[ error.code, - error.message == nil ? [NSNull null] : error.message, - error.details == nil ? [NSNull null] : error.details, + [self wrapNil:error.message], + [self wrapNil:error.details], ]]; } - (FlutterMethodCall*)decodeMethodCall:(NSData*)message { NSDictionary* dictionary = [[FlutterJSONMessageCodec sharedInstance] decode:message]; id method = dictionary[@"method"]; - id arguments = dictionary[@"args"]; + id arguments = [self unwrapNil:dictionary[@"args"]]; NSAssert([method isKindOfClass:[NSString class]], @"Invalid JSON method call"); return [FlutterMethodCall methodCallWithMethodName:method arguments:arguments]; } @@ -111,10 +139,20 @@ - (id)decodeEnvelope:(NSData*)envelope { NSArray* array = [[FlutterJSONMessageCodec sharedInstance] decode:envelope]; if (array.count == 1) - return array[0]; + return [self unwrapNil:array[0]]; NSAssert(array.count == 3, @"Invalid JSON envelope"); - NSAssert([array[0] isKindOfClass:[NSString class]], @"Invalid JSON envelope"); - NSAssert(array[1] == nil || [array[1] isKindOfClass:[NSString class]], @"Invalid JSON envelope"); - return [FlutterError errorWithCode:array[0] message:array[1] details:array[2]]; + id code = array[0]; + id message = [self unwrapNil:array[1]]; + id details = [self unwrapNil:array[2]]; + NSAssert([code isKindOfClass:[NSString class]], @"Invalid JSON envelope"); + NSAssert(message == nil || [message isKindOfClass:[NSString class]], @"Invalid JSON envelope"); + return [FlutterError errorWithCode:code message:message details:details]; +} + +- (id)wrapNil:(id)value { + return value == nil ? [NSNull null] : value; +} +- (id)unwrapNil:(id)value { + return value == [NSNull null] ? nil : value; } @end