From 390993d0701bb551bfc2072b31d08eafbe2a925c Mon Sep 17 00:00:00 2001 From: Mikkel Nygaard Ravn Date: Wed, 1 Mar 2017 14:35:41 +0100 Subject: [PATCH] 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. --- bin/internal/engine.version | 2 +- .../android/app/src/main/AndroidManifest.xml | 2 +- .../com/example/flutter/ExampleActivity.java | 73 +-- examples/platform_services/lib/main.dart | 47 +- packages/flutter/lib/foundation.dart | 1 + packages/flutter/lib/services.dart | 3 + .../lib/src/foundation/serialization.dart | 204 +++++++++ .../lib/src/services/message_codec.dart | 94 ++++ .../lib/src/services/message_codecs.dart | 421 ++++++++++++++++++ .../lib/src/services/platform_channel.dart | 191 ++++++++ .../lib/src/services/platform_messages.dart | 19 +- packages/flutter/lib/src/widgets/async.dart | 12 + .../test/foundation/serialization_test.dart | 80 ++++ .../test/services/platform_channel_test.dart | 108 +++++ packages/flutter/test/widgets/async_test.dart | 20 + 15 files changed, 1206 insertions(+), 71 deletions(-) create mode 100644 packages/flutter/lib/src/foundation/serialization.dart create mode 100644 packages/flutter/lib/src/services/message_codec.dart create mode 100644 packages/flutter/lib/src/services/message_codecs.dart create mode 100644 packages/flutter/lib/src/services/platform_channel.dart create mode 100644 packages/flutter/test/foundation/serialization_test.dart create mode 100644 packages/flutter/test/services/platform_channel_test.dart diff --git a/bin/internal/engine.version b/bin/internal/engine.version index 1e8135e9e0..9276c80d1b 100644 --- a/bin/internal/engine.version +++ b/bin/internal/engine.version @@ -1 +1 @@ -7f25cd0d65ca52a5fddb5f41abf5b82acbe14085 +74de13c0bde4eeb967391bd2a7ba973c525113b1 \ No newline at end of file diff --git a/examples/platform_services/android/app/src/main/AndroidManifest.xml b/examples/platform_services/android/app/src/main/AndroidManifest.xml index 37157b749a..b068bdc9d7 100644 --- a/examples/platform_services/android/app/src/main/AndroidManifest.xml +++ b/examples/platform_services/android/app/src/main/AndroidManifest.xml @@ -1,6 +1,6 @@ diff --git a/examples/platform_services/android/app/src/main/java/com/example/flutter/ExampleActivity.java b/examples/platform_services/android/app/src/main/java/com/example/flutter/ExampleActivity.java index f63582ce0b..45cc796ee7 100644 --- a/examples/platform_services/android/app/src/main/java/com/example/flutter/ExampleActivity.java +++ b/examples/platform_services/android/app/src/main/java/com/example/flutter/ExampleActivity.java @@ -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 +} diff --git a/examples/platform_services/lib/main.dart b/examples/platform_services/lib/main.dart index a0870d36f9..8a150dc5e0 100644 --- a/examples/platform_services/lib/main.dart +++ b/examples/platform_services/lib/main.dart @@ -13,8 +13,7 @@ class PlatformServices extends StatefulWidget { } class _PlatformServicesState extends State { - double _latitude; - double _longitude; + Future _locationRequest; @override Widget build(BuildContext context) { @@ -26,28 +25,42 @@ class _PlatformServicesState extends State { new Text('Hello from Flutter!'), new RaisedButton( child: new Text('Get Location'), - onPressed: _getLocation + onPressed: _requestLocation, ), - new Text('Latitude: $_latitude, Longitude: $_longitude'), - ] - ) - ) + new FutureBuilder( + future: _locationRequest, + builder: _buildLocation, + ), + ], + ), + ), ); } - Future _getLocation() async { - final Map message = {'provider': 'network'}; - final Map 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 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 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() { diff --git a/packages/flutter/lib/foundation.dart b/packages/flutter/lib/foundation.dart index b3876e8fc4..6bd52346bb 100644 --- a/packages/flutter/lib/foundation.dart +++ b/packages/flutter/lib/foundation.dart @@ -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'; diff --git a/packages/flutter/lib/services.dart b/packages/flutter/lib/services.dart index 7b515aede2..4c342104b5 100644 --- a/packages/flutter/lib/services.dart +++ b/packages/flutter/lib/services.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'; diff --git a/packages/flutter/lib/src/foundation/serialization.dart b/packages/flutter/lib/src/foundation/serialization.dart new file mode 100644 index 0000000000..a5dfb4affa --- /dev/null +++ b/packages/flutter/lib/src/foundation/serialization.dart @@ -0,0 +1,204 @@ +// 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; +} diff --git a/packages/flutter/lib/src/services/message_codec.dart b/packages/flutter/lib/src/services/message_codec.dart new file mode 100644 index 0000000000..2cdb2b2158 --- /dev/null +++ b/packages/flutter/lib/src/services/message_codec.dart @@ -0,0 +1,94 @@ +// 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 { + /// 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)'; +} diff --git a/packages/flutter/lib/src/services/message_codecs.dart b/packages/flutter/lib/src/services/message_codecs.dart new file mode 100644 index 0000000000..e7d640f3ca --- /dev/null +++ b/packages/flutter/lib/src/services/message_codecs.dart @@ -0,0 +1,421 @@ +// 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 { + 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 { + 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 { + // 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([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 { + // 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 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 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(length); + for (int i = 0; i < length; i++) { + result[i] = _readValue(buffer); + } + break; + case _kMap: + final int length = _readSize(buffer); + result = new Map(); + 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'); + } +} + diff --git a/packages/flutter/lib/src/services/platform_channel.dart b/packages/flutter/lib/src/services/platform_channel.dart new file mode 100644 index 0000000000..8289ca8008 --- /dev/null +++ b/packages/flutter/lib/src/services/platform_channel.dart @@ -0,0 +1,191 @@ +// 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 { + /// 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 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 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 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 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 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 receiveBroadcastStream([dynamic arguments]) { + StreamController controller; + controller = new StreamController.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; + } +} diff --git a/packages/flutter/lib/src/services/platform_messages.dart b/packages/flutter/lib/src/services/platform_messages.dart index f46fd5d600..f2a3d2d06a 100644 --- a/packages/flutter/lib/src/services/platform_messages.dart +++ b/packages/flutter/lib/src/services/platform_messages.dart @@ -1,4 +1,4 @@ -// 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 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 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 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 invokeMethod(String channel, String method, [ List args = const [] ]) { return sendJSON(channel, { '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 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 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 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 handler(dynamic message)) { if (handler == null) { setMockStringMessageHandler(channel, null); diff --git a/packages/flutter/lib/src/widgets/async.dart b/packages/flutter/lib/src/widgets/async.dart index bff6236179..9fb8cce7c4 100644 --- a/packages/flutter/lib/src/widgets/async.dart +++ b/packages/flutter/lib/src/widgets/async.dart @@ -204,6 +204,18 @@ class AsyncSnapshot { /// 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; diff --git a/packages/flutter/test/foundation/serialization_test.dart b/packages/flutter/test/foundation/serialization_test.dart new file mode 100644 index 0000000000..7a8b1ebb20 --- /dev/null +++ b/packages/flutter/test/foundation/serialization_test.dart @@ -0,0 +1,80 @@ +// 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([-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([-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([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 diff --git a/packages/flutter/test/services/platform_channel_test.dart b/packages/flutter/test/services/platform_channel_test.dart new file mode 100644 index 0000000000..17c9af76df --- /dev/null +++ b/packages/flutter/test/services/platform_channel_test.dart @@ -0,0 +1,108 @@ +// 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 = const StringCodec(); + const PlatformMessageChannel channel = const PlatformMessageChannel('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 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 methodCall = jsonMessage.decodeMessage(message); + if (methodCall[0] == 'sayHello') + return jsonMessage.encodeMessage(['${methodCall[1]} world']); + else + return jsonMessage.encodeMessage(['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([ + 'unknown', + 'Method not understood', + {'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({'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 methodCall = jsonMessage.decodeMessage(message); + if (methodCall[0] == 'listen') { + final String argument = methodCall[1]; + emitEvent(jsonMessage.encodeMessage([argument + '1'])); + emitEvent(jsonMessage.encodeMessage([argument + '2'])); + emitEvent(null); + return jsonMessage.encodeMessage([null]); + } else if (methodCall[0] == 'cancel') { + cancelled = true; + return jsonMessage.encodeMessage([null]); + } else { + fail('Expected listen or cancel'); + } + }, + ); + final List events = await channel.receiveBroadcastStream('hello').toList(); + expect(events, orderedEquals(['hello1', 'hello2'])); + await new Future.delayed(const Duration()); + expect(cancelled, isTrue); + }); + }); +} \ No newline at end of file diff --git a/packages/flutter/test/widgets/async_test.dart b/packages/flutter/test/widgets/async_test.dart index 0ecaa773af..54b4dc239c 100644 --- a/packages/flutter/test/widgets/async_test.dart +++ b/packages/flutter/test/widgets/async_test.dart @@ -11,6 +11,26 @@ void main() { Widget snapshotText(BuildContext context, AsyncSnapshot snapshot) { return new Text(snapshot.toString()); } + group('AsyncSnapshot', () { + test('requiring data succeeds if data is present', () { + expect( + new AsyncSnapshot.withData(ConnectionState.done, 'hello').requireData, + 'hello', + ); + }); + test('requiring data fails if there is an error', () { + expect( + () => new AsyncSnapshot.withError(ConnectionState.done, 'error').requireData, + throwsA(equals('error')), + ); + }); + test('requiring data fails if snapshot has neither data nor error', () { + expect( + () => new AsyncSnapshot.nothing().requireData, + throwsStateError, + ); + }); + }); group('Async smoke tests', () { testWidgets('FutureBuilder', (WidgetTester tester) async { await tester.pumpWidget(new FutureBuilder(