Bug 1367077 - 1. Move startup utility functions into GeckoViewUtils; r=snorp
☠☠ backed out by 763af7d6686d ☠ ☠
authorJim Chen <nchen@mozilla.com>
Wed, 13 Sep 2017 11:51:55 -0400
changeset 430084 3af67a62be1bddd81d4900300fbfcefe4e056466
parent 430083 93c0ebba5978da90527217f2c28319d78e65759d
child 430085 215f47ca940c83a7f3be50859397d120464046da
push id7761
push userjlund@mozilla.com
push dateFri, 15 Sep 2017 00:19:52 +0000
treeherdermozilla-beta@c38455951db4 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewerssnorp
bugs1367077
milestone57.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
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/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/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,162 @@
+/* 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));
+    }
+
+    if (ppmm || mm) {
+      let target = ppmm ? Services.ppmm : Services.mm;
+      let listener = msg => {
+        target.removeMessageListener(msg.name, listener);
+        if (!once) {
+          target.addMessageListener(msg.name, scope[name]);
+        }
+        scope[name].receiveMessage(msg);
+      };
+      (ppmm || mm).forEach(msg => target.addMessageListener(msg, listener));
+    }
+
+    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
@@ -8,10 +8,11 @@ EXTRA_JS_MODULES += [
     'AndroidLog.jsm',
     'GeckoViewContent.jsm',
     'GeckoViewContentModule.jsm',
     'GeckoViewModule.jsm',
     'GeckoViewNavigation.jsm',
     'GeckoViewProgress.jsm',
     'GeckoViewScroll.jsm',
     'GeckoViewSettings.jsm',
+    'GeckoViewUtils.jsm',
     'Messaging.jsm',
 ]