Unverified Commit 340c6803 authored by Mikkel Nygaard Ravn's avatar Mikkel Nygaard Ravn Committed by GitHub

Make standard codecs extensible (#15414)

parent 2d5ebd2a
......@@ -4,7 +4,9 @@
package com.yourcompany.channels;
import java.io.ByteArrayOutputStream;
import java.nio.ByteBuffer;
import java.util.Date;
import android.os.Bundle;
......@@ -20,9 +22,9 @@ public class MainActivity extends FlutterActivity {
setupMessageHandshake(new BasicMessageChannel<>(getFlutterView(), "binary-msg", BinaryCodec.INSTANCE));
setupMessageHandshake(new BasicMessageChannel<>(getFlutterView(), "string-msg", StringCodec.INSTANCE));
setupMessageHandshake(new BasicMessageChannel<>(getFlutterView(), "json-msg", JSONMessageCodec.INSTANCE));
setupMessageHandshake(new BasicMessageChannel<>(getFlutterView(), "std-msg", StandardMessageCodec.INSTANCE));
setupMessageHandshake(new BasicMessageChannel<>(getFlutterView(), "std-msg", ExtendedStandardMessageCodec.INSTANCE));
setupMethodHandshake(new MethodChannel(getFlutterView(), "json-method", JSONMethodCodec.INSTANCE));
setupMethodHandshake(new MethodChannel(getFlutterView(), "std-method", StandardMethodCodec.INSTANCE));
setupMethodHandshake(new MethodChannel(getFlutterView(), "std-method", new StandardMethodCodec(ExtendedStandardMessageCodec.INSTANCE)));
}
private <T> void setupMessageHandshake(final BasicMessageChannel<T> channel) {
......@@ -135,3 +137,49 @@ public class MainActivity extends FlutterActivity {
});
}
}
final class ExtendedStandardMessageCodec extends StandardMessageCodec {
public static final ExtendedStandardMessageCodec INSTANCE = new ExtendedStandardMessageCodec();
private static final byte DATE = (byte) 128;
private static final byte PAIR = (byte) 129;
@Override
protected void writeValue(ByteArrayOutputStream stream, Object value) {
if (value instanceof Date) {
stream.write(DATE);
writeLong(stream, ((Date) value).getTime());
} else if (value instanceof Pair) {
stream.write(PAIR);
writeValue(stream, ((Pair) value).left);
writeValue(stream, ((Pair) value).right);
} else {
super.writeValue(stream, value);
}
}
@Override
protected Object readValueOfType(byte type, ByteBuffer buffer) {
switch (type) {
case DATE:
return new Date(buffer.getLong());
case PAIR:
return new Pair(readValue(buffer), readValue(buffer));
default: return super.readValueOfType(type, buffer);
}
}
}
final class Pair {
public final Object left;
public final Object right;
public Pair(Object left, Object right) {
this.left = left;
this.right = right;
}
@Override
public String toString() {
return "Pair[" + left + ", " + right + "]";
}
}
......@@ -5,6 +5,82 @@
#include "AppDelegate.h"
#include "GeneratedPluginRegistrant.h"
@interface Pair : NSObject
@property(atomic, readonly, strong, nullable) NSObject* left;
@property(atomic, readonly, strong, nullable) NSObject* right;
- (instancetype)initWithLeft:(NSObject*)first right:(NSObject*)right;
@end
@implementation Pair
- (instancetype)initWithLeft:(NSObject*)left right:(NSObject*)right {
self = [super init];
_left = left;
_right = right;
return self;
}
@end
const UInt8 DATE = 128;
const UInt8 PAIR = 129;
@interface ExtendedWriter : FlutterStandardWriter
- (void)writeValue:(id)value;
@end
@implementation ExtendedWriter
- (void)writeValue:(id)value {
if ([value isKindOfClass:[NSDate class]]) {
[self writeByte:DATE];
NSDate* date = value;
NSTimeInterval time = date.timeIntervalSince1970;
SInt64 ms = (SInt64) (time * 1000.0);
[self writeBytes:&ms length:8];
} else if ([value isKindOfClass:[Pair class]]) {
Pair* pair = value;
[self writeByte:PAIR];
[self writeValue:pair.left];
[self writeValue:pair.right];
} else {
[super writeValue:value];
}
}
@end
@interface ExtendedReader : FlutterStandardReader
- (id)readValueOfType:(UInt8)type;
@end
@implementation ExtendedReader
- (id)readValueOfType:(UInt8)type {
switch (type) {
case DATE: {
SInt64 value;
[self readBytes:&value length:8];
NSTimeInterval time = [NSNumber numberWithLong:value].doubleValue / 1000.0;
return [NSDate dateWithTimeIntervalSince1970:time];
}
case PAIR: {
return [[Pair alloc] initWithLeft:[self readValue] right:[self readValue]];
}
default: return [super readValueOfType:type];
}
}
@end
@interface ExtendedReaderWriter : FlutterStandardReaderWriter
- (FlutterStandardWriter*)writerWithData:(NSMutableData*)data;
- (FlutterStandardReader*)readerWithData:(NSData*)data;
@end
@implementation ExtendedReaderWriter
- (FlutterStandardWriter*)writerWithData:(NSMutableData*)data {
return [[ExtendedWriter alloc] initWithData:data];
}
- (FlutterStandardReader*)readerWithData:(NSData*)data {
return [[ExtendedReader alloc] initWithData:data];
}
@end
@implementation AppDelegate
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
......@@ -13,6 +89,7 @@
FlutterViewController *flutterController =
(FlutterViewController *)self.window.rootViewController;
ExtendedReaderWriter* extendedReaderWriter = [ExtendedReaderWriter new];
[self setupMessagingHandshakeOnChannel:
[FlutterBasicMessageChannel messageChannelWithName:@"binary-msg"
binaryMessenger:flutterController
......@@ -28,7 +105,7 @@
[self setupMessagingHandshakeOnChannel:
[FlutterBasicMessageChannel messageChannelWithName:@"std-msg"
binaryMessenger:flutterController
codec:[FlutterStandardMessageCodec sharedInstance]]];
codec:[FlutterStandardMessageCodec codecWithReaderWriter:extendedReaderWriter]]];
[self setupMethodCallSuccessHandshakeOnChannel:
[FlutterMethodChannel methodChannelWithName:@"json-method"
binaryMessenger:flutterController
......@@ -36,7 +113,7 @@
[self setupMethodCallSuccessHandshakeOnChannel:
[FlutterMethodChannel methodChannelWithName:@"std-method"
binaryMessenger:flutterController
codec:[FlutterStandardMethodCodec sharedInstance]]];
codec:[FlutterStandardMethodCodec codecWithReaderWriter:extendedReaderWriter]]];
return [super application:application didFinishLaunchingWithOptions:launchOptions];
}
......
......@@ -10,6 +10,7 @@ import 'package:flutter_driver/driver_extension.dart';
import 'src/basic_messaging.dart';
import 'src/method_calls.dart';
import 'src/pair.dart';
import 'src/test_step.dart';
void main() {
......@@ -23,6 +24,7 @@ class TestApp extends StatefulWidget {
}
class _TestAppState extends State<TestApp> {
static final dynamic anUnknownValue = new DateTime.fromMillisecondsSinceEpoch(1520777802314);
static final List<dynamic> aList = <dynamic>[
false,
0,
......@@ -39,7 +41,7 @@ class _TestAppState extends State<TestApp> {
'd': 'hello',
'e': <dynamic>[
<String, dynamic>{'key': 42}
]
],
};
static final Uint8List someUint8s = new Uint8List.fromList(<int>[
0xBA,
......@@ -69,6 +71,10 @@ class _TestAppState extends State<TestApp> {
double.maxFinite,
double.infinity,
]);
static final dynamic aCompoundUnknownValue = <dynamic>[
anUnknownValue,
new Pair(anUnknownValue, aList),
];
static final List<TestStep> steps = <TestStep>[
() => methodCallJsonSuccessHandshake(null),
() => methodCallJsonSuccessHandshake(true),
......@@ -83,6 +89,8 @@ class _TestAppState extends State<TestApp> {
() => methodCallStandardSuccessHandshake('world'),
() => methodCallStandardSuccessHandshake(aList),
() => methodCallStandardSuccessHandshake(aMap),
() => methodCallStandardSuccessHandshake(anUnknownValue),
() => methodCallStandardSuccessHandshake(aCompoundUnknownValue),
() => methodCallJsonErrorHandshake(null),
() => methodCallJsonErrorHandshake('world'),
() => methodCallStandardErrorHandshake(null),
......@@ -138,6 +146,8 @@ class _TestAppState extends State<TestApp> {
() => basicStandardHandshake(<String, dynamic>{}),
() => basicStandardHandshake(<dynamic, dynamic>{7: true, false: -7}),
() => basicStandardHandshake(aMap),
() => basicStandardHandshake(anUnknownValue),
() => basicStandardHandshake(aCompoundUnknownValue),
() => basicBinaryMessageToUnknownChannel(),
() => basicStringMessageToUnknownChannel(),
() => basicJsonMessageToUnknownChannel(),
......
......@@ -5,8 +5,42 @@
import 'dart:async';
import 'dart:typed_data';
import 'package:flutter/services.dart';
import 'package:flutter/foundation.dart' show ReadBuffer, WriteBuffer;
import 'pair.dart';
import 'test_step.dart';
class ExtendedStandardMessageCodec extends StandardMessageCodec {
const ExtendedStandardMessageCodec();
static const int _kDateTime = 128;
static const int _kPair = 129;
@override
void writeValue(WriteBuffer buffer, dynamic value) {
if (value is DateTime) {
buffer.putUint8(_kDateTime);
buffer.putInt64(value.millisecondsSinceEpoch);
} else if (value is Pair) {
buffer.putUint8(_kPair);
writeValue(buffer, value.left);
writeValue(buffer, value.right);
} else {
super.writeValue(buffer, value);
}
}
@override
dynamic readValueOfType(int type, ReadBuffer buffer) {
switch (type) {
case _kDateTime:
return new DateTime.fromMillisecondsSinceEpoch(buffer.getInt64());
case _kPair:
return new Pair(readValue(buffer), readValue(buffer));
default: return super.readValueOfType(type, buffer);
}
}
}
Future<TestStepResult> basicBinaryHandshake(ByteData message) async {
const BasicMessageChannel<ByteData> channel =
const BasicMessageChannel<ByteData>(
......@@ -38,7 +72,7 @@ Future<TestStepResult> basicStandardHandshake(dynamic message) async {
const BasicMessageChannel<dynamic> channel =
const BasicMessageChannel<dynamic>(
'std-msg',
const StandardMessageCodec(),
const ExtendedStandardMessageCodec(),
);
return _basicMessageHandshake<dynamic>(
'Standard >${toString(message)}<', channel, message);
......@@ -74,7 +108,7 @@ Future<TestStepResult> basicStandardMessageToUnknownChannel() async {
const BasicMessageChannel<dynamic> channel =
const BasicMessageChannel<dynamic>(
'std-unknown',
const StandardMessageCodec(),
const ExtendedStandardMessageCodec(),
);
return _basicMessageToUnknownChannel<dynamic>('Standard', channel);
}
......
......@@ -4,6 +4,7 @@
import 'dart:async';
import 'package:flutter/services.dart';
import 'basic_messaging.dart';
import 'test_step.dart';
Future<TestStepResult> methodCallJsonSuccessHandshake(dynamic payload) async {
......@@ -27,22 +28,28 @@ Future<TestStepResult> methodCallJsonNotImplementedHandshake() async {
Future<TestStepResult> methodCallStandardSuccessHandshake(
dynamic payload) async {
const MethodChannel channel =
const MethodChannel('std-method', const StandardMethodCodec());
const MethodChannel channel = const MethodChannel(
'std-method',
const StandardMethodCodec(const ExtendedStandardMessageCodec()),
);
return _methodCallSuccessHandshake(
'Standard success($payload)', channel, payload);
}
Future<TestStepResult> methodCallStandardErrorHandshake(dynamic payload) async {
const MethodChannel channel =
const MethodChannel('std-method', const StandardMethodCodec());
const MethodChannel channel = const MethodChannel(
'std-method',
const StandardMethodCodec(const ExtendedStandardMessageCodec()),
);
return _methodCallErrorHandshake(
'Standard error($payload)', channel, payload);
}
Future<TestStepResult> methodCallStandardNotImplementedHandshake() async {
const MethodChannel channel =
const MethodChannel('std-method', const StandardMethodCodec());
const MethodChannel channel = const MethodChannel(
'std-method',
const StandardMethodCodec(const ExtendedStandardMessageCodec()),
);
return _methodCallNotImplementedHandshake(
'Standard notImplemented()', channel);
}
......
// Copyright 2018 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
/// A pair of values. Used for testing custom codecs.
class Pair {
final dynamic left;
final dynamic right;
Pair(this.left, this.right);
@override
String toString() => 'Pair[$left, $right]';
}
......@@ -7,6 +7,8 @@ import 'dart:typed_data';
import 'package:flutter/material.dart';
import 'pair.dart';
enum TestStatus { ok, pending, failed, complete }
typedef Future<TestStepResult> TestStep();
......@@ -147,6 +149,8 @@ bool _deepEquals(dynamic a, dynamic b) {
return b is List && _deepEqualsList(a, b);
if (a is Map)
return b is Map && _deepEqualsMap(a, b);
if (a is Pair)
return b is Pair && _deepEqualsPair(a, b);
return false;
}
......@@ -176,3 +180,7 @@ bool _deepEqualsMap(Map<dynamic, dynamic> a, Map<dynamic, dynamic> b) {
}
return true;
}
bool _deepEqualsPair(Pair a, Pair b) {
return _deepEquals(a.left, b.left) && _deepEquals(a.right, b.right);
}
......@@ -201,6 +201,9 @@ class JSONMethodCodec implements MethodCodec {
/// `FlutterStandardTypedData`
/// * [List]\: `NSArray`
/// * [Map]\: `NSDictionary`
///
/// The codec is extensible by subclasses overriding [writeValue] and
/// [readValueOfType].
class StandardMessageCodec implements MessageCodec<dynamic> {
// The codec serializes messages as outlined below. This format must
// match the Android and iOS counterparts.
......@@ -262,7 +265,7 @@ class StandardMessageCodec implements MessageCodec<dynamic> {
if (message == null)
return null;
final WriteBuffer buffer = new WriteBuffer();
_writeValue(buffer, message);
writeValue(buffer, message);
return buffer.done();
}
......@@ -271,26 +274,25 @@ class StandardMessageCodec implements MessageCodec<dynamic> {
if (message == null)
return null;
final ReadBuffer buffer = new ReadBuffer(message);
final dynamic result = _readValue(buffer);
final dynamic result = readValue(buffer);
if (buffer.hasRemaining)
throw const FormatException('Message corrupted');
return result;
}
static void _writeSize(WriteBuffer buffer, int value) {
assert(0 <= value && value <= 0xffffffff);
if (value < 254) {
buffer.putUint8(value);
} else if (value <= 0xffff) {
buffer.putUint8(254);
buffer.putUint16(value);
} else {
buffer.putUint8(255);
buffer.putUint32(value);
}
}
static void _writeValue(WriteBuffer buffer, dynamic value) {
/// Writes [value] to [buffer] by first writing a type discriminator
/// byte, then the value itself.
///
/// This method may be called recursively to serialize container values.
///
/// Type discriminators 0 through 127 inclusive are reserved for use by the
/// base class.
///
/// The codec can be extended by overriding this method, calling super
/// for values that the extension does not handle. Type discriminators
/// used by extensions must be greater than or equal to 128 in order to avoid
/// clashes with any later extensions to the base class.
void writeValue(WriteBuffer buffer, dynamic value) {
if (value == null) {
buffer.putUint8(_kNull);
} else if (value is bool) {
......@@ -309,57 +311,60 @@ class StandardMessageCodec implements MessageCodec<dynamic> {
} else if (value is String) {
buffer.putUint8(_kString);
final List<int> bytes = utf8.encoder.convert(value);
_writeSize(buffer, bytes.length);
writeSize(buffer, bytes.length);
buffer.putUint8List(bytes);
} else if (value is Uint8List) {
buffer.putUint8(_kUint8List);
_writeSize(buffer, value.length);
writeSize(buffer, value.length);
buffer.putUint8List(value);
} else if (value is Int32List) {
buffer.putUint8(_kInt32List);
_writeSize(buffer, value.length);
writeSize(buffer, value.length);
buffer.putInt32List(value);
} else if (value is Int64List) {
buffer.putUint8(_kInt64List);
_writeSize(buffer, value.length);
writeSize(buffer, value.length);
buffer.putInt64List(value);
} else if (value is Float64List) {
buffer.putUint8(_kFloat64List);
_writeSize(buffer, value.length);
writeSize(buffer, value.length);
buffer.putFloat64List(value);
} else if (value is List) {
buffer.putUint8(_kList);
_writeSize(buffer, value.length);
writeSize(buffer, value.length);
for (final dynamic item in value) {
_writeValue(buffer, item);
writeValue(buffer, item);
}
} else if (value is Map) {
buffer.putUint8(_kMap);
_writeSize(buffer, value.length);
writeSize(buffer, value.length);
value.forEach((dynamic key, dynamic value) {
_writeValue(buffer, key);
_writeValue(buffer, value);
writeValue(buffer, key);
writeValue(buffer, value);
});
} else {
throw new ArgumentError.value(value);
}
}
static int _readSize(ReadBuffer buffer) {
final int value = buffer.getUint8();
if (value < 254)
return value;
else if (value == 254)
return buffer.getUint16();
else
return buffer.getUint32();
}
static dynamic _readValue(ReadBuffer buffer) {
/// Reads a value from [buffer] as written by [writeValue].
///
/// This method is intended for use by subclasses overriding
/// [readValueOfType].
dynamic readValue(ReadBuffer buffer) {
if (!buffer.hasRemaining)
throw const FormatException('Message corrupted');
final int type = buffer.getUint8();
return readValueOfType(type, buffer);
}
/// Reads a value of the indicated [type] from [buffer].
///
/// The codec can be extended by overriding this method, calling super
/// for types that the extension does not handle.
dynamic readValueOfType(int type, ReadBuffer buffer) {
dynamic result;
switch (buffer.getUint8()) {
switch (type) {
case _kNull:
result = null;
break;
......@@ -379,7 +384,7 @@ class StandardMessageCodec implements MessageCodec<dynamic> {
// Flutter Engine APIs to use large ints have been deprecated on
// 2018-01-09 and will be made unavailable.
// TODO(mravn): remove this case once the APIs are unavailable.
final int length = _readSize(buffer);
final int length = readSize(buffer);
final String hex = utf8.decoder.convert(buffer.getUint8List(length));
result = int.parse(hex, radix: 16);
break;
......@@ -387,43 +392,77 @@ class StandardMessageCodec implements MessageCodec<dynamic> {
result = buffer.getFloat64();
break;
case _kString:
final int length = _readSize(buffer);
final int length = readSize(buffer);
result = utf8.decoder.convert(buffer.getUint8List(length));
break;
case _kUint8List:
final int length = _readSize(buffer);
final int length = readSize(buffer);
result = buffer.getUint8List(length);
break;
case _kInt32List:
final int length = _readSize(buffer);
final int length = readSize(buffer);
result = buffer.getInt32List(length);
break;
case _kInt64List:
final int length = _readSize(buffer);
final int length = readSize(buffer);
result = buffer.getInt64List(length);
break;
case _kFloat64List:
final int length = _readSize(buffer);
final int length = readSize(buffer);
result = buffer.getFloat64List(length);
break;
case _kList:
final int length = _readSize(buffer);
final int length = readSize(buffer);
result = new List<dynamic>(length);
for (int i = 0; i < length; i++) {
result[i] = _readValue(buffer);
result[i] = readValue(buffer);
}
break;
case _kMap:
final int length = _readSize(buffer);
final int length = readSize(buffer);
result = <dynamic, dynamic>{};
for (int i = 0; i < length; i++) {
result[_readValue(buffer)] = _readValue(buffer);
result[readValue(buffer)] = readValue(buffer);
}
break;
default: throw const FormatException('Message corrupted');
}
return result;
}
/// Writes a non-negative 32-bit integer [value] to [buffer]
/// using an expanding 1-5 byte encoding that optimizes for small values.
///
/// This method is intended for use by subclasses overriding
/// [writeValue].
void writeSize(WriteBuffer buffer, int value) {
assert(0 <= value && value <= 0xffffffff);
if (value < 254) {
buffer.putUint8(value);
} else if (value <= 0xffff) {
buffer.putUint8(254);
buffer.putUint16(value);
} else {
buffer.putUint8(255);
buffer.putUint32(value);
}
}
/// Reads a non-negative int from [buffer] as written by [writeSize].
///
/// This method is intended for use by subclasses overriding
/// [readValueOfType].
int readSize(ReadBuffer buffer) {
final int value = buffer.getUint8();
switch (value) {
case 254:
return buffer.getUint16();
case 255:
return buffer.getUint32();
default:
return value;
}
}
}
/// [MethodCodec] using the Flutter standard binary encoding.
......@@ -448,21 +487,24 @@ class StandardMethodCodec implements MethodCodec {
// string, the error message string, and the error details value.
/// Creates a [MethodCodec] using the Flutter standard binary encoding.
const StandardMethodCodec();
const StandardMethodCodec([this.messageCodec = const StandardMessageCodec()]);
/// The message codec that this method codec uses for encoding values.
final StandardMessageCodec messageCodec;
@override
ByteData encodeMethodCall(MethodCall call) {
final WriteBuffer buffer = new WriteBuffer();
StandardMessageCodec._writeValue(buffer, call.method);
StandardMessageCodec._writeValue(buffer, call.arguments);
messageCodec.writeValue(buffer, call.method);
messageCodec.writeValue(buffer, call.arguments);
return buffer.done();
}
@override
MethodCall decodeMethodCall(ByteData methodCall) {
final ReadBuffer buffer = new ReadBuffer(methodCall);
final dynamic method = StandardMessageCodec._readValue(buffer);
final dynamic arguments = StandardMessageCodec._readValue(buffer);
final dynamic method = messageCodec.readValue(buffer);
final dynamic arguments = messageCodec.readValue(buffer);
if (method is String && !buffer.hasRemaining)
return new MethodCall(method, arguments);
else
......@@ -473,7 +515,7 @@ class StandardMethodCodec implements MethodCodec {
ByteData encodeSuccessEnvelope(dynamic result) {
final WriteBuffer buffer = new WriteBuffer();
buffer.putUint8(0);
StandardMessageCodec._writeValue(buffer, result);
messageCodec.writeValue(buffer, result);
return buffer.done();
}
......@@ -481,9 +523,9 @@ class StandardMethodCodec implements MethodCodec {
ByteData encodeErrorEnvelope({@required String code, String message, dynamic details}) {
final WriteBuffer buffer = new WriteBuffer();
buffer.putUint8(1);
StandardMessageCodec._writeValue(buffer, code);
StandardMessageCodec._writeValue(buffer, message);
StandardMessageCodec._writeValue(buffer, details);
messageCodec.writeValue(buffer, code);
messageCodec.writeValue(buffer, message);
messageCodec.writeValue(buffer, details);
return buffer.done();
}
......@@ -494,10 +536,10 @@ class StandardMethodCodec implements MethodCodec {
throw const FormatException('Expected envelope, got nothing');
final ReadBuffer buffer = new ReadBuffer(envelope);
if (buffer.getUint8() == 0)
return StandardMessageCodec._readValue(buffer);
final dynamic errorCode = StandardMessageCodec._readValue(buffer);
final dynamic errorMessage = StandardMessageCodec._readValue(buffer);
final dynamic errorDetails = StandardMessageCodec._readValue(buffer);
return messageCodec.readValue(buffer);
final dynamic errorCode = messageCodec.readValue(buffer);
final dynamic errorMessage = messageCodec.readValue(buffer);
final dynamic errorDetails = messageCodec.readValue(buffer);
if (errorCode is String && (errorMessage == null || errorMessage is String) && !buffer.hasRemaining)
throw new PlatformException(code: errorCode, message: errorMessage, details: errorDetails);
else
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment