[fuchsia_webview_flutter] Register objects in frame before load

TESTED=current tests continue to pass, verified on webview_example and on device.

Change-Id: I39e8fab1633ce177429f9e6d22182f686afd4cd2
diff --git a/public/dart/fuchsia_webview_flutter/lib/src/fuchsia_web_services.dart b/public/dart/fuchsia_webview_flutter/lib/src/fuchsia_web_services.dart
index 46e10e1..536356c 100644
--- a/public/dart/fuchsia_webview_flutter/lib/src/fuchsia_web_services.dart
+++ b/public/dart/fuchsia_webview_flutter/lib/src/fuchsia_web_services.dart
@@ -3,12 +3,10 @@
 // found in the LICENSE file.
 
 import 'dart:async';
-import 'dart:convert' show utf8;
 import 'dart:io';
 
 import 'package:fidl/fidl.dart';
 import 'package:fidl_fuchsia_io/fidl_async.dart' as fidl_io;
-import 'package:fidl_fuchsia_mem/fidl_async.dart' as fidl_mem;
 import 'package:fidl_fuchsia_web/fidl_async.dart' as fidl_web;
 import 'package:fuchsia_logger/logger.dart';
 import 'package:fuchsia_scenic_flutter/child_view_connection.dart';
@@ -133,15 +131,36 @@
   // TODO(crbug.com/900391): Investigate if we can run the scripts in
   // isolated JS worlds.
   Future<String> evaluateJavascript(List<String> origins, String script) async {
-    final vmo = SizedVmo.fromUint8List(utf8.encode(script));
-    final buffer = fidl_mem.Buffer(vmo: vmo, size: vmo.size);
+    final buffer = utils.stringToBuffer(script);
     // TODO(nkosote): add catchError and decorate the error based on the error
     // code.
-    // TODO(miguelfrde): replace with executeJavaScript once chromium rolls.
-    await frame.executeJavaScriptNoResult(origins, buffer);
-    final resultVmo = SizedVmo.fromUint8List(utf8.encode('1'));
-    final resultBuffer = fidl_mem.Buffer(vmo: resultVmo, size: resultVmo.size);
-    return utils.bufferToString(resultBuffer);
+    final result = await frame.executeJavaScript(origins, buffer);
+    return utils.bufferToString(result);
+  }
+
+  /// Executes a UTF-8 encoded `script` for every subsequent page load where the
+  /// [`fuchsia.web.Frame`]'s URL has an origin reflected in `origins`. The script is executed
+  /// early, prior to the execution of the document's scripts.
+  ///
+  /// Scripts are identified by a client-managed identifier `id`. Any script previously injected
+  /// using the same `id` will be replaced.
+  ///
+  /// The order in which multiple bindings are executed is the same as the order in which the
+  /// bindings were added. If a script is added which clobbers an existing script of the same
+  /// `id`, the previous script's precedence in the injection order will be preserved.
+  ///
+  /// At least one `origins` entry must be specified. If a wildcard `"*"` is specified in
+  /// `origins`, then the script will be evaluated unconditionally.
+  ///
+  /// If an error occured, the [`fuchsia.web.FrameError`] will be set to one of these values:
+  /// - `BUFFER_NOT_UTF8`: `script` is not UTF-8 encoded.
+  /// - `INVALID_ORIGIN`: `origins` is an empty vector.
+  Future<void> evaluateJavascriptBeforeLoad(
+      int id, List<String> origins, String script) async {
+    final buffer = utils.stringToBuffer(script);
+    // TODO(miguelfrde): add catchError and decorate the error based on the error
+    // code.
+    await frame.addBeforeLoadJavaScript(id, origins, buffer);
   }
 
   /// Posts a message to the [fidl_web.Frame]'s onMessage handler.
