Commit 390993d0 authored by Mikkel Nygaard Ravn's avatar Mikkel Nygaard Ravn Committed by GitHub

PlatformXxxChannel concepts added to support Flutter/platform interop (#8394)

New concepts: PlatformMessageChannel (basic message send/receive superseding some existing  PlatformMessages methods), PlatformMethodChannel (method invocation and event streams), pluggable codecs for messages and method calls: unencoded binary, string, json, and 'standard' flutter binary encoding.
parent 41d81132
7f25cd0d65ca52a5fddb5f41abf5b82acbe14085
74de13c0bde4eeb967391bd2a7ba973c525113b1
\ No newline at end of file
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.example.platformservices"
package="com.example.flutter"
android:versionCode="1"
android:versionName="0.0.1">
......
......@@ -4,85 +4,60 @@
package com.example.flutter;
import android.app.Activity;
import android.content.Context;
import android.content.Intent;
import android.content.pm.ApplicationInfo;
import android.content.pm.PackageManager;
import android.location.Location;
import android.location.LocationManager;
import android.os.Bundle;
import android.util.Log;
import android.view.View;
import android.widget.Button;
import android.widget.TextView;
import io.flutter.app.FlutterActivity;
import io.flutter.view.FlutterMain;
import io.flutter.plugin.common.FlutterMethodChannel;
import io.flutter.plugin.common.FlutterMethodChannel.MethodCallHandler;
import io.flutter.plugin.common.FlutterMethodChannel.Response;
import io.flutter.plugin.common.MethodCall;
import io.flutter.view.FlutterView;
import java.io.File;
import org.json.JSONException;
import org.json.JSONObject;
public class ExampleActivity extends FlutterActivity {
private static final String TAG = "ExampleActivity";
private FlutterView flutterView;
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
flutterView = getFlutterView();
flutterView.addOnMessageListener("getLocation",
new FlutterView.OnMessageListener() {
@Override
public String onMessage(FlutterView view, String message) {
return onGetLocation(message);
new FlutterMethodChannel(getFlutterView(), "geo").setMethodCallHandler(new MethodCallHandler() {
@Override
public void onMethodCall(MethodCall call, Response response) {
if (call.method.equals("getLocation")) {
if (!(call.arguments instanceof String)) {
throw new IllegalArgumentException("Invalid argument type, String expected");
}
getLocation((String) call.arguments, response);
} else {
throw new IllegalArgumentException("Unknown method " + call.method);
}
});
}
});
}
private String onGetLocation(String json) {
String provider;
try {
JSONObject message = new JSONObject(json);
provider = message.getString("provider");
} catch (JSONException e) {
Log.e(TAG, "JSON exception", e);
return null;
}
private void getLocation(String provider, Response response) {
String locationProvider;
if (provider.equals("network")) {
locationProvider = LocationManager.NETWORK_PROVIDER;
} else if (provider.equals("gps")) {
locationProvider = LocationManager.GPS_PROVIDER;
} else {
return null;
throw new IllegalArgumentException("Unknown provider " + provider);
}
String permission = "android.permission.ACCESS_FINE_LOCATION";
Location location = null;
if (checkCallingOrSelfPermission(permission) == PackageManager.PERMISSION_GRANTED) {
LocationManager locationManager = (LocationManager) getSystemService(Context.LOCATION_SERVICE);
location = locationManager.getLastKnownLocation(locationProvider);
}
JSONObject reply = new JSONObject();
try {
Location location = locationManager.getLastKnownLocation(locationProvider);
if (location != null) {
reply.put("latitude", location.getLatitude());
reply.put("longitude", location.getLongitude());
response.success(new double[] { location.getLatitude(), location.getLongitude() });
} else {
reply.put("latitude", 0);
reply.put("longitude", 0);
response.error("unknown", "Location unknown", null);
}
} catch (JSONException e) {
Log.e(TAG, "JSON exception", e);
return null;
} else {
response.error("permission", "Access denied", null);
}
return reply.toString();
}
}
\ No newline at end of file
}
......@@ -13,8 +13,7 @@ class PlatformServices extends StatefulWidget {
}
class _PlatformServicesState extends State<PlatformServices> {
double _latitude;
double _longitude;
Future<dynamic> _locationRequest;
@override
Widget build(BuildContext context) {
......@@ -26,28 +25,42 @@ class _PlatformServicesState extends State<PlatformServices> {
new Text('Hello from Flutter!'),
new RaisedButton(
child: new Text('Get Location'),
onPressed: _getLocation
onPressed: _requestLocation,
),
new Text('Latitude: $_latitude, Longitude: $_longitude'),
]
)
)
new FutureBuilder<dynamic>(
future: _locationRequest,
builder: _buildLocation,
),
],
),
),
);
}
Future<Null> _getLocation() async {
final Map<String, String> message = <String, String>{'provider': 'network'};
final Map<String, dynamic> reply = await PlatformMessages.sendJSON('getLocation', message);
// If the widget was removed from the tree while the message was in flight,
// we want to discard the reply rather than calling setState to update our
// non-existent appearance.
if (!mounted)
return;
void _requestLocation() {
setState(() {
_latitude = reply['latitude'].toDouble();
_longitude = reply['longitude'].toDouble();
_locationRequest = const PlatformMethodChannel('geo').invokeMethod(
'getLocation',
'network',
);
});
}
Widget _buildLocation(BuildContext context, AsyncSnapshot<dynamic> snapshot) {
switch (snapshot.connectionState) {
case ConnectionState.none:
return new Text('Press button to request location');
case ConnectionState.waiting:
return new Text('Awaiting response...');
default:
try {
final List<double> location = snapshot.requireData;
return new Text('Lat. ${location[0]}, Long. ${location[1]}');
} on PlatformException catch (e) {
return new Text('Request failed: ${e.message}');
}
}
}
}
void main() {
......
......@@ -24,4 +24,5 @@ export 'src/foundation/licenses.dart';
export 'src/foundation/observer_list.dart';
export 'src/foundation/platform.dart';
export 'src/foundation/print.dart';
export 'src/foundation/serialization.dart';
export 'src/foundation/synchronous_future.dart';
......@@ -13,6 +13,8 @@ library services;
export 'src/services/asset_bundle.dart';
export 'src/services/binding.dart';
export 'src/services/clipboard.dart';
export 'src/services/message_codec.dart';
export 'src/services/message_codecs.dart';
export 'src/services/haptic_feedback.dart';
export 'src/services/image_cache.dart';
export 'src/services/image_decoder.dart';
......@@ -20,6 +22,7 @@ export 'src/services/image_provider.dart';
export 'src/services/image_resolution.dart';
export 'src/services/image_stream.dart';
export 'src/services/path_provider.dart';
export 'src/services/platform_channel.dart';
export 'src/services/platform_messages.dart';
export 'src/services/raw_keyboard.dart';
export 'src/services/system_chrome.dart';
......
// Copyright 2017 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.
import 'dart:typed_data';
import 'package:typed_data/typed_buffers.dart' show Uint8Buffer;
/// Write-only buffer for incrementally building a [ByteData] instance.
///
/// A WriteBuffer instance can be used only once. Attempts to reuse will result
/// in [NoSuchMethodError]s being thrown.
///
/// The byte order of serialized data is [Endianness.BIG_ENDIAN].
/// The byte order of deserialized data is [Endianness.HOST_ENDIAN].
class WriteBuffer {
Uint8Buffer _buffer;
ByteData _eightBytes;
Uint8List _eightBytesAsList;
WriteBuffer() {
_buffer = new Uint8Buffer();
_eightBytes = new ByteData(8);
_eightBytesAsList = _eightBytes.buffer.asUint8List();
}
void putUint8(int byte) {
_buffer.add(byte);
}
void putInt32(int value) {
putUint8(value >> 24);
putUint8(value >> 16);
putUint8(value >> 8);
putUint8(value);
}
void putInt64(int value) {
putUint8(value >> 56);
putUint8(value >> 48);
putUint8(value >> 40);
putUint8(value >> 32);
putUint8(value >> 24);
putUint8(value >> 16);
putUint8(value >> 8);
putUint8(value);
}
void putFloat64(double value) {
_eightBytes.setFloat64(0, value);
_buffer.addAll(_eightBytesAsList);
}
void putUint8List(Uint8List list) {
_buffer.addAll(list);
}
void putInt32List(Int32List list) {
_alignTo(4);
if (Endianness.HOST_ENDIAN == Endianness.BIG_ENDIAN) {
_buffer.addAll(list.buffer.asUint8List(list.offsetInBytes, 4 * list.length));
} else {
for (final int value in list) {
putInt32(value);
}
}
}
void putInt64List(Int64List list) {
_alignTo(8);
if (Endianness.HOST_ENDIAN == Endianness.BIG_ENDIAN) {
_buffer.addAll(list.buffer.asUint8List(list.offsetInBytes, 8 * list.length));
} else {
for (final int value in list) {
putInt64(value);
}
}
}
void putFloat64List(Float64List list) {
_alignTo(8);
if (Endianness.HOST_ENDIAN == Endianness.BIG_ENDIAN) {
_buffer.addAll(list.buffer.asUint8List(list.offsetInBytes, 8 * list.length));
} else {
for (final double value in list) {
putFloat64(value);
}
}
}
void _alignTo(int alignment) {
final int mod = _buffer.length % alignment;
if (mod != 0) {
for (int i = 0; i < alignment - mod; i++) {
_buffer.add(0);
}
}
}
ByteData done() {
final ByteData result = _buffer.buffer.asByteData(0, _buffer.lengthInBytes);
_buffer = null;
return result;
}
}
/// Read-only buffer for reading sequentially from a [ByteData] instance.
///
/// The byte order of serialized data is [Endianness.BIG_ENDIAN].
/// The byte order of deserialized data is [Endianness.HOST_ENDIAN].
class ReadBuffer {
final ByteData data;
int position = 0;
/// Creates a [ReadBuffer] for reading from the specified [data].
ReadBuffer(this.data) {
assert(data != null);
}
int getUint8() {
return data.getUint8(position++);
}
int getInt32() {
final int value = data.getInt32(position);
position += 4;
return value;
}
int getInt64() {
final int value = data.getInt64(position);
position += 8;
return value;
}
double getFloat64() {
final double value = data.getFloat64(position);
position += 8;
return value;
}
Uint8List getUint8List(int length) {
final Uint8List list = data.buffer.asUint8List(data.offsetInBytes + position, length);
position += length;
return list;
}
Int32List getInt32List(int length) {
_alignTo(4);
Int32List list;
if (Endianness.HOST_ENDIAN == Endianness.BIG_ENDIAN) {
list = data.buffer.asInt32List(data.offsetInBytes + position, length);
} else {
final ByteData invertedData = new ByteData(4 * length);
for (int i = 0; i < length; i++) {
invertedData.setInt32(i * 4, data.getInt32(position + i * 4, Endianness.HOST_ENDIAN));
}
list = new Int32List.view(invertedData.buffer);
}
position += 4 * length;
return list;
}
Int64List getInt64List(int length) {
_alignTo(8);
Int64List list;
if (Endianness.HOST_ENDIAN == Endianness.BIG_ENDIAN) {
list = data.buffer.asInt64List(data.offsetInBytes + position, length);
} else {
final ByteData invertedData = new ByteData(8 * length);
for (int i = 0; i < length; i++) {
invertedData.setInt64(i * 8, data.getInt64(position + i * 8, Endianness.HOST_ENDIAN));
}
list = new Int64List.view(invertedData.buffer);
}
position += 8 * length;
return list;
}
Float64List getFloat64List(int length) {
_alignTo(8);
Float64List list;
if (Endianness.HOST_ENDIAN == Endianness.BIG_ENDIAN) {
list = data.buffer.asFloat64List(data.offsetInBytes + position, length);
} else {
final ByteData invertedData = new ByteData(8 * length);
for (int i = 0; i < length; i++) {
invertedData.setFloat64(i * 8, data.getFloat64(position + i * 8, Endianness.HOST_ENDIAN));
}
list = new Float64List.view(invertedData.buffer);
}
position += 8 * length;
return list;
}
void _alignTo(int alignment) {
final int mod = position % alignment;
if (mod != 0) {
position += alignment - mod;
}
}
bool get hasRemaining => position < data.lengthInBytes;
}
// Copyright 2017 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.
import 'dart:typed_data';
import 'package:meta/meta.dart';
/// A message encoding/decoding mechanism.
///
/// Both operations throw [FormatException], if conversion fails.
///
/// See also:
///
/// * [PlatformMessageChannel], which use [MessageCodec]s for communication
/// between Flutter and platform plugins.
abstract class MessageCodec<T> {
/// Encodes the specified [message] in binary.
///
/// Returns `null` if the message is `null`.
ByteData encodeMessage(T message);
/// Decodes the specified [message] from binary.
///
/// Returns `null` if the message is `null`.
T decodeMessage(ByteData message);
}
/// A codec for method calls and enveloped results.
///
/// Result envelopes are binary messages with enough structure that the codec can
/// distinguish between a successful result and an error. In the former case,
/// the codec must be able to extract the result payload, possibly `null`. In
/// the latter case, the codec must be able to extract an error code string,
/// a (human-readable) error message string, and a value providing any
/// additional error details, possibly `null`. These data items are used to
/// populate a [PlatformException].
///
/// All operations throw [FormatException], if conversion fails.
///
/// See also:
///
/// * [PlatformMethodChannel], which use [MethodCodec]s for communication
/// between Flutter and platform plugins.
abstract class MethodCodec {
/// Encodes the specified method call in binary.
///
/// The [name] of the method must be non-null. The [arguments] may be `null`.
ByteData encodeMethodCall(String name, dynamic arguments);
/// Decodes the specified result [envelope] from binary.
///
/// Throws [PlatformException], if [envelope] represents an error.
dynamic decodeEnvelope(ByteData envelope);
}
/// Thrown to indicate that a platform interaction failed in the platform
/// plugin.
///
/// See also:
///
/// * [MethodCodec], which throws a [PlatformException], if a received result
/// envelope represents an error.
/// * [PlatformMethodChannel.invokeMethod], which completes the returned future
/// with a [PlatformException], if invoking the platform plugin method
/// results in an error envelope.
/// * [PlatformMethodChannel.receiveBroadcastStream], which emits
/// [PlatformException]s as error events, whenever an event received from the
/// platform plugin is wrapped in an error envelope.
class PlatformException implements Exception {
/// Creates a [PlatformException] with the specified error [code] and optional
/// [message], and with the optional error [details] which must be a valid
/// value for the [MethodCodec] involved in the interaction.
PlatformException({
@required this.code,
this.message,
this.details,
}) {
assert(code != null);
}
/// An error code.
final String code;
/// A human-readable error message, possibly `null`.
final String message;
/// Error details, possibly `null`.
final dynamic details;
@override
String toString() => 'PlatformException($code, $message, $details)';
}
// Copyright 2017 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.
import 'dart:convert';
import 'dart:typed_data';
import 'package:flutter/foundation.dart' show ReadBuffer, WriteBuffer;
import 'message_codec.dart';
/// [MessageCodec] with unencoded binary messages represented using [ByteData].
class BinaryCodec implements MessageCodec<ByteData> {
const BinaryCodec();
@override
ByteData decodeMessage(ByteData message) => message;
@override
ByteData encodeMessage(ByteData message) => message;
}
/// [MessageCodec] with UTF-8 encoded String messages.
class StringCodec implements MessageCodec<String> {
const StringCodec();
@override
String decodeMessage(ByteData message) {
if (message == null)
return null;
return UTF8.decoder.convert(message.buffer.asUint8List());
}
@override
ByteData encodeMessage(String message) {
if (message == null)
return null;
final Uint8List encoded = UTF8.encoder.convert(message);
return encoded.buffer.asByteData();
}
}
/// [MessageCodec] with UTF-8 encoded JSON messages.
///
/// Supported messages are acyclic values of these forms:
///
/// * `null`
/// * [bool]s
/// * [num]s
/// * [String]s
/// * [List]s of supported values
/// * [Map]s from strings to supported values
class JSONMessageCodec implements MessageCodec<dynamic> {
// The codec serializes messages as defined by the JSON codec of the
// dart:convert package. The format used must match the Android and
// iOS counterparts.
const JSONMessageCodec();
@override
ByteData encodeMessage(dynamic message) {
if (message == null)
return null;
return const StringCodec().encodeMessage(JSON.encode(message));
}
@override
dynamic decodeMessage(ByteData message) {
if (message == null)
return message;
return JSON.decode(const StringCodec().decodeMessage(message));
}
}
/// [MethodCodec] with UTF-8 encoded JSON method calls and result envelopes.
/// Values supported as method arguments and result payloads are those supported
/// by [JSONMessageCodec].
class JSONMethodCodec implements MethodCodec {
// The codec serializes method calls, and result envelopes as outlined below.
// This format must match the Android and iOS counterparts.
//
// * Individual values are serialized as defined by the JSON codec of the
// dart:convert package.
// * Method calls are serialized as two-element lists with the method name
// string as first element and the method call arguments as the second.
// * Reply envelopes are serialized as either:
// * one-element lists containing the successful result as its single
// element, or
// * three-element lists containing, in order, an error code String, an
// error message String, and an error details value.
const JSONMethodCodec();
@override
ByteData encodeMethodCall(String name, dynamic arguments) {
assert(name != null);
return const JSONMessageCodec().encodeMessage(<dynamic>[name, arguments]);
}
@override
dynamic decodeEnvelope(ByteData envelope) {
final dynamic decoded = const JSONMessageCodec().decodeMessage(envelope);
if (decoded is! List)
throw new FormatException('Expected envelope List, got $decoded');
if (decoded.length == 1)
return decoded[0];
if (decoded.length == 3
&& decoded[0] is String
&& (decoded[1] == null || decoded[1] is String))
throw new PlatformException(
code: decoded[0],
message: decoded[1],
details: decoded[2],
);
throw new FormatException('Invalid envelope $decoded');
}
}
/// [MessageCodec] using the Flutter standard binary encoding.
///
/// The standard encoding is guaranteed to be compatible with the corresponding
/// standard codec for FlutterMessageChannels on the host platform. These parts
/// of the Flutter SDK are evolved synchronously.
///
/// Supported messages are acyclic values of these forms:
///
/// * `null`
/// * [bool]s
/// * [num]s
/// * [String]s
/// * [Uint8List]s, [Int32List]s, [Int64List]s, [Float64List]s
/// * [List]s of supported values
/// * [Map]s from supported values to supported values
class StandardMessageCodec implements MessageCodec<dynamic> {
// The codec serializes messages as outlined below. This format must
// match the Android and iOS counterparts.
//
// * A single byte with one of the constant values below determines the
// type of the value.
// * The serialization of the value itself follows the type byte.
// * Lengths and sizes of serialized parts are encoded using an expanding
// format optimized for the common case of small non-negative integers:
// * values 0..<254 using one byte with that value;
// * values 254..<2^16 using three bytes, the first of which is 254, the
// next two the usual big-endian unsigned representation of the value;
// * values 2^16..<2^32 using five bytes, the first of which is 255, the
// next four the usual big-endian unsigned representation of the value.
// * null, true, and false have empty serialization; they are encoded directly
// in the type byte (using _kNull, _kTrue, _kFalse)
// * Integers representable in 32 bits are encoded using 4 bytes big-endian,
// two's complement representation.
// * Larger integers representable in 64 bits are encoded using 8 bytes
// big-endian, two's complement representation.
// * Still larger integers are encoded using their hexadecimal string
// representation. First the length of that is encoded in the expanding
// format, then follows the UTF-8 representation of the hex string.
// * doubles are encoded using the IEEE 754 64-bit double-precision binary
// format.
// * Strings are encoded using their UTF-8 representation. First the length
// of that in bytes is encoded using the expanding format, then follows the
// UTF-8 encoding itself.
// * Uint8Lists, Int32Lists, Int64Lists, and Float64Lists are encoded by first
// encoding the list's element count in the expanding format, then the
// smallest number of zero bytes needed to align the position in the full
// message with a multiple of the number of bytes per element, then the
// encoding of the list elements themselves, end-to-end with no additional
// type information, using big-endian two's complement or IEEE 754 as
// applicable.
// * Lists are encoded by first encoding their length in the expanding format,
// then follows the recursive encoding of each element value, including the
// type byte (Lists are assumed to be heterogeneous).
// * Maps are encoded by first encoding their length in the expanding format,
// then follows the recursive encoding of each key/value pair, including the
// type byte for both (Maps are assumed to be heterogeneous).
static const int _kNull = 0;
static const int _kTrue = 1;
static const int _kFalse = 2;
static const int _kInt32 = 3;
static const int _kInt64 = 4;
static const int _kLargeInt = 5;
static const int _kFloat64 = 6;
static const int _kString = 7;
static const int _kUint8List = 8;
static const int _kInt32List = 9;
static const int _kInt64List = 10;
static const int _kFloat64List = 11;
static const int _kList = 12;
static const int _kMap = 13;
const StandardMessageCodec();
@override
ByteData encodeMessage(dynamic message) {
if (message == null)
return null;
final WriteBuffer buffer = new WriteBuffer();
_writeValue(buffer, message);
return buffer.done();
}
@override
dynamic decodeMessage(ByteData message) {
if (message == null)
return null;
final ReadBuffer buffer = new ReadBuffer(message);
final dynamic result = _readValue(buffer);
if (buffer.hasRemaining)
throw new 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.putUint8(value >> 8);
buffer.putUint8(value & 0xff);
} else {
buffer.putUint8(255);
buffer.putUint8(value >> 24);
buffer.putUint8((value >> 16) & 0xff);
buffer.putUint8((value >> 8) & 0xff);
buffer.putUint8(value & 0xff);
}
}
static void _writeValue(WriteBuffer buffer, dynamic value) {
if (value == null) {
buffer.putUint8(_kNull);
} else if (value is bool) {
buffer.putUint8(value ? _kTrue : _kFalse);
} else if (value is int) {
if (-0x7fffffff <= value && value < 0x7fffffff) {
buffer.putUint8(_kInt32);
buffer.putInt32(value);
}
else if (-0x7fffffffffffffff <= value && value < 0x7fffffffffffffff) {
buffer.putUint8(_kInt64);
buffer.putInt64(value);
}
else {
buffer.putUint8(_kLargeInt);
final List<int> hex = UTF8.encoder.convert(value.toRadixString(16));
_writeSize(buffer, hex.length);
buffer.putUint8List(hex);
}
} else if (value is double) {
buffer.putUint8(_kFloat64);
buffer.putFloat64(value);
} else if (value is String) {
buffer.putUint8(_kString);
final List<int> bytes = UTF8.encoder.convert(value);
_writeSize(buffer, bytes.length);
buffer.putUint8List(bytes);
} else if (value is Uint8List) {
buffer.putUint8(_kUint8List);
_writeSize(buffer, value.length);
buffer.putUint8List(value);
} else if (value is Int32List) {
buffer.putUint8(_kInt32List);
_writeSize(buffer, value.length);
buffer.putInt32List(value);
} else if (value is Int64List) {
buffer.putUint8(_kInt64List);
_writeSize(buffer, value.length);
buffer.putInt64List(value);
} else if (value is Float64List) {
buffer.putUint8(_kFloat64List);
_writeSize(buffer, value.length);
buffer.putFloat64List(value);
} else if (value is List) {
buffer.putUint8(_kList);
_writeSize(buffer, value.length);
for (final dynamic item in value) {
_writeValue(buffer, item);
}
} else if (value is Map) {
buffer.putUint8(_kMap);
_writeSize(buffer, value.length);
value.forEach((dynamic key, dynamic 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.getUint8() << 8)
| buffer.getUint8();
} else {
return (buffer.getUint8() << 24)
| (buffer.getUint8() << 16)
| (buffer.getUint8() << 8)
| buffer.getUint8();
}
}
static dynamic _readValue(ReadBuffer buffer) {
if (!buffer.hasRemaining)
throw throw new FormatException('Message corrupted');
dynamic result;
switch (buffer.getUint8()) {
case _kNull:
result = null;
break;
case _kTrue:
result = true;
break;
case _kFalse:
result = false;
break;
case _kInt32:
result = buffer.getInt32();
break;
case _kInt64:
result = buffer.getInt64();
break;
case _kLargeInt:
final int length = _readSize(buffer);
final String hex = UTF8.decoder.convert(buffer.getUint8List(length));
result = int.parse(hex, radix: 16);
break;
case _kFloat64:
result = buffer.getFloat64();
break;
case _kString:
final int length = _readSize(buffer);
result = UTF8.decoder.convert(buffer.getUint8List(length));
break;
case _kUint8List:
final int length = _readSize(buffer);
result = buffer.getUint8List(length);
break;
case _kInt32List:
final int length = _readSize(buffer);
result = buffer.getInt32List(length);
break;
case _kInt64List:
final int length = _readSize(buffer);
result = buffer.getInt64List(length);
break;
case _kFloat64List:
final int length = _readSize(buffer);
result = buffer.getFloat64List(length);
break;
case _kList:
final int length = _readSize(buffer);
result = new List<dynamic>(length);
for (int i = 0; i < length; i++) {
result[i] = _readValue(buffer);
}
break;
case _kMap:
final int length = _readSize(buffer);
result = new Map<dynamic, dynamic>();
for (int i = 0; i < length; i++) {
result[_readValue(buffer)] = _readValue(buffer);
}
break;
default: throw new FormatException('Message corrupted');
}
return result;
}
}
/// [MethodCodec] using the Flutter standard binary encoding.
///
/// The standard codec is guaranteed to be compatible with the corresponding
/// standard codec for FlutterMethodChannels on the host platform. These parts
/// of the Flutter SDK are evolved synchronously.
///
/// Values supported as method arguments and result payloads are those supported
/// by [StandardMessageCodec].
class StandardMethodCodec implements MethodCodec {
// The codec method calls, and result envelopes as outlined below. This format
// must match the Android and iOS counterparts.
//
// * Individual values are encoded using [StandardMessageCodec].
// * Method calls are encoded using the concatenation of the encoding
// of the method name String and the arguments value.
// * Reply envelopes are encoded using first a single byte to distinguish the
// success case (0) from the error case (1). Then follows:
// * In the success case, the encoding of the result value.
// * In the error case, the concatenation of the encoding of the error code
// string, the error message string, and the error details value.
const StandardMethodCodec();
@override
ByteData encodeMethodCall(String name, dynamic arguments) {
assert(name != null);
final WriteBuffer buffer = new WriteBuffer();
StandardMessageCodec._writeValue(buffer, name);
StandardMessageCodec._writeValue(buffer, arguments);
return buffer.done();
}
@override
dynamic decodeEnvelope(ByteData envelope) {
// First byte is zero in success case, and non-zero otherwise.
if (envelope == null || envelope.lengthInBytes == 0)
throw new 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);
if (errorCode is String && (errorMessage == null || errorMessage is String))
throw new PlatformException(code: errorCode, message: errorMessage, details: errorDetails);
else
throw new FormatException('Invalid envelope');
}
}
// Copyright 2017 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.
import 'dart:async';
import 'dart:typed_data';
import 'package:flutter/foundation.dart';
import 'message_codec.dart';
import 'message_codecs.dart';
import 'platform_messages.dart';
/// A named channel for communicating with platform plugins using asynchronous
/// message passing.
///
/// Messages are encoded into binary before being sent, and binary messages
/// received are decoded into Dart values. The [MessageCodec] used must be
/// compatible with the one used by the platform plugin. This can be achieved
/// by creating a FlutterMessageChannel counterpart of this channel on the
/// platform side. The Dart type of messages sent and received is [T],
/// but only the values supported by the specified [MessageCodec] 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. Specifically, at most
/// one message handler can be registered with the channel name at any given
/// time.
class PlatformMessageChannel<T> {
/// Creates a [PlatformMessageChannel] with the specified [name] and [codec].
///
/// Neither [name] nor [codec] may be `null`.
const PlatformMessageChannel(this.name, this.codec);
/// The logical channel on which communication happens, not `null`.
final String name;
/// The message codec used by this channel, not `null`.
final MessageCodec<T> codec;
/// Sends the specified [message] to the platform plugins on this channel.
///
/// Returns a [Future] which completes to the received and decoded response,
/// or to a [FormatException], if encoding or decoding fails.
Future<T> send(T message) async {
return codec.decodeMessage(
await PlatformMessages.sendBinary(name, codec.encodeMessage(message))
);
}
/// Sets a callback for receiving messages from the platform plugins on this
/// channel.
///
/// The given callback will replace the currently registered callback for this
/// channel's name.
///
/// The handler's return value, if non-null, is sent back to the platform
/// plugins as a response.
void setMessageHandler(Future<T> handler(T message)) {
PlatformMessages.setBinaryMessageHandler(name, (ByteData message) async {
return codec.encodeMessage(await handler(codec.decodeMessage(message)));
});
}
/// Sets a mock callback for intercepting messages sent on this channel.
///
/// The given callback will replace the currently registered mock callback for
/// this channel, if any. To remove the mock handler, pass `null` as the
/// `handler` argument.
///
/// The handler's return value, if non-null, is used as a response.
///
/// This is intended for testing. Messages intercepted in this manner are not
/// sent to platform plugins.
void setMockMessageHandler(Future<T> handler(T message)) {
if (handler == null) {
PlatformMessages.setMockBinaryMessageHandler(name, null);
} else {
PlatformMessages.setMockBinaryMessageHandler(name, (ByteData message) async {
return codec.encodeMessage(await handler(codec.decodeMessage(message)));
});
}
}
}
/// A named channel for communicating with platform plugins using asynchronous
/// method calls and event streams.
///
/// Method calls are encoded into binary before being sent, and binary results
/// received are decoded into Dart values. The [MethodCodec] used must be
/// compatible with the one used by the platform plugin. This can be achieved
/// by creating a FlutterMethodChannel counterpart of this channel on the
/// platform side. The Dart type of messages sent and received is `dynamic`,
/// but only values supported by the specified [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.
class PlatformMethodChannel {
/// Creates a [PlatformMethodChannel] with the specified [name].
///
/// The [codec] used will be [StandardMethodCodec], unless otherwise
/// specified.
///
/// Neither [name] nor [codec] may be `null`.
const PlatformMethodChannel(this.name, [this.codec = const StandardMethodCodec()]);
/// The logical channel on which communication happens, not `null`.
final String name;
/// The message codec used by this channel, not `null`.
final MethodCodec codec;
/// Invokes a [method] on this channel with the specified [arguments].
///
/// Returns a [Future] which completes to one of the following:
///
/// * a result (possibly `null`), on successful invocation;
/// * a [PlatformException], if the invocation failed in the platform plugin;
/// * a [FormatException], if encoding or decoding failed.
Future<dynamic> invokeMethod(String method, [dynamic arguments]) async {
assert(method != null);
return codec.decodeEnvelope(await PlatformMessages.sendBinary(
name,
codec.encodeMethodCall(method, arguments),
));
}
/// Sets up a broadcast stream for receiving events on this channel.
///
/// Returns a broadcast [Stream] which emits events to listeners as follows:
///
/// * a decoded data event (possibly `null`) for each successful event
/// received from the platform plugin;
/// * an error event containing a [PlatformException] for each error event
/// received from the platform plugin;
/// * an error event containing a [FormatException] for each event received
/// where decoding fails;
/// * an error event containing a [PlatformException] or [FormatException]
/// whenever stream setup fails (stream setup is done only when listener
/// count changes from 0 to 1).
///
/// Notes for platform plugin implementers:
///
/// Plugins must expose methods named `listen` and `cancel` suitable for
/// invocations by [invokeMethod]. Both methods are invoked with the specified
/// [arguments].
///
/// Following the semantics of broadcast streams, `listen` will be called as
/// the first listener registers with the returned stream, and `cancel` when
/// the last listener cancels its registration. This pattern may repeat
/// indefinitely. Platform plugins should consume no stream-related resources
/// while listener count is zero.
Stream<dynamic> receiveBroadcastStream([dynamic arguments]) {
StreamController<dynamic> controller;
controller = new StreamController<dynamic>.broadcast(
onListen: () async {
PlatformMessages.setBinaryMessageHandler(
name, (ByteData reply) async {
if (reply == null) {
controller.close();
} else {
try {
controller.add(codec.decodeEnvelope(reply));
} catch (e) {
controller.addError(e);
}
}
}
);
try {
await invokeMethod('listen', arguments);
} catch (e) {
PlatformMessages.setBinaryMessageHandler(name, null);
controller.addError(e);
}
}, onCancel: () async {
PlatformMessages.setBinaryMessageHandler(name, null);
try {
await invokeMethod('cancel', arguments);
} catch (exception, stack) {
FlutterError.reportError(new FlutterErrorDetails(
exception: exception,
stack: stack,
library: 'services library',
context: 'while de-activating platform stream on channel $name',
));
}
}
);
return controller.stream;
}
}
// Copyright 2016 The Chromium Authors. All rights reserved.
// Copyright 2017 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.
......@@ -66,8 +66,7 @@ class PlatformMessages {
/// Typically called by [ServicesBinding] to handle platform messages received
/// from [ui.window.onPlatformMessage].
///
/// To register a handler for a given message channel, see
/// [setStringMessageHandler] and [setJSONMessageHandler].
/// To register a handler for a given message channel, see [PlatformChannel].
static Future<Null> handlePlatformMessage(
String channel, ByteData data, ui.PlatformMessageResponseCallback callback) async {
ByteData response;
......@@ -104,6 +103,8 @@ class PlatformMessages {
///
/// Returns a [Future] which completes to the received response, decoded as a
/// UTF-8 string, or to an error, if the decoding fails.
///
/// Deprecated, use [PlatformMessageChannel.send] instead.
static Future<String> sendString(String channel, String message) async {
return _decodeUTF8(await sendBinary(channel, _encodeUTF8(message)));
}
......@@ -115,6 +116,8 @@ class PlatformMessages {
/// Returns a [Future] which completes to the received response, decoded as a
/// UTF-8-encoded JSON representation of a JSON value (a [String], [bool],
/// [double], [List], or [Map]), or to an error, if the decoding fails.
///
/// Deprecated, use [PlatformMessageChannel.send] instead.
static Future<dynamic> sendJSON(String channel, dynamic json) async {
return _decodeJSON(await sendString(channel, _encodeJSON(json)));
}
......@@ -129,6 +132,8 @@ class PlatformMessages {
/// The response from the method call is decoded as UTF-8, then the UTF-8 is
/// decoded as JSON. The returned [Future] completes to this fully decoded
/// response, or to an error, if the decoding fails.
///
/// Deprecated, use [PlatformMethodChannel.invokeMethod] instead.
static Future<dynamic> invokeMethod(String channel, String method, [ List<dynamic> args = const <Null>[] ]) {
return sendJSON(channel, <String, dynamic>{
'method': method,
......@@ -155,6 +160,8 @@ class PlatformMessages {
///
/// The handler's return value, if non-null, is sent as a response, encoded as
/// a UTF-8 string.
///
/// Deprecated, use [PlatformMessageChannel.setMessageHandler] instead.
static void setStringMessageHandler(String channel, Future<String> handler(String message)) {
setBinaryMessageHandler(channel, (ByteData message) async {
return _encodeUTF8(await handler(_decodeUTF8(message)));
......@@ -169,6 +176,8 @@ class PlatformMessages {
///
/// The handler's return value, if non-null, is sent as a response, encoded as
/// JSON and then as a UTF-8 string.
///
/// Deprecated, use [PlatformMessageChannel.setMessageHandler] instead.
static void setJSONMessageHandler(String channel, Future<dynamic> handler(dynamic message)) {
setStringMessageHandler(channel, (String message) async {
return _encodeJSON(await handler(_decodeJSON(message)));
......@@ -205,6 +214,8 @@ class PlatformMessages {
///
/// This is intended for testing. Messages intercepted in this manner are not
/// sent to platform plugins.
///
/// Deprecated, use [PlatformMessageChannel.setMockMessageHandler] instead.
static void setMockStringMessageHandler(String channel, Future<String> handler(String message)) {
if (handler == null) {
setMockBinaryMessageHandler(channel, null);
......@@ -227,6 +238,8 @@ class PlatformMessages {
///
/// This is intended for testing. Messages intercepted in this manner are not
/// sent to platform plugins.
///
/// Deprecated, use [PlatformMessageChannel.setMockMessageHandler] instead.
static void setMockJSONMessageHandler(String channel, Future<dynamic> handler(dynamic message)) {
if (handler == null) {
setMockStringMessageHandler(channel, null);
......
......@@ -204,6 +204,18 @@ class AsyncSnapshot<T> {
/// Latest data received. Is `null`, if [error] is not.
final T data;
/// Returns latest data received, failing if there is no data.
///
/// Throws [error], if [hasError]. Throws [StateError], if neither [hasData]
/// nor [hasError].
T get requireData {
if (hasData)
return data;
if (hasError)
throw error;
throw new StateError('Snapshot has neither data nor error');
}
/// Latest error object received. Is `null`, if [data] is not.
final Object error;
......
// Copyright 2017 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.
import 'package:flutter/foundation.dart';
import 'package:test/test.dart';
import 'dart:typed_data';
void main() {
group('Write and read buffer round-trip', () {
test('of single byte', () {
final WriteBuffer write = new WriteBuffer();
write.putUint8(201);
final ByteData written = write.done();
expect(written.lengthInBytes, equals(1));
final ReadBuffer read = new ReadBuffer(written);
expect(read.getUint8(), equals(201));
});
test('of 32-bit integer', () {
final WriteBuffer write = new WriteBuffer();
write.putInt32(-9);
final ByteData written = write.done();
expect(written.lengthInBytes, equals(4));
final ReadBuffer read = new ReadBuffer(written);
expect(read.getInt32(), equals(-9));
});
test('of 64-bit integer', () {
final WriteBuffer write = new WriteBuffer();
write.putInt64(-9000000000000);
final ByteData written = write.done();
expect(written.lengthInBytes, equals(8));
final ReadBuffer read = new ReadBuffer(written);
expect(read.getInt64(), equals(-9000000000000));
});
test('of double', () {
final WriteBuffer write = new WriteBuffer();
write.putFloat64(3.14);
final ByteData written = write.done();
expect(written.lengthInBytes, equals(8));
final ReadBuffer read = new ReadBuffer(written);
expect(read.getFloat64(), equals(3.14));
});
test('of 32-bit int list when unaligned', () {
final Int32List integers = new Int32List.fromList(<int>[-99, 2, 99]);
final WriteBuffer write = new WriteBuffer();
write.putUint8(9);
write.putInt32List(integers);
final ByteData written = write.done();
expect(written.lengthInBytes, equals(16));
final ReadBuffer read = new ReadBuffer(written);
read.getUint8();
expect(read.getInt32List(3), equals(integers));
});
test('of 64-bit int list when unaligned', () {
final Int64List integers = new Int64List.fromList(<int>[-99, 2, 99]);
final WriteBuffer write = new WriteBuffer();
write.putUint8(9);
write.putInt64List(integers);
final ByteData written = write.done();
expect(written.lengthInBytes, equals(32));
final ReadBuffer read = new ReadBuffer(written);
read.getUint8();
expect(read.getInt64List(3), equals(integers));
});
test('of double list when unaligned', () {
final Float64List doubles = new Float64List.fromList(<double>[3.14, double.NAN]);
final WriteBuffer write = new WriteBuffer();
write.putUint8(9);
write.putFloat64List(doubles);
final ByteData written = write.done();
expect(written.lengthInBytes, equals(24));
final ReadBuffer read = new ReadBuffer(written);
read.getUint8();
final Float64List readDoubles = read.getFloat64List(2);
expect(readDoubles[0], equals(3.14));
expect(readDoubles[1], isNaN);
});
});
}
\ No newline at end of file
// Copyright 2017 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.
import 'dart:async';
import 'dart:typed_data';
import 'package:flutter/services.dart';
import 'package:test/test.dart';
void main() {
group('PlatformMessageChannel', () {
const MessageCodec<String> string = const StringCodec();
const PlatformMessageChannel<String> channel = const PlatformMessageChannel<String>('ch', string);
test('can send string message and get reply', () async {
PlatformMessages.setMockBinaryMessageHandler(
'ch',
(ByteData message) async => string.encodeMessage(string.decodeMessage(message) + ' world'),
);
final String reply = await channel.send('hello');
expect(reply, equals('hello world'));
});
test('can receive string message and send reply', () async {
channel.setMessageHandler((String message) async => message + ' world');
String reply;
await PlatformMessages.handlePlatformMessage(
'ch',
const StringCodec().encodeMessage('hello'),
(ByteData replyBinary) {
reply = string.decodeMessage(replyBinary);
}
);
expect(reply, equals('hello world'));
});
});
group('PlatformMethodChannel', () {
const MessageCodec<dynamic> jsonMessage = const JSONMessageCodec();
const MethodCodec jsonMethod = const JSONMethodCodec();
const PlatformMethodChannel channel = const PlatformMethodChannel('ch', jsonMethod);
test('can invoke method and get result', () async {
PlatformMessages.setMockBinaryMessageHandler(
'ch',
(ByteData message) async {
final List<dynamic> methodCall = jsonMessage.decodeMessage(message);
if (methodCall[0] == 'sayHello')
return jsonMessage.encodeMessage(<dynamic>['${methodCall[1]} world']);
else
return jsonMessage.encodeMessage(<dynamic>['unknown', null, null]);
},
);
final String result = await channel.invokeMethod('sayHello', 'hello');
expect(result, equals('hello world'));
});
test('can invoke method and get error', () async {
PlatformMessages.setMockBinaryMessageHandler(
'ch',
(ByteData message) async {
return jsonMessage.encodeMessage(<dynamic>[
'unknown',
'Method not understood',
<String, dynamic>{'a': 42, 'b': 3.14},
]);
},
);
try {
await channel.invokeMethod('sayHello', 'hello');
fail('Exception expected');
} on PlatformException catch(e) {
expect(e.code, equals('unknown'));
expect(e.message, equals('Method not understood'));
expect(e.details, equals(<String, dynamic>{'a': 42, 'b': 3.14}));
}
});
test('can receive event stream', () async {
void emitEvent(dynamic event) {
PlatformMessages.handlePlatformMessage(
'ch',
event,
(ByteData reply) {},
);
}
bool cancelled = false;
PlatformMessages.setMockBinaryMessageHandler(
'ch',
(ByteData message) async {
final List<dynamic> methodCall = jsonMessage.decodeMessage(message);
if (methodCall[0] == 'listen') {
final String argument = methodCall[1];
emitEvent(jsonMessage.encodeMessage(<dynamic>[argument + '1']));
emitEvent(jsonMessage.encodeMessage(<dynamic>[argument + '2']));
emitEvent(null);
return jsonMessage.encodeMessage(<dynamic>[null]);
} else if (methodCall[0] == 'cancel') {
cancelled = true;
return jsonMessage.encodeMessage(<dynamic>[null]);
} else {
fail('Expected listen or cancel');
}
},
);
final List<dynamic> events = await channel.receiveBroadcastStream('hello').toList();
expect(events, orderedEquals(<String>['hello1', 'hello2']));
await new Future<Null>.delayed(const Duration());
expect(cancelled, isTrue);
});
});
}
\ No newline at end of file
......@@ -11,6 +11,26 @@ void main() {
Widget snapshotText(BuildContext context, AsyncSnapshot<String> snapshot) {
return new Text(snapshot.toString());
}
group('AsyncSnapshot', () {
test('requiring data succeeds if data is present', () {
expect(
new AsyncSnapshot<String>.withData(ConnectionState.done, 'hello').requireData,
'hello',
);
});
test('requiring data fails if there is an error', () {
expect(
() => new AsyncSnapshot<String>.withError(ConnectionState.done, 'error').requireData,
throwsA(equals('error')),
);
});
test('requiring data fails if snapshot has neither data nor error', () {
expect(
() => new AsyncSnapshot<String>.nothing().requireData,
throwsStateError,
);
});
});
group('Async smoke tests', () {
testWidgets('FutureBuilder', (WidgetTester tester) async {
await tester.pumpWidget(new FutureBuilder<String>(
......
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