Bug 915884 - Add tests for Inter-App Communication API (part 1, create InterAppCommService.jsm). r=ferjm
authorGene Lian <clian@mozilla.com>
Mon, 02 Dec 2013 14:45:33 +0800
changeset 180301 223ae5cffe53969e49db04c28045b74ade4c581f
parent 180300 f21ca5a42ae7cfb8643be7f26c8761b48d23575d
child 180302 9917930779446e7dfd814b396e5b16b26da20423
push id272
push userpvanderbeken@mozilla.com
push dateMon, 05 May 2014 16:31:18 +0000
reviewersferjm
bugs915884
milestone31.0a1
Bug 915884 - Add tests for Inter-App Communication API (part 1, create InterAppCommService.jsm). r=ferjm
dom/apps/src/InterAppCommService.js
dom/apps/src/InterAppCommService.jsm
dom/apps/src/moz.build
--- a/dom/apps/src/InterAppCommService.js
+++ b/dom/apps/src/InterAppCommService.js
@@ -1,874 +1,28 @@
 /* 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, results: Cr } = Components;
 
-Cu.import("resource://gre/modules/Services.jsm");
 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
-Cu.import("resource://gre/modules/AppsUtils.jsm");
+Cu.import("resource://gre/modules/InterAppCommService.jsm");
 
-const DEBUG = false;
-function debug(aMsg) {
-  dump("-- InterAppCommService: " + Date.now() + ": " + aMsg + "\n");
+function InterAppCommServiceProxy() {
 }
 
-XPCOMUtils.defineLazyServiceGetter(this, "appsService",
-                                   "@mozilla.org/AppsService;1",
-                                   "nsIAppsService");
-
-XPCOMUtils.defineLazyServiceGetter(this, "ppmm",
-                                   "@mozilla.org/parentprocessmessagemanager;1",
-                                   "nsIMessageBroadcaster");
-
-XPCOMUtils.defineLazyServiceGetter(this, "UUIDGenerator",
-                                   "@mozilla.org/uuid-generator;1",
-                                   "nsIUUIDGenerator");
-
-XPCOMUtils.defineLazyServiceGetter(this, "messenger",
-                                   "@mozilla.org/system-message-internal;1",
-                                   "nsISystemMessagesInternal");
-
-const kMessages =["Webapps:Connect",
-                  "Webapps:GetConnections",
-                  "InterAppConnection:Cancel",
-                  "InterAppMessagePort:PostMessage",
-                  "InterAppMessagePort:Register",
-                  "InterAppMessagePort:Unregister",
-                  "child-process-shutdown"];
-
-function InterAppCommService() {
-  Services.obs.addObserver(this, "xpcom-shutdown", false);
-  Services.obs.addObserver(this, "inter-app-comm-select-app-result", false);
-
-  kMessages.forEach(function(aMsg) {
-    ppmm.addMessageListener(aMsg, this);
-  }, this);
-
-  // This matrix is used for saving the inter-app connection info registered in
-  // the app manifest. The object literal is defined as below:
-  //
-  // {
-  //   "keyword1": {
-  //     "subAppManifestURL1": {
-  //       /* subscribed info */
-  //     },
-  //     "subAppManifestURL2": {
-  //       /* subscribed info */
-  //     },
-  //     ...
-  //   },
-  //   "keyword2": {
-  //     "subAppManifestURL3": {
-  //       /* subscribed info */
-  //     },
-  //     ...
-  //   },
-  //   ...
-  // }
-  //
-  // For example:
-  //
-  // {
-  //   "foo": {
-  //     "app://subApp1.gaiamobile.org/manifest.webapp": {
-  //       pageURL: "app://subApp1.gaiamobile.org/handler.html",
-  //       description: "blah blah",
-  //       rules: { ... }
-  //     },
-  //     "app://subApp2.gaiamobile.org/manifest.webapp": {
-  //       pageURL: "app://subApp2.gaiamobile.org/handler.html",
-  //       description: "blah blah",
-  //       rules: { ... }
-  //     }
-  //   },
-  //   "bar": {
-  //     "app://subApp3.gaiamobile.org/manifest.webapp": {
-  //       pageURL: "app://subApp3.gaiamobile.org/handler.html",
-  //       description: "blah blah",
-  //       rules: { ... }
-  //     }
-  //   }
-  // }
-  //
-  // TODO Bug 908999 - Update registered connections when app gets uninstalled.
-  this._registeredConnections = {};
-
-  // This matrix is used for saving the permitted connections, which allows
-  // the messaging between publishers and subscribers. The object literal is
-  // defined as below:
-  //
-  // {
-  //   "keyword1": {
-  //     "pubAppManifestURL1": [
-  //       "subAppManifestURL1",
-  //       "subAppManifestURL2",
-  //       ...
-  //     ],
-  //     "pubAppManifestURL2": [
-  //       "subAppManifestURL3",
-  //       "subAppManifestURL4",
-  //       ...
-  //     ],
-  //     ...
-  //   },
-  //   "keyword2": {
-  //     "pubAppManifestURL3": [
-  //       "subAppManifestURL5",
-  //       ...
-  //     ],
-  //     ...
-  //   },
-  //   ...
-  // }
-  //
-  // For example:
-  //
-  // {
-  //   "foo": {
-  //     "app://pubApp1.gaiamobile.org/manifest.webapp": [
-  //       "app://subApp1.gaiamobile.org/manifest.webapp",
-  //       "app://subApp2.gaiamobile.org/manifest.webapp"
-  //     ],
-  //     "app://pubApp2.gaiamobile.org/manifest.webapp": [
-  //       "app://subApp3.gaiamobile.org/manifest.webapp",
-  //       "app://subApp4.gaiamobile.org/manifest.webapp"
-  //     ]
-  //   },
-  //   "bar": {
-  //     "app://pubApp3.gaiamobile.org/manifest.webapp": [
-  //       "app://subApp5.gaiamobile.org/manifest.webapp",
-  //     ]
-  //   }
-  // }
-  //
-  // TODO Bug 908999 - Update allowed connections when app gets uninstalled.
-  this._allowedConnections = {};
-
-  // This matrix is used for saving the caller info from the content process,
-  // which is indexed by a random UUID, to know where to return the promise
-  // resolvser's callback when the prompt UI for allowing connections returns.
-  // An example of the object literal is shown as below:
-  //
-  // {
-  //   "fooID": {
-  //     outerWindowID: 12,
-  //     requestID: 34,
-  //     target: pubAppTarget1
-  //   },
-  //   "barID": {
-  //     outerWindowID: 56,
-  //     requestID: 78,
-  //     target: pubAppTarget2
-  //   }
-  // }
-  //
-  // where |outerWindowID| is the ID of the window requesting the connection,
-  //       |requestID| is the ID specifying the promise resolver to return,
-  //       |target| is the target of the process requesting the connection.
-  this._promptUICallers = {};
-
-  // This matrix is used for saving the pair of message ports, which is indexed
-  // by a random UUID, so that each port can know whom it should talk to.
-  // An example of the object literal is shown as below:
-  //
-  // {
-  //   "UUID1": {
-  //     keyword: "keyword1",
-  //     publisher: {
-  //       manifestURL: "app://pubApp1.gaiamobile.org/manifest.webapp",
-  //       target: pubAppTarget1,
-  //       pageURL: "app://pubApp1.gaiamobile.org/caller.html",
-  //       messageQueue: [...]
-  //     },
-  //     subscriber: {
-  //       manifestURL: "app://subApp1.gaiamobile.org/manifest.webapp",
-  //       target: subAppTarget1,
-  //       pageURL: "app://pubApp1.gaiamobile.org/handler.html",
-  //       messageQueue: [...]
-  //     }
-  //   },
-  //   "UUID2": {
-  //     keyword: "keyword2",
-  //     publisher: {
-  //       manifestURL: "app://pubApp2.gaiamobile.org/manifest.webapp",
-  //       target: pubAppTarget2,
-  //       pageURL: "app://pubApp2.gaiamobile.org/caller.html",
-  //       messageQueue: [...]
-  //     },
-  //     subscriber: {
-  //       manifestURL: "app://subApp2.gaiamobile.org/manifest.webapp",
-  //       target: subAppTarget2,
-  //       pageURL: "app://pubApp2.gaiamobile.org/handler.html",
-  //       messageQueue: [...]
-  //     }
-  //   }
-  // }
-  this._messagePortPairs = {};
-}
-
-InterAppCommService.prototype = {
+InterAppCommServiceProxy.prototype = {
   registerConnection: function(aKeyword, aHandlerPageURI, aManifestURI,
                                aDescription, aRules) {
-    let manifestURL = aManifestURI.spec;
-    let pageURL = aHandlerPageURI.spec;
-
-    if (DEBUG) {
-      debug("registerConnection: aKeyword: " + aKeyword +
-            " manifestURL: " + manifestURL + " pageURL: " + pageURL +
-            " aDescription: " + aDescription +
-            " aRules.minimumAccessLevel: " + aRules.minimumAccessLevel +
-            " aRules.manifestURLs: " + aRules.manifestURLs +
-            " aRules.installOrigins: " + aRules.installOrigins);
-    }
-
-    let subAppManifestURLs = this._registeredConnections[aKeyword];
-    if (!subAppManifestURLs) {
-      subAppManifestURLs = this._registeredConnections[aKeyword] = {};
-    }
-
-    subAppManifestURLs[manifestURL] = {
-      pageURL: pageURL,
-      description: aDescription,
-      rules: aRules,
-      manifestURL: manifestURL
-    };
-  },
-
-  _matchMinimumAccessLevel: function(aRules, aAppStatus) {
-    if (!aRules || !aRules.minimumAccessLevel) {
-      if (DEBUG) {
-        debug("rules.minimumAccessLevel is not available. No need to match.");
-      }
-      return true;
-    }
-
-    let minAccessLevel = aRules.minimumAccessLevel;
-    switch (minAccessLevel) {
-      case "web":
-        if (aAppStatus == Ci.nsIPrincipal.APP_STATUS_INSTALLED ||
-            aAppStatus == Ci.nsIPrincipal.APP_STATUS_PRIVILEGED ||
-            aAppStatus == Ci.nsIPrincipal.APP_STATUS_CERTIFIED) {
-          return true;
-        }
-        break;
-      case "privileged":
-        if (aAppStatus == Ci.nsIPrincipal.APP_STATUS_PRIVILEGED ||
-            aAppStatus == Ci.nsIPrincipal.APP_STATUS_CERTIFIED) {
-          return true;
-        }
-        break;
-      case "certified":
-        if (aAppStatus == Ci.nsIPrincipal.APP_STATUS_CERTIFIED) {
-          return true;
-        }
-        break;
-    }
-
-    if (DEBUG) {
-      debug("rules.minimumAccessLevel is not matched!" +
-            " minAccessLevel: " + minAccessLevel +
-            " aAppStatus : " + aAppStatus);
-    }
-    return false;
-  },
-
-  _matchManifestURLs: function(aRules, aManifestURL) {
-    if (!aRules || !Array.isArray(aRules.manifestURLs)) {
-      if (DEBUG) {
-        debug("rules.manifestURLs is not available. No need to match.");
-      }
-      return true;
-    }
-
-    let manifestURLs = aRules.manifestURLs;
-    if (manifestURLs.indexOf(aManifestURL) != -1) {
-      return true;
-    }
-
-    if (DEBUG) {
-      debug("rules.manifestURLs is not matched!" +
-            " manifestURLs: " + manifestURLs +
-            " aManifestURL : " + aManifestURL);
-    }
-    return false;
-  },
-
-  _matchInstallOrigins: function(aRules, aInstallOrigin) {
-    if (!aRules || !Array.isArray(aRules.installOrigins)) {
-      if (DEBUG) {
-        debug("rules.installOrigins is not available. No need to match.");
-      }
-      return true;
-    }
-
-    let installOrigins = aRules.installOrigins;
-    if (installOrigins.indexOf(aInstallOrigin) != -1) {
-      return true;
-    }
-
-    if (DEBUG) {
-      debug("rules.installOrigins is not matched!" +
-            " installOrigins: " + installOrigins +
-            " installOrigin : " + aInstallOrigin);
-    }
-    return false;
-  },
-
-  _matchRules: function(aPubAppManifestURL, aPubRules,
-                        aSubAppManifestURL, aSubRules) {
-    let pubApp = appsService.getAppByManifestURL(aPubAppManifestURL);
-    let subApp = appsService.getAppByManifestURL(aSubAppManifestURL);
-
-    // TODO Bug 907068 In the initiative step, we only expose this API to
-    // certified apps to meet the time line. Eventually, we need to make
-    // it available for the non-certified apps as well. For now, only the
-    // certified apps can match the rules.
-    if (pubApp.appStatus != Ci.nsIPrincipal.APP_STATUS_CERTIFIED ||
-        subApp.appStatus != Ci.nsIPrincipal.APP_STATUS_CERTIFIED) {
-      if (DEBUG) {
-        debug("Only certified apps are allowed to do connections.");
-      }
-      return false;
-    }
-
-    if (!aPubRules && !aSubRules) {
-      if (DEBUG) {
-        debug("No rules for publisher and subscriber. No need to match.");
-      }
-      return true;
-    }
-
-    // Check minimumAccessLevel.
-    if (!this._matchMinimumAccessLevel(aPubRules, subApp.appStatus) ||
-        !this._matchMinimumAccessLevel(aSubRules, pubApp.appStatus)) {
-      return false;
-    }
-
-    // Check manifestURLs.
-    if (!this._matchManifestURLs(aPubRules, aSubAppManifestURL) ||
-        !this._matchManifestURLs(aSubRules, aPubAppManifestURL)) {
-      return false;
-    }
-
-    // Check installOrigins.
-    if (!this._matchInstallOrigins(aPubRules, subApp.installOrigin) ||
-        !this._matchInstallOrigins(aSubRules, pubApp.installOrigin)) {
-      return false;
-    }
-
-    // Check developers.
-    // TODO Do we really want to check this? This one seems naive.
-
-    if (DEBUG) debug("All rules are matched.");
-    return true;
-  },
-
-  _dispatchMessagePorts: function(aKeyword, aPubAppManifestURL,
-                                  aAllowedSubAppManifestURLs,
-                                  aTarget, aOuterWindowID, aRequestID) {
-    if (DEBUG) {
-      debug("_dispatchMessagePorts: aKeyword: " + aKeyword +
-            " aPubAppManifestURL: " + aPubAppManifestURL +
-            " aAllowedSubAppManifestURLs: " + aAllowedSubAppManifestURLs);
-    }
-
-    if (aAllowedSubAppManifestURLs.length == 0) {
-      if (DEBUG) debug("No apps are allowed to connect. Returning.");
-      aTarget.sendAsyncMessage("Webapps:Connect:Return:KO",
-                               { oid: aOuterWindowID, requestID: aRequestID });
-      return;
-    }
-
-    let subAppManifestURLs = this._registeredConnections[aKeyword];
-    if (!subAppManifestURLs) {
-      if (DEBUG) debug("No apps are subscribed to connect. Returning.");
-      aTarget.sendAsyncMessage("Webapps:Connect:Return:KO",
-                               { oid: aOuterWindowID, requestID: aRequestID });
-      return;
-    }
-
-    let messagePortIDs = [];
-    aAllowedSubAppManifestURLs.forEach(function(aAllowedSubAppManifestURL) {
-      let subscribedInfo = subAppManifestURLs[aAllowedSubAppManifestURL];
-      if (!subscribedInfo) {
-        if (DEBUG) {
-          debug("The sunscribed info is not available. Skipping: " +
-                aAllowedSubAppManifestURL);
-        }
-        return;
-      }
-
-      // The message port ID is aimed for identifying the coupling targets
-      // to deliver messages with each other. This ID is centrally generated
-      // by the parent and dispatched to both the sender and receiver ends
-      // for creating their own message ports respectively.
-      let messagePortID = UUIDGenerator.generateUUID().toString();
-      this._messagePortPairs[messagePortID] = {
-        keyword: aKeyword,
-        publisher: {
-          manifestURL: aPubAppManifestURL
-        },
-        subscriber: {
-          manifestURL: aAllowedSubAppManifestURL
-        }
-      };
-
-      // Fire system message to deliver the message port to the subscriber.
-      messenger.sendMessage("connection",
-        { keyword: aKeyword,
-          messagePortID: messagePortID },
-        Services.io.newURI(subscribedInfo.pageURL, null, null),
-        Services.io.newURI(subscribedInfo.manifestURL, null, null));
-
-      messagePortIDs.push(messagePortID);
-    }, this);
-
-    if (messagePortIDs.length == 0) {
-      if (DEBUG) debug("No apps are subscribed to connect. Returning.");
-      aTarget.sendAsyncMessage("Webapps:Connect:Return:KO",
-                               { oid: aOuterWindowID, requestID: aRequestID });
-      return;
-    }
-
-    // Return the message port IDs to open the message ports for the publisher.
-    if (DEBUG) debug("messagePortIDs: " + messagePortIDs);
-    aTarget.sendAsyncMessage("Webapps:Connect:Return:OK",
-                             { keyword: aKeyword,
-                               messagePortIDs: messagePortIDs,
-                               oid: aOuterWindowID, requestID: aRequestID });
-  },
-
-  _connect: function(aMessage, aTarget) {
-    let keyword = aMessage.keyword;
-    let pubRules = aMessage.rules;
-    let pubAppManifestURL = aMessage.manifestURL;
-    let outerWindowID = aMessage.outerWindowID;
-    let requestID = aMessage.requestID;
-
-    let subAppManifestURLs = this._registeredConnections[keyword];
-    if (!subAppManifestURLs) {
-      if (DEBUG) {
-        debug("No apps are subscribed for this connection. Returning.");
-      }
-      this._dispatchMessagePorts(keyword, pubAppManifestURL, [],
-                                 aTarget, outerWindowID, requestID);
-      return;
-    }
-
-    // Fetch the apps that used to be allowed to connect before, so that
-    // users don't need to select/allow them again. That is, we only pop up
-    // the prompt UI for the *new* connections.
-    let allowedSubAppManifestURLs = [];
-    let allowedPubAppManifestURLs = this._allowedConnections[keyword];
-    if (allowedPubAppManifestURLs &&
-        allowedPubAppManifestURLs[pubAppManifestURL]) {
-      allowedSubAppManifestURLs = allowedPubAppManifestURLs[pubAppManifestURL];
-    }
-
-    // Check rules to see if a subscribed app is allowed to connect.
-    let appsToSelect = [];
-    for (let subAppManifestURL in subAppManifestURLs) {
-      if (allowedSubAppManifestURLs.indexOf(subAppManifestURL) != -1) {
-        if (DEBUG) {
-          debug("Don't need to select again. Skipping: " + subAppManifestURL);
-        }
-        continue;
-      }
-
-      // Only rule-matched publishers/subscribers are allowed to connect.
-      let subscribedInfo = subAppManifestURLs[subAppManifestURL];
-      let subRules = subscribedInfo.rules;
-
-      let matched =
-        this._matchRules(pubAppManifestURL, pubRules,
-                         subAppManifestURL, subRules);
-      if (!matched) {
-        if (DEBUG) {
-          debug("Rules are not matched. Skipping: " + subAppManifestURL);
-        }
-        continue;
-      }
-
-      appsToSelect.push({
-        manifestURL: subAppManifestURL,
-        description: subscribedInfo.description
-      });
-    }
-
-    if (appsToSelect.length == 0) {
-      if (DEBUG) {
-        debug("No additional apps need to be selected for this connection. " +
-              "Just dispatch message ports for the existing connections.");
-      }
-
-      this._dispatchMessagePorts(keyword, pubAppManifestURL,
-                                 allowedSubAppManifestURLs,
-                                 aTarget, outerWindowID, requestID);
-      return;
-    }
-
-    // Remember the caller info with an UUID so that we can know where to
-    // return the promise resolver's callback when the prompt UI returns.
-    let callerID = UUIDGenerator.generateUUID().toString();
-    this._promptUICallers[callerID] = {
-      outerWindowID: outerWindowID,
-      requestID: requestID,
-      target: aTarget
-    };
-
-    // TODO Bug 897169 Temporarily disable the notification for popping up
-    // the prompt until the UX/UI for the prompt is confirmed.
-    //
-    // TODO Bug 908191 We need to change the way of interaction between API and
-    // run-time prompt from observer notification to xpcom-interface caller.
-    //
-    /*
-    if (DEBUG) debug("appsToSelect: " + appsToSelect);
-    Services.obs.notifyObservers(null, "inter-app-comm-select-app",
-      JSON.stringify({ callerID: callerID,
-                       manifestURL: pubAppManifestURL,
-                       keyword: keyword,
-                       appsToSelect: appsToSelect }));
-    */
-
-    // TODO Bug 897169 Simulate the return of the app-selected result by
-    // the prompt, which always allows the connection. This dummy codes
-    // will be removed when the UX/UI for the prompt is ready.
-    if (DEBUG) debug("appsToSelect: " + appsToSelect);
-    Services.obs.notifyObservers(null, 'inter-app-comm-select-app-result',
-      JSON.stringify({ callerID: callerID,
-                       manifestURL: pubAppManifestURL,
-                       keyword: keyword,
-                       selectedApps: appsToSelect }));
-  },
-
-  _getConnections: function(aMessage, aTarget) {
-    let outerWindowID = aMessage.outerWindowID;
-    let requestID = aMessage.requestID;
-
-    let connections = [];
-    for (let keyword in this._allowedConnections) {
-      let allowedPubAppManifestURLs = this._allowedConnections[keyword];
-      for (let allowedPubAppManifestURL in allowedPubAppManifestURLs) {
-        let allowedSubAppManifestURLs =
-          allowedPubAppManifestURLs[allowedPubAppManifestURL];
-        allowedSubAppManifestURLs.forEach(function(allowedSubAppManifestURL) {
-          connections.push({ keyword: keyword,
-                             pubAppManifestURL: allowedPubAppManifestURL,
-                             subAppManifestURL: allowedSubAppManifestURL });
-        });
-      }
-    }
-
-    aTarget.sendAsyncMessage("Webapps:GetConnections:Return:OK",
-                             { connections: connections,
-                               oid: outerWindowID, requestID: requestID });
-  },
-
-  _cancelConnection: function(aMessage) {
-    let keyword = aMessage.keyword;
-    let pubAppManifestURL = aMessage.pubAppManifestURL;
-    let subAppManifestURL = aMessage.subAppManifestURL;
-
-    let allowedPubAppManifestURLs = this._allowedConnections[keyword];
-    if (!allowedPubAppManifestURLs) {
-      if (DEBUG) debug("keyword is not found: " + keyword);
-      return;
-    }
-
-    let allowedSubAppManifestURLs =
-      allowedPubAppManifestURLs[pubAppManifestURL];
-    if (!allowedSubAppManifestURLs) {
-      if (DEBUG) debug("publisher is not found: " + pubAppManifestURL);
-      return;
-    }
-
-    let index = allowedSubAppManifestURLs.indexOf(subAppManifestURL);
-    if (index == -1) {
-      if (DEBUG) debug("subscriber is not found: " + subAppManifestURL);
-      return;
-    }
-
-    if (DEBUG) debug("Cancelling the connection.");
-    allowedSubAppManifestURLs.splice(index, 1);
-
-    // Clean up the parent entries if needed.
-    if (allowedSubAppManifestURLs.length == 0) {
-      delete allowedPubAppManifestURLs[pubAppManifestURL];
-      if (Object.keys(allowedPubAppManifestURLs).length == 0) {
-        delete this._allowedConnections[keyword];
-      }
-    }
-
-    if (DEBUG) debug("Unregistering message ports based on this connection.");
-    let messagePortIDs = [];
-    for (let messagePortID in this._messagePortPairs) {
-      let pair = this._messagePortPairs[messagePortID];
-      if (pair.keyword == keyword &&
-          pair.publisher.manifestURL == pubAppManifestURL &&
-          pair.subscriber.manifestURL == subAppManifestURL) {
-        messagePortIDs.push(messagePortID);
-      }
-    }
-    messagePortIDs.forEach(function(aMessagePortID) {
-      delete this._messagePortPairs[aMessagePortID];
-    }, this);
-  },
-
-  _identifyMessagePort: function(aMessagePortID, aManifestURL) {
-    let pair = this._messagePortPairs[aMessagePortID];
-    if (!pair) {
-      if (DEBUG) {
-        debug("Error! The message port ID is invalid: " + aMessagePortID +
-              ", which should have been generated by parent.");
-      }
-      return null;
-    }
-
-    // Check it the message port is for publisher.
-    if (pair.publisher.manifestURL == aManifestURL) {
-      return { pair: pair, isPublisher: true };
-    }
-
-    // Check it the message port is for subscriber.
-    if (pair.subscriber.manifestURL == aManifestURL) {
-      return { pair: pair, isPublisher: false };
-    }
-
-    if (DEBUG) {
-      debug("Error! The manifest URL is invalid: " + aManifestURL +
-            ", which might be a hacked app.");
-    }
-    return null;
-  },
-
-  _registerMessagePort: function(aMessage, aTarget) {
-    let messagePortID = aMessage.messagePortID;
-    let manifestURL = aMessage.manifestURL;
-    let pageURL = aMessage.pageURL;
-
-    let identity = this._identifyMessagePort(messagePortID, manifestURL);
-    if (!identity) {
-      if (DEBUG) {
-        debug("Cannot identify the message port. Failed to register.");
-      }
-      return;
-    }
-
-    if (DEBUG) debug("Registering message port for " + manifestURL);
-    let pair = identity.pair;
-    let isPublisher = identity.isPublisher;
-
-    let sender = isPublisher ? pair.publisher : pair.subscriber;
-    sender.target = aTarget;
-    sender.pageURL = pageURL;
-    sender.messageQueue = [];
-
-    // Check if the other port has queued messages. Deliver them if needed.
-    if (DEBUG) {
-      debug("Checking if the other port used to send messages but queued.");
-    }
-    let receiver = isPublisher ? pair.subscriber : pair.publisher;
-    if (receiver.messageQueue) {
-      while (receiver.messageQueue.length) {
-        let message = receiver.messageQueue.shift();
-        if (DEBUG) debug("Delivering message: " + JSON.stringify(message));
-        sender.target.sendAsyncMessage("InterAppMessagePort:OnMessage",
-                                       { message: message,
-                                         manifestURL: sender.manifestURL,
-                                         pageURL: sender.pageURL,
-                                         messagePortID: messagePortID });
-      }
-    }
-  },
-
-  _unregisterMessagePort: function(aMessage) {
-    let messagePortID = aMessage.messagePortID;
-    let manifestURL = aMessage.manifestURL;
-
-    let identity = this._identifyMessagePort(messagePortID, manifestURL);
-    if (!identity) {
-      if (DEBUG) {
-        debug("Cannot identify the message port. Failed to unregister.");
-      }
-      return;
-    }
-
-    if (DEBUG) {
-      debug("Unregistering message port for " + manifestURL);
-    }
-    delete this._messagePortPairs[messagePortID];
-  },
-
-  _removeTarget: function(aTarget) {
-    if (!aTarget) {
-      if (DEBUG) debug("Error! aTarget cannot be null/undefined in any way.");
-      return
-    }
-
-    if (DEBUG) debug("Unregistering message ports based on this target.");
-    let messagePortIDs = [];
-    for (let messagePortID in this._messagePortPairs) {
-      let pair = this._messagePortPairs[messagePortID];
-      if (pair.publisher.target === aTarget ||
-          pair.subscriber.target === aTarget) {
-        messagePortIDs.push(messagePortID);
-      }
-    }
-    messagePortIDs.forEach(function(aMessagePortID) {
-      delete this._messagePortPairs[aMessagePortID];
-    }, this);
-  },
-
-  _postMessage: function(aMessage) {
-    let messagePortID = aMessage.messagePortID;
-    let manifestURL = aMessage.manifestURL;
-    let message = aMessage.message;
-
-    let identity = this._identifyMessagePort(messagePortID, manifestURL);
-    if (!identity) {
-      if (DEBUG) debug("Cannot identify the message port. Failed to post.");
-      return;
-    }
-
-    let pair = identity.pair;
-    let isPublisher = identity.isPublisher;
-
-    let receiver = isPublisher ? pair.subscriber : pair.publisher;
-    if (!receiver.target) {
-      if (DEBUG) {
-        debug("The receiver's target is not ready yet. Queuing the message.");
-      }
-      let sender = isPublisher ? pair.publisher : pair.subscriber;
-      sender.messageQueue.push(message);
-      return;
-    }
-
-    if (DEBUG) debug("Delivering message: " + JSON.stringify(message));
-    receiver.target.sendAsyncMessage("InterAppMessagePort:OnMessage",
-                                     { manifestURL: receiver.manifestURL,
-                                       pageURL: receiver.pageURL,
-                                       messagePortID: messagePortID,
-                                       message: message });
-  },
-
-  _handleSelectcedApps: function(aData) {
-    let callerID = aData.callerID;
-    let caller = this._promptUICallers[callerID];
-    if (!caller) {
-      if (DEBUG) debug("Error! Cannot find the caller.");
-      return;
-    }
-
-    delete this._promptUICallers[callerID];
-
-    let outerWindowID = caller.outerWindowID;
-    let requestID = caller.requestID;
-    let target = caller.target;
-
-    let manifestURL = aData.manifestURL;
-    let keyword = aData.keyword;
-    let selectedApps = aData.selectedApps;
-
-    if (selectedApps.length == 0) {
-      if (DEBUG) debug("No apps are selected to connect.")
-      this._dispatchMessagePorts(keyword, manifestURL, [],
-                                 target, outerWindowID, requestID);
-      return;
-    }
-
-    // Find the entry of allowed connections to add the selected apps.
-    let allowedPubAppManifestURLs = this._allowedConnections[keyword];
-    if (!allowedPubAppManifestURLs) {
-      allowedPubAppManifestURLs = this._allowedConnections[keyword] = {};
-    }
-    let allowedSubAppManifestURLs = allowedPubAppManifestURLs[manifestURL];
-    if (!allowedSubAppManifestURLs) {
-      allowedSubAppManifestURLs = allowedPubAppManifestURLs[manifestURL] = [];
-    }
-
-    // Add the selected app into the existing set of allowed connections.
-    selectedApps.forEach(function(aSelectedApp) {
-      let allowedSubAppManifestURL = aSelectedApp.manifestURL;
-      if (allowedSubAppManifestURLs.indexOf(allowedSubAppManifestURL) == -1) {
-        allowedSubAppManifestURLs.push(allowedSubAppManifestURL);
-      }
-    });
-
-    // Finally, dispatch the message ports for the allowed connections,
-    // including the old connections and the newly selected connection.
-    this._dispatchMessagePorts(keyword, manifestURL, allowedSubAppManifestURLs,
-                               target, outerWindowID, requestID);
-  },
-
-  receiveMessage: function(aMessage) {
-    if (DEBUG) debug("receiveMessage: name: " + aMessage.name);
-    let message = aMessage.json;
-    let target = aMessage.target;
-
-    // To prevent the hacked child process from sending commands to parent
-    // to do illegal connections, we need to check its manifest URL.
-    if (aMessage.name !== "child-process-shutdown" &&
-        // TODO: fix bug 988142 to re-enable "InterAppMessagePort:Unregister".
-        aMessage.name !== "InterAppMessagePort:Unregister" &&
-        kMessages.indexOf(aMessage.name) != -1) {
-      if (!target.assertContainApp(message.manifestURL)) {
-        if (DEBUG) {
-          debug("Got message from a process carrying illegal manifest URL.");
-        }
-        return null;
-      }
-    }
-
-    switch (aMessage.name) {
-      case "Webapps:Connect":
-        this._connect(message, target);
-        break;
-      case "Webapps:GetConnections":
-        this._getConnections(message, target);
-        break;
-      case "InterAppConnection:Cancel":
-        this._cancelConnection(message);
-        break;
-      case "InterAppMessagePort:PostMessage":
-        this._postMessage(message);
-        break;
-      case "InterAppMessagePort:Register":
-        this._registerMessagePort(message, target);
-        break;
-      case "InterAppMessagePort:Unregister":
-        this._unregisterMessagePort(message);
-        break;
-      case "child-process-shutdown":
-        this._removeTarget(target);
-        break;
-    }
-  },
-
-  observe: function(aSubject, aTopic, aData) {
-    switch (aTopic) {
-      case "xpcom-shutdown":
-        Services.obs.removeObserver(this, "xpcom-shutdown");
-        Services.obs.removeObserver(this, "inter-app-comm-select-app-result");
-        kMessages.forEach(function(aMsg) {
-          ppmm.removeMessageListener(aMsg, this);
-        }, this);
-        ppmm = null;
-        break;
-      case "inter-app-comm-select-app-result":
-        if (DEBUG) debug("inter-app-comm-select-app-result: " + aData);
-        this._handleSelectcedApps(JSON.parse(aData));
-        break;
-    }
+    InterAppCommService.
+      registerConnection(aKeyword, aHandlerPageURI, aManifestURI,
+                         aDescription, aRules);
   },
 
   classID: Components.ID("{3dd15ce6-e7be-11e2-82bc-77967e7a63e6}"),
 
-  QueryInterface: XPCOMUtils.generateQI([Ci.nsIInterAppCommService,
-                                         Ci.nsIObserver])
+  QueryInterface: XPCOMUtils.generateQI([Ci.nsIInterAppCommService])
 }
 
-this.NSGetFactory = XPCOMUtils.generateNSGetFactory([InterAppCommService]);
+this.NSGetFactory = XPCOMUtils.generateNSGetFactory([InterAppCommServiceProxy]);
copy from dom/apps/src/InterAppCommService.js
copy to dom/apps/src/InterAppCommService.jsm
--- a/dom/apps/src/InterAppCommService.js
+++ b/dom/apps/src/InterAppCommService.jsm
@@ -1,16 +1,18 @@
 /* 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, results: Cr } = Components;
 
+this.EXPORTED_SYMBOLS = ["InterAppCommService"];
+
 Cu.import("resource://gre/modules/Services.jsm");
 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
 Cu.import("resource://gre/modules/AppsUtils.jsm");
 
 const DEBUG = false;
 function debug(aMsg) {
   dump("-- InterAppCommService: " + Date.now() + ": " + aMsg + "\n");
 }
@@ -34,17 +36,28 @@ XPCOMUtils.defineLazyServiceGetter(this,
 const kMessages =["Webapps:Connect",
                   "Webapps:GetConnections",
                   "InterAppConnection:Cancel",
                   "InterAppMessagePort:PostMessage",
                   "InterAppMessagePort:Register",
                   "InterAppMessagePort:Unregister",
                   "child-process-shutdown"];
 
-function InterAppCommService() {
+/**
+ * This module contains helpers for Inter-App Communication API [1] related
+ * purposes, which plays the role of the central service receiving messages
+ * from and interacting with the content processes.
+ *
+ * [1] https://wiki.mozilla.org/WebAPI/Inter_App_Communication_Alt_proposal
+ */
+
+this.InterAppCommService = {
+
+// For the convenience of review, will align init() by a follow-up patch.
+init: function() {
   Services.obs.addObserver(this, "xpcom-shutdown", false);
   Services.obs.addObserver(this, "inter-app-comm-select-app-result", false);
 
   kMessages.forEach(function(aMsg) {
     ppmm.addMessageListener(aMsg, this);
   }, this);
 
   // This matrix is used for saving the inter-app connection info registered in
@@ -202,19 +215,28 @@ function InterAppCommService() {
   //       manifestURL: "app://subApp2.gaiamobile.org/manifest.webapp",
   //       target: subAppTarget2,
   //       pageURL: "app://pubApp2.gaiamobile.org/handler.html",
   //       messageQueue: [...]
   //     }
   //   }
   // }
   this._messagePortPairs = {};
-}
+},
 
-InterAppCommService.prototype = {
+  /**
+   * Registration of a page that wants to be connected to other apps through
+   * the Inter-App Communication API.
+   *
+   * @param aKeyword        The connection's keyword.
+   * @param aHandlerPageURI The URI of the handler's page.
+   * @param aManifestURI    The webapp's manifest URI.
+   * @param aDescription    The connection's description.
+   * @param aRules          The connection's rules.
+   */
   registerConnection: function(aKeyword, aHandlerPageURI, aManifestURI,
                                aDescription, aRules) {
     let manifestURL = aManifestURI.spec;
     let pageURL = aHandlerPageURI.spec;
 
     if (DEBUG) {
       debug("registerConnection: aKeyword: " + aKeyword +
             " manifestURL: " + manifestURL + " pageURL: " + pageURL +
@@ -858,17 +880,12 @@ InterAppCommService.prototype = {
         }, this);
         ppmm = null;
         break;
       case "inter-app-comm-select-app-result":
         if (DEBUG) debug("inter-app-comm-select-app-result: " + aData);
         this._handleSelectcedApps(JSON.parse(aData));
         break;
     }
-  },
-
-  classID: Components.ID("{3dd15ce6-e7be-11e2-82bc-77967e7a63e6}"),
+  }
+};
 
-  QueryInterface: XPCOMUtils.generateQI([Ci.nsIInterAppCommService,
-                                         Ci.nsIObserver])
-}
-
-this.NSGetFactory = XPCOMUtils.generateNSGetFactory([InterAppCommService]);
+InterAppCommService.init();
--- a/dom/apps/src/moz.build
+++ b/dom/apps/src/moz.build
@@ -22,16 +22,17 @@ EXTRA_COMPONENTS += [
     'Webapps.js',
     'Webapps.manifest',
 ]
 
 EXTRA_JS_MODULES += [
     'AppDownloadManager.jsm',
     'AppsServiceChild.jsm',
     'FreeSpaceWatcher.jsm',
+    'InterAppCommService.jsm',
     'OfflineCacheInstaller.jsm',
     'PermissionsInstaller.jsm',
     'PermissionsTable.jsm',
     'StoreTrustAnchor.jsm',
 ]
 
 EXTRA_PP_JS_MODULES += [
     'AppsUtils.jsm',