// Copyright 2013 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. // KEEP THIS SYNCHRONIZED WITH ../web_ui/lib/channel_buffers.dart part of dart.ui; /// Deprecated. Migrate to [ChannelCallback] instead. /// /// Signature for [ChannelBuffers.drain]'s `callback` argument. /// /// The first argument is the data sent by the plugin. /// /// The second argument is a closure that, when called, will send messages /// back to the plugin. @Deprecated( 'Migrate to ChannelCallback instead. ' 'This feature was deprecated after v3.11.0-20.0.pre.', ) typedef DrainChannelCallback = Future Function(ByteData? data, PlatformMessageResponseCallback callback); /// Signature for [ChannelBuffers.setListener]'s `callback` argument. /// /// The first argument is the data sent by the plugin. /// /// The second argument is a closure that, when called, will send messages /// back to the plugin. /// /// See also: /// /// * [PlatformMessageResponseCallback], the type used for replies. typedef ChannelCallback = void Function(ByteData? data, PlatformMessageResponseCallback callback); /// The data and logic required to store and invoke a callback. /// /// This tracks (and applies) the [Zone]. class _ChannelCallbackRecord { _ChannelCallbackRecord(this._callback) : _zone = Zone.current; final ChannelCallback _callback; final Zone _zone; /// Call [callback] in [zone], using the given arguments. void invoke(ByteData? dataArg, PlatformMessageResponseCallback callbackArg) { _invoke2(_callback, _zone, dataArg, callbackArg); } } /// A saved platform message for a channel with its callback. class _StoredMessage { /// Wraps the data and callback for a platform message into /// a [_StoredMessage] instance. /// /// The first argument is a [ByteData] that represents the /// payload of the message and a [PlatformMessageResponseCallback] /// that represents the callback that will be called when the message /// is handled. _StoredMessage(this.data, this._callback) : _zone = Zone.current; /// Representation of the message's payload. final ByteData? data; /// Callback to be used when replying to the message. final PlatformMessageResponseCallback _callback; final Zone _zone; void invoke(ByteData? dataArg) { _invoke1(_callback, _zone, dataArg); } } /// The internal storage for a platform channel. /// /// This consists of a fixed-size circular queue of [_StoredMessage]s, /// and the channel's callback, if any has been registered. class _Channel { _Channel([this._capacity = ChannelBuffers.kDefaultBufferSize]) : _queue = collection.ListQueue<_StoredMessage>(_capacity); /// The underlying data for the buffered messages. final collection.ListQueue<_StoredMessage> _queue; /// The number of messages currently in the [_Channel]. /// /// This is equal to or less than the [capacity]. int get length => _queue.length; /// Whether to dump messages to the console when a message is /// discarded due to the channel overflowing. /// /// Has no effect in release builds. bool debugEnableDiscardWarnings = true; /// The number of messages that _can_ be stored in the [_Channel]. /// /// When additional messages are stored, earlier ones are discarded, /// in a first-in-first-out fashion. int get capacity => _capacity; int _capacity; /// Set the [capacity] of the channel to the given size. /// /// If the new size is smaller than the [length], the oldest /// messages are discarded until the capacity is reached. No /// message is shown in case of overflow, regardless of the /// value of [debugEnableDiscardWarnings]. set capacity(int newSize) { _capacity = newSize; _dropOverflowMessages(newSize); } /// Whether a microtask is queued to call [_drainStep]. /// /// This is used to queue messages received while draining, rather /// than sending them out of order. This generally cannot happen in /// production but is possible in test scenarios. /// /// This is also necessary to avoid situations where multiple drains are /// invoked simultaneously. For example, if a listener is set /// (queuing a drain), then unset, then set again (which would queue /// a drain again), all in one stack frame (not allowing the drain /// itself an opportunity to check if a listener is set). bool _draining = false; /// Adds a message to the channel. /// /// If the channel overflows, earlier messages are discarded, in a /// first-in-first-out fashion. See [capacity]. If /// [debugEnableDiscardWarnings] is true, this method returns true /// on overflow. It is the responsibility of the caller to show the /// warning message. bool push(_StoredMessage message) { if (!_draining && _channelCallbackRecord != null) { assert(_queue.isEmpty); _channelCallbackRecord!.invoke(message.data, message.invoke); return false; } if (_capacity <= 0) { return debugEnableDiscardWarnings; } final bool result = _dropOverflowMessages(_capacity - 1); _queue.addLast(message); return result; } /// Returns the first message in the channel and removes it. /// /// Throws when empty. _StoredMessage pop() => _queue.removeFirst(); /// Removes messages until [length] reaches `lengthLimit`. /// /// The callback of each removed message is invoked with null /// as its argument. /// /// If any messages are removed, and [debugEnableDiscardWarnings] is /// true, then returns true. The caller is responsible for showing /// the warning message in that case. bool _dropOverflowMessages(int lengthLimit) { var result = false; while (_queue.length > lengthLimit) { final _StoredMessage message = _queue.removeFirst(); message.invoke(null); // send empty reply to the plugin side result = true; } return result; } _ChannelCallbackRecord? _channelCallbackRecord; /// Sets the listener for this channel. /// /// When there is a listener, messages are sent immediately. /// /// If any messages were queued before the listener is added, /// they are drained asynchronously after this method returns. /// (See [_drain].) /// /// Only one listener may be set at a time. Setting a /// new listener clears the previous one. /// /// Callbacks are invoked in their own stack frame and /// use the zone that was current when the callback was /// registered. void setListener(ChannelCallback callback) { final needDrain = _channelCallbackRecord == null; _channelCallbackRecord = _ChannelCallbackRecord(callback); if (needDrain && !_draining) { _drain(); } } /// Clears the listener for this channel. /// /// When there is no listener, messages are queued, up to [capacity], /// and then discarded in a first-in-first-out fashion. void clearListener() { _channelCallbackRecord = null; } /// Drains all the messages in the channel (invoking the currently /// registered listener for each one). /// /// Each message is handled in its own microtask. No messages can /// be queued by plugins while the queue is being drained, but any /// microtasks queued by the handler itself will be processed before /// the next message is handled. /// /// The draining stops if the listener is removed. /// /// See also: /// /// * [setListener], which is used to register the callback. /// * [clearListener], which removes it. void _drain() { assert(!_draining); _draining = true; scheduleMicrotask(_drainStep); } /// Drains a single message and then reinvokes itself asynchronously. /// /// See [_drain] for more details. void _drainStep() { assert(_draining); if (_queue.isNotEmpty && _channelCallbackRecord != null) { final _StoredMessage message = pop(); _channelCallbackRecord!.invoke(message.data, message.invoke); scheduleMicrotask(_drainStep); } else { _draining = false; } } } /// The buffering and dispatch mechanism for messages sent by plugins /// on the engine side to their corresponding plugin code on the /// framework side. /// /// Messages for a channel are stored until a listener is provided for that channel, /// using [setListener]. Only one listener may be configured per channel. /// /// Typically these buffers are drained once a callback is set up on /// the [BinaryMessenger] in the Flutter framework. (See [setListener].) /// /// ## Channel names /// /// By convention, channels are normally named with a reverse-DNS prefix, a /// slash, and then a domain-specific name. For example, `com.example/demo`. /// /// Channel names cannot contain the U+0000 NULL character, because they /// are passed through APIs that use null-terminated strings. /// /// ## Buffer capacity and overflow /// /// Each channel has a finite buffer capacity and messages will /// be deleted in a first-in-first-out (FIFO) manner if the capacity is exceeded. /// /// By default buffers store one message per channel, and when a /// message overflows, in debug mode, a message is printed to the /// console. The message looks like the following: /// /// > A message on the com.example channel was discarded before it could be /// > handled. /// > This happens when a plugin sends messages to the framework side before the /// > framework has had an opportunity to register a listener. See the /// > ChannelBuffers API documentation for details on how to configure the channel /// > to expect more messages, or to expect messages to get discarded: /// > https://api.flutter.dev/flutter/dart-ui/ChannelBuffers-class.html /// /// There are tradeoffs associated with any size. The correct size /// should be chosen for the semantics of the channel. To change the /// size a plugin can send a message using the control channel, /// as described below. /// /// Size 0 is appropriate for channels where messages sent before /// the engine and framework are ready should be ignored. For /// example, a plugin that notifies the framework any time a /// radiation sensor detects an ionization event might set its size /// to zero since past ionization events are typically not /// interesting, only instantaneous readings are worth tracking. /// /// Size 1 is appropriate for level-triggered plugins. For example, /// a plugin that notifies the framework of the current value of a /// pressure sensor might leave its size at one (the default), while /// sending messages continually; once the framework side of the plugin /// registers with the channel, it will immediately receive the most /// up to date value and earlier messages will have been discarded. /// /// Sizes greater than one are appropriate for plugins where every /// message is important. For example, a plugin that itself /// registers with another system that has been buffering events, /// and immediately forwards all the previously-buffered events, /// would likely wish to avoid having any messages dropped on the /// floor. In such situations, it is important to select a size that /// will avoid overflows. It is also important to consider the /// potential for the framework side to never fully initialize (e.g. if /// the user starts the application, but terminates it soon /// afterwards, leaving time for the platform side of a plugin to /// run but not the framework side). /// /// ## The control channel /// /// A plugin can configure its channel's buffers by sending messages to the /// control channel, `dev.flutter/channel-buffers` (see [kControlChannelName]). /// /// There are two messages that can be sent to this control channel, to adjust /// the buffer size and to disable the overflow warnings. See [handleMessage] /// for details on these messages. class ChannelBuffers { /// Create a buffer pool for platform messages. /// /// It is generally not necessary to create an instance of this class; /// the global [channelBuffers] instance is the one used by the engine. ChannelBuffers(); /// The number of messages that channel buffers will store by default. static const int kDefaultBufferSize = 1; /// The name of the channel that plugins can use to communicate with the /// channel buffers system. /// /// These messages are handled by [handleMessage]. static const String kControlChannelName = 'dev.flutter/channel-buffers'; /// A mapping between a channel name and its associated [_Channel]. final Map _channels = {}; /// Adds a message (`data`) to the named channel buffer (`name`). /// /// The `callback` argument is a closure that, when called, will send messages /// back to the plugin. /// /// If a message overflows the channel, and the channel has not been /// configured to expect overflow, then, in debug mode, a message /// will be printed to the console warning about the overflow. /// /// Channel names cannot contain the U+0000 NULL character, because they /// are passed through APIs that use null-terminated strings. void push(String name, ByteData? data, PlatformMessageResponseCallback callback) { assert(!name.contains('\u0000'), 'Channel names must not contain U+0000 NULL characters.'); final _Channel channel = _channels.putIfAbsent(name, () => _Channel()); if (channel.push(_StoredMessage(data, callback))) { _printDebug( 'A message on the $name channel was discarded before it could be handled.\n' 'This happens when a plugin sends messages to the framework side before the ' 'framework has had an opportunity to register a listener. See the ChannelBuffers ' 'API documentation for details on how to configure the channel to expect more ' 'messages, or to expect messages to get discarded:\n' ' https://api.flutter.dev/flutter/dart-ui/ChannelBuffers-class.html\n' 'The capacity of the $name channel is ${channel._capacity} message${channel._capacity != 1 ? 's' : ''}.', ); } } /// Sets the listener for the specified channel. /// /// When there is a listener, messages are sent immediately. /// /// Each channel may have up to one listener set at a time. Setting /// a new listener on a channel with an existing listener clears the /// previous one. /// /// Callbacks are invoked in their own stack frame and /// use the zone that was current when the callback was /// registered. /// /// ## Draining /// /// If any messages were queued before the listener is added, /// they are drained asynchronously after this method returns. /// /// Each message is handled in its own microtask. No messages can /// be queued by plugins while the queue is being drained, but any /// microtasks queued by the handler itself will be processed before /// the next message is handled. /// /// The draining stops if the listener is removed. void setListener(String name, ChannelCallback callback) { assert(!name.contains('\u0000'), 'Channel names must not contain U+0000 NULL characters.'); final _Channel channel = _channels.putIfAbsent(name, () => _Channel()); channel.setListener(callback); sendChannelUpdate(name, listening: true); } /// Clears the listener for the specified channel. /// /// When there is no listener, messages on that channel are queued, /// up to [kDefaultBufferSize] (or the size configured via the /// control channel), and then discarded in a first-in-first-out /// fashion. void clearListener(String name) { final _Channel? channel = _channels[name]; if (channel != null) { channel.clearListener(); sendChannelUpdate(name, listening: false); } } @Native(symbol: 'PlatformConfigurationNativeApi::SendChannelUpdate') external static void _sendChannelUpdate(String name, bool listening); // TODO(matanlurey): have original authors document; see https://github.com/flutter/flutter/issues/151917. // ignore: public_member_api_docs void sendChannelUpdate(String name, {required bool listening}) => _sendChannelUpdate(name, listening); /// Deprecated. Migrate to [setListener] instead. /// /// Remove and process all stored messages for a given channel. /// /// This should be called once a channel is prepared to handle messages /// (i.e. when a message handler is set up in the framework). /// /// The messages are processed by calling the given `callback`. Each message /// is processed in its own microtask. @Deprecated( 'Migrate to setListener instead. ' 'This feature was deprecated after v3.11.0-20.0.pre.', ) Future drain(String name, DrainChannelCallback callback) async { final _Channel? channel = _channels[name]; while (channel != null && !channel._queue.isEmpty) { final _StoredMessage message = channel.pop(); await callback(message.data, message.invoke); } } /// Handle a control message. /// /// This is intended to be called by the platform messages dispatcher, forwarding /// messages from plugins to the [kControlChannelName] channel. /// /// Messages use the [StandardMethodCodec] format. There are two methods /// supported: `resize` and `overflow`. The `resize` method changes the size /// of the buffer, and the `overflow` method controls whether overflow is /// expected or not. /// /// ## `resize` /// /// The `resize` method takes as its argument a list with two values, first /// the channel name (a UTF-8 string less than 254 bytes long and not /// containing any null bytes), and second the allowed size of the channel /// buffer (an integer between 0 and 2147483647). /// /// Upon receiving the message, the channel's buffer is resized. If necessary, /// messages are silently discarded to ensure the buffer is no bigger than /// specified. /// /// For historical reasons, this message can also be sent using a bespoke /// format consisting of a UTF-8-encoded string with three parts separated /// from each other by U+000D CARRIAGE RETURN (CR) characters, the three parts /// being the string `resize`, the string giving the channel name, and then /// the string giving the decimal serialization of the new channel buffer /// size. For example: `resize\rchannel\r1` /// /// ## `overflow` /// /// The `overflow` method takes as its argument a list with two values, first /// the channel name (a UTF-8 string less than 254 bytes long and not /// containing any null bytes), and second a boolean which is true if overflow /// is expected and false if it is not. /// /// This sets a flag on the channel in debug mode. In release mode the message /// is silently ignored. The flag indicates whether overflow is expected on this /// channel. When the flag is set, messages are discarded silently. When the /// flag is cleared (the default), any overflow on the channel causes a message /// to be printed to the console, warning that a message was lost. void handleMessage(ByteData data) { // We hard-code the deserialization here because the StandardMethodCodec class // is part of the framework, not dart:ui. final Uint8List bytes = data.buffer.asUint8List(data.offsetInBytes, data.lengthInBytes); if (bytes[0] == 0x07) { // 7 = value code for string final int methodNameLength = bytes[1]; if (methodNameLength >= 254) { // lengths greater than 253 have more elaborate encoding throw Exception('Unrecognized message sent to $kControlChannelName (method name too long)'); } var index = 2; // where we are in reading the bytes final String methodName = utf8.decode(bytes.sublist(index, index + methodNameLength)); index += methodNameLength; switch (methodName) { case 'resize': if (bytes[index] != 0x0C) { // 12 = value code for list throw Exception( "Invalid arguments for 'resize' method sent to $kControlChannelName (arguments must be a two-element list, channel name and new capacity)", ); } index += 1; if (bytes[index] < 0x02) { // We ignore extra arguments, in case we need to support them in the future, hence <2 rather than !=2. throw Exception( "Invalid arguments for 'resize' method sent to $kControlChannelName (arguments must be a two-element list, channel name and new capacity)", ); } index += 1; if (bytes[index] != 0x07) { // 7 = value code for string throw Exception( "Invalid arguments for 'resize' method sent to $kControlChannelName (first argument must be a string)", ); } index += 1; final int channelNameLength = bytes[index]; if (channelNameLength >= 254) { // lengths greater than 253 have more elaborate encoding throw Exception( "Invalid arguments for 'resize' method sent to $kControlChannelName (channel name must be less than 254 characters long)", ); } index += 1; final String channelName = utf8.decode(bytes.sublist(index, index + channelNameLength)); if (channelName.contains('\u0000')) { throw Exception( "Invalid arguments for 'resize' method sent to $kControlChannelName (channel name must not contain any null bytes)", ); } index += channelNameLength; if (bytes[index] != 0x03) { // 3 = value code for uint32 throw Exception( "Invalid arguments for 'resize' method sent to $kControlChannelName (second argument must be an integer in the range 0 to 2147483647)", ); } index += 1; resize(channelName, data.getUint32(index, Endian.host)); case 'overflow': if (bytes[index] != 0x0C) { // 12 = value code for list throw Exception( "Invalid arguments for 'overflow' method sent to $kControlChannelName (arguments must be a two-element list, channel name and flag state)", ); } index += 1; if (bytes[index] < 0x02) { // We ignore extra arguments, in case we need to support them in the future, hence <2 rather than !=2. throw Exception( "Invalid arguments for 'overflow' method sent to $kControlChannelName (arguments must be a two-element list, channel name and flag state)", ); } index += 1; if (bytes[index] != 0x07) { // 7 = value code for string throw Exception( "Invalid arguments for 'overflow' method sent to $kControlChannelName (first argument must be a string)", ); } index += 1; final int channelNameLength = bytes[index]; if (channelNameLength >= 254) { // lengths greater than 253 have more elaborate encoding throw Exception( "Invalid arguments for 'overflow' method sent to $kControlChannelName (channel name must be less than 254 characters long)", ); } index += 1; final String channelName = utf8.decode(bytes.sublist(index, index + channelNameLength)); index += channelNameLength; if (bytes[index] != 0x01 && bytes[index] != 0x02) { // 1 = value code for true, 2 = value code for false throw Exception( "Invalid arguments for 'overflow' method sent to $kControlChannelName (second argument must be a boolean)", ); } allowOverflow(channelName, bytes[index] == 0x01); default: throw Exception("Unrecognized method '$methodName' sent to $kControlChannelName"); } } else { final List parts = utf8.decode(bytes).split('\r'); if (parts.length == 1 + /*arity=*/ 2 && parts[0] == 'resize') { resize(parts[1], int.parse(parts[2])); } else { // If the message couldn't be decoded as UTF-8, a FormatException will // have been thrown by utf8.decode() above. throw Exception('Unrecognized message $parts sent to $kControlChannelName.'); } } } /// Changes the capacity of the queue associated with the given channel. /// /// This could result in the dropping of messages if newSize is less /// than the current length of the queue. /// /// This is expected to be called by platform-specific plugin code (indirectly /// via the control channel), not by code on the framework side. See /// [handleMessage]. /// /// Calling this from framework code is redundant since by the time framework /// code can be running, it can just subscribe to the relevant channel and /// there is therefore no need for any buffering. void resize(String name, int newSize) { _Channel? channel = _channels[name]; if (channel == null) { assert(!name.contains('\u0000'), 'Channel names must not contain U+0000 NULL characters.'); channel = _Channel(newSize); _channels[name] = channel; } else { channel.capacity = newSize; } } /// Toggles whether the channel should show warning messages when discarding /// messages due to overflow. /// /// This is expected to be called by platform-specific plugin code (indirectly /// via the control channel), not by code on the framework side. See /// [handleMessage]. /// /// Calling this from framework code is redundant since by the time framework /// code can be running, it can just subscribe to the relevant channel and /// there is therefore no need for any messages to overflow. /// /// This method has no effect in release builds. void allowOverflow(String name, bool allowed) { assert(() { _Channel? channel = _channels[name]; if (channel == null && allowed) { assert(!name.contains('\u0000'), 'Channel names must not contain U+0000 NULL characters.'); channel = _Channel(); _channels[name] = channel; } channel?.debugEnableDiscardWarnings = !allowed; return true; }()); } } /// [ChannelBuffers] that allow the storage of messages between the /// Engine and the Framework. Typically messages that can't be delivered /// are stored here until the Framework is able to process them. /// /// See also: /// /// * [BinaryMessenger], where [ChannelBuffers] are typically read. final ChannelBuffers channelBuffers = ChannelBuffers();