@@ -162,9 +181,9 @@
     String message, {
     InterfaceRequest<fidl_web.MessagePort> outgoingMessagePortRequest,
   }) {
-    final vmo = SizedVmo.fromUint8List(utf8.encode(message));
+    final data = utils.stringToBuffer(message);
     var msg = fidl_web.WebMessage(
-      data: fidl_mem.Buffer(vmo: vmo, size: vmo.size),
+      data: data,
       outgoingTransfer: outgoingMessagePortRequest != null
           ? [
               fidl_web.OutgoingTransferable.withMessagePort(
diff --git a/public/dart/fuchsia_webview_flutter/lib/src/fuchsia_webview_platform_controller.dart b/public/dart/fuchsia_webview_flutter/lib/src/fuchsia_webview_platform_controller.dart
index 0f00952..f9a5f69 100644
--- a/public/dart/fuchsia_webview_flutter/lib/src/fuchsia_webview_platform_controller.dart
+++ b/public/dart/fuchsia_webview_flutter/lib/src/fuchsia_webview_platform_controller.dart
@@ -14,19 +14,26 @@
 import 'fuchsia_web_services.dart';
 import 'utils.dart' as utils;
 
+class _ChannelSubscription {
+  StreamSubscription<String> subscription;
+  final int id;
+  _ChannelSubscription(this.id, {this.subscription});
+}
+
 /// Fuchsia [WebViewPlatformController] implementation that serves as the entry
 /// point for all [fuchsia_webview_flutter/webview.dart]'s apis
 class FuchsiaWebViewPlatformController extends WebViewPlatformController {
   /// Helper class to interact with fuchsia web services
   FuchsiaWebServices _fuchsiaWebServices;
   String _currentUrl;
-  // Reason: sdk_version_set_literal unsupported until version 2.2
+  int _nextId = 0;
+  // Reason: not supported until v2.2 and we need to support earlier
+  // versions.
   // ignore: prefer_collection_literals
-  final _pendingChannels = Set<String>();
+  var _beforeLoadChannels = Set<String>();
 
   final WebViewPlatformCallbacksHandler _platformCallbacksHandler;
-  final _javascriptChannelSubscriptions =
-      <String, StreamSubscription<String>>{};
+  final _javascriptChannelSubscriptions = <String, _ChannelSubscription>{};
 
   /// Initializes [FuchsiaWebViewPlatformController]
   FuchsiaWebViewPlatformController(this._platformCallbacksHandler,
@@ -36,10 +43,10 @@
     fuchsiaWebServices.setNavigationEventListener(
         _WebviewNavigationEventListener(_onNavigationStateChanged));
     updateSettings(creationParams.webSettings);
+    _addBeforeLoadChannels(creationParams.javascriptChannelNames);
     if (creationParams.initialUrl != null) {
       loadUrl(creationParams.initialUrl, {});
     }
-    _pendingChannels.addAll(creationParams.javascriptChannelNames);
   }
 
   /// Returns [FuchsiaWebServices]
@@ -128,7 +135,9 @@
       Set<String> javascriptChannelNames) async {
     for (final channelName in javascriptChannelNames) {
       if (_javascriptChannelSubscriptions.containsKey(channelName)) {
-        await _javascriptChannelSubscriptions[channelName].cancel();
+        await _javascriptChannelSubscriptions[channelName]
+            .subscription
+            .cancel();
         _javascriptChannelSubscriptions.remove(channelName);
         await fuchsiaWebServices
             .evaluateJavascript(['*'], 'window.$channelName = undefined;');
@@ -157,7 +166,7 @@
   /// Close all remaining subscriptions and connections.
   void dispose() {
     for (final entry in _javascriptChannelSubscriptions.entries) {
-      entry.value.cancel();
+      entry.value.subscription.cancel();
     }
     fuchsiaWebServices.dispose();
   }
@@ -170,14 +179,19 @@
       _currentUrl = state.url;
     }
     if (state.isMainDocumentLoaded != null && state.isMainDocumentLoaded) {
-      final channelsToAdd =
-          _pendingChannels.union(_javascriptChannelSubscriptions.keys.toSet());
-      await _createChannelSubscriptions(channelsToAdd);
-      _pendingChannels.clear();
+      await _establishCommunication(_beforeLoadChannels);
       _platformCallbacksHandler.onPageFinished(_currentUrl);
     }
   }
 
+  /// Registers the javascript channels that will be loaded when the page loads.
+  /// Connection will be established when the page loads.
+  Future<void> _addBeforeLoadChannels(
+      Set<String> javascriptChannelNames) async {
+    _beforeLoadChannels = javascriptChannelNames;
+    await _createChannelSubscriptions(javascriptChannelNames, beforeLoad: true);
+  }
+
   /// For each channel in [javascriptChannelNames] creates an object with the
   /// channel name on window in the frame. That object will contain a
   /// `postMessage` method. Messages sent through that method will be received
@@ -189,55 +203,85 @@
   ///   3. The frame has a listener on window and will reply with a
   ///      share-port-ack and a port to which the frame will send messages.
   ///   4. Bind that port and start listening on it.
-  ///   5. When a message arrives on that port it is sent to the client through
+  ///   5. Notify the frame that we are ready to receive messages.
+  ///   6. When a message arrives on that port it is sent to the client through
   ///      the platform callback.
-  Future<void> _createChannelSubscriptions(
-      Set<String> javascriptChannelNames) async {
+  Future<void> _createChannelSubscriptions(Set<String> javascriptChannelNames,
+      {bool beforeLoad}) async {
     for (final channelName in javascriptChannelNames) {
       // Close any connections to that object (if any existed)
       if (_javascriptChannelSubscriptions.containsKey(channelName)) {
-        await _javascriptChannelSubscriptions[channelName].cancel();
+        await _javascriptChannelSubscriptions[channelName]
+            .subscription
+            .cancel();
+      } else {
+        _javascriptChannelSubscriptions[channelName] =
+            _ChannelSubscription(_nextId);
+        _nextId += 1;
       }
+    }
 
-      // Create a JavaScript object with one postMessage method. This object
-      // will be exposed on window.$channelName when the FIDL communication is
-      // established. Any window.$channelName already set will be removed.
-      final script = _scriptForChannel(channelName);
-      await evaluateJavascript(script);
+    // Create a JavaScript object with one postMessage method. This object
+    // will be exposed on window.$channelName when the FIDL communication is
+    // established. Any window.$channelName already set will be removed.
+    // If before load, the connection will happen once the page loads.
+    if (beforeLoad) {
+      await Future.wait(javascriptChannelNames.map((channel) {
+        // Reason: not supported until v2.2 and we need to support earlier
+        // versions.
+        // ignore: prefer_collection_literals
+        final script = _scriptForChannels(Set.from([channel]));
+        return fuchsiaWebServices.evaluateJavascriptBeforeLoad(
+            _javascriptChannelSubscriptions[channel].id, ['*'], script);
+      }));
+      return;
+    }
 
+    final script = _scriptForChannels(javascriptChannelNames);
+    await evaluateJavascript(script);
+
+    await _establishCommunication(javascriptChannelNames);
+  }
+
+  Future<void> _establishCommunication(Set<String> javascriptChannelNames) {
+    return Future.wait(javascriptChannelNames.map((channelName) async {
       // Creates the message channel connection.
       fidl_web.MessagePortProxy incomingPort;
       try {
-        incomingPort = await _bindIncomingPort();
+        incomingPort = await _bindIncomingPort(channelName);
       } on Exception catch (e) {
         log.warning('Failed to bind incoming port for $channelName: $e');
-        continue;
+        return;
       }
 
       // Subscribe for incoming messages.
       final incomingMessagesStream = _startReceivingMessages(incomingPort);
-      _javascriptChannelSubscriptions[channelName] =
+      _javascriptChannelSubscriptions[channelName].subscription =
           incomingMessagesStream.listen(
         (message) async {
           _platformCallbacksHandler.onJavaScriptChannelMessage(
               channelName, message);
         },
       );
-    }
+
+      // Notify of readiness
+      await fuchsiaWebServices.postMessage(
+          '*', 'share-port-$channelName-ready');
+    }));
   }
 
-  /// Communicates with the script injected by `_scriptForChannel` to get a port
+  /// Communicates with the script injected by `_scriptForChannels` to get a port
   /// from the web page with which to communicate with the page. See comments on
   /// `_createChannelSubscriptions` for details on the process.
-  Future<fidl_web.MessagePortProxy> _bindIncomingPort() async {
+  Future<fidl_web.MessagePortProxy> _bindIncomingPort(String channel) async {
     final messagePort = fidl_web.MessagePortProxy();
-    await fuchsiaWebServices.postMessage('*', 'share-port',
+    await fuchsiaWebServices.postMessage('*', 'share-port-$channel',
         outgoingMessagePortRequest: messagePort.ctrl.request());
 
     final msg = await messagePort.receiveMessage();
     final ackMsg = utils.bufferToString(msg.data);
-    if (ackMsg != 'share-port-ack') {
-      throw Exception('Expected "share-port-ack", got: "$ackMsg"');
+    if (ackMsg != 'share-port-ack-$channel') {
+      throw Exception('Expected "share-port-ack-$channel", got: "$ackMsg"');
     }
     if (msg.incomingTransfer == null || msg.incomingTransfer.isEmpty) {
       throw Exception('failed to provide an incoming message port');
@@ -250,34 +294,57 @@
   /// Script injected to the frame to create an object with the given name on
   /// window. See comments on `_createChannelSubscriptions` for details on the
   /// process.
-  String _scriptForChannel(String channelName) {
-    return """
+  String _scriptForChannels(Set<String> channelNames) {
+    return channelNames.map((channel) => '''
         (function() {
-          function init$channelName(event) {
-            if (event.data == 'share-port' && event.ports && event.ports.length > 0) {
-              console.log("Registering channel $channelName");
-              const messageChannel = new MessageChannel();
-              event.ports[0].postMessage('share-port-ack', [messageChannel.port2]);
-              window.$channelName = new $channelName(messageChannel);
-              window.removeEventListener('message', init$channelName, true);
-            }
-          }
-
-          window.addEventListener('message', init$channelName, true);
-
-          class $channelName {
-            constructor(messageChannel) {
-              this._messageChannel = messageChannel;
+          class $channel {
+            constructor() {
+              this._messageChannel = null;
+              this._pendingMessages = [];
+              this._isReady = false;
             }
 
             postMessage(message) {
-              this._messageChannel.port1.postMessage(message);
+              if (this._isReady) {
+                this._messageChannel.port1.postMessage(message);
+              } else {
+                this._pendingMessages.push(message);
+              }
+            }
+
+            _ready() {
+              for (const pendingMessage of this._pendingMessages) {
+                this._messageChannel.port1.postMessage(pendingMessage);
+              }
+              this._isReady = true;
+              this._pendingMessages = [];
+            }
+
+            _setMessageChannel(channel) {
+              this._messageChannel = channel;
             }
           }
 
-          window.$channelName = undefined;
+          window.$channel = new $channel();
+
+          function initializer$channel(event) {
+            if (event.data) {
+              if (event.data == 'share-port-$channel' && event.ports && event.ports.length > 0) {
+                console.log('Registering channel $channel');
+                const messageChannel = new MessageChannel();
+                window.$channel._setMessageChannel(messageChannel);
+                event.ports[0].postMessage('share-port-ack-$channel', [messageChannel.port2]);
+              }
+              if (event.data == 'share-port-$channel-ready') {
+                console.log('Channel $channel ready');
+                window.$channel._ready();
+                window.removeEventListener('message', initializer$channel, true);
+              }
+            }
+          }
+          window.addEventListener('message', initializer$channel, true);
         })();
-      """;
+      ''').join('\n');
   }
 
   /// Listens for messages on the incoming port and streams them.
diff --git a/public/dart/fuchsia_webview_flutter/lib/src/utils.dart b/public/dart/fuchsia_webview_flutter/lib/src/utils.dart
index e0d18ea..72d7027 100644
--- a/public/dart/fuchsia_webview_flutter/lib/src/utils.dart
+++ b/public/dart/fuchsia_webview_flutter/lib/src/utils.dart
@@ -13,3 +13,9 @@
   dataVmo.close();
   return utf8.decode(data.bytesAsUint8List());
 }
+
+/// Writes a string into a VMO and a FIDL transport buffer.
+fidl_mem.Buffer stringToBuffer(String string) {
+  final vmo = SizedVmo.fromUint8List(utf8.encode(string));
+  return fidl_mem.Buffer(vmo: vmo, size: vmo.size);
+}