Bug 963490 - Implement a b2g component to easily interact with all frames. r=vingtetun, r=janx, r=jryans
authorAlexandre Poirot <poirot.alex@gmail.com>
Mon, 30 Jun 2014 06:16:00 -0400
changeset 191503 c94f041648c656e75e005f81c51fee4acff4363f
parent 191502 78517fe424721a392e0d7f78cdcfb315036a55a4
child 191504 1b526b7613379e2f78e7777c63711cc18bc83777
push id27052
push userkwierso@gmail.com
push dateTue, 01 Jul 2014 01:22:26 +0000
treeherdermozilla-central@a3af97c421d3 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersvingtetun, janx, jryans
bugs963490
milestone33.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 963490 - Implement a b2g component to easily interact with all frames. r=vingtetun, r=janx, r=jryans
b2g/chrome/content/devtools.js
b2g/components/AppFrames.jsm
b2g/components/SystemAppProxy.jsm
b2g/components/moz.build
toolkit/devtools/server/actors/webapps.js
--- a/b2g/chrome/content/devtools.js
+++ b/b2g/chrome/content/devtools.js
@@ -22,26 +22,26 @@ XPCOMUtils.defineLazyGetter(this, 'WebCo
 XPCOMUtils.defineLazyGetter(this, 'EventLoopLagFront', function() {
   return devtools.require('devtools/server/actors/eventlooplag').EventLoopLagFront;
 });
 
 XPCOMUtils.defineLazyGetter(this, 'MemoryFront', function() {
   return devtools.require('devtools/server/actors/memory').MemoryFront;
 });
 
+Cu.import('resource://gre/modules/AppFrames.jsm');
 
 /**
  * The Developer HUD is an on-device developer tool that displays widgets,
  * showing visual debug information about apps. Each widget corresponds to a
  * metric as tracked by a metric watcher (e.g. consoleWatcher).
  */
 let developerHUD = {
 
   _targets: new Map(),
-  _frames: new Map(),
   _client: null,
   _conn: null,
   _watchers: [],
   _logging: true,
 
   /**
    * This method registers a metric watcher that will watch one or more metrics
    * on app frames that are being tracked. A watcher must implement the
@@ -72,44 +72,36 @@ let developerHUD = {
     this._client = new DebuggerClient(transport);
 
     for (let w of this._watchers) {
       if (w.init) {
         w.init(this._client);
       }
     }
 
-    Services.obs.addObserver(this, 'remote-browser-shown', false);
-    Services.obs.addObserver(this, 'inprocess-browser-shown', false);
-    Services.obs.addObserver(this, 'message-manager-disconnect', false);
+    AppFrames.addObserver(this);
 
-    let systemapp = document.querySelector('#systemapp');
-    this.trackFrame(systemapp);
-
-    let frames = systemapp.contentWindow.document.querySelectorAll('iframe[mozapp]');
-    for (let frame of frames) {
+    for (let frame of AppFrames.list()) {
       this.trackFrame(frame);
     }
 
     SettingsListener.observe('hud.logging', this._logging, enabled => {
       this._logging = enabled;
     });
   },
 
   uninit: function dwp_uninit() {
     if (!this._client)
       return;
 
     for (let frame of this._targets.keys()) {
       this.untrackFrame(frame);
     }
 
-    Services.obs.removeObserver(this, 'remote-browser-shown');
-    Services.obs.removeObserver(this, 'inprocess-browser-shown');
-    Services.obs.removeObserver(this, 'message-manager-disconnect');
+    AppFrames.removeObserver(this);
 
     this._client.close();
     delete this._client;
   },
 
   /**
    * This method will ask all registered watchers to track and update metrics
    * on an app frame.
@@ -135,51 +127,22 @@ let developerHUD = {
         w.untrackTarget(target);
       }
 
       target.destroy();
       this._targets.delete(frame);
     }
   },
 
-  observe: function dwp_observe(subject, topic, data) {
-    if (!this._client)
-      return;
-
-    let frame;
-
-    switch(topic) {
+  onAppFrameCreated: function (frame, isFirstAppFrame) {
+    this.trackFrame(frame);
+  },
 
-      // listen for frame creation in OOP (device) as well as in parent process (b2g desktop)
-      case 'remote-browser-shown':
-      case 'inprocess-browser-shown':
-        let frameLoader = subject;
-        // get a ref to the app <iframe>
-        frameLoader.QueryInterface(Ci.nsIFrameLoader);
-        // Ignore notifications that aren't from a BrowserOrApp
-        if (!frameLoader.ownerIsBrowserOrAppFrame) {
-          return;
-        }
-        frame = frameLoader.ownerElement;
-        if (!frame.appManifestURL) // Ignore all frames but app frames
-          return;
-        this.trackFrame(frame);
-        this._frames.set(frameLoader.messageManager, frame);
-        break;
-
-      // Every time an iframe is destroyed, its message manager also is
-      case 'message-manager-disconnect':
-        let mm = subject;
-        frame = this._frames.get(mm);
-        if (!frame)
-          return;
-        this.untrackFrame(frame);
-        this._frames.delete(mm);
-        break;
-    }
+  onAppFrameDestroyed: function (frame, isLastAppFrame) {
+    this.untrackFrame(frame);
   },
 
   log: function dwp_log(message) {
     if (this._logging) {
       dump(DEVELOPER_HUD_LOG_PREFIX + ': ' + message + '\n');
     }
   }
 
new file mode 100644
--- /dev/null
+++ b/b2g/components/AppFrames.jsm
@@ -0,0 +1,136 @@
+/* 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';
+
+this.EXPORTED_SYMBOLS = ['AppFrames'];
+
+const Cu = Components.utils;
+const Ci = Components.interfaces;
+
+Cu.import('resource://gre/modules/Services.jsm');
+Cu.import('resource://gre/modules/SystemAppProxy.jsm');
+
+const listeners = [];
+
+const Observer = {
+  // Save a map of (MessageManager => Frame) to be able to dispatch
+  // the FrameDestroyed event with a frame reference.
+  _frames: new Map(),
+
+  // Also save current number of iframes opened by app
+  _apps: new Map(),
+
+  start: function () {
+    Services.obs.addObserver(this, 'remote-browser-shown', false);
+    Services.obs.addObserver(this, 'inprocess-browser-shown', false);
+    Services.obs.addObserver(this, 'message-manager-disconnect', false);
+
+    SystemAppProxy.getAppFrames().forEach((frame) => {
+      let mm = frame.QueryInterface(Ci.nsIFrameLoaderOwner).frameLoader.messageManager;
+      this._frames.set(mm, frame);
+      let mozapp = frame.getAttribute('mozapp');
+      this._apps.set(mozapp, (this._apps.get(mozapp) || 0) + 1);
+    });
+  },
+
+  stop: function () {
+    Services.obs.removeObserver(this, 'remote-browser-shown');
+    Services.obs.removeObserver(this, 'inprocess-browser-shown');
+    Services.obs.removeObserver(this, 'message-manager-disconnect');
+    this._frames.clear();
+    this._apps.clear();
+  },
+
+  observe: function (subject, topic, data) {
+    switch(topic) {
+
+      // Listen for frame creation in OOP (device) as well as in parent process (b2g desktop)
+      case 'remote-browser-shown':
+      case 'inprocess-browser-shown':
+        let frameLoader = subject;
+
+        // get a ref to the app <iframe>
+        frameLoader.QueryInterface(Ci.nsIFrameLoader);
+        let frame = frameLoader.ownerElement;
+        let mm = frame.QueryInterface(Ci.nsIFrameLoaderOwner).frameLoader.messageManager;
+        this.onMessageManagerCreated(mm, frame);
+        break;
+
+      // Every time an iframe is destroyed, its message manager also is
+      case 'message-manager-disconnect':
+        this.onMessageManagerDestroyed(subject);
+        break;
+    }
+  },
+
+  onMessageManagerCreated: function (mm, frame) {
+    this._frames.set(mm, frame);
+
+    let mozapp = frame.getAttribute('mozapp');
+    let count = (this._apps.get(mozapp) || 0) + 1;
+    this._apps.set(mozapp, count);
+
+    let isFirstAppFrame = (count === 1);
+    listeners.forEach(function (listener) {
+      try {
+        listener.onAppFrameCreated(frame, isFirstAppFrame);
+      } catch(e) {
+        dump('Exception while calling Frames.jsm listener:' + e + '\n' + e.stack + '\n');
+      }
+    });
+  },
+
+  onMessageManagerDestroyed: function (mm) {
+    let frame = this._frames.get(mm);
+    if (!frame) {
+      // We receive an event for a non mozapp message manager
+      return;
+    }
+
+    this._frames.delete(mm);
+
+    let mozapp = frame.getAttribute('mozapp');
+    let count = (this._apps.get(mozapp) || 0) - 1;
+    this._apps.set(mozapp, count);
+
+    let isLastAppFrame = (count === 0);
+    listeners.forEach(function (listener) {
+      try {
+        listener.onAppFrameDestroyed(frame, isLastAppFrame);
+      } catch(e) {
+        dump('Exception while calling Frames.jsm listener:' + e + '\n' + e.stack + '\n');
+      }
+    });
+  }
+
+};
+
+const AppFrames = this.AppFrames = {
+
+  list: () => SystemAppProxy.getAppFrames(),
+
+  addObserver: function (listener) {
+    if (listeners.indexOf(listener) !== -1) {
+      return;
+    }
+
+    listeners.push(listener);
+    if (listeners.length == 1) {
+      Observer.start();
+    }
+  },
+
+  removeObserver: function (listener) {
+    let idx = listeners.indexOf(listener);
+    if (idx !== -1) {
+      listeners.splice(idx, 1);
+    }
+    if (listeners.length === 0) {
+      Observer.stop();
+    }
+  }
+
+};
+
--- a/b2g/components/SystemAppProxy.jsm
+++ b/b2g/components/SystemAppProxy.jsm
@@ -110,13 +110,32 @@ let SystemAppProxy = {
     if (content) {
       content.removeEventListener.apply(content, arguments);
     } else {
       let idx = this._pendingListeners.indexOf(listener);
       if (idx != -1) {
         this._pendingListeners.splice(idx, 1);
       }
     }
+  },
+
+  getAppFrames: function systemApp_getAppFrames() {
+    let systemAppFrame = this._frame;
+    if (!systemAppFrame) {
+      return [];
+    }
+
+    let list = [systemAppFrame];
+
+    // List all app frames hosted in the system app: the homescreen,
+    // all regular apps, activities, rocket bar, attention screen and the keyboard.
+    // Bookmark apps and other system app internal frames like captive portal
+    // are also hosted in system app, but they are not using mozapp attribute.
+    let frames = systemAppFrame.contentDocument.querySelectorAll("iframe[mozapp]");
+    for (let i = 0; i < frames.length; i++) {
+      list.push(frames[i]);
+    }
+
+    return list;
   }
-
 };
 this.SystemAppProxy = SystemAppProxy;
 
--- a/b2g/components/moz.build
+++ b/b2g/components/moz.build
@@ -40,16 +40,17 @@ EXTRA_PP_COMPONENTS += [
 
 if CONFIG['MOZ_UPDATER']:
     EXTRA_PP_COMPONENTS += [
         'UpdatePrompt.js',
     ]
 
 EXTRA_JS_MODULES += [
     'AlertsHelper.jsm',
+    'AppFrames.jsm',
     'ContentRequestHelper.jsm',
     'ErrorPage.jsm',
     'SignInToWebsite.jsm',
     'SystemAppProxy.jsm',
     'TelURIParser.jsm',
     'WebappsUpdater.jsm',
 ]
 
--- a/toolkit/devtools/server/actors/webapps.js
+++ b/toolkit/devtools/server/actors/webapps.js
@@ -11,16 +11,23 @@ let CC = Components.Constructor;
 
 Cu.import("resource://gre/modules/NetUtil.jsm");
 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
 Cu.import("resource://gre/modules/osfile.jsm");
 Cu.import("resource://gre/modules/FileUtils.jsm");
 
 let {Promise: promise} = Cu.import("resource://gre/modules/Promise.jsm", {});
 
+DevToolsUtils.defineLazyGetter(this, "AppFrames", () => {
+  try {
+    return Cu.import("resource://gre/modules/AppFrames.jsm", {}).AppFrames;
+  } catch(e) {}
+  return null;
+});
+
 function debug(aMsg) {
   /*
   Cc["@mozilla.org/consoleservice;1"]
     .getService(Ci.nsIConsoleService)
     .logStringMessage("--*-- WebappsActor : " + aMsg);
   */
 }
 
@@ -836,66 +843,71 @@ WebappsActor.prototype = {
     }
 
     reg.close(app);
 
     return {};
   },
 
   _appFrames: function () {
-    // For now, we only support app frames on b2g
-    if (Services.appinfo.ID != "{3c2e2abc-06d4-11e1-ac3b-374f68613e61}") {
-      return;
-    }
-    // Register the system app
-    let chromeWindow = Services.wm.getMostRecentWindow('navigator:browser');
-    let systemAppFrame = chromeWindow.shell.contentBrowser;
-    yield systemAppFrame;
-
-    // Register apps hosted in the system app. i.e. the homescreen, all regular
-    // apps and the keyboard.
-    // Bookmark apps and other system app internal frames like captive portal
-    // are also hosted in system app, but they are not using mozapp attribute.
-    let frames = systemAppFrame.contentDocument.querySelectorAll("iframe[mozapp]");
-    for (let i = 0; i < frames.length; i++) {
-      yield frames[i];
+    // Try to filter on b2g and mulet
+    if (AppFrames) {
+      return AppFrames.list();
+    } else {
+      return [];
     }
   },
 
   listRunningApps: function (aRequest) {
     debug("listRunningApps\n");
 
     let appPromises = [];
     let apps = [];
 
-    for each (let frame in this._appFrames()) {
+    for (let frame of this._appFrames()) {
       let manifestURL = frame.getAttribute("mozapp");
 
+      // _appFrames can return more than one frame with the same manifest url
+      if (apps.indexOf(manifestURL) != -1) {
+        continue;
+      }
+
       appPromises.push(this._isAppAllowedForURL(manifestURL).then(allowed => {
         if (allowed) {
           apps.push(manifestURL);
         }
       }));
     }
 
     return promise.all(appPromises).then(() => {
       return { apps: apps };
     });
   },
 
   getAppActor: function ({ manifestURL }) {
     debug("getAppActor\n");
 
+    // Connects to the main app frame, whose `name` attribute
+    // is set to 'main' by gaia. If for any reason, gaia doesn't set any
+    // frame as main, no frame matches, then we connect arbitrary
+    // to the first app frame...
     let appFrame = null;
-    for each (let frame in this._appFrames()) {
+    let frames = [];
+    for (let frame of this._appFrames()) {
       if (frame.getAttribute("mozapp") == manifestURL) {
-        appFrame = frame;
-        break;
+        if (frame.name == "main") {
+          appFrame = frame;
+          break;
+        }
+        frames.push(frame);
       }
     }
+    if (!appFrame && frames.length > 0) {
+      appFrame = frames[0];
+    }
 
     let notFoundError = {
       error: "appNotFound",
       message: "Unable to find any opened app whose manifest " +
                "is '" + manifestURL + "'"
     };
 
     if (!appFrame) {
@@ -926,84 +938,76 @@ WebappsActor.prototype = {
                              .then(onConnect);
       }
 
       return { actor: actor };
     });
   },
 
   watchApps: function () {
-    this._openedApps = new Set();
     // For now, app open/close events are only implement on b2g
-    if (Services.appinfo.ID == "{3c2e2abc-06d4-11e1-ac3b-374f68613e61}") {
-      let chromeWindow = Services.wm.getMostRecentWindow('navigator:browser');
-      let systemAppFrame = chromeWindow.getContentWindow();
-      systemAppFrame.addEventListener("appwillopen", this);
-      systemAppFrame.addEventListener("appterminated", this);
+    if (AppFrames) {
+      AppFrames.addObserver(this);
     }
     Services.obs.addObserver(this, "webapps-installed", false);
     Services.obs.addObserver(this, "webapps-uninstall", false);
 
     return {};
   },
 
   unwatchApps: function () {
-    this._openedApps = null;
-    if (Services.appinfo.ID == "{3c2e2abc-06d4-11e1-ac3b-374f68613e61}") {
-      let chromeWindow = Services.wm.getMostRecentWindow('navigator:browser');
-      let systemAppFrame = chromeWindow.getContentWindow();
-      systemAppFrame.removeEventListener("appwillopen", this);
-      systemAppFrame.removeEventListener("appterminated", this);
+    if (AppFrames) {
+      AppFrames.removeObserver(this);
     }
     Services.obs.removeObserver(this, "webapps-installed", false);
     Services.obs.removeObserver(this, "webapps-uninstall", false);
 
     return {};
   },
 
-  handleEvent: function (event) {
-    let manifestURL;
-    switch(event.type) {
-      case "appwillopen":
-        manifestURL = event.detail.manifestURL;
+  onAppFrameCreated: function (frame, isFirstAppFrame) {
+    if (!isFirstAppFrame) {
+      return;
+    }
 
-        // Ignore the event if we already received an appwillopen for this app
-        // (appwillopen is also fired when the app has been moved to background
-        // and get back to foreground)
-        if (this._openedApps.has(manifestURL)) {
-          return;
-        }
-        this._openedApps.add(manifestURL);
+    let manifestURL = frame.appManifestURL;
+    // Only track app frames
+    if (!manifestURL) {
+      return;
+    }
 
-        this._isAppAllowedForURL(manifestURL).then(allowed => {
-          if (allowed) {
-            this.conn.send({ from: this.actorID,
-                             type: "appOpen",
-                             manifestURL: manifestURL
-                           });
-          }
-        });
+    this._isAppAllowedForURL(manifestURL).then(allowed => {
+      if (allowed) {
+        this.conn.send({ from: this.actorID,
+                         type: "appOpen",
+                         manifestURL: manifestURL
+                       });
+      }
+    });
+  },
 
-        break;
+  onAppFrameDestroyed: function (frame, isLastAppFrame) {
+    if (!isLastAppFrame) {
+      return;
+    }
 
-      case "appterminated":
-        manifestURL = event.detail.manifestURL;
-        this._openedApps.delete(manifestURL);
+    let manifestURL = frame.appManifestURL;
+    // Only track app frames
+    if (!manifestURL) {
+      return;
+    }
 
-        this._isAppAllowedForURL(manifestURL).then(allowed => {
-          if (allowed) {
-            this.conn.send({ from: this.actorID,
-                             type: "appClose",
-                             manifestURL: manifestURL
-                           });
-          }
-        });
-
-        break;
-    }
+    this._isAppAllowedForURL(manifestURL).then(allowed => {
+      if (allowed) {
+        this.conn.send({ from: this.actorID,
+                         type: "appClose",
+                         manifestURL: manifestURL
+                       });
+      }
+    });
   },
 
   observe: function (subject, topic, data) {
     let app = JSON.parse(data);
     if (topic == "webapps-installed") {
       this.conn.send({ from: this.actorID,
                        type: "appInstall",
                        manifestURL: app.manifestURL