binding.dart 23.9 KB
Newer Older
Ian Hickson's avatar
Ian Hickson committed
1
// Copyright 2014 The Flutter Authors. All rights reserved.
2 3 4
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

5
import 'dart:convert' show json;
6
import 'dart:developer' as developer;
7
import 'dart:io' show exit;
8
import 'dart:ui' as ui show SingletonFlutterWindow, Brightness, PlatformDispatcher, window;
9
// Before adding any more dart:ui imports, please read the README.
10

11
import 'package:meta/meta.dart';
12 13 14

import 'assertions.dart';
import 'basic_types.dart';
15
import 'constants.dart';
16
import 'debug.dart';
17
import 'object.dart';
18
import 'platform.dart';
19
import 'print.dart';
20 21 22 23 24 25 26 27

/// Signature for service extensions.
///
/// The returned map must not contain the keys "type" or "method", as
/// they will be replaced before the value is sent to the client. The
/// "type" key will be set to the string `_extensionType` to indicate
/// that this is a return value from a service extension, and the
/// "method" key will be set to the full name of the method.
28
typedef ServiceExtensionCallback = Future<Map<String, dynamic>> Function(Map<String, String> parameters);
29

30 31 32
/// Base class for mixins that provide singleton services (also known as
/// "bindings").
///
33
/// To use this class in an `on` clause of a mixin, inherit from it and implement
34 35 36
/// [initInstances()]. The mixin is guaranteed to only be constructed once in
/// the lifetime of the app (more precisely, it will assert if constructed twice
/// in checked mode).
37
///
38 39 40 41 42 43 44
/// The top-most layer used to write the application will have a concrete class
/// that inherits from [BindingBase] and uses all the various [BindingBase]
/// mixins (such as [ServicesBinding]). For example, the Widgets library in
/// Flutter introduces a binding called [WidgetsFlutterBinding]. The relevant
/// library defines how to create the binding. It could be implied (for example,
/// [WidgetsFlutterBinding] is automatically started from [runApp]), or the
/// application might be required to explicitly call the constructor.
45
abstract class BindingBase {
46 47 48 49 50 51
  /// Default abstract constructor for bindings.
  ///
  /// First calls [initInstances] to have bindings initialize their
  /// instance pointers and other state, then calls
  /// [initServiceExtensions] to have bindings initialize their
  /// observatory service extensions, if any.
52
  BindingBase() {
53 54
    developer.Timeline.startSync('Framework initialization');

55 56 57
    assert(!_debugInitialized);
    initInstances();
    assert(_debugInitialized);
58 59 60 61

    assert(!_debugServiceExtensionsRegistered);
    initServiceExtensions();
    assert(_debugServiceExtensionsRegistered);
62

63
    developer.postEvent('Flutter.FrameworkInitialization', <String, String>{});
64

65
    developer.Timeline.finishSync();
66 67 68
  }

  static bool _debugInitialized = false;
69
  static bool _debugServiceExtensionsRegistered = false;
70

71
  /// The main window to which this binding is bound.
72
  ///
73 74 75 76
  /// A number of additional bindings are defined as extensions of
  /// [BindingBase], e.g., [ServicesBinding], [RendererBinding], and
  /// [WidgetsBinding]. Each of these bindings define behaviors that interact
  /// with a [ui.SingletonFlutterWindow].
77
  ///
78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118
  /// Each of these other bindings could individually access a
  /// [ui.SingletonFlutterWindow] statically, but that would preclude the
  /// ability to test its behaviors with a fake window for verification
  /// purposes.  Therefore, [BindingBase] exposes this
  /// [ui.SingletonFlutterWindow] for use by other bindings.  A subclass of
  /// [BindingBase], such as [TestWidgetsFlutterBinding], can override this
  /// accessor to return a different [ui.SingletonFlutterWindow] implementation,
  /// such as a [TestWindow].
  ///
  /// The `window` is a singleton meant for use by applications that only have a
  /// single main window. In addition to the properties of [ui.FlutterWindow],
  /// `window` provides access to platform-specific properties and callbacks
  /// available on the [platformDispatcher].
  ///
  /// For applications designed for more than one main window, prefer using the
  /// [platformDispatcher] to access available views via
  /// [ui.PlatformDispatcher.views].
  ///
  /// However, multiple window support is not yet implemented, so currently this
  /// provides access to the one and only window.
  // TODO(gspencergoog): remove the preceding note once multi-window support is
  // active.
  ui.SingletonFlutterWindow get window => ui.window;

