binding.dart 14.5 KB
Newer Older
1 2 3 4
// Copyright 2016 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

5 6 7
import 'dart:async';
import 'dart:convert' show JSON;
import 'dart:developer' as developer;
8
import 'dart:io' show exit;
9

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

import 'assertions.dart';
import 'basic_types.dart';
14
import 'platform.dart';
15 16 17 18 19 20 21 22

/// 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.
23
typedef Future<Map<String, String>> ServiceExtensionCallback(Map<String, String> parameters);
24

25 26 27 28
/// Base class for mixins that provide singleton services (also known as
/// "bindings").
///
/// To use this class in a mixin, inherit from it and implement
29 30 31
/// [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).
32 33 34
///
/// The top-most layer used to write the application will have a
/// concrete class that inherits from BindingBase and uses all the
35
/// various BindingBase mixins (such as [ServicesBinding]). For example, the
36
/// Widgets library in flutter introduces a binding called
37
/// [WidgetsFlutterBinding]. The relevant library defines how to create
38
/// the binding. It could be implied (for example,
39
/// [WidgetsFlutterBinding] is automatically started from [runApp]), or
40 41 42
/// the application might be required to explicitly call the
/// constructor.
abstract class BindingBase {
43 44 45 46 47 48
  /// 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.
49
  BindingBase() {
50 51
    developer.Timeline.startSync('Framework initialization');

52 53 54
    assert(!_debugInitialized);
    initInstances();
    assert(_debugInitialized);
55 56 57 58

    assert(!_debugServiceExtensionsRegistered);
    initServiceExtensions();
    assert(_debugServiceExtensionsRegistered);
59

60
    developer.postEvent('Flutter.FrameworkInitialization', <String, String>{});
61

62
    developer.Timeline.finishSync();
63 64 65
  }

  static bool _debugInitialized = false;
66
  static bool _debugServiceExtensionsRegistered = false;
67 68 69 70 71 72 73 74 75

  /// 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()`.
76 77
  @protected
  @mustCallSuper
78
  void initInstances() {
79
    assert(!_debugInitialized);
80 81 82
    assert(() { _debugInitialized = true; return true; });
  }

83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102
  /// 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.
  ///
  /// Service extensions are only exposed when the observatory is
  /// included in the build, which should only happen in checked mode
  /// and in profile mode.
  ///
  /// See also:
  ///
  ///  * <https://github.com/dart-lang/sdk/blob/master/runtime/vm/service/service.md#rpcs-requests-and-responses>
103 104
  @protected
  @mustCallSuper
105 106 107 108 109 110
  void initServiceExtensions() {
    assert(!_debugServiceExtensionsRegistered);
    registerSignalServiceExtension(
      name: 'reassemble',
      callback: reassembleApplication
    );
111 112 113 114
    registerSignalServiceExtension(
      name: 'exit',
      callback: _exitApplication
    );
115 116
    registerSignalServiceExtension(
      name: 'frameworkPresent',
117
      callback: () => new Future<Null>.value()
118
    );
119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148
    assert(() {
      registerServiceExtension(
        name: 'platformOverride',
        callback: (Map<String, String> parameters) async {
          if (parameters.containsKey('value')) {
            switch (parameters['value']) {
              case 'android':
                debugDefaultTargetPlatformOverride = TargetPlatform.android;
                break;
              case 'iOS':
                debugDefaultTargetPlatformOverride = TargetPlatform.iOS;
                break;
              case 'fuchsia':
                debugDefaultTargetPlatformOverride = TargetPlatform.fuchsia;
                break;
              case 'default':
              default:
                debugDefaultTargetPlatformOverride = null;
            }
            await reassembleApplication();
          }
          return <String, String>{
            'value': defaultTargetPlatform
                     .toString()
                     .substring('$TargetPlatform.'.length),
          };
        }
      );
      return true;
    });
149 150 151
    assert(() { _debugServiceExtensionsRegistered = true; return true; });
  }

152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188
  /// 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
  /// primarily intended for development features, in particular to allow
  /// [reassembleApplication] to block input while it walks the tree (which it
  /// partially does asynchronously).
  ///
  /// The [Future] returned by the `callback` argument is returned by [lockEvents].
  @protected
  Future<Null> lockEvents(Future<Null> callback()) {
    assert(callback != null);
    _lockCount += 1;
    final Future<Null> future = callback();
    assert(future != null, 'The lockEvents() callback returned null; it should return a Future<Null> that completes when the lock is to expire.');
    future.whenComplete(() {
      _lockCount -= 1;
      if (!locked)
        unlocked();
    });
    return future;
  }

  /// Called by [lockEvents] when events get unlocked.
  ///
  /// This should flush any events that were queued while [locked] was true.
  @protected
189
  @mustCallSuper
190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211
  void unlocked() {
    assert(!locked);
  }

  /// Cause the entire application to redraw.
  ///
  /// 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.
212
  Future<Null> reassembleApplication() {
213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228
    return lockEvents(performReassemble);
  }

