Bug 963239 - Implements "SystemAppProxy.jsm" to abtract and ease interacting with the system app from platform code. r=vingtetun
authorAlexandre Poirot <poirot.alex@gmail.com>
Mon, 07 Apr 2014 09:59:48 -0400
changeset 195872 c50c4788f3133de3338628705a9f6fa85c46cd6b
parent 195871 93182cb517e8474f170533f4e7c5135da1c29ef0
child 195873 a6d44e79977c20b63129693cfe439a3842a89ccd
push id3624
push userasasaki@mozilla.com
push dateMon, 09 Jun 2014 21:49:01 +0000
treeherdermozilla-beta@b1a5da15899a [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersvingtetun
bugs963239
milestone31.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 963239 - Implements "SystemAppProxy.jsm" to abtract and ease interacting with the system app from platform code. r=vingtetun
b2g/chrome/content/payment.js
b2g/chrome/content/shell.js
b2g/chrome/content/test/mochitest/RecordingStatusChromeScript.js
b2g/components/ActivitiesGlue.js
b2g/components/ContentPermissionPrompt.js
b2g/components/FxAccountsMgmtService.jsm
b2g/components/FxAccountsUIGlue.js
b2g/components/PaymentGlue.js
b2g/components/SignInToWebsite.jsm
b2g/components/SystemAppProxy.jsm
b2g/components/UpdatePrompt.js
b2g/components/WebappsUpdater.jsm
b2g/components/moz.build
b2g/components/test/mochitest/mochitest.ini
b2g/components/test/mochitest/permission_handler_chrome.js
b2g/components/test/mochitest/systemapp_helper.js
b2g/components/test/mochitest/test_systemapp.html
b2g/components/test/unit/test_fxaccounts.js
dom/inputmethod/Keyboard.jsm
--- a/b2g/chrome/content/payment.js
+++ b/b2g/chrome/content/payment.js
@@ -41,16 +41,19 @@ if (_debug) {
 XPCOMUtils.defineLazyServiceGetter(this, "cpmm",
                                    "@mozilla.org/childprocessmessagemanager;1",
                                    "nsIMessageSender");
 
 XPCOMUtils.defineLazyServiceGetter(this, "uuidgen",
                                    "@mozilla.org/uuid-generator;1",
                                    "nsIUUIDGenerator");
 
+XPCOMUtils.defineLazyModuleGetter(this, "SystemAppProxy",
+                                  "resource://gre/modules/SystemAppProxy.jsm");
+
 #ifdef MOZ_B2G_RIL
 XPCOMUtils.defineLazyServiceGetter(this, "gRil",
                                    "@mozilla.org/ril;1",
                                    "nsIRadioInterfaceLayer");
 
 XPCOMUtils.defineLazyServiceGetter(this, "iccProvider",
                                    "@mozilla.org/ril/content-helper;1",
                                    "nsIIccProvider");
@@ -192,18 +195,16 @@ PaymentSettings.prototype = {
   }
 };
 #endif
 
 const kClosePaymentFlowEvent = "close-payment-flow-dialog";
 
 let gRequestId;
 
-let gBrowser = Services.wm.getMostRecentWindow("navigator:browser");
-
 let PaymentProvider = {
 #ifdef MOZ_B2G_RIL
   __exposedProps__: {
     paymentSuccess: "r",
     paymentFailed: "r",
     paymentServiceId: "rw",
     iccInfo: "r",
     sendSilentSms: "r",
@@ -224,46 +225,41 @@ let PaymentProvider = {
   },
 
   _closePaymentFlowDialog: function _closePaymentFlowDialog(aCallback) {
     // After receiving the payment provider confirmation about the
     // successful or failed payment flow, we notify the UI to close the
     // payment flow dialog and return to the caller application.
     let id = kClosePaymentFlowEvent + "-" + uuidgen.generateUUID().toString();
 
-    let content = gBrowser.getContentWindow();
-    if (!content) {
-      return;
-    }
-
     let detail = {
       type: kClosePaymentFlowEvent,
       id: id,
       requestId: gRequestId
     };
 
     // In order to avoid race conditions, we wait for the UI to notify that
     // it has successfully closed the payment flow and has recovered the
     // caller app, before notifying the parent process to fire the success
     // or error event over the DOMRequest.
-    content.addEventListener("mozContentEvent",
-                             function closePaymentFlowReturn(evt) {
+    SystemAppProxy.addEventListener("mozContentEvent",
+                               function closePaymentFlowReturn(evt) {
       if (evt.detail.id == id && aCallback) {
         aCallback();
       }
 
-      content.removeEventListener("mozContentEvent",
+      SystemAppProxy.removeEventListener("mozContentEvent",
                                   closePaymentFlowReturn);
 
       let glue = Cc["@mozilla.org/payment/ui-glue;1"]
                    .createInstance(Ci.nsIPaymentUIGlue);
       glue.cleanup();
     });
 
-    gBrowser.shell.sendChromeEvent(detail);
+    SystemAppProxy.dispatchEvent(detail);
 
 #ifdef MOZ_B2G_RIL
     this._cleanUp();
 #endif
   },
 
   paymentSuccess: function paymentSuccess(aResult) {
     if (_debug) {
@@ -473,14 +469,14 @@ addMessageListener("Payment:LoadShim", f
 addEventListener("DOMWindowCreated", function(e) {
   content.wrappedJSObject.mozPaymentProvider = PaymentProvider;
 });
 
 #ifdef MOZ_B2G_RIL
 // If the trusted dialog is not closed via paymentSuccess or paymentFailed
 // a mozContentEvent with type 'cancel' is sent from the UI. We need to listen
 // for this event to clean up the silent sms observers if any exists.
-gBrowser.getContentWindow().addEventListener("mozContentEvent", function(e) {
+SystemAppProxy.addEventListener("mozContentEvent", function(e) {
   if (e.detail.type === "cancel") {
     PaymentProvider._cleanUp();
   }
 });
 #endif
--- a/b2g/chrome/content/shell.js
+++ b/b2g/chrome/content/shell.js
@@ -25,16 +25,19 @@ Cu.import('resource://gre/modules/SignIn
 SignInToWebsiteController.init();
 
 #ifdef MOZ_SERVICES_FXACCOUNTS
 Cu.import('resource://gre/modules/FxAccountsMgmtService.jsm');
 #endif
 
 Cu.import('resource://gre/modules/DownloadsAPI.jsm');
 
+XPCOMUtils.defineLazyModuleGetter(this, "SystemAppProxy",
+                                  "resource://gre/modules/SystemAppProxy.jsm");
+
 Cu.import('resource://gre/modules/Webapps.jsm');
 DOMApplicationRegistry.allAppsLaunchable = true;
 
 XPCOMUtils.defineLazyServiceGetter(Services, 'env',
                                    '@mozilla.org/process/environment;1',
                                    'nsIEnvironment');
 
 XPCOMUtils.defineLazyServiceGetter(Services, 'ss',
@@ -335,16 +338,18 @@ var shell = {
 
     window.addEventListener('MozApplicationManifest', this);
     window.addEventListener('mozfullscreenchange', this);
     window.addEventListener('MozAfterPaint', this);
     window.addEventListener('sizemodechange', this);
     window.addEventListener('unload', this);
     this.contentBrowser.addEventListener('mozbrowserloadstart', this, true);
 
+    SystemAppProxy.registerFrame(this.contentBrowser);
+
     CustomEventManager.init();
     WebappsHelper.init();
     UserAgentOverrides.init();
     IndexedDBPromptHelper.init();
     CaptivePortalLoginHelper.init();
 
     this.contentBrowser.src = homeURL;
     this.isHomeLoaded = false;
@@ -661,16 +666,17 @@ var shell = {
       shell.isHomeLoaded = true;
 
 #ifdef MOZ_WIDGET_GONK
       libcutils.property_set('sys.boot_completed', '1');
 #endif
 
       Services.obs.notifyObservers(null, "browser-ui-startup-complete", "");
 
+      SystemAppProxy.setIsReady();
       if ('pendingChromeEvents' in shell) {
         shell.pendingChromeEvents.forEach((shell.sendChromeEvent).bind(shell));
       }
       delete shell.pendingChromeEvents;
     });
   }
 };
 
--- a/b2g/chrome/content/test/mochitest/RecordingStatusChromeScript.js
+++ b/b2g/chrome/content/test/mochitest/RecordingStatusChromeScript.js
@@ -1,30 +1,30 @@
 /* Any copyright is dedicated to the Public Domain.
    http://creativecommons.org/publicdomain/zero/1.0/ */
 
 const { classes: Cc, interfaces: Ci, results: Cr, utils: Cu } = Components;
 const { Services } = Cu.import('resource://gre/modules/Services.jsm');
+const { SystemAppProxy } = Cu.import('resource://gre/modules/SystemAppProxy.jsm');
 
 var processId;
 
 function peekChildId(aSubject, aTopic, aData) {
   Services.obs.removeObserver(peekChildId, 'recording-device-events');
   Services.obs.removeObserver(peekChildId, 'recording-device-ipc-events');
   let props = aSubject.QueryInterface(Ci.nsIPropertyBag2);
   if (props.hasKey('childID')) {
     processId = props.get('childID');
   }
 }
 
 addMessageListener('init-chrome-event', function(message) {
   // listen mozChromeEvent and forward to content process.
-  let browser = Services.wm.getMostRecentWindow('navigator:browser');
   let type = message.type;
-  browser.addEventListener('mozChromeEvent', function(event) {
+  SystemAppProxy.addEventListener('mozChromeEvent', function(event) {
     let details = event.detail;
     if (details.type === type) {
       sendAsyncMessage('chrome-event', details);
     }
   }, true);
 
   Services.obs.addObserver(peekChildId, 'recording-device-events', false);
   Services.obs.addObserver(peekChildId, 'recording-device-ipc-events', false);
--- a/b2g/components/ActivitiesGlue.js
+++ b/b2g/components/ActivitiesGlue.js
@@ -6,16 +6,19 @@
 
 const Cc = Components.classes;
 const Ci = Components.interfaces;
 const Cu = Components.utils;
 
 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
 Cu.import("resource://gre/modules/Services.jsm");
 
+XPCOMUtils.defineLazyModuleGetter(this, "SystemAppProxy",
+                                  "resource://gre/modules/SystemAppProxy.jsm");
+
 function ActivitiesDialog() {
   this._id = 0;
 
   this.activities = [];
 }
 
 ActivitiesDialog.prototype = {
   run: function ap_run() {
@@ -26,38 +29,36 @@ ActivitiesDialog.prototype = {
     activity.list.forEach(function(item) {
       choices.push({ manifest: item.manifest, icon: item.icon });
     });
 
 
     // Keep up the frond-end of an activity choice. The messages contains
     // a list of {names, icons} for applications able to handle this particular
     // activity. The front-end should display a UI to pick one.
-    let browser = Services.wm.getMostRecentWindow("navigator:browser");
-    let content = browser.getContentWindow();
     let detail = {
       type: "activity-choice",
       id: id,
       name: activity.name,
       choices: choices
     };
 
     // Listen the resulting choice from the front-end. If there is no choice,
     // let's return -1, which means the user has cancelled the dialog.
-    content.addEventListener("mozContentEvent", function act_getChoice(evt) {
+    SystemAppProxy.addEventListener("mozContentEvent", function act_getChoice(evt) {
       if (evt.detail.id != id)
         return;
 
-      content.removeEventListener("mozContentEvent", act_getChoice);
+      SystemAppProxy.removeEventListener("mozContentEvent", act_getChoice);
       activity.callback.handleEvent(evt.detail.value !== undefined
                                       ? evt.detail.value
                                       : -1);
     });
 
-    browser.shell.sendChromeEvent(detail);
+    SystemAppProxy.dispatchEvent(detail);
   },
 
   chooseActivity: function ap_chooseActivity(aName, aActivities, aCallback) {
     this.activities.push({
       name: aName,
       list: aActivities,
       callback: aCallback
     });
--- a/b2g/components/ContentPermissionPrompt.js
+++ b/b2g/components/ContentPermissionPrompt.js
@@ -34,16 +34,19 @@ var secMan = Cc["@mozilla.org/scriptsecu
 
 let permissionSpecificChecker = {};
 
 XPCOMUtils.defineLazyServiceGetter(this,
                                    "AudioManager",
                                    "@mozilla.org/telephony/audiomanager;1",
                                    "nsIAudioManager");
 
+XPCOMUtils.defineLazyModuleGetter(this, "SystemAppProxy",
+                                  "resource://gre/modules/SystemAppProxy.jsm");
+
 /**
  * aTypesInfo is an array of {permission, access, action, deny} which keeps
  * the information of each permission. This arrary is initialized in
  * ContentPermissionPrompt.prompt and used among functions.
  *
  * aTypesInfo[].permission : permission name
  * aTypesInfo[].access     : permission name + request.access
  * aTypesInfo[].action     : the default action of this permission
@@ -341,27 +344,22 @@ ContentPermissionPrompt.prototype = {
       if (callback) {
         callback();
       }
       request.cancel();
     });
   },
 
   sendToBrowserWindow: function(type, request, requestId, typesInfo, callback) {
-    let browser = Services.wm.getMostRecentWindow("navigator:browser");
-    let content = browser.getContentWindow();
-    if (!content)
-      return;
-
     if (callback) {
-      content.addEventListener("mozContentEvent", function contentEvent(evt) {
+      SystemAppProxy.addEventListener("mozContentEvent", function contentEvent(evt) {
         let detail = evt.detail;
         if (detail.id != requestId)
           return;
-        evt.target.removeEventListener(evt.type, contentEvent);
+        SystemAppProxy.removeEventListener("mozContentEvent", contentEvent);
 
         callback(detail.type, detail.remember, detail.choices);
       })
     }
 
     let principal = request.principal;
     let isApp = principal.appStatus != Ci.nsIPrincipal.APP_STATUS_NOT_INSTALLED;
     let remember = (principal.appStatus == Ci.nsIPrincipal.APP_STATUS_PRIVILEGED ||
@@ -378,23 +376,20 @@ ContentPermissionPrompt.prototype = {
       type: type,
       permissions: permissions,
       id: requestId,
       origin: principal.origin,
       isApp: isApp,
       remember: remember
     };
 
-    if (!isApp) {
-      browser.shell.sendChromeEvent(details);
-      return;
+    if (isApp) {
+      details.manifestURL = DOMApplicationRegistry.getManifestURLByLocalId(principal.appId);
     }
-
-    details.manifestURL = DOMApplicationRegistry.getManifestURLByLocalId(principal.appId);
-    browser.shell.sendChromeEvent(details);
+    SystemAppProxy.dispatchEvent(details);
   },
 
   classID: Components.ID("{8c719f03-afe0-4aac-91ff-6c215895d467}"),
 
   QueryInterface: XPCOMUtils.generateQI([Ci.nsIContentPermissionPrompt])
 };
 
 (function() {
--- a/b2g/components/FxAccountsMgmtService.jsm
+++ b/b2g/components/FxAccountsMgmtService.jsm
@@ -24,62 +24,54 @@ const { classes: Cc, interfaces: Ci, uti
 
 Cu.import("resource://gre/modules/Services.jsm");
 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
 Cu.import("resource://gre/modules/FxAccountsCommon.js");
 
 XPCOMUtils.defineLazyModuleGetter(this, "FxAccountsManager",
   "resource://gre/modules/FxAccountsManager.jsm");
 
-this.FxAccountsMgmtService = {
+XPCOMUtils.defineLazyModuleGetter(this, "SystemAppProxy",
+                                  "resource://gre/modules/SystemAppProxy.jsm");
 
-  _sendChromeEvent: function(aEventName, aMsg) {
-    if (!this._shell) {
-      return;
-    }
-    log.debug("Chrome event " + JSON.stringify(aMsg));
-    this._shell.sendCustomEvent(aEventName, aMsg);
-  },
-
+this.FxAccountsMgmtService = {
   _onFulfill: function(aMsgId, aData) {
-    this._sendChromeEvent("mozFxAccountsChromeEvent", {
+    SystemAppProxy._sendCustomEvent("mozFxAccountsChromeEvent", {
       id: aMsgId,
       data: aData ? aData : null
     });
   },
 
   _onReject: function(aMsgId, aReason) {
-    this._sendChromeEvent("mozFxAccountsChromeEvent", {
+    SystemAppProxy._sendCustomEvent("mozFxAccountsChromeEvent", {
       id: aMsgId,
       error: aReason ? aReason : null
     });
   },
 
   init: function() {
     Services.obs.addObserver(this, "content-start", false);
     Services.obs.addObserver(this, ONLOGIN_NOTIFICATION, false);
     Services.obs.addObserver(this, ONVERIFIED_NOTIFICATION, false);
     Services.obs.addObserver(this, ONLOGOUT_NOTIFICATION, false);
   },
 
   observe: function(aSubject, aTopic, aData) {
     log.debug("Observed " + aTopic);
     switch (aTopic) {
       case "content-start":
-        this._shell = Services.wm.getMostRecentWindow("navigator:browser").shell;
-        let content = this._shell.contentBrowser.contentWindow;
-        content.addEventListener("mozFxAccountsContentEvent",
-                                 FxAccountsMgmtService);
+        SystemAppProxy.addEventListener("mozFxAccountsContentEvent",
+                                        FxAccountsMgmtService);
         Services.obs.removeObserver(this, "content-start");
         break;
       case ONLOGIN_NOTIFICATION:
       case ONVERIFIED_NOTIFICATION:
       case ONLOGOUT_NOTIFICATION:
         // FxAccounts notifications have the form of fxaccounts:*
-        this._sendChromeEvent("mozFxAccountsUnsolChromeEvent", {
+        SystemAppProxy._sendCustomEvent("mozFxAccountsUnsolChromeEvent", {
           eventName: aTopic.substring(aTopic.indexOf(":") + 1)
         });
         break;
     }
   },
 
   handleEvent: function(aEvent) {
     let msg = aEvent.detail;
--- a/b2g/components/FxAccountsUIGlue.js
+++ b/b2g/components/FxAccountsUIGlue.js
@@ -10,62 +10,57 @@ Cu.import("resource://gre/modules/XPCOMU
 Cu.import("resource://gre/modules/Services.jsm");
 Cu.import("resource://gre/modules/Promise.jsm");
 Cu.import("resource://gre/modules/FxAccountsCommon.js");
 
 XPCOMUtils.defineLazyServiceGetter(this, "uuidgen",
                                    "@mozilla.org/uuid-generator;1",
                                    "nsIUUIDGenerator");
 
+XPCOMUtils.defineLazyModuleGetter(this, "SystemAppProxy",
+                                  "resource://gre/modules/SystemAppProxy.jsm");
+
 function FxAccountsUIGlue() {
 }
 
 FxAccountsUIGlue.prototype = {
 
-  _browser: Services.wm.getMostRecentWindow("navigator:browser"),
-
   _contentRequest: function(aEventName, aData) {
     let deferred = Promise.defer();
 
-    let content = this._browser.getContentWindow();
-    if (!content) {
-      deferred.reject("InternalErrorNoContent");
-      return;
-    }
-
     let id = uuidgen.generateUUID().toString();
 
-    content.addEventListener("mozFxAccountsRPContentEvent",
-                             function onContentEvent(result) {
+    SystemAppProxy.addEventListener("mozFxAccountsRPContentEvent",
+                                    function onContentEvent(result) {
       let msg = result.detail;
       if (!msg || !msg.id || msg.id != id) {
         deferred.reject("InternalErrorWrongContentEvent");
-        content.removeEventListener("mozFxAccountsRPContentEvent",
-                                    onContentEvent);
+        SystemAppProxy.removeEventListener("mozFxAccountsRPContentEvent",
+                                           onContentEvent);
         return;
       }
 
       log.debug("Got content event " + JSON.stringify(msg));
 
       if (msg.error) {
         deferred.reject(msg);
       } else {
         deferred.resolve(msg.result);
       }
-      content.removeEventListener("mozFxAccountsRPContentEvent",
-                                  onContentEvent);
+      SystemAppProxy.removeEventListener("mozFxAccountsRPContentEvent",
+                                         onContentEvent);
     });
 
     let detail = {
        eventName: aEventName,
        id: id,
        data: aData
     };
     log.debug("Send chrome event " + JSON.stringify(detail));
-    this._browser.shell.sendCustomEvent("mozFxAccountsUnsolChromeEvent", detail);
+    SystemAppProxy._sendCustomEvent("mozFxAccountsUnsolChromeEvent", detail);
 
     return deferred.promise;
   },
 
   signInFlow: function() {
     return this._contentRequest("openFlow");
   },
 
--- a/b2g/components/PaymentGlue.js
+++ b/b2g/components/PaymentGlue.js
@@ -18,16 +18,19 @@ const kOpenPaymentConfirmationEvent = "o
 const kOpenPaymentFlowEvent = "open-payment-flow-dialog";
 
 const PREF_DEBUG = "dom.payment.debug";
 
 XPCOMUtils.defineLazyServiceGetter(this, "uuidgen",
                                    "@mozilla.org/uuid-generator;1",
                                    "nsIUUIDGenerator");
 
+XPCOMUtils.defineLazyModuleGetter(this, "SystemAppProxy",
+                                  "resource://gre/modules/SystemAppProxy.jsm");
+
 function PaymentUI() {
   try {
     this._debug =
       Services.prefs.getPrefType(PREF_DEBUG) == Ci.nsIPrefBranch.PREF_BOOL
       && Services.prefs.getBoolPref(PREF_DEBUG);
   } catch(e) {
     this._debug = false;
   }
@@ -40,23 +43,16 @@ PaymentUI.prototype = {
                                                         aSuccessCb,
                                                         aErrorCb) {
     let _error = function _error(errorMsg) {
       if (aErrorCb) {
         aErrorCb.onresult(aRequestId, errorMsg);
       }
     };
 
-    let browser = Services.wm.getMostRecentWindow("navigator:browser");
-    let content = browser.getContentWindow();
-    if (!content) {
-      _error("NO_CONTENT_WINDOW");
-      return;
-    }
-
     // The UI should listen for mozChromeEvent 'open-payment-confirmation-dialog'
     // type in order to create and show the payment request confirmation frame
     // embeded within a trusted dialog.
     let id = kOpenPaymentConfirmationEvent + "-" + this.getRandomId();
     let detail = {
       type: kOpenPaymentConfirmationEvent,
       id: id,
       requestId: aRequestId,
@@ -73,41 +69,34 @@ PaymentUI.prototype = {
       }
 
       if (msg.userSelection && aSuccessCb) {
         aSuccessCb.onresult(aRequestId, msg.userSelection);
       } else if (msg.errorMsg) {
         _error(msg.errorMsg);
       }
 
-      content.removeEventListener("mozContentEvent", this._handleSelection);
+      SystemAppProxy.removeEventListener("mozContentEvent", this._handleSelection);
       this._handleSelection = null;
     }).bind(this);
-    content.addEventListener("mozContentEvent", this._handleSelection);
+    SystemAppProxy.addEventListener("mozContentEvent", this._handleSelection);
 
-    browser.shell.sendChromeEvent(detail);
+    SystemAppProxy.dispatchEvent(detail);
   },
 
   showPaymentFlow: function showPaymentFlow(aRequestId,
                                             aPaymentFlowInfo,
                                             aErrorCb) {
     let _error = function _error(errorMsg) {
       if (aErrorCb) {
         aErrorCb.onresult(aRequestId, errorMsg);
       }
     };
 
     // We ask the UI to browse to the selected payment flow.
-    let browser = Services.wm.getMostRecentWindow("navigator:browser");
-    let content = browser.getContentWindow();
-    if (!content) {
-      _error("NO_CONTENT_WINDOW");
-      return;
-    }
-
     let id = kOpenPaymentFlowEvent + "-" + this.getRandomId();
     let detail = {
       type: kOpenPaymentFlowEvent,
       id: id,
       requestId: aRequestId,
       uri: aPaymentFlowInfo.uri,
       method: aPaymentFlowInfo.requestMethod,
       jwt: aPaymentFlowInfo.jwt
@@ -118,24 +107,24 @@ PaymentUI.prototype = {
     // content.
     this._loadPaymentShim = (function _loadPaymentShim(evt) {
       let msg = evt.detail;
       if (msg.id != id) {
         return;
       }
 
       if (msg.errorMsg) {
-        content.removeEventListener("mozContentEvent", this._loadPaymentShim);
+        SystemAppProxy.removeEventListener("mozContentEvent", this._loadPaymentShim);
         this._loadPaymentShim = null;
         _error("ERROR_LOADING_PAYMENT_SHIM: " + msg.errorMsg);
         return;
       }
 
       if (!msg.frame) {
-        content.removeEventListener("mozContentEvent", this._loadPaymentShim);
+        SystemAppProxy.removeEventListener("mozContentEvent", this._loadPaymentShim);
         this._loadPaymentShim = null;
         _error("ERROR_LOADING_PAYMENT_SHIM");
         return;
       }
 
       // Try to load the payment shim file containing the payment callbacks
       // in the content script.
       let frame = msg.frame;
@@ -147,66 +136,60 @@ PaymentUI.prototype = {
         mm.sendAsyncMessage("Payment:LoadShim", { requestId: aRequestId });
       } catch (e) {
         if (this._debug) {
           this.LOG("Error loading " + kPaymentShimFile + " as a frame script: "
                     + e);
         }
         _error("ERROR_LOADING_PAYMENT_SHIM");
       } finally {
-        content.removeEventListener("mozContentEvent", this._loadPaymentShim);
+        SystemAppProxy.removeEventListener("mozContentEvent", this._loadPaymentShim);
         this._loadPaymentShim = null;
       }
     }).bind(this);
-    content.addEventListener("mozContentEvent", this._loadPaymentShim);
+    SystemAppProxy.addEventListener("mozContentEvent", this._loadPaymentShim);
 
     // We also listen for UI notifications about a closed payment flow. The UI
     // should provide the reason of the closure within the 'errorMsg' parameter
     this._notifyPayFlowClosed = (function _notifyPayFlowClosed(evt) {
       let msg = evt.detail;
       if (msg.id != id) {
         return;
       }
 
       if (msg.type != 'cancel') {
         return;
       }
 
       if (msg.errorMsg) {
         _error(msg.errorMsg);
       }
-      content.removeEventListener("mozContentEvent",
-                                  this._notifyPayFlowClosed);
+      SystemAppProxy.removeEventListener("mozContentEvent",
+                                         this._notifyPayFlowClosed);
       this._notifyPayFlowClosed = null;
     }).bind(this);
-    content.addEventListener("mozContentEvent",
-                             this._notifyPayFlowClosed);
+    SystemAppProxy.addEventListener("mozContentEvent",
+                               this._notifyPayFlowClosed);
 
-    browser.shell.sendChromeEvent(detail);
+    SystemAppProxy.dispatchEvent(detail);
   },
 
   cleanup: function cleanup() {
-    let browser = Services.wm.getMostRecentWindow("navigator:browser");
-    let content = browser.getContentWindow();
-    if (!content) {
-      return;
-    }
-
     if (this._handleSelection) {
-      content.removeEventListener("mozContentEvent", this._handleSelection);
+      SystemAppProxy.removeEventListener("mozContentEvent", this._handleSelection);
       this._handleSelection = null;
     }
 
     if (this._notifyPayFlowClosed) {
-      content.removeEventListener("mozContentEvent", this._notifyPayFlowClosed);
+      SystemAppProxy.removeEventListener("mozContentEvent", this._notifyPayFlowClosed);
       this._notifyPayFlowClosed = null;
     }
 
     if (this._loadPaymentShim) {
-      content.removeEventListener("mozContentEvent", this._loadPaymentShim);
+      SystemAppProxy.removeEventListener("mozContentEvent", this._loadPaymentShim);
       this._loadPaymentShim = null;
     }
   },
 
   getRandomId: function getRandomId() {
     return uuidgen.generateUUID().toString();
   },
 
--- a/b2g/components/SignInToWebsite.jsm
+++ b/b2g/components/SignInToWebsite.jsm
@@ -81,16 +81,19 @@ XPCOMUtils.defineLazyModuleGetter(this, 
                                   "resource://gre/modules/identity/IdentityUtils.jsm");
 
 XPCOMUtils.defineLazyModuleGetter(this, "IdentityService",
                                   "resource://gre/modules/identity/MinimalIdentity.jsm");
 
 XPCOMUtils.defineLazyModuleGetter(this, "Logger",
                                   "resource://gre/modules/identity/LogUtils.jsm");
 
+XPCOMUtils.defineLazyModuleGetter(this, "SystemAppProxy",
+                                  "resource://gre/modules/SystemAppProxy.jsm");
+
 // The default persona uri; can be overwritten with toolkit.identity.uri pref.
 // Do this if you want to repoint to a different service for testing.
 // There's no point in setting up an observer to monitor the pref, as b2g prefs
 // can only be overwritten when the profie is recreated.  So just get the value
 // on start-up.
 let kPersonaUri = "https://firefoxos.persona.org";
 try {
   kPersonaUri = Services.prefs.getCharPref("toolkit.identity.uri");
@@ -117,36 +120,20 @@ const kIdentityDelegateReady = "identity
 const kIdentityControllerDoMethod = "identity-controller-doMethod";
 
 function log(...aMessageArgs) {
   Logger.log.apply(Logger, ["SignInToWebsiteController"].concat(aMessageArgs));
 }
 
 log("persona uri =", kPersonaUri);
 
-/*
- * ContentInterface encapsulates the our content functions.  There are only two:
- *
- * getContent       - return the current content window
- * sendChromeEvent  - send a chromeEvent from the browser shell
- */
-let ContentInterface = {
-  _getBrowser: function SignInToWebsiteController__getBrowser() {
-    return Services.wm.getMostRecentWindow("navigator:browser");
-  },
-
-  getContent: function SignInToWebsiteController_getContent() {
-    return this._getBrowser().getContentWindow();
-  },
-
-  sendChromeEvent: function SignInToWebsiteController_sendChromeEvent(detail) {
-    detail.uri = kPersonaUri;
-    this._getBrowser().shell.sendChromeEvent(detail);
-  }
-};
+function sendChromeEvent(details) {
+  details.uri = kPersonaUri;
+  SystemAppProxy.dispatchEvent(details);
+}
 
 function Pipe() {
   this._watchers = [];
 }
 
 Pipe.prototype = {
   init: function pipe_init() {
     Services.obs.addObserver(this, "identity-child-process-shutdown", false);
@@ -212,43 +199,36 @@ Pipe.prototype = {
 
     if (this._watchers.length === 0) {
       log("No more watchers; clean up persona host iframe");
       let detail = {
         type: kCloseIdentityDialog
       };
       log('telling content to close the dialog');
       // tell content to close the dialog
-      ContentInterface.sendChromeEvent(detail);
+      sendChromeEvent(detail);
     }
   },
 
   communicate: function(aRpOptions, aContentOptions, aMessageCallback) {
     let rpID = aRpOptions.id;
     let rpMM = aRpOptions.mm;
     if (rpMM) {
       this._addWatcher(rpID, rpMM);
     }
 
     log("RP options:", aRpOptions, "\n  content options:", aContentOptions);
 
     // This content variable is injected into the scope of
     // kIdentityShimFile, where it is used to access the BrowserID object
     // and its internal API.
-    let content = ContentInterface.getContent();
     let mm = null;
     let uuid = getRandomId();
     let self = this;
 
-    if (!content) {
-      log("ERROR: what the what? no content window?");
-      // aErrorCb.onresult("NO_CONTENT_WINDOW");
-      return;
-    }
-
     function removeMessageListeners() {
       if (mm) {
         mm.removeMessageListener(kIdentityDelegateFinished, identityDelegateFinished);
         mm.removeMessageListener(kIdentityControllerDoMethod, aMessageCallback);
       }
     }
 
     function identityDelegateFinished() {
@@ -256,31 +236,31 @@ Pipe.prototype = {
 
       let detail = {
         type: kDoneIdentityDialog,
         showUI: aContentOptions.showUI || false,
         id: kDoneIdentityDialog + "-" + uuid,
         requestId: aRpOptions.id
       };
       log('received delegate finished; telling content to close the dialog');
-      ContentInterface.sendChromeEvent(detail);
+      sendChromeEvent(detail);
       self._removeWatchers(rpID, rpMM);
     }
 
-    content.addEventListener("mozContentEvent", function getAssertion(evt) {
+    SystemAppProxy.addEventListener("mozContentEvent", function getAssertion(evt) {
       let msg = evt.detail;
       if (!msg.id.match(uuid)) {
         return;
       }
 
       switch (msg.id) {
         case kOpenIdentityDialog + '-' + uuid:
           if (msg.type === 'cancel') {
             // The user closed the dialog.  Clean up and call cancel.
-            content.removeEventListener("mozContentEvent", getAssertion);
+            SystemAppProxy.removeEventListener("mozContentEvent", getAssertion);
             removeMessageListeners();
             aMessageCallback({json: {method: "cancel"}});
           } else {
             // The window has opened.  Inject the identity shim file containing
             // the callbacks in the content script.  This could be either the
             // visible popup that the user interacts with, or it could be an
             // invisible frame.
             let frame = evt.detail.frame;
@@ -304,17 +284,17 @@ Pipe.prototype = {
             mm.sendAsyncMessage(aContentOptions.message, aRpOptions);
           }
           break;
 
         case kDoneIdentityDialog + '-' + uuid:
           // Received our assertion.  The message manager callbacks will handle
           // communicating back to the IDService.  All we have to do is remove
           // this listener.
-          content.removeEventListener("mozContentEvent", getAssertion);
+          SystemAppProxy.removeEventListener("mozContentEvent", getAssertion);
           break;
 
         default:
           log("ERROR - Unexpected message: id=" + msg.id + ", type=" + msg.type + ", errorMsg=" + msg.errorMsg);
           break;
       }
 
     });
@@ -325,17 +305,17 @@ Pipe.prototype = {
     // available in the context.
     let detail = {
       type: kOpenIdentityDialog,
       showUI: aContentOptions.showUI || false,
       id: kOpenIdentityDialog + "-" + uuid,
       requestId: aRpOptions.id
     };
 
-    ContentInterface.sendChromeEvent(detail);
+    sendChromeEvent(detail);
   }
 
 };
 
 /*
  * The controller sits between the IdentityService used by DOMIdentity
  * and a content process launches an (invisible) iframe or (visible)
  * trusty UI.  Using an injected js script (identity.js), the
new file mode 100644
--- /dev/null
+++ b/b2g/components/SystemAppProxy.jsm
@@ -0,0 +1,114 @@
+/* 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');
+Cu.import('resource://gre/modules/Services.jsm');
+
+this.EXPORTED_SYMBOLS = ['SystemAppProxy'];
+
+let SystemAppProxy = {
+  _frame: null,
+  _isReady: false,
+  _pendingEvents: [],
+  _pendingListeners: [],
+
+  // To call when a new system app iframe is created
+  registerFrame: function (frame) {
+    this._isReady = false;
+    this._frame = frame;
+
+    // Register all DOM event listeners added before we got a ref to the app iframe
+    this._pendingListeners
+        .forEach((args) =>
+                 this.addEventListener.apply(this, args));
+    this._pendingListeners = [];
+  },
+
+  // To call when it is ready to receive events
+  setIsReady: function () {
+    if (this._isReady) {
+      Cu.reportError('SystemApp has already been declared as being ready.');
+    }
+    this._isReady = true;
+
+    // Dispatch all events being queued while the system app was still loading
+    this._pendingEvents
+        .forEach(([type, details]) =>
+                 this._sendCustomEvent(type, details));
+    this._pendingEvents = [];
+  },
+
+  /*
+   * Common way to send an event to the system app.
+   *
+   * // In gecko code:
+   *   SystemAppProxy.sendCustomEvent('foo', { data: 'bar' });
+   * // In system app:
+   *   window.addEventListener('foo', function (event) {
+   *     event.details == 'bar'
+   *   });
+   */
+  _sendCustomEvent: function systemApp_sendCustomEvent(type, details) {
+    let content = this._frame ? this._frame.contentWindow : null;
+
+    // If the system app isn't ready yet,
+    // queue events until someone calls setIsLoaded
+    if (!this._isReady || !content) {
+      this._pendingEvents.push([type, details]);
+      return null;
+    }
+
+    let event = content.document.createEvent('CustomEvent');
+
+    let payload;
+    // If the root object already has __exposedProps__,
+    // we consider the caller already wrapped (correctly) the object.
+    if ('__exposedProps__' in details) {
+      payload = details;
+    } else {
+      payload = details ? Cu.cloneInto(details, content) : {};
+    }
+
+    event.initCustomEvent(type, true, false, payload);
+    content.dispatchEvent(event);
+
+    return event;
+  },
+
+  // Now deprecated, use sendCustomEvent with a custom event name
+  dispatchEvent: function systemApp_sendChromeEvent(details) {
+    return this._sendCustomEvent('mozChromeEvent', details);
+  },
+
+  // Listen for dom events on the system app
+  addEventListener: function systemApp_addEventListener() {
+    let content = this._frame ? this._frame.contentWindow : null;
+    if (!content) {
+      this._pendingListeners.push(arguments);
+      return false;
+    }
+
+    content.addEventListener.apply(content, arguments);
+    return true;
+  },
+
+  removeEventListener: function systemApp_removeEventListener(name, listener) {
+    let content = this._frame ? this._frame.contentWindow : null;
+    if (content) {
+      content.removeEventListener.apply(content, arguments);
+    } else {
+      let idx = this._pendingListeners.indexOf(listener);
+      if (idx != -1) {
+        this._pendingListeners.splice(idx, 1);
+      }
+    }
+  }
+
+};
+this.SystemAppProxy = SystemAppProxy;
+
--- a/b2g/components/UpdatePrompt.js
+++ b/b2g/components/UpdatePrompt.js
@@ -56,16 +56,19 @@ function useSettings() {
   // and trying to use settings in this scenario causes lots of weird
   // assertions at shutdown time.
   if (typeof useSettings.result === "undefined") {
     useSettings.result = !Services.env.get("XPCSHELL_TEST_PROFILE_DIR");
   }
   return useSettings.result;
 }
 
+XPCOMUtils.defineLazyModuleGetter(this, "SystemAppProxy",
+                                  "resource://gre/modules/SystemAppProxy.jsm");
+
 function UpdateCheckListener(updatePrompt) {
   this._updatePrompt = updatePrompt;
 }
 
 UpdateCheckListener.prototype = {
   QueryInterface: XPCOMUtils.generateQI([Ci.nsIUpdateCheckListener]),
 
   _updatePrompt: null,
@@ -128,34 +131,27 @@ UpdatePrompt.prototype = {
                                          Ci.nsIProgressEventSink,
                                          Ci.nsIObserver]),
   _xpcom_factory: XPCOMUtils.generateSingletonFactory(UpdatePrompt),
 
   _update: null,
   _applyPromptTimer: null,
   _waitingForIdle: false,
   _updateCheckListner: null,
-  _pendingEvents: [],
 
   get applyPromptTimeout() {
     return Services.prefs.getIntPref(PREF_APPLY_PROMPT_TIMEOUT);
   },
 
   get applyIdleTimeout() {
     return Services.prefs.getIntPref(PREF_APPLY_IDLE_TIMEOUT);
   },
 
-  handleContentStart: function UP_handleContentStart(shell) {
-    let content = shell.contentBrowser.contentWindow;
-    content.addEventListener("mozContentEvent", this);
-
-    for (let i = 0; i < this._pendingEvents.length; i++) {
-      shell.sendChromeEvent(this._pendingEvents[i]);
-    }
-    this._pendingEvents.length = 0;
+  handleContentStart: function UP_handleContentStart() {
+    SystemAppProxy.addEventListener("mozContentEvent", this);
   },
 
   // nsIUpdatePrompt
 
   // FIXME/bug 737601: we should have users opt-in to downloading
   // updates when on a billed pipe.  Initially, opt-in for 3g, but
   // that doesn't cover all cases.
   checkForUpdates: function UP_checkForUpdates() { },
@@ -285,25 +281,22 @@ UpdatePrompt.prototype = {
     this._update = aUpdate;
     return this.sendChromeEvent(aType, detail);
   },
 
   sendChromeEvent: function UP_sendChromeEvent(aType, aDetail) {
     let detail = aDetail || {};
     detail.type = aType;
 
-    let browser = Services.wm.getMostRecentWindow("navigator:browser");
-    if (!browser) {
-      this._pendingEvents.push(detail);
+    let sent = SystemAppProxy.dispatchEvent(detail);
+    if (!sent) {
       log("Warning: Couldn't send update event " + aType +
           ": no content browser. Will send again when content becomes available.");
       return false;
     }
-
-    browser.shell.sendChromeEvent(detail);
     return true;
   },
 
   handleAvailableResult: function UP_handleAvailableResult(aDetail) {
     // If the user doesn't choose "download", the updater will implicitly call
     // showUpdateAvailable again after a certain period of time
     switch (aDetail.result) {
       case "download":
--- a/b2g/components/WebappsUpdater.jsm
+++ b/b2g/components/WebappsUpdater.jsm
@@ -11,44 +11,40 @@ const Cu = Components.utils;
 
 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
 Cu.import("resource://gre/modules/Services.jsm");
 
 XPCOMUtils.defineLazyServiceGetter(this, "settings",
                                    "@mozilla.org/settingsService;1",
                                    "nsISettingsService");
 
+XPCOMUtils.defineLazyModuleGetter(this, "SystemAppProxy",
+                                  "resource://gre/modules/SystemAppProxy.jsm");
+
 function debug(aStr) {
   //dump("--*-- WebappsUpdater: " + aStr);
 }
 
 this.WebappsUpdater = {
   _checkingApps: false,
-  _pendingEvents: [],
 
-  handleContentStart: function(aShell) {
-    let content = aShell.contentBrowser.contentWindow;
-    this._pendingEvents.forEach(aShell.sendChromeEvent);
-
-    this._pendingEvents.length = 0;
+  handleContentStart: function() {
   },
 
   sendChromeEvent: function(aType, aDetail) {
     let detail = aDetail || {};
     detail.type = aType;
 
-    let browser = Services.wm.getMostRecentWindow("navigator:browser");
-    if (!browser) {
-      this._pendingEvents.push(detail);
+    let sent = SystemAppProxy.dispatchEvent(detail);
+    if (!sent) {
       debug("Warning: Couldn't send update event " + aType +
           ": no content browser. Will send again when content becomes available.");
       return false;
     }
 
-    browser.shell.sendChromeEvent(detail);
     return true;
   },
 
   _appsUpdated: function(aApps) {
     debug("appsUpdated: " + aApps.length + " apps to update");
     let lock = settings.createLock();
     lock.set("apps.updateStatus", "check-complete", null);
     this.sendChromeEvent("apps-update-check", { apps: aApps });
--- a/b2g/components/moz.build
+++ b/b2g/components/moz.build
@@ -1,15 +1,15 @@
 # -*- Mode: python; c-basic-offset: 4; indent-tabs-mode: nil; tab-width: 40 -*-
 # vim: set filetype=python:
 # 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/.
 
-TEST_DIRS += ['test']
+DIRS += ['test']
 
 EXTRA_COMPONENTS += [
     'ActivitiesGlue.js',
     'AlertsService.js',
     'B2GAboutRedirector.js',
     'ContentHandler.js',
     'ContentPermissionPrompt.js',
     'FilePicker.js',
@@ -37,16 +37,17 @@ EXTRA_PP_COMPONENTS += [
 if CONFIG['MOZ_UPDATER']:
     EXTRA_PP_COMPONENTS += [
         'UpdatePrompt.js',
     ]
 
 EXTRA_JS_MODULES += [
     'ErrorPage.jsm',
     'SignInToWebsite.jsm',
+    'SystemAppProxy.jsm',
     'TelURIParser.jsm',
     'WebappsUpdater.jsm',
 ]
 
 if CONFIG['MOZ_WIDGET_TOOLKIT'] != 'gonk':
     EXTRA_JS_MODULES += [
       'GlobalSimulatorScreen.jsm'
     ]
--- a/b2g/components/test/mochitest/mochitest.ini
+++ b/b2g/components/test/mochitest/mochitest.ini
@@ -1,10 +1,14 @@
 [DEFAULT]
-run-if = toolkit == "gonk"
 support-files =
   permission_handler_chrome.js
   SandboxPromptTest.html
   filepicker_path_handler_chrome.js
+  systemapp_helper.js
 
 [test_sandbox_permission.html]
+run-if = toolkit == "gonk"
 [test_filepicker_path.html]
+run-if = toolkit == "gonk"
 [test_permission_deny.html]
+run-if = toolkit == "gonk"
+[test_systemapp.html]
--- a/b2g/components/test/mochitest/permission_handler_chrome.js
+++ b/b2g/components/test/mochitest/permission_handler_chrome.js
@@ -8,49 +8,29 @@ function debug(str) {
   dump("CHROME PERMISSON HANDLER -- " + str + "\n");
 }
 
 const Cc = Components.classes;
 const Ci = Components.interfaces;
 const Cu = Components.utils;
 
 const { Services } = Cu.import("resource://gre/modules/Services.jsm");
-
-let browser = Services.wm.getMostRecentWindow("navigator:browser");
-let shell;
+const { SystemAppProxy } = Cu.import("resource://gre/modules/SystemAppProxy.jsm");
 
-function loadShell() {
-  if (!browser) {
-    debug("no browser");
-    return false;
+let eventHandler = function(evt) {
+  if (!evt.detail || evt.detail.type !== "permission-prompt") {
+    return;
   }
-  shell = browser.shell;
-  return true;
-}
 
-function getContentWindow() {
-  return shell.contentBrowser.contentWindow;
-}
+  sendAsyncMessage("permission-request", evt.detail);
+};
 
-if (loadShell()) {
-  let content = getContentWindow();
-  let eventHandler = function(evt) {
-    if (!evt.detail || evt.detail.type !== "permission-prompt") {
-      return;
-    }
-
-    sendAsyncMessage("permission-request", evt.detail);
-  };
-
-  content.addEventListener("mozChromeEvent", eventHandler);
+SystemAppProxy.addEventListener("mozChromeEvent", eventHandler);
 
-  // need to remove ChromeEvent listener after test finished.
-  addMessageListener("teardown", function() {
-    content.removeEventListener("mozChromeEvent", eventHandler);
-  });
+// need to remove ChromeEvent listener after test finished.
+addMessageListener("teardown", function() {
+  SystemAppProxy.removeEventListener("mozChromeEvent", eventHandler);
+});
 
-  addMessageListener("permission-response", function(detail) {
-    let event = content.document.createEvent('CustomEvent');
-    event.initCustomEvent('mozContentEvent', true, true, detail);
-    content.dispatchEvent(event);
-  });
-}
+addMessageListener("permission-response", function(detail) {
+  SystemAppProxy._sendCustomEvent('mozContentEvent', detail);
+});
 
new file mode 100644
--- /dev/null
+++ b/b2g/components/test/mochitest/systemapp_helper.js
@@ -0,0 +1,141 @@
+const Cu = Components.utils;
+
+const { Services } = Cu.import("resource://gre/modules/Services.jsm");
+
+// Load a duplicated copy of the jsm to prevent messing with the currently running one
+let scope = {};
+Services.scriptloader.loadSubScript("resource://gre/modules/SystemAppProxy.jsm", scope);
+const { SystemAppProxy } = scope;
+
+let frame;
+
+let index = -1;
+function next() {
+  index++;
+  if (index >= steps.length) {
+    assert.ok(false, "Shouldn't get here!");
+    return;
+  }
+  try {
+    steps[index]();
+  } catch(ex) {
+    assert.ok(false, "Caught exception: " + ex);
+  }
+}
+
+// Listen for events received by the system app document
+// to ensure that we receive all of them, in an expected order and time
+let isLoaded = false;
+let n = 0;
+function listener(event) {
+  if (!isLoaded) {
+    assert.ok(false, "Received event before the iframe is ready");
+    return;
+  }
+  n++;
+  if (n == 1) {
+    assert.equal(event.type, "mozChromeEvent");
+    assert.equal(event.detail.name, "first");
+  } else if (n == 2) {
+    assert.equal(event.type, "custom");
+    assert.equal(event.detail.name, "second");
+
+    next(); // call checkEventDispatching
+  } else if (n == 3) {
+    assert.equal(event.type, "custom");
+    assert.equal(event.detail.name, "third");
+  } else if (n == 4) {
+    assert.equal(event.type, "mozChromeEvent");
+    assert.equal(event.detail.name, "fourth");
+
+    next(); // call checkEventListening();
+  } else {
+    assert.ok(false, "Unexpected event of type " + event.type);
+  }
+}
+
+
+let steps = [
+  function waitForWebapps() {
+    // We are using webapps API later in this test and we need to ensure
+    // it is fully initialized before trying to use it
+    let { DOMApplicationRegistry } =  Cu.import('resource://gre/modules/Webapps.jsm', {});
+    DOMApplicationRegistry.registryReady.then(function () {
+      next();
+    });
+  },
+
+  function earlyEvents() {
+    // Immediately try to send events
+    SystemAppProxy.dispatchEvent({ name: "first" });
+    SystemAppProxy._sendCustomEvent("custom", { name: "second" });
+    next();
+  },
+
+  function createFrame() {
+    // Create a fake system app frame
+    let win = Services.wm.getMostRecentWindow("navigator:browser");
+    let doc = win.document;
+    frame = doc.createElement("iframe");
+    doc.documentElement.appendChild(frame);
+
+    // Ensure that events are correctly sent to the frame.
+    // `listener` is going to call next()
+    frame.contentWindow.addEventListener("mozChromeEvent", listener);
+    frame.contentWindow.addEventListener("custom", listener);
+
+    // Ensure that listener being registered before the system app is ready
+    // are correctly removed from the pending list
+    function removedListener() {
+      assert(false, "Listener isn't correctly removed from the pending list");
+    }
+    SystemAppProxy.addEventListener("mozChromeEvent", removedListener);
+    SystemAppProxy.removeEventListener("mozChromeEvent", removedListener);
+
+    // Register it to the JSM
+    SystemAppProxy.registerFrame(frame);
+    assert.ok(true, "Frame created and registered");
+
+    frame.contentWindow.addEventListener("load", function onload() {
+      frame.contentWindow.removeEventListener("load", onload);
+      assert.ok(true, "Frame document loaded");
+
+      // Declare that the iframe is now loaded.
+      // That should dispatch early events
+      isLoaded = true;
+      SystemAppProxy.setIsReady();
+      assert.ok(true, "Frame declared as loaded");
+
+      // Once pending events are received,
+      // we will run checkEventDispatching from `listener` function
+    });
+
+    frame.setAttribute("src", "data:text/html,system app");
+  },
+
+  function checkEventDispatching() {
+    // Send events after the iframe is ready,
+    // they should be dispatched right away
+    SystemAppProxy._sendCustomEvent("custom", { name: "third" });
+    SystemAppProxy.dispatchEvent({ name: "fourth" });
+    // Once this 4th event is received, we will run checkEventListening
+  },
+
+  function checkEventListening() {
+    SystemAppProxy.addEventListener("mozContentEvent", function onContentEvent(event) {
+      assert.equal(event.detail.name, "first-content", "received a system app event");
+      SystemAppProxy.removeEventListener("mozContentEvent", onContentEvent);
+
+      next();
+    });
+    let win = frame.contentWindow;
+    win.dispatchEvent(new win.CustomEvent("mozContentEvent", { detail: {name: "first-content"} }));
+  },
+
+  function endOfTest() {
+    frame.remove();
+    sendAsyncMessage("finish");
+  }
+];
+
+next();
new file mode 100644
--- /dev/null
+++ b/b2g/components/test/mochitest/test_systemapp.html
@@ -0,0 +1,31 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=963239
+-->
+<head>
+  <meta charset="utf-8">
+  <title>SystemAppProxy Test</title>
+  <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+  <script type="application/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=963239">SystemAppProxy.jsm</a>
+<script type="application/javascript">
+
+"use strict";
+
+var gUrl = SimpleTest.getTestFileURL("systemapp_helper.js");
+var gScript = SpecialPowers.loadChromeScript(gUrl);
+
+SimpleTest.waitForExplicitFinish();
+gScript.addMessageListener("finish", function () {
+  SimpleTest.ok(true, "chrome test script finished");
+  gScript.destroy();
+  SimpleTest.finish();
+});
+
+</script>
+</pre>
+</body>
+</html>
--- a/b2g/components/test/unit/test_fxaccounts.js
+++ b/b2g/components/test/unit/test_fxaccounts.js
@@ -11,30 +11,29 @@ Cu.import("resource://services-common/ut
 Cu.import("resource://testing-common/httpd.js");
 
 XPCOMUtils.defineLazyModuleGetter(this, "FxAccountsMgmtService",
                                   "resource://gre/modules/FxAccountsMgmtService.jsm",
                                   "FxAccountsMgmtService");
 
 // At end of test, restore original state
 const ORIGINAL_AUTH_URI = Services.prefs.getCharPref("identity.fxaccounts.auth.uri");
-const ORIGINAL_SHELL = FxAccountsMgmtService._shell;
+let { SystemAppProxy } = Cu.import("resource://gre/modules/FxAccountsMgmtService.jsm");
+const ORIGINAL_SENDCUSTOM = SystemAppProxy._sendCustomEvent;
 do_register_cleanup(function() {
   Services.prefs.setCharPref("identity.fxaccounts.auth.uri", ORIGINAL_AUTH_URI);
-  FxAccountsMgmtService._shell = ORIGINAL_SHELL;
+  SystemAppProxy._sendCustomEvent = ORIGINAL_SENDCUSTOM;
 });
 
 // Make profile available so that fxaccounts can store user data
 do_get_profile();
 
-// Mock the b2g shell; make message passing possible
-let mockShell = {
-  sendCustomEvent: function(aEventName, aMsg) {
-    Services.obs.notifyObservers({wrappedJSObject: aMsg}, aEventName, null);
-  },
+// Mock the system app proxy; make message passing possible
+let mockSendCustomEvent = function(aEventName, aMsg) {
+  Services.obs.notifyObservers({wrappedJSObject: aMsg}, aEventName, null);
 };
 
 function run_test() {
   run_next_test();
 }
 
 add_task(function test_overall() {
   do_check_neq(FxAccountsMgmtService, null);
@@ -139,17 +138,17 @@ add_test(function test_invalidEmailCase_
       default:
         do_throw("wat!");
         break;
     }
   }
 
   Services.obs.addObserver(onMessage, "mozFxAccountsChromeEvent", false);
 
-  FxAccountsMgmtService._shell = mockShell;
+  SystemAppProxy._sendCustomEvent = mockSendCustomEvent;
 
   // Trigger signIn using an email with incorrect capitalization
   FxAccountsMgmtService.handleEvent({
     detail: {
       id: "signIn",
       data: {
         method: "signIn",
         accountId: clientEmail,
--- a/dom/inputmethod/Keyboard.jsm
+++ b/dom/inputmethod/Keyboard.jsm
@@ -11,16 +11,19 @@ const Cc = Components.classes;
 const Ci = Components.interfaces;
 
 Cu.import('resource://gre/modules/Services.jsm');
 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
 
 XPCOMUtils.defineLazyServiceGetter(this, "ppmm",
   "@mozilla.org/parentprocessmessagemanager;1", "nsIMessageBroadcaster");
 
+XPCOMUtils.defineLazyModuleGetter(this, "SystemAppProxy",
+                                  "resource://gre/modules/SystemAppProxy.jsm");
+
 this.Keyboard = {
   _formMM: null,     // The current web page message manager.
   _keyboardMM: null, // The keyboard app message manager.
   _systemMessageName: [
     'SetValue', 'RemoveFocus', 'SetSelectedOption', 'SetSelectedOptions'
   ],
 
   _messageNames: [
@@ -226,17 +229,17 @@ this.Keyboard = {
     this.sendToKeyboard(newEventName, msg.data);
   },
 
   handleFocusChange: function keyboardHandleFocusChange(msg) {
     this.forwardEvent('Keyboard:FocusChange', msg);
 
     // Chrome event, used also to render value selectors; that's why we need
     // the info about choices / min / max here as well...
-    this.sendChromeEvent({
+    SystemAppProxy.dispatchEvent({
       type: 'inputmethod-contextchange',
       inputType: msg.data.type,
       value: msg.data.value,
       choices: JSON.stringify(msg.data.choices),
       min: msg.data.min,
       max: msg.data.max
     });
   },
@@ -261,23 +264,23 @@ this.Keyboard = {
     this.sendToForm('Forms:Select:Blur', {});
   },
 
   replaceSurroundingText: function keyboardReplaceSurroundingText(msg) {
     this.sendToForm('Forms:ReplaceSurroundingText', msg.data);
   },
 
   showInputMethodPicker: function keyboardShowInputMethodPicker() {
-    this.sendChromeEvent({
+    SystemAppProxy.dispatchEvent({
       type: "inputmethod-showall"
     });
   },
 
   switchToNextInputMethod: function keyboardSwitchToNextInputMethod() {
-    this.sendChromeEvent({
+    SystemAppProxy.dispatchEvent({
       type: "inputmethod-next"
     });
   },
 
   getText: function keyboardGetText(msg) {
     this.sendToForm('Forms:GetText', msg.data);
   },
 
@@ -307,19 +310,12 @@ this.Keyboard = {
   _layouts: null,
   setLayouts: function keyboardSetLayoutCount(layouts) {
     // The input method plugins may not have loaded yet,
     // cache the layouts so on init we can respond immediately instead
     // of going back and forth between keyboard_manager
     this._layouts = layouts;
 
     this.sendToKeyboard('Keyboard:LayoutsChange', layouts);
-  },
-
-  sendChromeEvent: function(event) {
-    let browser = Services.wm.getMostRecentWindow("navigator:browser");
-    if (browser && browser.shell) {
-      browser.shell.sendChromeEvent(event);;
-    }
   }
 };
 
 this.Keyboard.init();