blob: d4456b06f68958bec7433f32e94d64038afdd180 [file] [log] [blame]
// Copyright 2014 The Flutter 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:developer';
import 'package:flute/foundation.dart';
import '_background_isolate_binary_messenger_io.dart'
;
import 'binary_messenger.dart';
import 'binding.dart';
import 'debug.dart' show debugProfilePlatformChannels;
import 'message_codec.dart';
import 'message_codecs.dart';
export '_background_isolate_binary_messenger_io.dart'
;
export 'binary_messenger.dart' show BinaryMessenger;
export 'binding.dart' show RootIsolateToken;
export 'message_codec.dart' show MessageCodec, MethodCall, MethodCodec;
bool _debugProfilePlatformChannelsIsRunning = false;
const Duration _debugProfilePlatformChannelsRate = Duration(seconds: 1);
final Expando<BinaryMessenger> _debugBinaryMessengers = Expando<BinaryMessenger>();
class _ProfiledBinaryMessenger implements BinaryMessenger {
const _ProfiledBinaryMessenger(this.proxy, this.channelTypeName, this.codecTypeName);
final BinaryMessenger proxy;
final String channelTypeName;
final String codecTypeName;
@override
Future<void> handlePlatformMessage(String channel, ByteData? data, PlatformMessageResponseCallback? callback) {
return proxy.handlePlatformMessage(channel, data, callback);
}
Future<ByteData?>? sendWithPostfix(String channel, String postfix, ByteData? message) async {
final TimelineTask task = TimelineTask();
_debugRecordUpStream(channelTypeName, '$channel$postfix', codecTypeName, message);
task.start('Platform Channel send $channel$postfix');
final ByteData? result;
try {
result = await proxy.send(channel, message);
} finally {
task.finish();
}
_debugRecordDownStream(channelTypeName, '$channel$postfix', codecTypeName, result);
return result;
}
@override
Future<ByteData?>? send(String channel, ByteData? message) =>
sendWithPostfix(channel, '', message);
@override
void setMessageHandler(String channel, MessageHandler? handler) {
proxy.setMessageHandler(channel, handler);
}
}
class _PlatformChannelStats {
_PlatformChannelStats(this.channel, this.codec, this.type);
final String channel;
final String codec;
final String type;
int _upCount = 0;
int _upBytes = 0;
int get upBytes => _upBytes;
void addUpStream(int bytes) {
_upCount += 1;
_upBytes += bytes;
}
int _downCount = 0;
int _downBytes = 0;
int get downBytes => _downBytes;
void addDownStream(int bytes) {
_downCount += 1;
_downBytes += bytes;
}
double get averageUpPayload => _upBytes / _upCount;
double get averageDownPayload => _downBytes / _downCount;
}
final Map<String, _PlatformChannelStats> _debugProfilePlatformChannelsStats = <String, _PlatformChannelStats>{};
Future<void> _debugLaunchProfilePlatformChannels() async {
if (!_debugProfilePlatformChannelsIsRunning) {
_debugProfilePlatformChannelsIsRunning = true;
await Future<dynamic>.delayed(_debugProfilePlatformChannelsRate);
_debugProfilePlatformChannelsIsRunning = false;
final StringBuffer log = StringBuffer();
log.writeln('Platform Channel Stats:');
final List<_PlatformChannelStats> allStats =
_debugProfilePlatformChannelsStats.values.toList();
// Sort highest combined bandwidth first.
allStats.sort((_PlatformChannelStats x, _PlatformChannelStats y) =>
(y.upBytes + y.downBytes) - (x.upBytes + x.downBytes));
for (final _PlatformChannelStats stats in allStats) {
log.writeln(
' (name:"${stats.channel}" type:"${stats.type}" codec:"${stats.codec}" upBytes:${stats.upBytes} upBytes_avg:${stats.averageUpPayload.toStringAsFixed(1)} downBytes:${stats.downBytes} downBytes_avg:${stats.averageDownPayload.toStringAsFixed(1)})');
}
debugPrint(log.toString());
_debugProfilePlatformChannelsStats.clear();
}
}
void _debugRecordUpStream(String channelTypeName, String name,
String codecTypeName, ByteData? bytes) {
final _PlatformChannelStats stats =
_debugProfilePlatformChannelsStats[name] ??=
_PlatformChannelStats(name, codecTypeName, channelTypeName);
stats.addUpStream(bytes?.lengthInBytes ?? 0);
_debugLaunchProfilePlatformChannels();
}
void _debugRecordDownStream(String channelTypeName, String name,
String codecTypeName, ByteData? bytes) {
final _PlatformChannelStats stats =
_debugProfilePlatformChannelsStats[name] ??=
_PlatformChannelStats(name, codecTypeName, channelTypeName);
stats.addDownStream(bytes?.lengthInBytes ?? 0);
_debugLaunchProfilePlatformChannels();
}
BinaryMessenger _findBinaryMessenger() {
return !kIsWeb && ServicesBinding.rootIsolateToken == null
? BackgroundIsolateBinaryMessenger.instance
: ServicesBinding.instance.defaultBinaryMessenger;
}
/// 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 basic message channel 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 use of unsupported values should be considered programming errors, and
/// will result in exceptions being thrown. The null message is supported
/// for all codecs.
///
/// The logical identity of the channel is given by its name. Identically named
/// channels will interfere with each other's communication.
///
/// All [BasicMessageChannel]s provided by the Flutter framework guarantee FIFO
/// ordering. Applications can assume messages sent via a built-in
/// [BasicMessageChannel] are delivered in the same order as they're sent.
///
/// See: <https://flutter.dev/platform-channels/>
class BasicMessageChannel<T> {
/// Creates a [BasicMessageChannel] with the specified [name], [codec] and [binaryMessenger].
///
/// The [name] and [codec] arguments cannot be null. The default [ServicesBinding.defaultBinaryMessenger]
/// instance is used if [binaryMessenger] is null.
const BasicMessageChannel(this.name, this.codec, { BinaryMessenger? binaryMessenger })
: assert(name != null),
assert(codec != null),
_binaryMessenger = binaryMessenger;
/// 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;
/// The messenger which sends the bytes for this channel.
///
/// On the root isolate or web, this defaults to the
/// [ServicesBinding.defaultBinaryMessenger]. In other contexts the default
/// value is a [BackgroundIsolateBinaryMessenger] from
/// [BackgroundIsolateBinaryMessenger.ensureInitialized].
BinaryMessenger get binaryMessenger {
final BinaryMessenger result = _binaryMessenger ?? _findBinaryMessenger();
return !kReleaseMode && debugProfilePlatformChannels
? _debugBinaryMessengers[this] ??= _ProfiledBinaryMessenger(
// ignore: no_runtimetype_tostring
result, runtimeType.toString(), codec.runtimeType.toString())
: result;
}
final BinaryMessenger? _binaryMessenger;
/// Sends the specified [message] to the platform plugins on this channel.
///
/// Returns a [Future] which completes to the received response, which may
/// be null.
Future<T?> send(T message) async {
return codec.decodeMessage(await binaryMessenger.send(name, codec.encodeMessage(message)));
}
/// Sets a callback for receiving messages from the platform plugins on this
/// channel. Messages may be null.
///
/// The given callback will replace the currently registered callback for this
/// channel, if any. To remove the handler, pass null as the `handler`
/// argument.
///
/// The handler's return value is sent back to the platform plugins as a
/// message reply. It may be null.
void setMessageHandler(Future<T> Function(T? message)? handler) {
if (handler == null) {
binaryMessenger.setMessageHandler(name, null);
} else {
binaryMessenger.setMessageHandler(name, (ByteData? message) async {
return codec.encodeMessage(await handler(codec.decodeMessage(message)));
});
}
}
// Looking for setMockMessageHandler?
// See this shim package: packages/flutter_test/lib/src/deprecated.dart
}
/// A named channel for communicating with platform plugins using asynchronous
/// method calls.
///
/// 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 method channel counterpart of this channel on the
/// platform side. The Dart type of arguments and results is `dynamic`,
/// but only values supported by the specified [MethodCodec] can be used.
/// The use of unsupported values should be considered programming errors, and
/// will result in exceptions being thrown. The null value is supported
/// for all codecs.
///
/// The logical identity of the channel is given by its name. Identically named
/// channels will interfere with each other's communication.
///
/// {@template flutter.services.method_channel.FIFO}
/// All [MethodChannel]s provided by the Flutter framework guarantee FIFO
/// ordering. Applications can assume method calls sent via a built-in
/// [MethodChannel] are received by the platform plugins in the same order as
/// they're sent.
/// {@endtemplate}
///
/// See: <https://flutter.dev/platform-channels/>
class MethodChannel {
/// Creates a [MethodChannel] with the specified [name].
///
/// The [codec] used will be [StandardMethodCodec], unless otherwise
/// specified.
///
/// The [name] and [codec] arguments cannot be null. The default [ServicesBinding.defaultBinaryMessenger]
/// instance is used if [binaryMessenger] is null.
const MethodChannel(this.name, [this.codec = const StandardMethodCodec(), BinaryMessenger? binaryMessenger ])
: assert(name != null),
assert(codec != null),
_binaryMessenger = binaryMessenger;
/// The logical channel on which communication happens, not null.
final String name;
/// The message codec used by this channel, not null.
final MethodCodec codec;
/// The messenger which sends the bytes for this channel.
///
/// On the root isolate or web, this defaults to the
/// [ServicesBinding.defaultBinaryMessenger]. In other contexts the default
/// value is a [BackgroundIsolateBinaryMessenger] from
/// [BackgroundIsolateBinaryMessenger.ensureInitialized].
BinaryMessenger get binaryMessenger {
final BinaryMessenger result = _binaryMessenger ?? _findBinaryMessenger();
return !kReleaseMode && debugProfilePlatformChannels
? _debugBinaryMessengers[this] ??= _ProfiledBinaryMessenger(
// ignore: no_runtimetype_tostring
result, runtimeType.toString(), codec.runtimeType.toString())
: result;
}
final BinaryMessenger? _binaryMessenger;
/// Backend implementation of [invokeMethod].
///
/// The `method` and `arguments` arguments are used to create a [MethodCall]
/// object that is passed to the [codec]'s [MethodCodec.encodeMethodCall]
/// method. The resulting message is then sent to the embedding using the
/// [binaryMessenger]'s [BinaryMessenger.send] method.
///
/// If the result is null and `missingOk` is true, this returns null. (This is
/// the behavior of [OptionalMethodChannel.invokeMethod].)
///
/// If the result is null and `missingOk` is false, this throws a
/// [MissingPluginException]. (This is the behavior of
/// [MethodChannel.invokeMethod].)
///
/// Otherwise, the result is decoded using the [codec]'s
/// [MethodCodec.decodeEnvelope] method.
///
/// The `T` type argument is the expected return type. It is treated as
/// nullable.
@optionalTypeArgs
Future<T?> _invokeMethod<T>(String method, { required bool missingOk, dynamic arguments }) async {
assert(method != null);
final ByteData input = codec.encodeMethodCall(MethodCall(method, arguments));
final ByteData? result =
!kReleaseMode && debugProfilePlatformChannels ?
await (binaryMessenger as _ProfiledBinaryMessenger).sendWithPostfix(name, '#$method', input) :
await binaryMessenger.send(name, input);
if (result == null) {
if (missingOk) {
return null;
}
throw MissingPluginException('No implementation found for method $method on channel $name');
}
return codec.decodeEnvelope(result) as T?;
}
/// Invokes a [method] on this channel with the specified [arguments].
///
/// The static type of [arguments] is `dynamic`, but only values supported by
/// the [codec] of this channel can be used. The same applies to the returned
/// result. The values supported by the default codec and their platform-specific
/// counterparts are documented with [StandardMessageCodec].
///
/// The generic argument `T` of the method can be inferred by the surrounding
/// context, or provided explicitly. If it does not match the returned type of
/// the channel, a [TypeError] will be thrown at runtime. `T` cannot be a class
/// with generics other than `dynamic`. For example, `Map<String, String>`
/// is not supported but `Map<dynamic, dynamic>` or `Map` is.
///
/// 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 [MissingPluginException], if the method has not been implemented by a
/// platform plugin.
///
/// The following code snippets demonstrate how to invoke platform methods
/// in Dart using a MethodChannel and how to implement those methods in Java
/// (for Android) and Objective-C (for iOS).
///
/// {@tool snippet}
///
/// The code might be packaged up as a musical plugin, see
/// <https://flutter.dev/developing-packages/>:
///
/// ```dart
/// class Music {
/// // Class cannot be instantiated.
/// const Music._();
///
/// static const MethodChannel _channel = MethodChannel('music');
///
/// static Future<bool> isLicensed() async {
/// // invokeMethod returns a Future<T?>, so we handle the case where
/// // the return value is null by treating null as false.
/// return _channel.invokeMethod<bool>('isLicensed').then<bool>((bool? value) => value ?? false);
/// }
///
/// static Future<List<Song>> songs() async {
/// // invokeMethod here returns a Future<dynamic> that completes to a
/// // List<dynamic> with Map<dynamic, dynamic> entries. Post-processing
/// // code thus cannot assume e.g. List<Map<String, String>> even though
/// // the actual values involved would support such a typed container.
/// // The correct type cannot be inferred with any value of `T`.
/// final List<dynamic>? songs = await _channel.invokeMethod<List<dynamic>>('getSongs');
/// return songs?.cast<Map<String, Object?>>().map<Song>(Song.fromJson).toList() ?? <Song>[];
/// }
///
/// static Future<void> play(Song song, double volume) async {
/// // Errors occurring on the platform side cause invokeMethod to throw
/// // PlatformExceptions.
/// try {
/// return _channel.invokeMethod('play', <String, dynamic>{
/// 'song': song.id,
/// 'volume': volume,
/// });
/// } on PlatformException catch (e) {
/// throw ArgumentError('Unable to play ${song.title}: ${e.message}');
/// }
/// }
/// }
///
/// class Song {
/// Song(this.id, this.title, this.artist);
///
/// final String id;
/// final String title;
/// final String artist;
///
/// static Song fromJson(Map<String, Object?> json) {
/// return Song(json['id']! as String, json['title']! as String, json['artist']! as String);
/// }
/// }
/// ```
/// {@end-tool}
///
/// {@tool snippet}
///
/// Java (for Android):
///
/// ```java
/// // Assumes existence of an Android MusicApi.
/// public class MusicPlugin implements MethodCallHandler {
/// @Override
/// public void onMethodCall(MethodCall call, Result result) {
/// switch (call.method) {
/// case "isLicensed":
/// result.success(MusicApi.checkLicense());
/// break;
/// case "getSongs":
/// final List<MusicApi.Track> tracks = MusicApi.getTracks();
/// final List<Object> json = ArrayList<>(tracks.size());
/// for (MusicApi.Track track : tracks) {
/// json.add(track.toJson()); // Map<String, Object> entries
/// }
/// result.success(json);
/// break;
/// case "play":
/// final String song = call.argument("song");
/// final double volume = call.argument("volume");
/// try {
/// MusicApi.playSongAtVolume(song, volume);
/// result.success(null);
/// } catch (MusicalException e) {
/// result.error("playError", e.getMessage(), null);
/// }
/// break;
/// default:
/// result.notImplemented();
/// }
/// }
/// // Other methods elided.
/// }
/// ```
/// {@end-tool}
///
/// {@tool snippet}
///
/// Objective-C (for iOS):
///
/// ```objectivec
/// @interface MusicPlugin : NSObject<FlutterPlugin>
/// @end
///
/// // Assumes existence of an iOS Broadway Play Api.
/// @implementation MusicPlugin
/// - (void)handleMethodCall:(FlutterMethodCall*)call result:(FlutterResult)result {
/// if ([@"isLicensed" isEqualToString:call.method]) {
/// result([NSNumber numberWithBool:[BWPlayApi isLicensed]]);
/// } else if ([@"getSongs" isEqualToString:call.method]) {
/// NSArray* items = [BWPlayApi items];
/// NSMutableArray* json = [NSMutableArray arrayWithCapacity:items.count];
/// for (final BWPlayItem* item in items) {
/// [json addObject:@{ @"id":item.itemId, @"title":item.name, @"artist":item.artist }];
/// }
/// result(json);
/// } else if ([@"play" isEqualToString:call.method]) {
/// NSString* itemId = call.arguments[@"song"];
/// NSNumber* volume = call.arguments[@"volume"];
/// NSError* error = nil;
/// BOOL success = [BWPlayApi playItem:itemId volume:volume.doubleValue error:&error];
/// if (success) {
/// result(nil);
/// } else {
/// result([FlutterError errorWithCode:[NSString stringWithFormat:@"Error %ld", error.code]
/// message:error.domain
/// details:error.localizedDescription]);
/// }
/// } else {
/// result(FlutterMethodNotImplemented);
/// }
/// }
/// // Other methods elided.
/// @end
/// ```
/// {@end-tool}
///
/// See also:
///
/// * [invokeListMethod], for automatically returning typed lists.
/// * [invokeMapMethod], for automatically returning typed maps.
/// * [StandardMessageCodec] which defines the payload values supported by
/// [StandardMethodCodec].
/// * [JSONMessageCodec] which defines the payload values supported by
/// [JSONMethodCodec].
/// * <https://api.flutter.dev/javadoc/io/flutter/plugin/common/MethodCall.html>
/// for how to access method call arguments on Android.
@optionalTypeArgs
Future<T?> invokeMethod<T>(String method, [ dynamic arguments ]) {
return _invokeMethod<T>(method, missingOk: false, arguments: arguments);
}
/// An implementation of [invokeMethod] that can return typed lists.
///
/// Dart generics are reified, meaning that an untyped `List<dynamic>` cannot
/// masquerade as a `List<T>`. Since [invokeMethod] can only return dynamic
/// maps, we instead create a new typed list using [List.cast].
///
/// See also:
///
/// * [invokeMethod], which this call delegates to.
Future<List<T>?> invokeListMethod<T>(String method, [ dynamic arguments ]) async {
final List<dynamic>? result = await invokeMethod<List<dynamic>>(method, arguments);
return result?.cast<T>();
}
/// An implementation of [invokeMethod] that can return typed maps.
///
/// Dart generics are reified, meaning that an untyped `Map<dynamic, dynamic>`
/// cannot masquerade as a `Map<K, V>`. Since [invokeMethod] can only return
/// dynamic maps, we instead create a new typed map using [Map.cast].
///
/// See also:
///
/// * [invokeMethod], which this call delegates to.
Future<Map<K, V>?> invokeMapMethod<K, V>(String method, [ dynamic arguments ]) async {
final Map<dynamic, dynamic>? result = await invokeMethod<Map<dynamic, dynamic>>(method, arguments);
return result?.cast<K, V>();
}
/// Sets a callback for receiving method calls on this channel.
///
/// The given callback will replace the currently registered callback for this
/// channel, if any. To remove the handler, pass null as the
/// `handler` argument.
///
/// If the future returned by the handler completes with a result, that value
/// is sent back to the platform plugin caller wrapped in a success envelope
/// as defined by the [codec] of this channel. If the future completes with
/// a [PlatformException], the fields of that exception will be used to
/// populate an error envelope which is sent back instead. If the future
/// completes with a [MissingPluginException], an empty reply is sent
/// similarly to what happens if no method call handler has been set.
/// Any other exception results in an error envelope being sent.
void setMethodCallHandler(Future<dynamic> Function(MethodCall call)? handler) {
assert(
_binaryMessenger != null || ServicesBinding.instance != null,
'Cannot set the method call handler before the binary messenger has been initialized. '
'This happens when you call setMethodCallHandler() before the WidgetsFlutterBinding '
'has been initialized. You can fix this by either calling WidgetsFlutterBinding.ensureInitialized() '
'before this or by passing a custom BinaryMessenger instance to MethodChannel().',
);
binaryMessenger.setMessageHandler(
name,
handler == null
? null
: (ByteData? message) => _handleAsMethodCall(message, handler),
);
}
Future<ByteData?> _handleAsMethodCall(ByteData? message, Future<dynamic> Function(MethodCall call) handler) async {
final MethodCall call = codec.decodeMethodCall(message);
try {
return codec.encodeSuccessEnvelope(await handler(call));
} on PlatformException catch (e) {
return codec.encodeErrorEnvelope(
code: e.code,
message: e.message,
details: e.details,
);
} on MissingPluginException {
return null;
} catch (error) {
return codec.encodeErrorEnvelope(code: 'error', message: error.toString());
}
}
// Looking for setMockMethodCallHandler or checkMethodCallHandler?
// See this shim package: packages/flutter_test/lib/src/deprecated.dart
}
/// A [MethodChannel] that ignores missing platform plugins.
///
/// When [invokeMethod] fails to find the platform plugin, it returns null
/// instead of throwing an exception.
///
/// {@macro flutter.services.method_channel.FIFO}
class OptionalMethodChannel extends MethodChannel {
/// Creates a [MethodChannel] that ignores missing platform plugins.
const OptionalMethodChannel(super.name, [super.codec, super.binaryMessenger]);
@override
Future<T?> invokeMethod<T>(String method, [ dynamic arguments ]) async {
return super._invokeMethod<T>(method, missingOk: true, arguments: arguments);
}
}
/// A named channel for communicating with platform plugins using event streams.
///
/// Stream setup requests are encoded into binary before being sent,
/// and binary events and errors 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 an [EventChannel] counterpart of
/// this channel on the platform side. The Dart type of events sent and received
/// is `dynamic`, but only values supported by the specified [MethodCodec] can
/// be used.
///
/// The logical identity of the channel is given by its name. Identically named
/// channels will interfere with each other's communication.
///
/// See: <https://flutter.dev/platform-channels/>
class EventChannel {
/// Creates an [EventChannel] with the specified [name].
///
/// The [codec] used will be [StandardMethodCodec], unless otherwise
/// specified.
///
/// Neither [name] nor [codec] may be null. The default [ServicesBinding.defaultBinaryMessenger]
/// instance is used if [binaryMessenger] is null.
const EventChannel(this.name, [this.codec = const StandardMethodCodec(), BinaryMessenger? binaryMessenger])
: assert(name != null),
assert(codec != null),
_binaryMessenger = binaryMessenger;
/// The logical channel on which communication happens, not null.
final String name;
/// The message codec used by this channel, not null.
final MethodCodec codec;
/// The messenger which sends the bytes for this channel.
///
/// On the root isolate or web, this defaults to the
/// [ServicesBinding.defaultBinaryMessenger]. In other contexts the default
/// value is a [BackgroundIsolateBinaryMessenger] from
/// [BackgroundIsolateBinaryMessenger.ensureInitialized].
BinaryMessenger get binaryMessenger =>
_binaryMessenger ?? _findBinaryMessenger();
final BinaryMessenger? _binaryMessenger;
/// 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.
///
/// Errors occurring during stream activation or deactivation are reported
/// through the [FlutterError] facility. Stream activation happens only when
/// stream listener count changes from 0 to 1. Stream deactivation happens
/// only when stream listener count changes from 1 to 0.
Stream<dynamic> receiveBroadcastStream([ dynamic arguments ]) {
final MethodChannel methodChannel = MethodChannel(name, codec);
late StreamController<dynamic> controller;
controller = StreamController<dynamic>.broadcast(onListen: () async {
binaryMessenger.setMessageHandler(name, (ByteData? reply) async {
if (reply == null) {
controller.close();
} else {
try {
controller.add(codec.decodeEnvelope(reply));
} on PlatformException catch (e) {
controller.addError(e);
}
}
return null;
});
try {
await methodChannel.invokeMethod<void>('listen', arguments);
} catch (exception, stack) {
FlutterError.reportError(FlutterErrorDetails(
exception: exception,
stack: stack,
library: 'services library',
context: ErrorDescription('while activating platform stream on channel $name'),
));
}
}, onCancel: () async {
binaryMessenger.setMessageHandler(name, null);
try {
await methodChannel.invokeMethod<void>('cancel', arguments);
} catch (exception, stack) {
FlutterError.reportError(FlutterErrorDetails(
exception: exception,
stack: stack,
library: 'services library',
context: ErrorDescription('while de-activating platform stream on channel $name'),
));
}
});
return controller.stream;
}
}