mirror of
https://github.com/flutter/flutter.git
synced 2026-02-20 02:29:02 +08:00
Fix corner case errors in platform channels (#3631)
This commit is contained in:
parent
faa4aba80f
commit
3211d2fca2
@ -154,12 +154,7 @@ public final class BasicMessageChannel<T> {
|
||||
handler.onMessage(codec.decodeMessage(message), new Reply<T>() {
|
||||
@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) {
|
||||
|
||||
@ -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 {
|
||||
* <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 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);
|
||||
|
||||
@ -91,13 +91,8 @@ public final class EventChannel {
|
||||
/**
|
||||
* Handles a request to set up an event stream.
|
||||
*
|
||||
* <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>
|
||||
* <p>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.</p>
|
||||
*
|
||||
* @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.
|
||||
*
|
||||
* <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>
|
||||
* <p>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.</p>
|
||||
*
|
||||
* @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
|
||||
|
||||
@ -26,7 +26,12 @@ public final class JSONMessageCodec implements MessageCodec<Object> {
|
||||
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
|
||||
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@ -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()}.</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>
|
||||
* <p>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.</p>
|
||||
*
|
||||
* @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));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -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
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user