  /// The [ui.PlatformDispatcher] to which this binding is bound.
  ///
  /// A number of additional bindings are defined as extensions of
  /// [BindingBase], e.g., [ServicesBinding], [RendererBinding], and
  /// [WidgetsBinding]. Each of these bindings define behaviors that interact
  /// with a [ui.PlatformDispatcher], e.g., [ServicesBinding] registers a
  /// [ui.PlatformDispatcher.onPlatformMessage] handler, and [RendererBinding]
  /// registers [ui.PlatformDispatcher.onMetricsChanged],
  /// [ui.PlatformDispatcher.onTextScaleFactorChanged],
  /// [ui.PlatformDispatcher.onSemanticsEnabledChanged], and
  /// [ui.PlatformDispatcher.onSemanticsAction] handlers.
  ///
  /// Each of these other bindings could individually access a
  /// [ui.PlatformDispatcher] statically, but that would preclude the ability to
  /// test these behaviors with a fake platform dispatcher for verification
  /// purposes. Therefore, [BindingBase] exposes this [ui.PlatformDispatcher]
  /// for use by other bindings. A subclass of [BindingBase], such as
119
  /// [TestWidgetsFlutterBinding], can override this accessor to return a
120 121
  /// different [ui.PlatformDispatcher] implementation.
  ui.PlatformDispatcher get platformDispatcher => ui.PlatformDispatcher.instance;
122

123 124 125 126 127 128 129 130
  /// The initialization method. Subclasses override this method to hook into
  /// the platform and otherwise configure their services. Subclasses must call
  /// "super.initInstances()".
  ///
  /// By convention, if the service is to be provided as a singleton, it should
  /// be exposed as `MixinClassName.instance`, a static getter that returns
  /// `MixinClassName._instance`, a static field that is set by
  /// `initInstances()`.
131 132
  @protected
  @mustCallSuper
133
  void initInstances() {
134
    assert(!_debugInitialized);
135 136 137 138
    assert(() {
      _debugInitialized = true;
      return true;
    }());
139 140
  }

141 142 143 144 145 146 147 148 149 150 151 152 153
  /// Called when the binding is initialized, to register service
  /// extensions.
  ///
  /// Bindings that want to expose service extensions should overload
  /// this method to register them using calls to
  /// [registerSignalServiceExtension],
  /// [registerBoolServiceExtension],
  /// [registerNumericServiceExtension], and
  /// [registerServiceExtension] (in increasing order of complexity).
  ///
  /// Implementations of this method must call their superclass
  /// implementation.
  ///
154
  /// {@macro flutter.foundation.BindingBase.registerServiceExtension}
155 156 157 158
  ///
  /// See also:
  ///
  ///  * <https://github.com/dart-lang/sdk/blob/master/runtime/vm/service/service.md#rpcs-requests-and-responses>
159 160
  @protected
  @mustCallSuper
161 162
  void initServiceExtensions() {
    assert(!_debugServiceExtensionsRegistered);
163 164 165 166 167 168 169 170 171

    assert(() {
      registerSignalServiceExtension(
        name: 'reassemble',
        callback: reassembleApplication,
      );
      return true;
    }());

172
    if (!kReleaseMode && !kIsWeb) {
173 174 175 176 177 178
      registerSignalServiceExtension(
        name: 'exit',
        callback: _exitApplication,
      );
    }

179
    assert(() {
180
      const String platformOverrideExtensionName = 'platformOverride';
181
      registerServiceExtension(
182
        name: platformOverrideExtensionName,
183 184 185 186 187 188
        callback: (Map<String, String> parameters) async {
          if (parameters.containsKey('value')) {
            switch (parameters['value']) {
              case 'android':
                debugDefaultTargetPlatformOverride = TargetPlatform.android;
                break;
189 190 191
              case 'fuchsia':
                debugDefaultTargetPlatformOverride = TargetPlatform.fuchsia;
                break;
192 193 194
              case 'iOS':
                debugDefaultTargetPlatformOverride = TargetPlatform.iOS;
                break;
195 196 197
              case 'linux':
                debugDefaultTargetPlatformOverride = TargetPlatform.linux;
                break;
198 199 200
              case 'macOS':
                debugDefaultTargetPlatformOverride = TargetPlatform.macOS;
                break;
201 202
              case 'windows':
                debugDefaultTargetPlatformOverride = TargetPlatform.windows;
203 204 205 206 207
                break;
              case 'default':
              default:
                debugDefaultTargetPlatformOverride = null;
            }
208 209 210 211
            _postExtensionStateChangedEvent(
              platformOverrideExtensionName,
              defaultTargetPlatform.toString().substring('$TargetPlatform.'.length),
            );
212 213
            await reassembleApplication();
          }
214
          return <String, dynamic>{
215 216 217 218
            'value': defaultTargetPlatform
                     .toString()
                     .substring('$TargetPlatform.'.length),
          };
219
        },
220
      );
221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238

      const String brightnessOverrideExtensionName = 'brightnessOverride';
      registerServiceExtension(
        name: brightnessOverrideExtensionName,
        callback: (Map<String, String> parameters) async {
          if (parameters.containsKey('value')) {
            switch (parameters['value']) {
              case 'Brightness.light':
                debugBrightnessOverride = ui.Brightness.light;
                break;
              case 'Brightness.dark':
                debugBrightnessOverride = ui.Brightness.dark;
                break;
              default:
                debugBrightnessOverride = null;
            }
            _postExtensionStateChangedEvent(
              brightnessOverrideExtensionName,
239
              (debugBrightnessOverride ?? window.platformBrightness).toString(),
240 241 242 243
            );
            await reassembleApplication();
          }
          return <String, dynamic>{
244
            'value': (debugBrightnessOverride ?? window.platformBrightness).toString(),
245 246 247
          };
        },
      );
248

249 250 251 252 253 254 255 256
      registerStringServiceExtension(
        name: 'connectedVmServiceUri',
        getter: () async => connectedVmServiceUri ?? '',
        setter: (String uri) async {
          connectedVmServiceUri = uri;
        },
      );

257 258 259 260 261 262 263 264
      registerStringServiceExtension(
        name: 'activeDevToolsServerAddress',
        getter: () async => activeDevToolsServerAddress ?? '',
        setter: (String serverAddress) async {
          activeDevToolsServerAddress = serverAddress;
        },
      );

265
      return true;
266
    }());
267 268 269 270
    assert(() {
      _debugServiceExtensionsRegistered = true;
      return true;
    }());
271 272
  }

273 274 275 276 277 278 279 280 281 282 283 284 285 286
  /// Whether [lockEvents] is currently locking events.
  ///
  /// Binding subclasses that fire events should check this first, and if it is
  /// set, queue events instead of firing them.
  ///
  /// Events should be flushed when [unlocked] is called.
  @protected
  bool get locked => _lockCount > 0;
  int _lockCount = 0;

  /// Locks the dispatching of asynchronous events and callbacks until the
  /// callback's future completes.
  ///
  /// This causes input lag and should therefore be avoided when possible. It is
287 288 289
  /// primarily intended for use during non-user-interactive time such as to
  /// allow [reassembleApplication] to block input while it walks the tree
  /// (which it partially does asynchronously).
290 291 292
  ///
  /// The [Future] returned by the `callback` argument is returned by [lockEvents].
  @protected
293
  Future<void> lockEvents(Future<void> Function() callback) {
294 295
    developer.Timeline.startSync('Lock events');

296 297
    assert(callback != null);
    _lockCount += 1;
298
    final Future<void> future = callback();
299
    assert(future != null, 'The lockEvents() callback returned null; it should return a Future<void> that completes when the lock is to expire.');
300 301
    future.whenComplete(() {
      _lockCount -= 1;
302 303
      if (!locked) {
        developer.Timeline.finishSync();
304
        unlocked();
305
      }
306 307 308 309 310 311 312 313
    });
    return future;
  }

  /// Called by [lockEvents] when events get unlocked.
  ///
  /// This should flush any events that were queued while [locked] was true.
  @protected
314
  @mustCallSuper
315 316 317 318
  void unlocked() {
    assert(!locked);
  }

319
  /// Cause the entire application to redraw, e.g. after a hot reload.
320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336
  ///
  /// This is used by development tools when the application code has changed,
  /// to cause the application to pick up any changed code. It can be triggered
  /// manually by sending the `ext.flutter.reassemble` service extension signal.
  ///
  /// This method is very computationally expensive and should not be used in
  /// production code. There is never a valid reason to cause the entire
  /// application to repaint in production. All aspects of the Flutter framework
  /// know how to redraw when necessary. It is only necessary in development
  /// when the code is literally changed on the fly (e.g. in hot reload) or when
  /// debug flags are being toggled.
  ///
  /// While this method runs, events are locked (e.g. pointer events are not
  /// dispatched).
  ///
  /// Subclasses (binding classes) should override [performReassemble] to react
  /// to this method being called. This method itself should not be overridden.
337
  Future<void> reassembleApplication() {
338 339 340 341
    return lockEvents(performReassemble);
  }

  /// This method is called by [reassembleApplication] to actually cause the
342
  /// application to reassemble, e.g. after a hot reload.
343
  ///
344
  /// Bindings are expected to use this method to re-register anything that uses
345 346 347 348 349 350 351 352
  /// closures, so that they do not keep pointing to old code, and to flush any
  /// caches of previously computed values, in case the new code would compute
  /// them differently. For example, the rendering layer triggers the entire
  /// application to repaint when this is called.
  ///
  /// Do not call this method directly. Instead, use [reassembleApplication].
  @mustCallSuper
  @protected
353
  Future<void> performReassemble() {
354
    FlutterError.resetErrorCount();
355
    return Future<void>.value();
356
  }
357 358 359 360 361

  /// Registers a service extension method with the given name (full
  /// name "ext.flutter.name"), which takes no arguments and returns
  /// no value.
  ///
362
  /// Calls the `callback` callback when the service extension is called.
363
  ///
364
  /// {@macro flutter.foundation.BindingBase.registerServiceExtension}
365
  @protected
366
  void registerSignalServiceExtension({
367 368
    required String name,
    required AsyncCallback callback,
369 370 371 372 373 374
  }) {
    assert(name != null);
    assert(callback != null);
    registerServiceExtension(
      name: name,
      callback: (Map<String, String> parameters) async {
375
        await callback();
376
        return <String, dynamic>{};
377
      },
378 379 380 381 382 383 384 385 386 387
    );
  }

  /// Registers a service extension method with the given name (full
  /// name "ext.flutter.name"), which takes a single argument
  /// "enabled" which can have the value "true" or the value "false"
  /// or can be omitted to read the current value. (Any value other
  /// than "true" is considered equivalent to "false". Other arguments
  /// are ignored.)
  ///
388 389
  /// Calls the `getter` callback to obtain the value when
  /// responding to the service extension method being called.
390
  ///
391 392
  /// Calls the `setter` callback with the new value when the
  /// service extension method is called with a new value.
393
  ///
394
  /// {@macro flutter.foundation.BindingBase.registerServiceExtension}
395
  @protected
396
  void registerBoolServiceExtension({
397 398 399
    required String name,
    required AsyncValueGetter<bool> getter,
    required AsyncValueSetter<bool> setter,
400 401 402 403 404
  }) {
    assert(name != null);
    assert(getter != null);
    assert(setter != null);
    registerServiceExtension(
405
      name: name,
406
      callback: (Map<String, String> parameters) async {
407
        if (parameters.containsKey('enabled')) {
408
          await setter(parameters['enabled'] == 'true');
409 410
          _postExtensionStateChangedEvent(name, await getter() ? 'true' : 'false');
        }
411
        return <String, dynamic>{'enabled': await getter() ? 'true' : 'false'};
412
      },
413 414 415 416 417 418 419 420 421
    );
  }

