blob: 9b15db89cf4483b3b63122ff3e5cfa7d0496ce77 [file] [log] [blame]
// 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/src/ui/channel_buffers.dart
// @dart = 2.12
part of dart.ui;
/// 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.
// TODO(ianh): deprecate this once the framework is migrated to [ChannelCallback].
typedef DrainChannelCallback = Future<void> 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<ByteData?, PlatformMessageResponseCallback>(_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) {
bool 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 bool 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].)
///
/// ## 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 channels 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<String, _Channel> _channels = <String, _Channel>{};
/// 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.
void push(String name, ByteData? data, PlatformMessageResponseCallback callback) {
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'
);
}
}
/// 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) {
final _Channel channel = _channels.putIfAbsent(name, () => _Channel());
channel.setListener(callback);
}
/// 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();
}
/// 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.
// TODO(ianh): deprecate once framework uses [setListener].
Future<void> 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 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 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)');
int 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));
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));
break;
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);
break;
default:
throw Exception('Unrecognized method \'$methodName\' sent to $kControlChannelName');
}
} else {
final List<String> 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) {
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) {
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();