| 1 | // Copyright 2014 The Flutter Authors. All rights reserved. |
| 2 | // Use of this source code is governed by a BSD-style license that can be |
| 3 | // found in the LICENSE file. |
| 4 | |
| 5 | import 'dart:async'; |
| 6 | import 'dart:developer'; |
| 7 | |
| 8 | import 'package:flutter/foundation.dart'; |
| 9 | |
| 10 | import '_background_isolate_binary_messenger_io.dart' |
| 11 | if (dart.library.js_util) '_background_isolate_binary_messenger_web.dart' ; |
| 12 | |
| 13 | import 'binary_messenger.dart'; |
| 14 | import 'binding.dart'; |
| 15 | import 'debug.dart'; |
| 16 | import 'message_codec.dart'; |
| 17 | import 'message_codecs.dart'; |
| 18 | |
| 19 | export '_background_isolate_binary_messenger_io.dart' |
| 20 | if (dart.library.js_util) '_background_isolate_binary_messenger_web.dart' ; |
| 21 | |
| 22 | export 'binary_messenger.dart' show BinaryMessenger; |
| 23 | export 'binding.dart' show RootIsolateToken; |
| 24 | export 'message_codec.dart' show MessageCodec, MethodCall, MethodCodec; |
| 25 | |
| 26 | /// Profile and print statistics on Platform Channel usage. |
| 27 | /// |
| 28 | /// When this is true statistics about the usage of Platform Channels will be |
| 29 | /// printed out periodically to the console and Timeline events will show the |
| 30 | /// time between sending and receiving a message (encoding and decoding time |
| 31 | /// excluded). |
| 32 | /// |
| 33 | /// The statistics include the total bytes transmitted and the average number of |
| 34 | /// bytes per invocation in the last quantum. "Up" means in the direction of |
| 35 | /// Flutter to the host platform, "down" is the host platform to flutter. |
| 36 | bool get shouldProfilePlatformChannels => |
| 37 | kProfilePlatformChannels || (!kReleaseMode && debugProfilePlatformChannels); |
| 38 | |
| 39 | /// Controls whether platform channel usage can be debugged in release mode. |
| 40 | /// |
| 41 | /// See also: |
| 42 | /// |
| 43 | /// * [shouldProfilePlatformChannels], which checks both |
| 44 | /// [kProfilePlatformChannels] and [debugProfilePlatformChannels] for the |
| 45 | /// current run mode. |
| 46 | /// * [debugProfilePlatformChannels], which determines whether platform |
| 47 | /// channel usage can be debugged in non-release mode. |
| 48 | const bool kProfilePlatformChannels = false; |
| 49 | |
| 50 | bool _profilePlatformChannelsIsRunning = false; |
| 51 | const Duration _profilePlatformChannelsRate = Duration(seconds: 1); |
| 52 | final Expando<BinaryMessenger> _profiledBinaryMessengers = Expando<BinaryMessenger>(); |
| 53 | |
| 54 | class _ProfiledBinaryMessenger implements BinaryMessenger { |
| 55 | const _ProfiledBinaryMessenger(this.proxy, this.channelTypeName, this.codecTypeName); |
| 56 | final BinaryMessenger proxy; |
| 57 | final String channelTypeName; |
| 58 | final String codecTypeName; |
| 59 | |
| 60 | @override |
| 61 | Future<void> handlePlatformMessage( |
| 62 | String channel, |
| 63 | ByteData? data, |
| 64 | PlatformMessageResponseCallback? callback, |
| 65 | ) { |
| 66 | return proxy.handlePlatformMessage(channel, data, callback); |
| 67 | } |
| 68 | |
| 69 | Future<ByteData?>? sendWithPostfix(String channel, String postfix, ByteData? message) async { |
| 70 | _debugRecordUpStream(channelTypeName, ' $channel$postfix' , codecTypeName, message); |
| 71 | final TimelineTask timelineTask = TimelineTask() |
| 72 | ..start('Platform Channel send $channel$postfix' ); |
| 73 | final ByteData? result;
|
| 74 | try {
|
| 75 | result = await proxy.send(channel, message);
|
| 76 | } finally {
|
| 77 | timelineTask.finish();
|
| 78 | }
|
| 79 | _debugRecordDownStream(channelTypeName, ' $channel$postfix' , codecTypeName, result);
|
| 80 | return result;
|
| 81 | }
|
| 82 |
|
| 83 | @override
|
| 84 | Future<ByteData?>? send(String channel, ByteData? message) =>
|
| 85 | sendWithPostfix(channel, '' , message);
|
| 86 |
|
| 87 | @override
|
| 88 | void setMessageHandler(String channel, MessageHandler? handler) {
|
| 89 | proxy.setMessageHandler(channel, handler);
|
| 90 | }
|
| 91 | }
|
| 92 |
|
| 93 | class _PlatformChannelStats {
|
| 94 | _PlatformChannelStats(this.channel, this.codec, this.type);
|
| 95 |
|
| 96 | final String channel;
|
| 97 | final String codec;
|
| 98 | final String type;
|
| 99 |
|
| 100 | int _upCount = 0;
|
| 101 | int _upBytes = 0;
|
| 102 | int get upBytes => _upBytes;
|
| 103 | void addUpStream(int bytes) {
|
| 104 | _upCount += 1;
|
| 105 | _upBytes += bytes;
|
| 106 | }
|
| 107 |
|
| 108 | int _downCount = 0;
|
| 109 | int _downBytes = 0;
|
| 110 | int get downBytes => _downBytes;
|
| 111 | void addDownStream(int bytes) {
|
| 112 | _downCount += 1;
|
| 113 | _downBytes += bytes;
|
| 114 | }
|
| 115 |
|
| 116 | double get averageUpPayload => _upBytes / _upCount;
|
| 117 | double get averageDownPayload => _downBytes / _downCount;
|
| 118 | }
|
| 119 |
|
| 120 | final Map<String, _PlatformChannelStats> _profilePlatformChannelsStats =
|
| 121 | <String, _PlatformChannelStats>{};
|
| 122 |
|
| 123 | Future<void> _debugLaunchProfilePlatformChannels() async {
|
| 124 | if (!_profilePlatformChannelsIsRunning) {
|
| 125 | _profilePlatformChannelsIsRunning = true;
|
| 126 | await Future<dynamic>.delayed(_profilePlatformChannelsRate);
|
| 127 | _profilePlatformChannelsIsRunning = false;
|
| 128 | final StringBuffer log = StringBuffer();
|
| 129 | log.writeln('Platform Channel Stats:' );
|
| 130 | final List<_PlatformChannelStats> allStats = _profilePlatformChannelsStats.values.toList();
|
| 131 | // Sort highest combined bandwidth first.
|
| 132 | allStats.sort(
|
| 133 | (_PlatformChannelStats x, _PlatformChannelStats y) =>
|
| 134 | (y.upBytes + y.downBytes) - (x.upBytes + x.downBytes),
|
| 135 | );
|
| 136 | for (final _PlatformChannelStats stats in allStats) {
|
| 137 | log.writeln(
|
| 138 | ' (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)})' ,
|
| 139 | );
|
| 140 | }
|
| 141 | debugPrint(log.toString());
|
| 142 | _profilePlatformChannelsStats.clear();
|
| 143 | }
|
| 144 | }
|
| 145 |
|
| 146 | void _debugRecordUpStream(
|
| 147 | String channelTypeName,
|
| 148 | String name,
|
| 149 | String codecTypeName,
|
| 150 | ByteData? bytes,
|
| 151 | ) {
|
| 152 | final _PlatformChannelStats stats = _profilePlatformChannelsStats[name] ??= _PlatformChannelStats(
|
| 153 | name,
|
| 154 | codecTypeName,
|
| 155 | channelTypeName,
|
| 156 | );
|
| 157 | stats.addUpStream(bytes?.lengthInBytes ?? 0);
|
| 158 | _debugLaunchProfilePlatformChannels();
|
| 159 | }
|
| 160 |
|
| 161 | void _debugRecordDownStream(
|
| 162 | String channelTypeName,
|
| 163 | String name,
|
| 164 | String codecTypeName,
|
| 165 | ByteData? bytes,
|
| 166 | ) {
|
| 167 | final _PlatformChannelStats stats = _profilePlatformChannelsStats[name] ??= _PlatformChannelStats(
|
| 168 | name,
|
| 169 | codecTypeName,
|
| 170 | channelTypeName,
|
| 171 | );
|
| 172 | stats.addDownStream(bytes?.lengthInBytes ?? 0);
|
| 173 | _debugLaunchProfilePlatformChannels();
|
| 174 | }
|
| 175 |
|
| 176 | BinaryMessenger _findBinaryMessenger() {
|
| 177 | return !kIsWeb && ServicesBinding.rootIsolateToken == null
|
| 178 | ? BackgroundIsolateBinaryMessenger.instance
|
| 179 | : ServicesBinding.instance.defaultBinaryMessenger;
|
| 180 | }
|
| 181 |
|
| 182 | /// A named channel for communicating with platform plugins using asynchronous
|
| 183 | /// message passing.
|
| 184 | ///
|
| 185 | /// Messages are encoded into binary before being sent, and binary messages
|
| 186 | /// received are decoded into Dart values. The [MessageCodec] used must be
|
| 187 | /// compatible with the one used by the platform plugin. This can be achieved
|
| 188 | /// by creating a basic message channel counterpart of this channel on the
|
| 189 | /// platform side. The Dart type of messages sent and received is [T],
|
| 190 | /// but only the values supported by the specified [MessageCodec] can be used.
|
| 191 | /// The use of unsupported values should be considered programming errors, and
|
| 192 | /// will result in exceptions being thrown. The null message is supported
|
| 193 | /// for all codecs.
|
| 194 | ///
|
| 195 | /// The logical identity of the channel is given by its name. Identically named
|
| 196 | /// channels will interfere with each other's communication.
|
| 197 | ///
|
| 198 | /// All [BasicMessageChannel]s provided by the Flutter framework guarantee FIFO
|
| 199 | /// ordering. Applications can assume messages sent via a built-in
|
| 200 | /// [BasicMessageChannel] are delivered in the same order as they're sent.
|
| 201 | ///
|
| 202 | /// See: <https://flutter.dev/to/platform-channels/>
|
| 203 | class BasicMessageChannel<T> {
|
| 204 | /// Creates a [BasicMessageChannel] with the specified [name], [codec] and
|
| 205 | /// [binaryMessenger].
|
| 206 | ///
|
| 207 | /// The default [ServicesBinding.defaultBinaryMessenger] instance is used if
|
| 208 | /// [binaryMessenger] is null.
|
| 209 | const BasicMessageChannel(this.name, this.codec, {BinaryMessenger? binaryMessenger})
|
| 210 | : _binaryMessenger = binaryMessenger;
|
| 211 |
|
| 212 | /// The logical channel on which communication happens, not null.
|
| 213 | final String name;
|
| 214 |
|
| 215 | /// The message codec used by this channel, not null.
|
| 216 | final MessageCodec<T> codec;
|
| 217 |
|
| 218 | /// The messenger which sends the bytes for this channel.
|
| 219 | ///
|
| 220 | /// On the root isolate or web, this defaults to the
|
| 221 | /// [ServicesBinding.defaultBinaryMessenger]. In other contexts the default
|
| 222 | /// value is a [BackgroundIsolateBinaryMessenger] from
|
| 223 | /// [BackgroundIsolateBinaryMessenger.ensureInitialized].
|
| 224 | BinaryMessenger get binaryMessenger {
|
| 225 | final BinaryMessenger result = _binaryMessenger ?? _findBinaryMessenger();
|
| 226 | return shouldProfilePlatformChannels
|
| 227 | ? _profiledBinaryMessengers[this] ??= _ProfiledBinaryMessenger(
|
| 228 | result,
|
| 229 | runtimeType.toString(), // ignore: no_runtimetype_tostring
|
| 230 | codec.runtimeType.toString(),
|
| 231 | )
|
| 232 | : result;
|
| 233 | }
|
| 234 |
|
| 235 | final BinaryMessenger? _binaryMessenger;
|
| 236 |
|
| 237 | /// Sends the specified [message] to the platform plugins on this channel.
|
| 238 | ///
|
| 239 | /// Returns a [Future] which completes to the received response, which may
|
| 240 | /// be null.
|
| 241 | Future<T?> send(T message) async {
|
| 242 | return codec.decodeMessage(await binaryMessenger.send(name, codec.encodeMessage(message)));
|
| 243 | }
|
| 244 |
|
| 245 | /// Sets a callback for receiving messages from the platform plugins on this
|
| 246 | /// channel. Messages may be null.
|
| 247 | ///
|
| 248 | /// The given callback will replace the currently registered callback for this
|
| 249 | /// channel, if any. To remove the handler, pass null as the `handler`
|
| 250 | /// argument.
|
| 251 | ///
|
| 252 | /// The handler's return value is sent back to the platform plugins as a
|
| 253 | /// message reply. It may be null.
|
| 254 | void setMessageHandler(Future<T> Function(T? message)? handler) {
|
| 255 | if (handler == null) {
|
| 256 | binaryMessenger.setMessageHandler(name, null);
|
| 257 | } else {
|
| 258 | binaryMessenger.setMessageHandler(name, (ByteData? message) async {
|
| 259 | return codec.encodeMessage(await handler(codec.decodeMessage(message)));
|
| 260 | });
|
| 261 | }
|
| 262 | }
|
| 263 |
|
| 264 | // Looking for setMockMessageHandler?
|
| 265 | // See this shim package: packages/flutter_test/lib/src/deprecated.dart
|
| 266 | }
|
| 267 |
|
| 268 | /// A named channel for communicating with platform plugins using asynchronous
|
| 269 | /// method calls.
|
| 270 | ///
|
| 271 | /// Method calls are encoded into binary before being sent, and binary results
|
| 272 | /// received are decoded into Dart values. The [MethodCodec] used must be
|
| 273 | /// compatible with the one used by the platform plugin. This can be achieved
|
| 274 | /// by creating a method channel counterpart of this channel on the
|
| 275 | /// platform side. The Dart type of arguments and results is `dynamic`,
|
| 276 | /// but only values supported by the specified [MethodCodec] can be used.
|
| 277 | /// The use of unsupported values should be considered programming errors, and
|
| 278 | /// will result in exceptions being thrown. The null value is supported
|
| 279 | /// for all codecs.
|
| 280 | ///
|
| 281 | /// The logical identity of the channel is given by its name. Identically named
|
| 282 | /// channels will interfere with each other's communication.
|
| 283 | ///
|
| 284 | /// {@template flutter.services.method_channel.FIFO}
|
| 285 | /// All [MethodChannel]s provided by the Flutter framework guarantee FIFO
|
| 286 | /// ordering. Applications can assume method calls sent via a built-in
|
| 287 | /// [MethodChannel] are received by the platform plugins in the same order as
|
| 288 | /// they're sent.
|
| 289 | /// {@endtemplate}
|
| 290 | ///
|
| 291 | /// See: <https://flutter.dev/to/platform-channels/>
|
| 292 | @pragma('vm:keep-name' )
|
| 293 | class MethodChannel {
|
| 294 | /// Creates a [MethodChannel] with the specified [name].
|
| 295 | ///
|
| 296 | /// The [codec] used will be [StandardMethodCodec], unless otherwise
|
| 297 | /// specified.
|
| 298 | ///
|
| 299 | /// The default [ServicesBinding.defaultBinaryMessenger] instance is used if
|
| 300 | /// [binaryMessenger] is null.
|
| 301 | const MethodChannel(
|
| 302 | this.name, [
|
| 303 | this.codec = const StandardMethodCodec(),
|
| 304 | BinaryMessenger? binaryMessenger,
|
| 305 | ]) : _binaryMessenger = binaryMessenger;
|
| 306 |
|
| 307 | /// The logical channel on which communication happens, not null.
|
| 308 | final String name;
|
| 309 |
|
| 310 | /// The message codec used by this channel, not null.
|
| 311 | final MethodCodec codec;
|
| 312 |
|
| 313 | /// The messenger which sends the bytes for this channel.
|
| 314 | ///
|
| 315 | /// On the root isolate or web, this defaults to the
|
| 316 | /// [ServicesBinding.defaultBinaryMessenger]. In other contexts the default
|
| 317 | /// value is a [BackgroundIsolateBinaryMessenger] from
|
| 318 | /// [BackgroundIsolateBinaryMessenger.ensureInitialized].
|
| 319 | BinaryMessenger get binaryMessenger {
|
| 320 | final BinaryMessenger result = _binaryMessenger ?? _findBinaryMessenger();
|
| 321 | return shouldProfilePlatformChannels
|
| 322 | ? _profiledBinaryMessengers[this] ??= _ProfiledBinaryMessenger(
|
| 323 | result,
|
| 324 | runtimeType.toString(), // ignore: no_runtimetype_tostring
|
| 325 | codec.runtimeType.toString(),
|
| 326 | )
|
| 327 | : result;
|
| 328 | }
|
| 329 |
|
| 330 | final BinaryMessenger? _binaryMessenger;
|
| 331 |
|
| 332 | /// Backend implementation of [invokeMethod].
|
| 333 | ///
|
| 334 | /// The `method` and `arguments` arguments are used to create a [MethodCall]
|
| 335 | /// object that is passed to the [codec]'s [MethodCodec.encodeMethodCall]
|
| 336 | /// method. The resulting message is then sent to the embedding using the
|
| 337 | /// [binaryMessenger]'s [BinaryMessenger.send] method.
|
| 338 | ///
|
| 339 | /// If the result is null and `missingOk` is true, this returns null. (This is
|
| 340 | /// the behavior of [OptionalMethodChannel.invokeMethod].)
|
| 341 | ///
|
| 342 | /// If the result is null and `missingOk` is false, this throws a
|
| 343 | /// [MissingPluginException]. (This is the behavior of
|
| 344 | /// [MethodChannel.invokeMethod].)
|
| 345 | ///
|
| 346 | /// Otherwise, the result is decoded using the [codec]'s
|
| 347 | /// [MethodCodec.decodeEnvelope] method.
|
| 348 | ///
|
| 349 | /// The `T` type argument is the expected return type. It is treated as
|
| 350 | /// nullable.
|
| 351 | @optionalTypeArgs
|
| 352 | Future<T?> _invokeMethod<T>(String method, {required bool missingOk, dynamic arguments}) async {
|
| 353 | final ByteData input = codec.encodeMethodCall(MethodCall(method, arguments));
|
| 354 | final ByteData? result = shouldProfilePlatformChannels
|
| 355 | ? await (binaryMessenger as _ProfiledBinaryMessenger).sendWithPostfix(
|
| 356 | name,
|
| 357 | '# $method' ,
|
| 358 | input,
|
| 359 | )
|
| 360 | : await binaryMessenger.send(name, input);
|
| 361 | if (result == null) {
|
| 362 | if (missingOk) {
|
| 363 | return null;
|
| 364 | }
|
| 365 | throw MissingPluginException('No implementation found for method $method on channel $name' );
|
| 366 | }
|
| 367 | return codec.decodeEnvelope(result) as T?;
|
| 368 | }
|
| 369 |
|
| 370 | /// Invokes a [method] on this channel with the specified [arguments].
|
| 371 | ///
|
| 372 | /// The static type of [arguments] is `dynamic`, but only values supported by
|
| 373 | /// the [codec] of this channel can be used. The same applies to the returned
|
| 374 | /// result. The values supported by the default codec and their platform-specific
|
| 375 | /// counterparts are documented with [StandardMessageCodec].
|
| 376 | ///
|
| 377 | /// The generic argument `T` of the method can be inferred by the surrounding
|
| 378 | /// context, or provided explicitly. If it does not match the returned type of
|
| 379 | /// the channel, a [TypeError] will be thrown at runtime. `T` cannot be a class
|
| 380 | /// with generics other than `dynamic`. For example, `Map<String, String>`
|
| 381 | /// is not supported but `Map<dynamic, dynamic>` or `Map` is.
|
| 382 | ///
|
| 383 | /// Returns a [Future] which completes to one of the following:
|
| 384 | ///
|
| 385 | /// * a result (possibly null), on successful invocation;
|
| 386 | /// * a [PlatformException], if the invocation failed in the platform plugin;
|
| 387 | /// * a [MissingPluginException], if the method has not been implemented by a
|
| 388 | /// platform plugin.
|
| 389 | ///
|
| 390 | /// The following code snippets demonstrate how to invoke platform methods
|
| 391 | /// in Dart using a MethodChannel and how to implement those methods in Java
|
| 392 | /// (for Android) and Objective-C (for iOS).
|
| 393 | ///
|
| 394 | /// {@tool snippet}
|
| 395 | ///
|
| 396 | /// The code might be packaged up as a musical plugin, see
|
| 397 | /// <https://flutter.dev/to/develop-packages>:
|
| 398 | ///
|
| 399 | /// ```dart
|
| 400 | /// abstract final class Music {
|
| 401 | /// static const MethodChannel _channel = MethodChannel('music');
|
| 402 | ///
|
| 403 | /// static Future<bool> isLicensed() async {
|
| 404 | /// // invokeMethod returns a Future, so we handle the case where
|
| 405 | /// // the return value is null by treating null as false.
|
| 406 | /// return _channel.invokeMethod<bool>('isLicensed').then<bool>((bool? value) => value ?? false);
|
| 407 | /// }
|
| 408 | ///
|
| 409 | /// static Future<List<Song>> songs() async {
|
| 410 | /// // invokeMethod here returns a Future that completes to a
|
| 411 | /// // List with Map entries. Post-processing
|
| 412 | /// // code thus cannot assume e.g. List
|
| 413 | /// // the actual values involved would support such a typed container.
|
| 414 | /// // The correct type cannot be inferred with any value of `T`.
|
| 415 | /// final List<dynamic>? songs = await _channel.invokeMethod<List<dynamic>>('getSongs');
|
| 416 | /// return songs?.cast<Map<String, Object?>>().map<Song>(Song.fromJson).toList() ?? <Song>[];
|
| 417 | /// }
|
| 418 | ///
|
| 419 | /// static Future<void> play(Song song, double volume) async {
|
| 420 | /// // Errors occurring on the platform side cause invokeMethod to throw
|
| 421 | /// // PlatformExceptions.
|
| 422 | /// try {
|
| 423 | /// return _channel.invokeMethod('play', <String, dynamic>{
|
| 424 | /// 'song': song.id,
|
| 425 | /// 'volume': volume,
|
| 426 | /// });
|
| 427 | /// } on PlatformException catch (e) {
|
| 428 | /// throw ArgumentError('Unable to play ${song.title}: ${e.message}');
|
| 429 | /// }
|
| 430 | /// }
|
| 431 | /// }
|
| 432 | ///
|
| 433 | /// class Song {
|
| 434 | /// Song(this.id, this.title, this.artist);
|
| 435 | ///
|
| 436 | /// final String id;
|
| 437 | /// final String title;
|
| 438 | /// final String artist;
|
| 439 | ///
|
| 440 | /// static Song fromJson(Map<String, Object?> json) {
|
| 441 | /// return Song(json['id']! as String, json['title']! as String, json['artist']! as String);
|
| 442 | /// }
|
| 443 | /// }
|
| 444 | /// ```
|
| 445 | /// {@end-tool}
|
| 446 | ///
|
| 447 | /// {@tool snippet}
|
| 448 | ///
|
| 449 | /// Java (for Android):
|
| 450 | ///
|
| 451 | /// ```java
|
| 452 | /// // Assumes existence of an Android MusicApi.
|
| 453 | /// public class MusicPlugin implements MethodCallHandler {
|
| 454 | /// @Override
|
| 455 | /// public void onMethodCall(MethodCall call, Result result) {
|
| 456 | /// switch (call.method) {
|
| 457 | /// case "isLicensed":
|
| 458 | /// result.success(MusicApi.checkLicense());
|
| 459 | /// break;
|
| 460 | /// case "getSongs":
|
| 461 | /// final List<MusicApi.Track> tracks = MusicApi.getTracks();
|
| 462 | /// final List<Object> json = ArrayList<>(tracks.size());
|
| 463 | /// for (MusicApi.Track track : tracks) {
|
| 464 | /// json.add(track.toJson()); // Map entries
|
| 465 | /// }
|
| 466 | /// result.success(json);
|
| 467 | /// break;
|
| 468 | /// case "play":
|
| 469 | /// final String song = call.argument("song");
|
| 470 | /// final double volume = call.argument("volume");
|
| 471 | /// try {
|
| 472 | /// MusicApi.playSongAtVolume(song, volume);
|
| 473 | /// result.success(null);
|
| 474 | /// } catch (MusicalException e) {
|
| 475 | /// result.error("playError", e.getMessage(), null);
|
| 476 | /// }
|
| 477 | /// break;
|
| 478 | /// default:
|
| 479 | /// result.notImplemented();
|
| 480 | /// }
|
| 481 | /// }
|
| 482 | /// // Other methods elided.
|
| 483 | /// }
|
| 484 | /// ```
|
| 485 | /// {@end-tool}
|
| 486 | ///
|
| 487 | /// {@tool snippet}
|
| 488 | ///
|
| 489 | /// Objective-C (for iOS):
|
| 490 | ///
|
| 491 | /// ```objectivec
|
| 492 | /// @interface MusicPlugin : NSObject<FlutterPlugin>
|
| 493 | /// @end
|
| 494 | ///
|
| 495 | /// // Assumes existence of an iOS Broadway Play Api.
|
| 496 | /// @implementation MusicPlugin
|
| 497 | /// - (void)handleMethodCall:(FlutterMethodCall*)call result:(FlutterResult)result {
|
| 498 | /// if ([@"isLicensed" isEqualToString:call.method]) {
|
| 499 | /// result([NSNumber numberWithBool:[BWPlayApi isLicensed]]);
|
| 500 | /// } else if ([@"getSongs" isEqualToString:call.method]) {
|
| 501 | /// NSArray* items = [BWPlayApi items];
|
| 502 | /// NSMutableArray* json = [NSMutableArray arrayWithCapacity:items.count];
|
| 503 | /// for (final BWPlayItem* item in items) {
|
| 504 | /// [json addObject:@{ @"id":item.itemId, @"title":item.name, @"artist":item.artist }];
|
| 505 | /// }
|
| 506 | /// result(json);
|
| 507 | /// } else if ([@"play" isEqualToString:call.method]) {
|
| 508 | /// NSString* itemId = call.arguments[@"song"];
|
| 509 | /// NSNumber* volume = call.arguments[@"volume"];
|
| 510 | /// NSError* error = nil;
|
| 511 | /// BOOL success = [BWPlayApi playItem:itemId volume:volume.doubleValue error:&error];
|
| 512 | /// if (success) {
|
| 513 | /// result(nil);
|
| 514 | /// } else {
|
| 515 | /// result([FlutterError errorWithCode:[NSString stringWithFormat:@"Error %ld", error.code]
|
| 516 | /// message:error.domain
|
| 517 | /// details:error.localizedDescription]);
|
| 518 | /// }
|
| 519 | /// } else {
|
| 520 | /// result(FlutterMethodNotImplemented);
|
| 521 | /// }
|
| 522 | /// }
|
| 523 | /// // Other methods elided.
|
| 524 | /// @end
|
| 525 | /// ```
|
| 526 | /// {@end-tool}
|
| 527 | ///
|
| 528 | /// See also:
|
| 529 | ///
|
| 530 | /// * [invokeListMethod], for automatically returning typed lists.
|
| 531 | /// * [invokeMapMethod], for automatically returning typed maps.
|
| 532 | /// * [StandardMessageCodec] which defines the payload values supported by
|
| 533 | /// [StandardMethodCodec].
|
| 534 | /// * [JSONMessageCodec] which defines the payload values supported by
|
| 535 | /// [JSONMethodCodec].
|
| 536 | /// * <https://api.flutter.dev/javadoc/io/flutter/plugin/common/MethodCall.html>
|
| 537 | /// for how to access method call arguments on Android.
|
| 538 | @optionalTypeArgs
|
| 539 | Future<T?> invokeMethod<T>(String method, [dynamic arguments]) {
|
| 540 | return _invokeMethod<T>(method, missingOk: false, arguments: arguments);
|
| 541 | }
|
| 542 |
|
| 543 | /// An implementation of [invokeMethod] that can return typed lists.
|
| 544 | ///
|
| 545 | /// Dart generics are reified, meaning that an untyped `List<dynamic>` cannot
|
| 546 | /// masquerade as a `List<T>`. Since [invokeMethod] can only return dynamic
|
| 547 | /// lists, we instead create a new typed list using [List.cast].
|
| 548 | ///
|
| 549 | /// See also:
|
| 550 | ///
|
| 551 | /// * [invokeMethod], which this call delegates to.
|
| 552 | Future<List<T>?> invokeListMethod<T>(String method, [dynamic arguments]) async {
|
| 553 | final List<dynamic>? result = await invokeMethod<List<dynamic>>(method, arguments);
|
| 554 | return result?.cast<T>();
|
| 555 | }
|
| 556 |
|
| 557 | /// An implementation of [invokeMethod] that can return typed maps.
|
| 558 | ///
|
| 559 | /// Dart generics are reified, meaning that an untyped `Map<dynamic, dynamic>`
|
| 560 | /// cannot masquerade as a `Map<K, V>`. Since [invokeMethod] can only return
|
| 561 | /// dynamic maps, we instead create a new typed map using [Map.cast].
|
| 562 | ///
|
| 563 | /// See also:
|
| 564 | ///
|
| 565 | /// * [invokeMethod], which this call delegates to.
|
| 566 | Future<Map<K, V>?> invokeMapMethod<K, V>(String method, [dynamic arguments]) async {
|
| 567 | final Map<dynamic, dynamic>? result = await invokeMethod<Map<dynamic, dynamic>>(
|
| 568 | method,
|
| 569 | arguments,
|
| 570 | );
|
| 571 | return result?.cast<K, V>();
|
| 572 | }
|
| 573 |
|
| 574 | /// Sets a callback for receiving method calls on this channel.
|
| 575 | ///
|
| 576 | /// The given callback will replace the currently registered callback for this
|
| 577 | /// channel, if any. To remove the handler, pass null as the
|
| 578 | /// `handler` argument.
|
| 579 | ///
|
| 580 | /// If the future returned by the handler completes with a result, that value
|
| 581 | /// is sent back to the platform plugin caller wrapped in a success envelope
|
| 582 | /// as defined by the [codec] of this channel. If the future completes with
|
| 583 | /// a [PlatformException], the fields of that exception will be used to
|
| 584 | /// populate an error envelope which is sent back instead. If the future
|
| 585 | /// completes with a [MissingPluginException], an empty reply is sent
|
| 586 | /// similarly to what happens if no method call handler has been set.
|
| 587 | /// Any other exception results in an error envelope being sent.
|
| 588 | void setMethodCallHandler(Future<dynamic> Function(MethodCall call)? handler) {
|
| 589 | assert(
|
| 590 | _binaryMessenger != null || BindingBase.debugBindingType() != null,
|
| 591 | 'Cannot set the method call handler before the binary messenger has been initialized. '
|
| 592 | 'This happens when you call setMethodCallHandler() before the WidgetsFlutterBinding '
|
| 593 | 'has been initialized. You can fix this by either calling WidgetsFlutterBinding.ensureInitialized() '
|
| 594 | 'before this or by passing a custom BinaryMessenger instance to MethodChannel().' ,
|
| 595 | );
|
| 596 | binaryMessenger.setMessageHandler(
|
| 597 | name,
|
| 598 | handler == null ? null : (ByteData? message) => _handleAsMethodCall(message, handler),
|
| 599 | );
|
| 600 | }
|
| 601 |
|
| 602 | Future<ByteData?> _handleAsMethodCall(
|
| 603 | ByteData? message,
|
| 604 | Future<dynamic> Function(MethodCall call) handler,
|
| 605 | ) async {
|
| 606 | final MethodCall call = codec.decodeMethodCall(message);
|
| 607 | try {
|
| 608 | return codec.encodeSuccessEnvelope(await handler(call));
|
| 609 | } on PlatformException catch (e) {
|
| 610 | return codec.encodeErrorEnvelope(code: e.code, message: e.message, details: e.details);
|
| 611 | } on MissingPluginException {
|
| 612 | return null;
|
| 613 | } catch (error) {
|
| 614 | return codec.encodeErrorEnvelope(code: 'error' , message: error.toString());
|
| 615 | }
|
| 616 | }
|
| 617 |
|
| 618 | // Looking for setMockMethodCallHandler or checkMethodCallHandler?
|
| 619 | // See this shim package: packages/flutter_test/lib/src/deprecated.dart
|
| 620 | }
|
| 621 |
|
| 622 | /// A [MethodChannel] that ignores missing platform plugins.
|
| 623 | ///
|
| 624 | /// When [invokeMethod] fails to find the platform plugin, it returns null
|
| 625 | /// instead of throwing an exception.
|
| 626 | ///
|
| 627 | /// {@macro flutter.services.method_channel.FIFO}
|
| 628 | class OptionalMethodChannel extends MethodChannel {
|
| 629 | /// Creates a [MethodChannel] that ignores missing platform plugins.
|
| 630 | const OptionalMethodChannel(super.name, [super.codec, super.binaryMessenger]);
|
| 631 |
|
| 632 | @override
|
| 633 | Future<T?> invokeMethod<T>(String method, [dynamic arguments]) async {
|
| 634 | return super._invokeMethod<T>(method, missingOk: true, arguments: arguments);
|
| 635 | }
|
| 636 | }
|
| 637 |
|
| 638 | /// A named channel for communicating with platform plugins using event streams.
|
| 639 | ///
|
| 640 | /// Stream setup requests are encoded into binary before being sent,
|
| 641 | /// and binary events and errors received are decoded into Dart values.
|
| 642 | /// The [MethodCodec] used must be compatible with the one used by the platform
|
| 643 | /// plugin. This can be achieved by creating an [EventChannel] counterpart of
|
| 644 | /// this channel on the platform side. The Dart type of events sent and received
|
| 645 | /// is `dynamic`, but only values supported by the specified [MethodCodec] can
|
| 646 | /// be used.
|
| 647 | ///
|
| 648 | /// The logical identity of the channel is given by its name. Identically named
|
| 649 | /// channels will interfere with each other's communication.
|
| 650 | ///
|
| 651 | /// See: <https://flutter.dev/to/platform-channels/>
|
| 652 | class EventChannel {
|
| 653 | /// Creates an [EventChannel] with the specified [name].
|
| 654 | ///
|
| 655 | /// The [codec] used will be [StandardMethodCodec], unless otherwise
|
| 656 | /// specified.
|
| 657 | ///
|
| 658 | /// Neither [name] nor [codec] may be null. The default [ServicesBinding.defaultBinaryMessenger]
|
| 659 | /// instance is used if [binaryMessenger] is null.
|
| 660 | const EventChannel(
|
| 661 | this.name, [
|
| 662 | this.codec = const StandardMethodCodec(),
|
| 663 | BinaryMessenger? binaryMessenger,
|
| 664 | ]) : _binaryMessenger = binaryMessenger;
|
| 665 |
|
| 666 | /// The logical channel on which communication happens, not null.
|
| 667 | final String name;
|
| 668 |
|
| 669 | /// The message codec used by this channel, not null.
|
| 670 | final MethodCodec codec;
|
| 671 |
|
| 672 | /// The messenger which sends the bytes for this channel.
|
| 673 | ///
|
| 674 | /// On the root isolate or web, this defaults to the
|
| 675 | /// [ServicesBinding.defaultBinaryMessenger]. In other contexts the default
|
| 676 | /// value is a [BackgroundIsolateBinaryMessenger] from
|
| 677 | /// [BackgroundIsolateBinaryMessenger.ensureInitialized].
|
| 678 | BinaryMessenger get binaryMessenger => _binaryMessenger ?? _findBinaryMessenger();
|
| 679 | final BinaryMessenger? _binaryMessenger;
|
| 680 |
|
| 681 | /// Sets up a broadcast stream for receiving events on this channel.
|
| 682 | ///
|
| 683 | /// Returns a broadcast [Stream] which emits events to listeners as follows:
|
| 684 | ///
|
| 685 | /// * a decoded data event (possibly null) for each successful event
|
| 686 | /// received from the platform plugin;
|
| 687 | /// * an error event containing a [PlatformException] for each error event
|
| 688 | /// received from the platform plugin.
|
| 689 | ///
|
| 690 | /// Errors occurring during stream activation or deactivation are reported
|
| 691 | /// through the [FlutterError] facility. Stream activation happens only when
|
| 692 | /// stream listener count changes from 0 to 1. Stream deactivation happens
|
| 693 | /// only when stream listener count changes from 1 to 0.
|
| 694 | Stream<dynamic> receiveBroadcastStream([dynamic arguments]) {
|
| 695 | final MethodChannel methodChannel = MethodChannel(name, codec);
|
| 696 | late StreamController<dynamic> controller;
|
| 697 | controller = StreamController<dynamic>.broadcast(
|
| 698 | onListen: () async {
|
| 699 | binaryMessenger.setMessageHandler(name, (ByteData? reply) async {
|
| 700 | if (reply == null) {
|
| 701 | controller.close();
|
| 702 | } else {
|
| 703 | try {
|
| 704 | controller.add(codec.decodeEnvelope(reply));
|
| 705 | } on PlatformException catch (e) {
|
| 706 | controller.addError(e);
|
| 707 | }
|
| 708 | }
|
| 709 | return null;
|
| 710 | });
|
| 711 | try {
|
| 712 | await methodChannel.invokeMethod<void>('listen' , arguments);
|
| 713 | } catch (exception, stack) {
|
| 714 | FlutterError.reportError(
|
| 715 | FlutterErrorDetails(
|
| 716 | exception: exception,
|
| 717 | stack: stack,
|
| 718 | library: 'services library' ,
|
| 719 | context: ErrorDescription('while activating platform stream on channel $name' ),
|
| 720 | ),
|
| 721 | );
|
| 722 | }
|
| 723 | },
|
| 724 | onCancel: () async {
|
| 725 | binaryMessenger.setMessageHandler(name, null);
|
| 726 | try {
|
| 727 | await methodChannel.invokeMethod<void>('cancel' , arguments);
|
| 728 | } catch (exception, stack) {
|
| 729 | FlutterError.reportError(
|
| 730 | FlutterErrorDetails(
|
| 731 | exception: exception,
|
| 732 | stack: stack,
|
| 733 | library: 'services library' ,
|
| 734 | context: ErrorDescription('while de-activating platform stream on channel $name' ),
|
| 735 | ),
|
| 736 | );
|
| 737 | }
|
| 738 | },
|
| 739 | );
|
| 740 | return controller.stream;
|
| 741 | }
|
| 742 | }
|
| 743 |
|