  /// Registers a service extension method with the given name (full
  /// name "ext.flutter.name"), which takes a single argument with the
  /// same name as the method which, if present, must have a value
  /// that can be parsed by [double.parse], and can be omitted to read
  /// the current value. (Other arguments are ignored.)
  ///
422 423
  /// Calls the `getter` callback to obtain the value when
  /// responding to the service extension method being called.
424
  ///
425 426
  /// Calls the `setter` callback with the new value when the
  /// service extension method is called with a new value.
427
  ///
428
  /// {@macro flutter.foundation.BindingBase.registerServiceExtension}
429
  @protected
430
  void registerNumericServiceExtension({
431 432 433
    required String name,
    required AsyncValueGetter<double> getter,
    required AsyncValueSetter<double> setter,
434 435 436 437 438 439 440
  }) {
    assert(name != null);
    assert(getter != null);
    assert(setter != null);
    registerServiceExtension(
      name: name,
      callback: (Map<String, String> parameters) async {
441
        if (parameters.containsKey(name)) {
442
          await setter(double.parse(parameters[name]!));
443 444
          _postExtensionStateChangedEvent(name, (await getter()).toString());
        }
445
        return <String, dynamic>{name: (await getter()).toString()};
446
      },
447 448 449
    );
  }

450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478
  /// Sends an event when a service extension's state is changed.
  ///
  /// Clients should listen for this event to stay aware of the current service
  /// extension state. Any service extension that manages a state should call
  /// this method on state change.
  ///
  /// `value` reflects the newly updated service extension value.
  ///
  /// This will be called automatically for service extensions registered via
  /// [registerBoolServiceExtension], [registerNumericServiceExtension], or
  /// [registerStringServiceExtension].
  void _postExtensionStateChangedEvent(String name, dynamic value) {
    postEvent(
      'Flutter.ServiceExtensionStateChanged',
      <String, dynamic>{
        'extension': 'ext.flutter.$name',
        'value': value,
      },
    );
  }

