mirror of
https://github.com/flutter/flutter.git
synced 2025-06-03 00:51:18 +00:00
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.
This commit is contained in:
parent
41d81132f9
commit
390993d070
@ -1 +1 @@
|
||||
7f25cd0d65ca52a5fddb5f41abf5b82acbe14085
|
||||
74de13c0bde4eeb967391bd2a7ba973c525113b1
|
@ -1,6 +1,6 @@
|
||||
<?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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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';
|
||||
|
204
packages/flutter/lib/src/foundation/serialization.dart
Normal file
204
packages/flutter/lib/src/foundation/serialization.dart
Normal file
@ -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;
|
||||
}
|
94
packages/flutter/lib/src/services/message_codec.dart
Normal file
94
packages/flutter/lib/src/services/message_codec.dart
Normal file
@ -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<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)';
|
||||
}
|
421
packages/flutter/lib/src/services/message_codecs.dart
Normal file
421
packages/flutter/lib/src/services/message_codecs.dart
Normal file
@ -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<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');
|
||||
}
|
||||
}
|
||||
|
191
packages/flutter/lib/src/services/platform_channel.dart
Normal file
191
packages/flutter/lib/src/services/platform_channel.dart
Normal file
@ -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<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;
|
||||
}
|
||||
}
|
@ -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<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;
|
||||
|
||||
|
80
packages/flutter/test/foundation/serialization_test.dart
Normal file
80
packages/flutter/test/foundation/serialization_test.dart
Normal file
@ -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(<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);
|
||||
});
|
||||
});
|
||||
}
|
108
packages/flutter/test/services/platform_channel_test.dart
Normal file
108
packages/flutter/test/services/platform_channel_test.dart
Normal file
@ -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> 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);
|
||||
});
|
||||
});
|
||||
}
|
@ -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>(
|
||||
|
Loading…
Reference in New Issue
Block a user