Bug 1367079 - 3. Add utility for registering lazy WindowEventDispatcher listener; r=snorp
authorJim Chen <nchen@mozilla.com>
Thu, 14 Sep 2017 17:50:58 -0400
changeset 665157 7f1d3a461631ba73c54fc3079dbc933889a711b3
parent 665156 1af8aae9f13783e8497f9a0ca91a07e0524503de
child 665158 c185dd417782d8e8a1d9ab7c75211d6b09ac8573
push id79943
push userbmo:tchiovoloni@mozilla.com
push dateThu, 14 Sep 2017 23:59:48 +0000
reviewerssnorp
bugs1367079
milestone57.0a1
Bug 1367079 - 3. Add utility for registering lazy WindowEventDispatcher listener; r=snorp Add `GeckoViewUtils.registerLazyWindowEventListener` that, similar to `addLazyEventListener`, will register a lazy event listener with the per-window EventDispatcher. MozReview-Commit-ID: AX3EQGpmdw
mobile/android/modules/geckoview/GeckoViewUtils.jsm
mobile/android/modules/geckoview/Messaging.jsm
--- a/mobile/android/modules/geckoview/GeckoViewUtils.jsm
+++ b/mobile/android/modules/geckoview/GeckoViewUtils.jsm
@@ -27,18 +27,18 @@ var GeckoViewUtils = {
    *                  component must include the line
    *                  "this.wrappedJSObject = this;" in its constructor.
    * @param module    If specified, load the object from a JS module.
    * @param init      For non-scripts, optional post-load initialization function.
    * @param observers If specified, listen to specified observer notifications.
    * @param ppmm      If specified, listen to specified process messages.
    * @param mm        If specified, listen to specified frame messages.
    * @param ged       If specified, listen to specified global EventDispatcher events.
-   * @param once      If specified, only listen to the specified
-   *                  notifications/messages once.
+   * @param once      if true, only listen to the specified
+   *                  events/messages/notifications once.
    */
   addLazyGetter: function(scope, name, {script, service, module, handler,
                                         observers, ppmm, mm, ged, init, once}) {
     if (script) {
       XPCOMUtils.defineLazyScriptGetter(scope, name, script);
     } else {
       XPCOMUtils.defineLazyGetter(scope, name, _ => {
         let ret = undefined;
@@ -98,70 +98,118 @@ var GeckoViewUtils = {
           EventDispatcher.instance.registerListener(scope[name], event);
         }
         scope[name].onEvent(event, data, callback);
       };
       EventDispatcher.instance.registerListener(listener, ged);
     }
   },
 
+  _addLazyListeners: function(events, handler, scope, name, addFn, handleFn) {
+    if (!handler) {
+      handler = (_ => Array.isArray(name) ? name.map(n => scope[n]) : scope[name]);
+    }
+    let listener = (...args) => {
+      let handlers = handler(...args);
+      if (!handlers) {
+          return;
+      }
+      if (!Array.isArray(handlers)) {
+        handlers = [handlers];
+      }
+      handleFn(handlers, listener, args);
+    };
+    if (Array.isArray(events)) {
+      addFn(events, listener);
+    } else {
+      addFn([events], listener);
+    }
+  },
+
   /**
    * Add lazy event listeners that only load the actual handler when an event
    * is being handled.
    *
    * @param target  Event target for the event listeners.
    * @param events  Event name as a string or array.
    * @param handler If specified, function that, for a given event, returns the
    *                actual event handler as an object or an array of objects.
    *                If handler is not specified, the actual event handler is
    *                specified using the scope and name pair.
    * @param scope   See handler.
    * @param name    See handler.
    * @param options Options for addEventListener.
    */
   addLazyEventListener: function(target, events, {handler, scope, name, options}) {
-    if (!handler) {
-      handler = (_ => Array.isArray(name) ? name.map(n => scope[n]) : scope[name]);
-    }
-    let listener = event => {
-      let handlers = handler(event);
-      if (!handlers) {
-          return;
-      }
-      if (!Array.isArray(handlers)) {
-        handlers = [handlers];
-      }
-      if (!options || !options.once) {
-        target.removeEventListener(event.type, listener, options);
-        handlers.forEach(handler => target.addEventListener(event.type, handler, options));
-      }
-      handlers.forEach(handler => handler.handleEvent(event));
-    };
-    if (Array.isArray(events)) {
-      events.forEach(event => target.addEventListener(event, listener, options));
-    } else {
-      target.addEventListener(events, listener, options);
-    }
+    this._addLazyListeners(events, handler, scope, name,
+      (events, listener) => {
+        events.forEach(event => target.addEventListener(event, listener, options));
+      },
+      (handlers, listener, args) => {
+        if (!options || !options.once) {
+          target.removeEventListener(args[0].type, listener, options);
+          handlers.forEach(handler =>
+            target.addEventListener(args[0].type, handler, options));
+        }
+        handlers.forEach(handler => handler.handleEvent(args[0]));
+      });
+  },
+
+  /**
+   * Add lazy event listeners on the per-window EventDispatcher, and only load
+   * the actual handler when an event is being handled.
+   *
+   * @param window  Window with the target EventDispatcher.
+   * @param events  Event name as a string or array.
+   * @param handler If specified, function that, for a given event, returns the
+   *                actual event handler as an object or an array of objects.
+   *                If handler is not specified, the actual event handler is
+   *                specified using the scope and name pair.
+   * @param scope   See handler.
+   * @param name    See handler.
+   * @param once    If true, only listen to the specified events once.
+   */
+  registerLazyWindowEventListener: function(window, events,
+                                            {handler, scope, name, once}) {
+    let dispatcher = this.getDispatcherForWindow(window);
+
+    this._addLazyListeners(events, handler, scope, name,
+      (events, listener) => {
+        dispatcher.registerListener(listener, events);
+      },
+      (handlers, listener, args) => {
+        if (!once) {
+          dispatcher.unregisterListener(listener, args[0]);
+          handlers.forEach(handler =>
+            dispatcher.registerListener(handler, args[0]));
+        }
+        handlers.forEach(handler => handler.onEvent(...args));
+      });
   },
 
   /**
    * Return the outermost chrome DOM window (the XUL window) for a given DOM
    * window.
    *
    * @param aWin a DOM window.
    */
   getChromeWindow: function(aWin) {
-    return aWin.QueryInterface(Ci.nsIInterfaceRequestor)
+    return aWin &&
+           aWin.QueryInterface(Ci.nsIInterfaceRequestor)
                .getInterface(Ci.nsIDocShell).QueryInterface(Ci.nsIDocShellTreeItem)
                .rootTreeItem.QueryInterface(Ci.nsIInterfaceRequestor)
                .getInterface(Ci.nsIDOMWindow);
   },
 
   /**
    * Return the per-nsWindow EventDispatcher for a given DOM window.
    *
    * @param aWin a DOM window.
    */
   getDispatcherForWindow: function(aWin) {
-    let win = this.getChromeWindow(aWin.top);
-    return win.WindowEventDispatcher || EventDispatcher.for(win);
+    try {
+      let win = this.getChromeWindow(aWin.top);
+      return win.WindowEventDispatcher || EventDispatcher.for(win);
+    } catch (e) {
+      return null;
+    }
   },
 };
--- a/mobile/android/modules/geckoview/Messaging.jsm
+++ b/mobile/android/modules/geckoview/Messaging.jsm
@@ -211,32 +211,48 @@ DispatcherDelegate.prototype = {
       });
     },
   },
 };
 
 var EventDispatcher = {
   instance: new DispatcherDelegate(IS_PARENT_PROCESS ? Services.androidBridge : undefined),
 
+  /**
+   * Return an EventDispatcher instance for a chrome DOM window. In a content
+   * process, return a proxy through the message manager that automatically
+   * forwards events to the main process.
+   *
+   * To force using a message manager proxy (for example in a frame script
+   * environment), call forMessageManager.
+   *
+   * @param aWindow a chrome DOM window.
+   */
   for: function(aWindow) {
     let view = aWindow && aWindow.arguments && aWindow.arguments[0] &&
                aWindow.arguments[0].QueryInterface(Ci.nsIAndroidView);
 
     if (!view) {
-      let mm = aWindow && aWindow.messageManager;
+      let mm = !IS_PARENT_PROCESS && aWindow && aWindow.messageManager;
       if (!mm) {
         throw new Error("window is not a GeckoView-connected window and does" +
                         " not have a message manager");
       }
       return this.forMessageManager(mm);
     }
 
     return new DispatcherDelegate(view);
   },
 
+  /**
+   * Return an EventDispatcher instance for a message manager associated with a
+   * window.
+   *
+   * @param aWindow a message manager.
+   */
   forMessageManager: function(aMessageManager) {
     return new DispatcherDelegate(null, aMessageManager);
   },
 
   receiveMessage: function(aMsg) {
     // aMsg.data includes keys: global, event, data, uuid
     let callback;
     if (aMsg.data.uuid) {