  /// All events dispatched by a [BindingBase] use this method instead of
  /// calling [developer.postEvent] directly so that tests for [BindingBase]
  /// can track which events were dispatched by overriding this method.
  @protected
  void postEvent(String eventKind, Map<String, dynamic> eventData) {
    developer.postEvent(eventKind, eventData);
  }

479 480 481 482 483 484 485 486 487 488
  /// Registers a service extension method with the given name (full name
  /// "ext.flutter.name"), which optionally takes a single argument with the
  /// name "value". If the argument is omitted, the value is to be read,
  /// otherwise it is to be set. Returns the current value.
  ///
  /// Calls the `getter` callback to obtain the value when
  /// responding to the service extension method being called.
  ///
  /// Calls the `setter` callback with the new value when the
  /// service extension method is called with a new value.
489
  ///
490
  /// {@macro flutter.foundation.BindingBase.registerServiceExtension}
491
  @protected
492
  void registerStringServiceExtension({
493 494 495
    required String name,
    required AsyncValueGetter<String> getter,
    required AsyncValueSetter<String> setter,
496 497 498 499 500 501 502
  }) {
    assert(name != null);
    assert(getter != null);
    assert(setter != null);
    registerServiceExtension(
      name: name,
      callback: (Map<String, String> parameters) async {
503
        if (parameters.containsKey('value')) {
504
          await setter(parameters['value']!);
505 506
          _postExtensionStateChangedEvent(name, await getter());
        }
507
        return <String, dynamic>{'value': await getter()};
508
      },
509 510 511
    );
  }

512 513 514 515 516 517 518 519 520
  /// Registers a service extension method with the given name (full name
  /// "ext.flutter.name").
  ///
  /// The given callback is called when the extension method is called. The
  /// callback must return a [Future] that either eventually completes to a
  /// return value in the form of a name/value map where the values can all be
  /// converted to JSON using `json.encode()` (see [JsonEncoder]), or fails. In
  /// case of failure, the failure is reported to the remote caller and is
  /// dumped to the logs.
521 522
  ///
  /// The returned map will be mutated.
523
  ///
524
  /// {@template flutter.foundation.BindingBase.registerServiceExtension}
525 526 527 528 529 530 531
  /// A registered service extension can only be activated if the vm-service
  /// is included in the build, which only happens in debug and profile mode.
  /// Although a service extension cannot be used in release mode its code may
  /// still be included in the Dart snapshot and blow up binary size if it is
  /// not wrapped in a guard that allows the tree shaker to remove it (see
  /// sample code below).
  ///
532
  /// {@tool snippet}
533
  /// The following code registers a service extension that is only included in
534
  /// debug builds.
535 536
  ///
  /// ```dart
537 538 539 540 541 542
  /// void myRegistrationFunction() {
  ///   assert(() {
  ///     // Register your service extension here.
  ///     return true;
  ///   }());
  /// }
543
  /// ```
544
  /// {@end-tool}
545
  ///
546
  /// {@tool snippet}
547
  /// A service extension registered with the following code snippet is
548
  /// available in debug and profile mode.
549 550
  ///
  /// ```dart
551 552 553 554 555 556
  /// void myRegistrationFunction() {
  ///   // kReleaseMode is defined in the 'flutter/foundation.dart' package.
  ///   if (!kReleaseMode) {
  ///     // Register your service extension here.
  ///   }
  /// }
557
  /// ```
558
  /// {@end-tool}
559 560 561
  ///
  /// Both guards ensure that Dart's tree shaker can remove the code for the
  /// service extension in release builds.
562
  /// {@endtemplate}
563
  @protected
564
  void registerServiceExtension({
565 566
    required String name,
    required ServiceExtensionCallback callback,
567 568 569 570 571 572
  }) {
    assert(name != null);
    assert(callback != null);
    final String methodName = 'ext.flutter.$name';
    developer.registerExtension(methodName, (String method, Map<String, String> parameters) async {
      assert(method == methodName);
573 574 575 576 577
      assert(() {
        if (debugInstrumentationEnabled)
          debugPrint('service extension method received: $method($parameters)');
        return true;
      }());
578 579 580 581 582 583 584 585 586 587 588

      // VM service extensions are handled as "out of band" messages by the VM,
      // which means they are handled at various times, generally ASAP.
      // Notably, this includes being handled in the middle of microtask loops.
      // While this makes sense for some service extensions (e.g. "dump current
      // stack trace", which explicitly doesn't want to wait for a loop to
      // complete), Flutter extensions need not be handled with such high
      // priority. Further, handling them with such high priority exposes us to
      // the possibility that they're handled in the middle of a frame, which
      // breaks many assertions. As such, we ensure they we run the callbacks
      // on the outer event loop here.
589
      await debugInstrumentAction<void>('Wait for outer event loop', () {
590
        return Future<void>.delayed(Duration.zero);
591
      });
592

593
      Object? caughtException;
594 595
      StackTrace? caughtStack;
      late Map<String, dynamic> result;
596 597 598 599 600 601 602 603 604
      try {
        result = await callback(parameters);
      } catch (exception, stack) {
        caughtException = exception;
        caughtStack = stack;
      }
      if (caughtException == null) {
        result['type'] = '_extensionType';
        result['method'] = method;
605
        return developer.ServiceExtensionResponse.result(json.encode(result));
606
      } else {
607
        FlutterError.reportError(FlutterErrorDetails(
608 609
          exception: caughtException,
          stack: caughtStack,
610
          context: ErrorDescription('during a service extension callback for "$method"'),
611
        ));
612
        return developer.ServiceExtensionResponse.error(
613
          developer.ServiceExtensionResponse.extensionError,
614
          json.encode(<String, String>{
615 616
            'exception': caughtException.toString(),
            'stack': caughtStack.toString(),
617
            'method': method,
618
          }),
619 620 621 622 623
        );
      }
    });
  }

624
  @override
625
  String toString() => '<${objectRuntimeType(this, 'BindingBase')}>';
626
}
627 628

/// Terminate the Flutter application.
629
Future<void> _exitApplication() async {
630 631
  exit(0);
}