Bug 1367077 - 1. Move startup utility functions into GeckoViewUtils; r=snorp draft
authorJim Chen <nchen@mozilla.com>
Fri, 15 Sep 2017 14:44:49 -0400
changeset 665599 a966e2ee0dae223cbdf14679938d1db5ad0a126f
parent 665088 8e818b5e9b6bef0fc1a5c527ecf30b0d56a02f14
child 665600 1521490e5d2c13650a951251666fe00f038b8c14
push id80116
push userbmo:nchen@mozilla.com
push dateFri, 15 Sep 2017 18:46:16 +0000
reviewerssnorp
bugs1367077
milestone57.0a1
Bug 1367077 - 1. Move startup utility functions into GeckoViewUtils; r=snorp Move `addLazyGetter` and `addLazyEventListener` utility functions from GeckoViewStartup.js into GeckoViewUtils.jsm, so they can be used for both Fennec and standalone GeckoView. Also switch to "chrome-document-loaded" for loading DownloadNotifications because that's later in the startup sequence. MozReview-Commit-ID: 1caMtufkHGR
mobile/android/chrome/content/WebrtcUI.js
mobile/android/components/BrowserCLH.js
mobile/android/components/geckoview/GeckoViewStartup.js
mobile/android/modules/DownloadNotifications.jsm
mobile/android/modules/geckoview/GeckoViewUtils.jsm
mobile/android/modules/geckoview/moz.build
--- a/mobile/android/chrome/content/WebrtcUI.js
+++ b/mobile/android/chrome/content/WebrtcUI.js
@@ -310,17 +310,17 @@ var WebrtcUI = {
       requestType = "Camera";
     else
       return;
 
     let chromeWin = this.getChromeWindow(aContentWindow);
     let uri = aContentWindow.document.documentURIObject;
     let host = uri.host;
     let requestor = (chromeWin.BrowserApp && chromeWin.BrowserApp.manifest) ?
-          "'" + BrowserApp.manifest.name + "'" : host;
+          "'" + chromeWin.BrowserApp.manifest.name + "'" : host;
     let message = Strings.browser.formatStringFromName("getUserMedia.share" + requestType + ".message", [ requestor ], 1);
 
     let options = { inputs: [] };
     if (videoDevices.length > 1 || audioDevices.length > 0) {
       // videoSource is both the string used for l10n lookup and the object that will be returned
       this._addDevicesToOptions(videoDevices, "videoSource", options);
     }
 
--- a/mobile/android/components/BrowserCLH.js
+++ b/mobile/android/components/BrowserCLH.js
@@ -1,21 +1,21 @@
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
-const Cc = Components.classes;
-const Ci = Components.interfaces;
-const Cu = Components.utils;
+const {classes: Cc, interfaces: Ci, utils: Cu} = Components;
 
 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
-Cu.import("resource://gre/modules/Services.jsm");
 
-XPCOMUtils.defineLazyModuleGetter(this, "AppConstants",
-                                  "resource://gre/modules/AppConstants.jsm");
+XPCOMUtils.defineLazyModuleGetters(this, {
+  AppConstants: "resource://gre/modules/AppConstants.jsm",
+  GeckoViewUtils: "resource://gre/modules/GeckoViewUtils.jsm",
+  Services: "resource://gre/modules/Services.jsm",
+});
 
 var Strings = {};
 
 XPCOMUtils.defineLazyGetter(Strings, "brand", _ =>
         Services.strings.createBundle("chrome://branding/locale/brand.properties"));
 XPCOMUtils.defineLazyGetter(Strings, "browser", _ =>
         Services.strings.createBundle("chrome://browser/locale/browser.properties"));
 XPCOMUtils.defineLazyGetter(Strings, "reader", _ =>
@@ -35,68 +35,41 @@ BrowserCLH.prototype = {
     let url = registry.convertChromeURL(Services.io.newURI("chrome://browser/content/aboutHome.xhtml")).spec;
     // Like jar:file:///data/app/org.mozilla.fennec-2.apk!/
     url = url.substring(4, url.indexOf("!/") + 2);
 
     let protocolHandler = Services.io.getProtocolHandler("resource").QueryInterface(Ci.nsIResProtocolHandler);
     protocolHandler.setSubstitution("android", Services.io.newURI(url));
   },
 
-  addObserverScripts: function(aScripts) {
-    aScripts.forEach(item => {
-      let {name, topics, script} = item;
-      XPCOMUtils.defineLazyGetter(this, name, _ => {
-        let sandbox = {};
-        if (script.endsWith(".jsm")) {
-          Cu.import(script, sandbox);
-        } else {
-          Services.scriptloader.loadSubScript(script, sandbox);
-        }
-        return sandbox[name];
-      });
-      let observer = (subject, topic, data) => {
-        Services.obs.removeObserver(observer, topic);
-        if (!item.once) {
-          Services.obs.addObserver(this[name], topic);
-        }
-        this[name].observe(subject, topic, data); // Explicitly notify new observer
-      };
-      topics.forEach(topic => {
-        Services.obs.addObserver(observer, topic);
-      });
-    });
-  },
-
   observe: function(subject, topic, data) {
     switch (topic) {
-      case "app-startup":
+      case "app-startup": {
         this.setResourceSubstitutions();
 
-        let observerScripts = [{
-          name: "DownloadNotifications",
-          script: "resource://gre/modules/DownloadNotifications.jsm",
-          topics: ["chrome-document-interactive"],
+        GeckoViewUtils.addLazyGetter(this, "DownloadNotifications", {
+          module: "resource://gre/modules/DownloadNotifications.jsm",
+          observers: ["chrome-document-loaded"],
           once: true,
-        }];
+        });
         if (AppConstants.MOZ_WEBRTC) {
-          observerScripts.push({
-            name: "WebrtcUI",
+          GeckoViewUtils.addLazyGetter(this, "WebrtcUI", {
             script: "chrome://browser/content/WebrtcUI.js",
-            topics: [
+            observers: [
               "getUserMedia:ask-device-permission",
               "getUserMedia:request",
               "PeerConnection:request",
               "recording-device-events",
               "VideoCapture:Paused",
               "VideoCapture:Resumed",
             ],
           });
         }
-        this.addObserverScripts(observerScripts);
         break;
+      }
     }
   },
 
   // QI
   QueryInterface: XPCOMUtils.generateQI([Ci.nsIObserver]),
 
   // XPCOMUtils factory
   classID: Components.ID("{be623d20-d305-11de-8a39-0800200c9a66}")
--- a/mobile/android/components/geckoview/GeckoViewStartup.js
+++ b/mobile/android/components/geckoview/GeckoViewStartup.js
@@ -1,150 +1,87 @@
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 const {classes: Cc, interfaces: Ci, utils: Cu} = Components;
 
 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
 
-XPCOMUtils.defineLazyModuleGetter(this, "Services",
-                                  "resource://gre/modules/Services.jsm");
+XPCOMUtils.defineLazyModuleGetters(this, {
+  GeckoViewUtils: "resource://gre/modules/GeckoViewUtils.jsm",
+  Services: "resource://gre/modules/Services.jsm",
+});
 
 function GeckoViewStartup() {
 }
 
 GeckoViewStartup.prototype = {
   classID: Components.ID("{8e993c34-fdd6-432c-967e-f995d888777f}"),
 
   QueryInterface: XPCOMUtils.generateQI([Ci.nsIObserver]),
 
-  addLazyGetter: function({name, script, service, module,
-                           observers, ppmm, mm, init, once}) {
-    if (script) {
-      XPCOMUtils.defineLazyScriptGetter(this, name, script);
-    } else if (module) {
-      XPCOMUtils.defineLazyGetter(this, name, _ => {
-        let sandbox = {};
-        Cu.import(module, sandbox);
-        if (init) {
-          init.call(this, sandbox[name]);
-        }
-        return sandbox[name];
-      });
-    } else if (service) {
-      XPCOMUtils.defineLazyGetter(this, name, _ =>
-        Cc[service].getService(Ci.nsISupports).wrappedJSObject);
-    }
-
-    if (observers) {
-      let observer = (subject, topic, data) => {
-        Services.obs.removeObserver(observer, topic);
-        if (!once) {
-          Services.obs.addObserver(this[name], topic);
-        }
-        this[name].observe(subject, topic, data); // Explicitly notify new observer
-      };
-      observers.forEach(topic => Services.obs.addObserver(observer, topic));
-    }
-
-    if (ppmm || mm) {
-      let target = ppmm ? Services.ppmm : Services.mm;
-      let listener = msg => {
-        target.removeMessageListener(msg.name, listener);
-        if (!once) {
-          target.addMessageListener(msg.name, this[name]);
-        }
-        this[name].receiveMessage(msg);
-      };
-      (ppmm || mm).forEach(msg => target.addMessageListener(msg, listener));
-    }
-  },
-
-  addLazyEventListener: function({name, target, events, options}) {
-    let listener = event => {
-      if (!options || !options.once) {
-        target.removeEventListener(event.type, listener, options);
-        target.addEventListener(event.type, this[name], options);
-      }
-      this[name].handleEvent(event);
-    };
-    events.forEach(event => target.addEventListener(event, listener, options));
-  },
-
   /* ----------  nsIObserver  ---------- */
   observe: function(aSubject, aTopic, aData) {
     switch (aTopic) {
       case "app-startup": {
         // Parent and content process.
         Services.obs.addObserver(this, "chrome-document-global-created");
         Services.obs.addObserver(this, "content-document-global-created");
 
-        this.addLazyGetter({
-          name: "GeckoViewPermission",
+        GeckoViewUtils.addLazyGetter(this, "GeckoViewPermission", {
           service: "@mozilla.org/content-permission/prompt;1",
           observers: [
             "getUserMedia:ask-device-permission",
             "getUserMedia:request",
             "PeerConnection:request",
           ],
         });
 
         if (Services.appinfo.processType != Services.appinfo.PROCESS_TYPE_DEFAULT) {
           // Content process only.
-          this.addLazyGetter({
-            name: "GeckoViewPrompt",
+          GeckoViewUtils.addLazyGetter(this, "GeckoViewPrompt", {
             service: "@mozilla.org/prompter;1",
           });
         }
         break;
       }
 
       case "profile-after-change": {
         // Parent process only.
         // ContentPrefServiceParent is needed for e10s file picker.
-        this.addLazyGetter({
-          name: "ContentPrefServiceParent",
+        GeckoViewUtils.addLazyGetter(this, "ContentPrefServiceParent", {
           module: "resource://gre/modules/ContentPrefServiceParent.jsm",
           init: cpsp => cpsp.alwaysInit(),
           ppmm: [
             "ContentPrefs:FunctionCall",
             "ContentPrefs:AddObserverForName",
             "ContentPrefs:RemoveObserverForName",
           ],
         });
 
-        this.addLazyGetter({
-          name: "GeckoViewPrompt",
+        GeckoViewUtils.addLazyGetter(this, "GeckoViewPrompt", {
           service: "@mozilla.org/prompter;1",
           mm: [
             "GeckoView:Prompt",
           ],
         });
         break;
       }
 
       case "chrome-document-global-created":
       case "content-document-global-created": {
-        let win = aSubject.QueryInterface(Ci.nsIInterfaceRequestor)
-                          .getInterface(Ci.nsIDocShell).QueryInterface(Ci.nsIDocShellTreeItem)
-                          .rootTreeItem.QueryInterface(Ci.nsIInterfaceRequestor)
-                          .getInterface(Ci.nsIDOMWindow);
+        let win = GeckoViewUtils.getChromeWindow(aSubject);
         if (win !== aSubject) {
           // Only attach to top-level windows.
           return;
         }
 
-        this.addLazyEventListener({
-          name: "GeckoViewPrompt",
-          target: win,
-          events: [
-            "click",
-            "contextmenu",
-          ],
+        GeckoViewUtils.addLazyEventListener(win, ["click", "contextmenu"], {
+          handler: _ => this.GeckoViewPrompt,
           options: {
             capture: false,
             mozSystemGroup: true,
           },
         });
         break;
       }
     }
--- a/mobile/android/modules/DownloadNotifications.jsm
+++ b/mobile/android/modules/DownloadNotifications.jsm
@@ -42,17 +42,17 @@ const kButtons = {
 };
 
 var notifications = new Map();
 
 var DownloadNotifications = {
   _notificationKey: "downloads",
 
   observe: function(subject, topic, data) {
-    if (topic === "chrome-document-interactive") {
+    if (topic === "chrome-document-loaded") {
       this.init();
     }
   },
 
   init: function() {
     Downloads.getList(Downloads.ALL)
              .then(list => list.addView(this))
              .then(() => this._viewAdded = true, Cu.reportError);
new file mode 100644
--- /dev/null
+++ b/mobile/android/modules/geckoview/GeckoViewUtils.jsm
@@ -0,0 +1,167 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+"use strict"
+
+const { classes: Cc, interfaces: Ci, utils: Cu } = Components;
+
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+
+XPCOMUtils.defineLazyModuleGetters(this, {
+  Services: "resource://gre/modules/Services.jsm",
+  EventDispatcher: "resource://gre/modules/Messaging.jsm",
+});
+
+this.EXPORTED_SYMBOLS = ["GeckoViewUtils"];
+
+var GeckoViewUtils = {
+  /**
+   * Define a lazy getter that loads an object from external code, and
+   * optionally handles observer and/or message manager notifications for the
+   * object, so the object only loads when a notification is received.
+   *
+   * @param scope     Scope for holding the loaded object.
+   * @param name      Name of the object to load.
+   * @param script    If specified, load the object from a JS subscript.
+   * @param service   If specified, load the object from a JS component; the
+   *                  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.
+   */
+  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;
+        if (module) {
+          ret = Cu.import(module, {})[name];
+        } else if (service) {
+          ret = Cc[service].getService(Ci.nsISupports).wrappedJSObject;
+        } else if (typeof handler === "function") {
+          ret = {
+            handleEvent: handler,
+            observe: handler,
+            onEvent: handler,
+            receiveMessage: handler,
+          };
+        } else if (handler) {
+          ret = handler;
+        }
+        if (ret && init) {
+          init.call(scope, ret);
+        }
+        return ret;
+      });
+    }
+
+    if (observers) {
+      let observer = (subject, topic, data) => {
+        Services.obs.removeObserver(observer, topic);
+        if (!once) {
+          Services.obs.addObserver(scope[name], topic);
+        }
+        scope[name].observe(subject, topic, data); // Explicitly notify new observer
+      };
+      observers.forEach(topic => Services.obs.addObserver(observer, topic));
+    }
+
+    let addMMListener = (target, names) => {
+      let listener = msg => {
+        target.removeMessageListener(msg.name, listener);
+        if (!once) {
+          target.addMessageListener(msg.name, scope[name]);
+        }
+        scope[name].receiveMessage(msg);
+      };
+      names.forEach(msg => target.addMessageListener(msg, listener));
+    };
+    if (ppmm) {
+      addMMListener(Services.ppmm, ppmm);
+    }
+    if (mm) {
+      addMMListener(Services.mm, mm);
+    }
+
+    if (ged) {
+      let listener = (event, data, callback) => {
+        EventDispatcher.instance.unregisterListener(listener, event);
+        if (!once) {
+          EventDispatcher.instance.registerListener(scope[name], event);
+        }
+        scope[name].onEvent(event, data, callback);
+      };
+      EventDispatcher.instance.registerListener(listener, ged);
+    }
+  },
+
+  /**
+   * 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);
+    }
+  },
+
+  /**
+   * 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)
+               .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);
+  },
+};
--- a/mobile/android/modules/geckoview/moz.build
+++ b/mobile/android/modules/geckoview/moz.build
@@ -9,10 +9,11 @@ EXTRA_JS_MODULES += [
     'GeckoViewContent.jsm',
     'GeckoViewContentModule.jsm',
     'GeckoViewModule.jsm',
     'GeckoViewNavigation.jsm',
     'GeckoViewProgress.jsm',
     'GeckoViewScroll.jsm',
     'GeckoViewSettings.jsm',
     'GeckoViewTab.jsm',
+    'GeckoViewUtils.jsm',
     'Messaging.jsm',
 ]