  /// This method is called by [reassembleApplication] to actually cause the
  /// application to reassemble.
  ///
  /// Bindings are expected to use this method to reregister anything that uses
  /// 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
  Future<Null> performReassemble() {
229
    FlutterError.resetErrorCount();
230
    return new Future<Null>.value();
231
  }
232 233 234 235 236

  /// Registers a service extension method with the given name (full
  /// name "ext.flutter.name"), which takes no arguments and returns
  /// no value.
  ///
237
  /// Calls the `callback` callback when the service extension is called.
238
  @protected
239
  void registerSignalServiceExtension({
240
    @required String name,
241
    @required AsyncCallback callback
242 243 244 245 246 247
  }) {
    assert(name != null);
    assert(callback != null);
    registerServiceExtension(
      name: name,
      callback: (Map<String, String> parameters) async {
248
        await callback();
249
        return <String, String>{};
250 251 252 253 254 255 256 257 258 259 260
      }
    );
  }

  /// 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.)
  ///
261 262
  /// Calls the `getter` callback to obtain the value when
  /// responding to the service extension method being called.
263
  ///
264 265
  /// Calls the `setter` callback with the new value when the
  /// service extension method is called with a new value.
266
  @protected
267
  void registerBoolServiceExtension({
268
    @required String name,
269 270
    @required AsyncValueGetter<bool> getter,
    @required AsyncValueSetter<bool> setter
271 272 273 274 275
  }) {
    assert(name != null);
    assert(getter != null);
    assert(setter != null);
    registerServiceExtension(
276
      name: name,
277 278
      callback: (Map<String, String> parameters) async {
        if (parameters.containsKey('enabled'))
279
          await setter(parameters['enabled'] == 'true');
280
        return <String, String>{ 'enabled': await getter() ? 'true' : 'false' };
281 282 283 284 285 286 287 288 289 290
      }
    );
  }

  /// 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.)
  ///
291 292
  /// Calls the `getter` callback to obtain the value when
  /// responding to the service extension method being called.
293
  ///
294 295
  /// Calls the `setter` callback with the new value when the
  /// service extension method is called with a new value.
296
  @protected
297
  void registerNumericServiceExtension({
298
    @required String name,
299 300
    @required AsyncValueGetter<double> getter,
    @required AsyncValueSetter<double> setter
301 302 303 304 305 306 307 308
  }) {
    assert(name != null);
    assert(getter != null);
    assert(setter != null);
    registerServiceExtension(
      name: name,
      callback: (Map<String, String> parameters) async {
        if (parameters.containsKey(name))
309
          await setter(double.parse(parameters[name]));
310
        return <String, String>{ name: (await getter()).toString() };
311 312 313 314
      }
    );
  }

315 316 317 318 319 320 321 322 323 324
  /// 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.
325
  @protected
326 327
  void registerStringServiceExtension({
    @required String name,
328 329
    @required AsyncValueGetter<String> getter,
    @required AsyncValueSetter<String> setter
330 331 332 333 334 335 336 337
  }) {
    assert(name != null);
    assert(getter != null);
    assert(setter != null);
    registerServiceExtension(
      name: name,
      callback: (Map<String, String> parameters) async {
        if (parameters.containsKey('value'))
338
          await setter(parameters['value']);
339
        return <String, String>{ 'value': await getter() };
340 341 342 343
      }
    );
  }

344
  /// Registers a service extension method with the given name (full
345
  /// name "ext.flutter.name"). The given callback is called when the
346 347 348
  /// 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
349
  /// JSON using `JSON.encode()` (see [JsonCodec.encode]), or fails. In case of failure, the
350 351 352 353
  /// failure is reported to the remote caller and is dumped to the
  /// logs.
  ///
  /// The returned map will be mutated.
354
  @protected
355
  void registerServiceExtension({
356 357 358 359 360 361 362 363 364 365
    @required String name,
    @required ServiceExtensionCallback callback
  }) {
    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);
      dynamic caughtException;
      StackTrace caughtStack;
366
      Map<String, String> result;
367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384
      try {
        result = await callback(parameters);
      } catch (exception, stack) {
        caughtException = exception;
        caughtStack = stack;
      }
      if (caughtException == null) {
        result['type'] = '_extensionType';
        result['method'] = method;
        return new developer.ServiceExtensionResponse.result(JSON.encode(result));
      } else {
        FlutterError.reportError(new FlutterErrorDetails(
          exception: caughtException,
          stack: caughtStack,
          context: 'during a service extension callback for "$method"'
        ));
        return new developer.ServiceExtensionResponse.error(
          developer.ServiceExtensionResponse.extensionError,
385
          JSON.encode(<String, String>{
386 387
            'exception': caughtException.toString(),
            'stack': caughtStack.toString(),
388
            'method': method,
389 390 391 392 393 394
          })
        );
      }
    });
  }

395 396 397
  @override
  String toString() => '<$runtimeType>';
}
398 399

/// Terminate the Flutter application.
400
Future<Null> _exitApplication() async {
401 402
  exit(0);
}