Bug 822712 - SimplePush - Implementation. r=dougt, jst, jlebar
authorNikhil Marathe <nsm.nikhil@gmail.com>
Thu, 28 Mar 2013 20:49:41 -0700
changeset 126604 2aaf82b852e7
parent 126603 e93a4da26856
child 126605 bfced2ecc0cf
push id24489
push userdougt@mozilla.com
push date2013-03-29 03:50 +0000
treeherdermozilla-central@bfced2ecc0cf [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersdougt, jst, jlebar
bugs822712
milestone22.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 822712 - SimplePush - Implementation. r=dougt, jst, jlebar
b2g/app/b2g.js
b2g/installer/package-manifest.in
browser/installer/package-manifest.in
browser/installer/removed-files.in
dom/apps/src/PermissionsTable.jsm
dom/dom-config.mk
dom/messages/SystemMessagePermissionsChecker.jsm
dom/moz.build
dom/push/Makefile.in
dom/push/moz.build
dom/push/src/Makefile.in
dom/push/src/Push.js
dom/push/src/Push.manifest
dom/push/src/PushService.js
dom/push/src/PushService.manifest
dom/push/src/moz.build
dom/push/tests/moz.build
extensions/cookie/Permission.txt
mobile/android/installer/package-manifest.in
--- a/b2g/app/b2g.js
+++ b/b2g/app/b2g.js
@@ -383,16 +383,27 @@ pref("dom.sms.enabled", true);
 pref("dom.sms.strict7BitEncoding", false); // Disabled by default.
 
 // Temporary permission hack for WebContacts
 pref("dom.mozContacts.enabled", true);
 
 // WebAlarms
 pref("dom.mozAlarms.enabled", true);
 
+// SimplePush
+// serverURL to be assigned by services team
+pref("services.push.serverURL", "");
+pref("services.push.userAgentID", "");
+// exponential back-off start is 5 seconds like in HTTP/1.1
+pref("services.push.retryBaseInterval", 5000);
+// exponential back-off end is 20 minutes
+pref("services.push.maxRetryInterval", 1200000);
+// How long before a DOMRequest errors as timeout
+pref("services.push.requestTimeout", 10000);
+
 // NetworkStats
 #ifdef MOZ_B2G_RIL
 pref("dom.mozNetworkStats.enabled", true);
 pref("ril.lastKnownMcc", "724");
 #endif
 
 // WebSettings
 pref("dom.mozSettings.enabled", true);
--- a/b2g/installer/package-manifest.in
+++ b/b2g/installer/package-manifest.in
@@ -189,16 +189,17 @@
 @BINPATH@/components/dom_geolocation.xpt
 @BINPATH@/components/dom_media.xpt
 @BINPATH@/components/dom_network.xpt
 @BINPATH@/components/dom_notification.xpt
 @BINPATH@/components/dom_html.xpt
 @BINPATH@/components/dom_indexeddb.xpt
 @BINPATH@/components/dom_offline.xpt
 @BINPATH@/components/dom_payment.xpt
+@BINPATH@/components/dom_push.xpt
 @BINPATH@/components/dom_json.xpt
 #ifdef MOZ_B2G_RIL
 @BINPATH@/components/dom_mms.xpt
 #endif
 @BINPATH@/components/dom_browserelement.xpt
 @BINPATH@/components/dom_messages.xpt
 @BINPATH@/components/dom_power.xpt
 @BINPATH@/components/dom_quota.xpt
@@ -500,16 +501,20 @@
 @BINPATH@/components/captivedetect.js
 #endif
 @BINPATH@/components/TelemetryPing.js
 @BINPATH@/components/TelemetryPing.manifest
 @BINPATH@/components/Webapps.js
 @BINPATH@/components/Webapps.manifest
 @BINPATH@/components/AppsService.js
 @BINPATH@/components/AppsService.manifest
+@BINPATH@/components/Push.js
+@BINPATH@/components/Push.manifest
+@BINPATH@/components/PushService.js
+@BINPATH@/components/PushService.manifest
 
 @BINPATH@/components/nsDOMIdentity.js
 @BINPATH@/components/nsIDService.js
 @BINPATH@/components/Identity.manifest
 
 @BINPATH@/components/SystemMessageInternal.js
 @BINPATH@/components/SystemMessageManager.js
 @BINPATH@/components/SystemMessageManager.manifest
--- a/browser/installer/package-manifest.in
+++ b/browser/installer/package-manifest.in
@@ -192,16 +192,17 @@
 @BINPATH@/components/dom_geolocation.xpt
 @BINPATH@/components/dom_media.xpt
 @BINPATH@/components/dom_network.xpt
 @BINPATH@/components/dom_notification.xpt
 @BINPATH@/components/dom_html.xpt
 @BINPATH@/components/dom_indexeddb.xpt
 @BINPATH@/components/dom_offline.xpt
 @BINPATH@/components/dom_json.xpt
+@BINPATH@/components/dom_push.xpt
 #ifdef MOZ_B2G_RIL
 @BINPATH@/components/dom_mms.xpt
 #endif
 @BINPATH@/components/dom_browserelement.xpt
 @BINPATH@/components/dom_power.xpt
 @BINPATH@/components/dom_quota.xpt
 @BINPATH@/components/dom_range.xpt
 @BINPATH@/components/dom_settings.xpt
@@ -499,16 +500,20 @@
 @BINPATH@/components/messageWakeupService.js
 @BINPATH@/components/messageWakeupService.manifest
 @BINPATH@/components/SettingsManager.js
 @BINPATH@/components/SettingsManager.manifest
 @BINPATH@/components/Webapps.js
 @BINPATH@/components/Webapps.manifest
 @BINPATH@/components/AppsService.js
 @BINPATH@/components/AppsService.manifest
+@BINPATH@/components/Push.js
+@BINPATH@/components/Push.manifest
+@BINPATH@/components/PushService.js
+@BINPATH@/components/PushService.manifest
 @BINPATH@/components/nsDOMIdentity.js
 @BINPATH@/components/nsIDService.js
 @BINPATH@/components/Identity.manifest
 @BINPATH@/components/recording-cmdline.js
 @BINPATH@/components/recording-cmdline.manifest
 
 @BINPATH@/components/PermissionSettings.js
 @BINPATH@/components/PermissionSettings.manifest
--- a/browser/installer/removed-files.in
+++ b/browser/installer/removed-files.in
@@ -1217,16 +1217,17 @@ xpicleanup@BIN_SUFFIX@
   components/dom_css.xpt
   components/dom_events.xpt
   components/dom_geolocation.xpt
   components/dom_devicestorage.xpt
   components/dom_html.xpt
   components/dom_json.xpt
   components/dom_loadsave.xpt
   components/dom_offline.xpt
+  components/dom_push.xpt
   components/dom_range.xpt
   components/dom_sidebar.xpt
   components/dom_smil.xpt
   components/dom_storage.xpt
   components/dom_stylesheets.xpt
   components/dom_svg.xpt
   components/dom_threads.xpt
   components/dom_traversal.xpt
--- a/dom/apps/src/PermissionsTable.jsm
+++ b/dom/apps/src/PermissionsTable.jsm
@@ -119,16 +119,21 @@ this.PermissionsTable =  { geolocation: 
                              privileged: DENY_ACTION,
                              certified: ALLOW_ACTION
                            },
                            power: {
                              app: DENY_ACTION,
                              privileged: DENY_ACTION,
                              certified: ALLOW_ACTION
                            },
+                           push: {
+                            app: ALLOW_ACTION,
+                            privileged: ALLOW_ACTION,
+                            certified: ALLOW_ACTION
+                           },
                            settings: {
                              app: DENY_ACTION,
                              privileged: DENY_ACTION,
                              certified: ALLOW_ACTION,
                              access: ["read", "write"],
                              additional: ["indexedDB-chrome-settings"]
                            },
                            permissions: {
--- a/dom/dom-config.mk
+++ b/dom/dom-config.mk
@@ -3,16 +3,17 @@
 # file, You can obtain one at http://mozilla.org/MPL/2.0/.
 
 DOM_SRCDIRS = \
   dom/base \
   dom/battery \
   dom/encoding \
   dom/file \
   dom/power \
+  dom/push \
   dom/quota \
   dom/media \
   dom/network/src \
   dom/settings \
   dom/phonenumberutils \
   dom/contacts \
   dom/permission \
   dom/alarm \
--- a/dom/messages/SystemMessagePermissionsChecker.jsm
+++ b/dom/messages/SystemMessagePermissionsChecker.jsm
@@ -69,16 +69,22 @@ this.SystemMessagePermissionsTable = {
   },
   "headset-button": { },
   "icc-stkcommand": {
     "settings": ["read", "write"]
   },
   "notification": {
     "desktop-notification": []
   },
+  "push": {
+  	"push": []
+  },
+  "push-register": {
+  	"push": []
+  },
   "sms-received": {
     "sms": []
   },
   "sms-sent": {
     "sms": []
   },
   "telephony-new-call": {
     "telephony": []
--- a/dom/moz.build
+++ b/dom/moz.build
@@ -21,16 +21,17 @@ interfaces = [
     'xpath',
     'xul',
     'storage',
     'json',
     'offline',
     'geolocation',
     'notification',
     'permission',
+    'push',
     'svg',
     'smil',
     'apps',
     'gamepad',
 ]
 
 PARALLEL_DIRS += ['interfaces/' + i for i in interfaces]
 
@@ -46,16 +47,17 @@ PARALLEL_DIRS += [
     'phonenumberutils',
     'alarm',
     'devicestorage',
     'encoding',
     'file',
     'media',
     'messages',
     'power',
+    'push',
     'quota',
     'settings',
     'mobilemessage',
     'mms',
     'src',
     'time',
     'locales',
     'network',
new file mode 100644
--- /dev/null
+++ b/dom/push/Makefile.in
@@ -0,0 +1,14 @@
+# 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/.
+
+DEPTH            = @DEPTH@
+topsrcdir        = @top_srcdir@
+srcdir           = @srcdir@
+VPATH            = @srcdir@
+
+relativesrcdir   = @relativesrcdir@
+
+include $(DEPTH)/config/autoconf.mk
+
+include $(topsrcdir)/config/rules.mk
new file mode 100644
--- /dev/null
+++ b/dom/push/moz.build
@@ -0,0 +1,6 @@
+# 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/.
+
+PARALLEL_DIRS += ['src']
new file mode 100644
--- /dev/null
+++ b/dom/push/src/Makefile.in
@@ -0,0 +1,20 @@
+# 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/.
+
+DEPTH     = @DEPTH@
+topsrcdir = @top_srcdir@
+srcdir    = @srcdir@
+VPATH     = @srcdir@
+
+include $(DEPTH)/config/autoconf.mk
+
+EXTRA_COMPONENTS = \
+  Push.js \
+  Push.manifest \
+  PushService.js \
+  PushService.manifest \
+  $(NULL)
+
+include $(topsrcdir)/config/rules.mk
+
new file mode 100644
--- /dev/null
+++ b/dom/push/src/Push.js
@@ -0,0 +1,152 @@
+/* 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";
+
+function debug(s) {
+  // dump("-*- Push.js: " + s + "\n");
+}
+
+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");
+Cu.import("resource://gre/modules/DOMRequestHelper.jsm");
+Cu.import("resource://gre/modules/AppsUtils.jsm");
+
+const PUSH_CID = Components.ID("{c7ad4f42-faae-4e8b-9879-780a72349945}");
+
+/**
+ * The Push component runs in the child process and exposes the SimplePush API
+ * to the web application. The PushService running in the parent process is the
+ * one actually performing all operations.
+ */
+function Push()
+{
+  debug("Push Constructor");
+}
+
+Push.prototype = {
+  __proto__: DOMRequestIpcHelper.prototype,
+
+  classID : PUSH_CID,
+
+  QueryInterface : XPCOMUtils.generateQI([Ci.nsIDOMGlobalPropertyInitializer]),
+
+  init: function(aWindow) {
+    debug("init()");
+
+    let principal = aWindow.document.nodePrincipal;
+
+    this._pageURL = principal.URI;
+
+    let appsService = Cc["@mozilla.org/AppsService;1"]
+                        .getService(Ci.nsIAppsService);
+    this._app = appsService.getAppByLocalId(principal.appId);
+    this._manifestURL = appsService.getManifestURLByLocalId(principal.appId);
+    if (!this._manifestURL)
+      return null;
+
+    let perm = Services.perms.testExactPermissionFromPrincipal(principal,
+                                                               "push");
+    if (perm != Ci.nsIPermissionManager.ALLOW_ACTION)
+      return null;
+
+    this.initHelper(aWindow, []);
+
+    this.initMessageListener([
+      "PushService:Register:OK",
+      "PushService:Register:KO",
+      "PushService:Unregister:OK",
+      "PushService:Unregister:KO",
+      "PushService:Registrations:OK",
+      "PushService:Registrations:KO"
+    ]);
+
+    this._cpmm = Cc["@mozilla.org/childprocessmessagemanager;1"]
+                   .getService(Ci.nsISyncMessageSender);
+
+    var self = this;
+    return {
+      register: self.register.bind(self),
+      unregister: self.unregister.bind(self),
+      registrations: self.registrations.bind(self),
+      __exposedProps__: {
+        register: "r",
+        unregister: "r",
+        registrations: "r"
+      }
+    };
+  },
+
+  receiveMessage: function(aMessage) {
+    debug("receiveMessage()");
+    let request = this.getRequest(aMessage.data.requestID);
+    let json = aMessage.data;
+    if (!request) {
+      debug("No request " + json.requestID);
+      return;
+    }
+
+    switch (aMessage.name) {
+      case "PushService:Register:OK":
+        Services.DOMRequest.fireSuccess(request, json.pushEndpoint);
+        break;
+      case "PushService:Register:KO":
+        Services.DOMRequest.fireError(request, json.error);
+        break;
+      case "PushService:Unregister:OK":
+        Services.DOMRequest.fireSuccess(request, json.pushEndpoint);
+        break;
+      case "PushService:Unregister:KO":
+        Services.DOMRequest.fireError(request, json.error);
+        break;
+      case "PushService:Registrations:OK":
+        Services.DOMRequest.fireSuccess(request, json.registrations);
+        break;
+      case "PushService:Registrations:KO":
+        Services.DOMRequest.fireError(request, json.error);
+        break;
+      default:
+        debug("NOT IMPLEMENTED! receiveMessage for " + aMessage.name);
+    }
+  },
+
+  register: function() {
+    debug("register()");
+    var req = this.createRequest();
+    this._cpmm.sendAsyncMessage("Push:Register", {
+                                  pageURL: this._pageURL.spec,
+                                  manifestURL: this._manifestURL,
+                                  requestID: this.getRequestId(req)
+                                });
+    return req;
+  },
+
+  unregister: function(aPushEndpoint) {
+    debug("unregister(" + aPushEndpoint + ")");
+    var req = this.createRequest();
+    this._cpmm.sendAsyncMessage("Push:Unregister", {
+                                  pageURL: this._pageURL.spec,
+                                  manifestURL: this._manifestURL,
+                                  requestID: this.getRequestId(req),
+                                  pushEndpoint: aPushEndpoint
+                                });
+    return req;
+  },
+
+  registrations: function() {
+    debug("registrations()");
+    var req = this.createRequest();
+    this._cpmm.sendAsyncMessage("Push:Registrations", {
+                                  manifestURL: this._manifestURL,
+                                  requestID: this.getRequestId(req)
+                                });
+    return req;
+  }
+}
+
+this.NSGetFactory = XPCOMUtils.generateNSGetFactory([Push]);
new file mode 100644
--- /dev/null
+++ b/dom/push/src/Push.manifest
@@ -0,0 +1,4 @@
+component {c7ad4f42-faae-4e8b-9879-780a72349945} Push.js
+contract @mozilla.org/Push;1 {c7ad4f42-faae-4e8b-9879-780a72349945}
+category JavaScript-navigator-property push @mozilla.org/Push;1
+
new file mode 100644
--- /dev/null
+++ b/dom/push/src/PushService.js
@@ -0,0 +1,1114 @@
+/* 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";
+
+function debug(s) {
+  // dump("-*- PushService.js: " + s + "\n");
+}
+
+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");
+Cu.import("resource://gre/modules/IndexedDBHelper.jsm");
+Cu.import("resource://gre/modules/Timer.jsm");
+Cu.import("resource://gre/modules/services-common/preferences.js");
+Cu.import("resource://gre/modules/services-common/utils.js");
+Cu.import("resource://gre/modules/commonjs/sdk/core/promise.js");
+
+const kPUSHDB_DB_NAME = "push";
+const kPUSHDB_DB_VERSION = 1; // Change this if the IndexedDB format changes
+const kPUSHDB_STORE_NAME = "push";
+const kCONFLICT_RETRY_ATTEMPTS = 3; // If channelID registration says 409, how
+                                    // many times to retry with a new channelID
+
+const kERROR_CHID_CONFLICT = 409;   // Error code sent by push server if this
+                                    // channel already exists on the server.
+
+const kCHILD_PROCESS_MESSAGES = ["Push:Register", "Push:Unregister",
+                                 "Push:Registrations"];
+
+// This is a singleton
+this.PushDB = function PushDB(aGlobal) {
+  debug("PushDB()");
+
+  // set the indexeddb database
+  let idbManager = Cc["@mozilla.org/dom/indexeddb/manager;1"]
+                     .getService(Ci.nsIIndexedDatabaseManager);
+  idbManager.initWindowless(aGlobal);
+  this.initDBHelper(kPUSHDB_DB_NAME, kPUSHDB_DB_VERSION,
+                    [kPUSHDB_STORE_NAME], aGlobal);
+};
+
+this.PushDB.prototype = {
+  __proto__: IndexedDBHelper.prototype,
+
+  upgradeSchema: function(aTransaction, aDb, aOldVersion, aNewVersion) {
+    debug("PushDB.upgradeSchema()")
+
+    let objectStore = aDb.createObjectStore(kPUSHDB_STORE_NAME,
+                                            { keyPath: "channelID" });
+
+    // index to fetch records based on endpoints. used by unregister
+    objectStore.createIndex("pushEndpoint", "pushEndpoint", { unique: true });
+    // index to fetch records per manifest, so we can identify endpoints
+    // associated with an app. Since an app can have multiple endpoints
+    // uniqueness cannot be enforced
+    objectStore.createIndex("manifestURL", "manifestURL", { unique: false });
+  },
+
+  /*
+   * @param aChannelRecord
+   *        The record to be added.
+   * @param aSuccessCb
+   *        Callback function to invoke with result ID.
+   * @param aErrorCb [optional]
+   *        Callback function to invoke when there was an error.
+   */
+  put: function(aChannelRecord, aSuccessCb, aErrorCb) {
+    debug("put()");
+
+    this.newTxn(
+      "readwrite",
+      kPUSHDB_STORE_NAME,
+      function txnCb(aTxn, aStore) {
+        debug("Going to put " + aChannelRecord.channelID);
+        aStore.put(aChannelRecord).onsuccess = function setTxnResult(aEvent) {
+          debug("Request successful. Updated record ID: " + aEvent.target.result);
+        };
+      },
+      aSuccessCb,
+      aErrorCb
+    );
+  },
+
+  /*
+   * @param aChannelID
+   *        The ID of record to be deleted.
+   * @param aSuccessCb
+   *        Callback function to invoke with result.
+   * @param aErrorCb [optional]
+   *        Callback function to invoke when there was an error.
+   */
+  delete: function(aChannelID, aSuccessCb, aErrorCb) {
+    debug("delete()");
+
+    this.newTxn(
+      "readwrite",
+      kPUSHDB_STORE_NAME,
+      function txnCb(aTxn, aStore) {
+        debug("Going to delete " + aChannelID);
+        aStore.delete(aChannelID);
+      },
+      aSuccessCb,
+      aErrorCb
+    );
+  },
+
+  getByPushEndpoint: function(aPushEndpoint, aSuccessCb, aErrorCb) {
+    debug("getByPushEndpoint()");
+
+    this.newTxn(
+      "readonly",
+      kPUSHDB_STORE_NAME,
+      function txnCb(aTxn, aStore) {
+        aTxn.result = undefined;
+
+        var index = aStore.index("pushEndpoint");
+        index.get(aPushEndpoint).onsuccess = function setTxnResult(aEvent) {
+          aTxn.result = aEvent.target.result;
+          debug("Fetch successful " + aEvent.target.result);
+        }
+      },
+      aSuccessCb,
+      aErrorCb
+    );
+  },
+
+  getByChannelID: function(aChannelID, aSuccessCb, aErrorCb) {
+    debug("getByChannelID()");
+
+    this.newTxn(
+      "readonly",
+      kPUSHDB_STORE_NAME,
+      function txnCb(aTxn, aStore) {
+        aTxn.result = undefined;
+
+        aStore.get(aChannelID).onsuccess = function setTxnResult(aEvent) {
+          aTxn.result = aEvent.target.result;
+          debug("Fetch successful " + aEvent.target.result);
+        }
+      },
+      aSuccessCb,
+      aErrorCb
+    );
+  },
+
+  getAllByManifestURL: function(aManifestURL, aSuccessCb, aErrorCb) {
+    debug("getAllByManifestURL()");
+    if (!aManifestURL) {
+      if (typeof aErrorCb == "function") {
+        aErrorCb("PushDB.getAllByManifestURL: Got undefined aManifestURL");
+      }
+      return;
+    }
+    this.newTxn(
+      "readonly",
+      kPUSHDB_STORE_NAME,
+      function txnCb(aTxn, aStore) {
+        var index = aStore.index("manifestURL");
+        index.mozGetAll().onsuccess = function(event) {
+          aTxn.result = event.target.result;
+        }
+      },
+      aSuccessCb,
+      aErrorCb
+    );
+  },
+
+  getAllChannelIDs: function(aSuccessCb, aErrorCb) {
+    debug("getAllChannelIDs()");
+
+    this.newTxn(
+      "readonly",
+      kPUSHDB_STORE_NAME,
+      function txnCb(aTxn, aStore) {
+        aStore.mozGetAll().onsuccess = function(event) {
+          aTxn.result = event.target.result;
+        }
+      },
+      aSuccessCb,
+      aErrorCb
+    );
+  },
+
+  drop: function(aSuccessCb, aErrorCb) {
+    debug("drop()");
+    this.newTxn(
+      "readwrite",
+      kPUSHDB_STORE_NAME,
+      function txnCb(aTxn, aStore) {
+        aStore.clear();
+      },
+      aSuccessCb(),
+      aErrorCb()
+    );
+  }
+};
+
+/**
+ * A proxy between the PushService and the WebSocket. The listener is used so
+ * that the PushService can silence messages from the WebSocket by setting
+ * PushWebSocketListener._pushService to null. This is required because
+ * a WebSocket can continue to send messages or errors after it has been
+ * closed but the PushService may not be interested in these. It's easier to
+ * stop listening than to have checks at specific points.
+ */
+this.PushWebSocketListener = function(pushService) {
+  this._pushService = pushService;
+}
+
+this.PushWebSocketListener.prototype = {
+  onStart: function(context) {
+    if (!this._pushService)
+        return;
+    this._pushService._wsOnStart(context);
+  },
+
+  onStop: function(context, statusCode) {
+    if (!this._pushService)
+        return;
+    this._pushService._wsOnStop(context, statusCode);
+  },
+
+  onAcknowledge: function(context, size) {
+    // EMPTY
+  },
+
+  onBinaryMessageAvailable: function(context, message) {
+    // EMPTY
+  },
+
+  onMessageAvailable: function(context, message) {
+    if (!this._pushService)
+        return;
+    this._pushService._wsOnMessageAvailable(context, message);
+  },
+
+  onServerClose: function(context, aStatusCode, aReason) {
+    if (!this._pushService)
+        return;
+    this._pushService._wsOnServerClose(context, aStatusCode, aReason);
+  }
+}
+
+/**
+ * The implementation of the SimplePush system. This runs in the B2G parent
+ * process and is started on boot. It uses WebSockets to communicate with the
+ * server and PushDB (IndexedDB) for persistence.
+ */
+function PushService()
+{
+  debug("PushService Constructor.");
+}
+
+// websocket states
+// websocket is off
+const STATE_SHUT_DOWN = 0;
+// websocket has been opened on client side, waiting for successful open
+// (_wsOnStart)
+const STATE_WAITING_FOR_WS_START = 1;
+// websocket opened, hello sent, waiting for server reply (_handleHelloReply)
+const STATE_WAITING_FOR_HELLO = 2;
+// websocket operational, handshake completed, begin protocol messaging
+const STATE_READY = 3;
+
+PushService.prototype = {
+  classID : Components.ID("{0ACE8D15-9B15-41F4-992F-C88820421DBF}"),
+
+  QueryInterface : XPCOMUtils.generateQI([Ci.nsIObserver]),
+
+  observe: function observe(aSubject, aTopic, aData) {
+    switch (aTopic) {
+      case "app-startup":
+        Services.obs.addObserver(this, "final-ui-startup", false);
+        Services.obs.addObserver(this, "profile-change-teardown", false);
+        break;
+      case "final-ui-startup":
+        Services.obs.removeObserver(this, "final-ui-startup");
+        this.init();
+        break;
+      case "profile-change-teardown":
+        Services.obs.removeObserver(this, "profile-change-teardown");
+        this._shutdown();
+        break;
+
+      case "nsPref:changed":
+        if (aData == "services.push.serverURL") {
+          debug("services.push.serverURL changed! websocket. new value " +
+                this._prefs.get("serverURL"));
+          this._shutdownWS();
+        }
+        break;
+      case "timer-callback":
+        if (aSubject == this._requestTimeoutTimer) {
+          if (Object.keys(this._pendingRequests).length == 0)
+            this._requestTimeoutTimer.cancel();
+
+          for (var channelID in this._pendingRequests) {
+            var duration = Date.now() - this._pendingRequests[channelID].ctime;
+            if (duration > this._requestTimeout) {
+              debug("Request timeout: Removing " + channelID);
+              this._pendingRequests[channelID]
+                .deferred.reject({status: 0, error: "Timeout"});
+
+              delete this._pendingRequests[channelID];
+              for (var i = this._requestQueue.length - 1; i >= 0; --i)
+                if (this._requestQueue[i].channelID == channelID)
+                  this._requestQueue.splice(i, 1);
+            }
+          }
+        }
+        else if (aSubject == this._retryTimeoutTimer) {
+          this._beginWSSetup();
+        }
+    }
+  },
+
+  _prefs : new Preferences("services.push."),
+
+  get _UAID() {
+    return this._prefs.get("userAgentID");
+  },
+
+  set _UAID(newID) {
+    if (typeof(newID) !== "string") {
+      debug("Got invalid, non-string UAID " + newID +
+            ". Not updating userAgentID");
+      return;
+    }
+    debug("New _UAID: " + newID);
+    this._prefs.set("userAgentID", newID);
+  },
+
+  // keeps requests buffered if the websocket disconnects or is not connected
+  _requestQueue: [],
+  _ws: null,
+  _pendingRequests: {},
+  _currentState: STATE_SHUT_DOWN,
+  _requestTimeout: 0,
+  _requestTimeoutTimer: null,
+
+  /**
+   * How retries work:  The goal is to ensure websocket is always up on
+   * networks not supporting UDP. So the websocket should only be shutdown if
+   * onServerClose indicates UDP wakeup.  If WS is closed due to socket error,
+   * _socketError() is called.  The retry timer is started and when it times
+   * out, beginWSSetup() is called again.
+   *
+   * On a successful connection, the timer is cancelled if it is running and
+   * the values are reset to defaults.
+   *
+   * If we are in the middle of a timeout (i.e. waiting), but
+   * a register/unregister is called, we don't want to wait around anymore.
+   * _sendRequest will automatically call beginWSSetup(), which will cancel the
+   * timer. In addition since the state will have changed, even if a pending
+   * timer event comes in (because the timer fired the event before it was
+   * cancelled), so the connection won't be reset.
+   */
+  _retryTimeoutTimer: null,
+  _retryFailCount: 0,
+
+  init: function() {
+    debug("init()");
+    this._db = new PushDB(this);
+
+    let ppmm = Cc["@mozilla.org/parentprocessmessagemanager;1"]
+                 .getService(Ci.nsIMessageBroadcaster);
+
+    kCHILD_PROCESS_MESSAGES.forEach(function addMessage(msgName) {
+        ppmm.addMessageListener(msgName, this);
+    }.bind(this));
+
+    this._requestTimeout = this._prefs.get("requestTimeout");
+
+    this._db.getAllChannelIDs(
+      function(channelIDs) {
+        if (channelIDs.length > 0) {
+          debug("Found registered channelIDs. Starting WebSocket");
+          this._beginWSSetup();
+        }
+      }.bind(this),
+
+      function(error) {
+        debug("db error " + error);
+      }
+    );
+
+    // This is only used for testing. Different tests require connecting to
+    // slightly different URLs.
+    this._prefs.observe("serverURL", this);
+  },
+
+  _shutdownWS: function() {
+    debug("shutdownWS()");
+    this._currentState = STATE_SHUT_DOWN;
+    if (this._wsListener)
+      this._wsListener._pushService = null;
+    try {
+        this._ws.close(0, null);
+    } catch (e) {}
+    this._ws = null;
+  },
+
+  _shutdown: function() {
+    debug("_shutdown()");
+    this._db.close();
+    this._db = null;
+
+    // All pending requests (ideally none) are dropped at this point. We
+    // shouldn't have any applications performing registration/unregistration
+    // or receiving notifications.
+    this._shutdownWS();
+
+    debug("shutdown complete!");
+  },
+
+  // aStatusCode is an NS error from Components.results
+  _socketError: function(aStatusCode) {
+    debug("socketError()");
+
+    // Calculate new timeout, but cap it to
+    var retryTimeout = this._prefs.get("retryBaseInterval") *
+                        Math.pow(2, this._retryFailCount);
+
+    // It is easier to express the max interval as a pref in milliseconds,
+    // rather than have it as a number and make people do the calculation of
+    // retryBaseInterval * 2^maxRetryFailCount.
+    retryTimeout = Math.min(retryTimeout, this._prefs.get("maxRetryInterval"));
+
+    this._retryFailCount++;
+
+    debug("Retry in " + retryTimeout + " Try number " + this._retryFailCount);
+
+    if (!this._retryTimeoutTimer) {
+      this._retryTimeoutTimer = Cc["@mozilla.org/timer;1"]
+                                  .createInstance(Ci.nsITimer);
+    }
+
+    this._retryTimeoutTimer.init(this,
+                                 retryTimeout,
+                                 Ci.nsITimer.TYPE_ONE_SHOT);
+  },
+
+  _beginWSSetup: function() {
+    debug("beginWSSetup()");
+    if (this._currentState != STATE_SHUT_DOWN) {
+      debug("_beginWSSetup: Not in shutdown state! Current state " +
+            this._currentState);
+      return;
+    }
+
+    var serverURL = this._prefs.get("serverURL");
+    if (!serverURL) {
+      debug("No services.push.serverURL found!");
+      return;
+    }
+
+    var uri;
+    try {
+      uri = Services.io.newURI(serverURL, null, null);
+    } catch(e) {
+      debug("Error creating valid URI from services.push.serverURL (" +
+            serverURL + ")");
+      return;
+    }
+
+    if (uri.scheme === "wss") {
+      this._ws = Cc["@mozilla.org/network/protocol;1?name=wss"]
+                   .createInstance(Ci.nsIWebSocketChannel);
+    }
+    else if (uri.scheme === "ws") {
+      debug("Push over an insecure connection (ws://) is not allowed!");
+      return;
+    }
+    else {
+      debug("Unsupported websocket scheme " + uri.scheme);
+      return;
+    }
+
+    debug("serverURL: " + uri.spec);
+    this._wsListener = new PushWebSocketListener(this);
+    this._ws.protocol = "push-notification";
+    this._ws.asyncOpen(uri, serverURL, this._wsListener, null);
+    this._currentState = STATE_WAITING_FOR_WS_START;
+  },
+
+  /**
+   * Protocol handler invoked by server message.
+   */
+  _handleHelloReply: function(reply) {
+    debug("handleHelloReply()");
+    if (this._currentState != STATE_WAITING_FOR_HELLO) {
+      debug("Unexpected state " + this._currentState +
+            "(expected STATE_WAITING_FOR_HELLO)");
+      this._shutdownWS();
+      return;
+    }
+
+    if (typeof reply.uaid !== "string") {
+      debug("No UAID received or non string UAID received");
+      this._shutdownWS();
+      return;
+    }
+
+    if (reply.uaid === "") {
+      debug("Empty UAID received!");
+      this._shutdownWS();
+      return;
+    }
+
+    // To avoid sticking extra large values sent by an evil server into prefs.
+    if (reply.uaid.length > 128) {
+      debug("UAID received from server was too long: " +
+            reply.uaid);
+      this._shutdownWS();
+      return;
+    }
+
+    function finishHandshake() {
+      this._UAID = reply.uaid;
+      this._currentState = STATE_READY;
+      this._processNextRequestInQueue();
+    }
+
+    // By this point we've got a UAID from the server that we are ready to
+    // accept.
+    //
+    // If we already had a valid UAID before, we have to ask apps to
+    // re-register.
+    if (this._UAID && this._UAID != reply.uaid) {
+      debug("got new UAID: all re-register");
+      this._dropRegistrations()
+        .then(
+          function() {
+            // Apps that have no prior registrations, but are in the pending
+            // queue won't get a push-register, which is correct.
+            this._notifyAllAppsRegister();
+            finishHandshake.bind(this)();
+          }.bind(this),
+          function(error) {
+            debug("Error deleting all registrations. SHOULD NEVER HAPPEN!");
+            this._shutdownWS();
+            return;
+          }.bind(this)
+        );
+
+      return;
+    }
+
+    // otherwise we are good to go
+    finishHandshake.bind(this)();
+  },
+
+  /**
+   * Protocol handler invoked by server message.
+   */
+  _handleRegisterReply: function(reply) {
+    debug("handleRegisterReply()");
+    if (typeof reply.channelID !== "string" ||
+        typeof this._pendingRequests[reply.channelID] !== "object")
+      return;
+
+    var tmp = this._pendingRequests[reply.channelID];
+    delete this._pendingRequests[reply.channelID];
+    if (Object.keys(this._pendingRequests).length == 0 &&
+        this._requestTimeoutTimer)
+      this._requestTimeoutTimer.cancel();
+
+    if (reply.status == 200) {
+      tmp.deferred.resolve(reply);
+    } else {
+      tmp.deferred.reject(reply);
+    }
+  },
+
+  /**
+   * Protocol handler invoked by server message.
+   */
+  _handleUnregisterReply: function(reply) {
+    debug("handleUnregisterReply()");
+    if (typeof reply.channelID !== "string" ||
+        typeof this._pendingRequests[reply.channelID] !== "object")
+      return;
+
+    var tmp = this._pendingRequests[reply.channelID];
+    delete this._pendingRequests[reply.channelID];
+    if (Object.keys(this._pendingRequests).length == 0 &&
+        this._requestTimeoutTimer)
+      this._requestTimeoutTimer.cancel();
+
+    if (reply.status == 200) {
+      tmp.deferred.resolve(reply);
+    } else {
+      tmp.deferred.reject(reply);
+    }
+  },
+
+  /**
+   * Protocol handler invoked by server message.
+   */
+  _handleNotificationReply: function(reply) {
+    debug("handleNotificationReply()");
+    if (typeof reply.updates !== 'object') {
+      debug("No 'updates' field in response. Type = " + typeof reply.updates);
+      return;
+    }
+
+    debug("Reply updates: " + reply.updates.length);
+    for (var i = 0; i < reply.updates.length; i++) {
+      var update = reply.updates[i];
+      debug("Update: " + update.channelID + ": " + update.version);
+      if (typeof update.channelID !== "string") {
+        debug("Invalid update literal at index " + i);
+        continue;
+      }
+
+      if (update.version === undefined) {
+        debug("update.version does not exist");
+        continue;
+      }
+
+      var version = update.version;
+
+      if (typeof version === "string") {
+        version = parseInt(version, 10);
+      }
+
+      if (typeof version === "number" && version >= 0) {
+        // FIXME(nsm): this relies on app update notification being infallible!
+        // eventually fix this
+        this._receivedUpdate(update.channelID, version);
+        this._sendAck(update.channelID, version);
+      }
+    }
+  },
+
+  // FIXME(nsm): batch acks for efficiency reasons.
+  _sendAck: function(channelID, version) {
+    debug("sendAck()");
+    this._send('ack', {
+      updates: [{channelID: channelID, version: version}]
+    });
+  },
+
+  /*
+   * Must be used only by request/response style calls over the websocket.
+   */
+  _sendRequest: function(action, data) {
+    debug("sendRequest() " + action);
+    if (typeof data.channelID !== "string") {
+      debug("Received non-string channelID");
+      return;
+    }
+
+    var deferred = Promise.defer();
+
+    if (Object.keys(this._pendingRequests).length == 0) {
+      // start the timer since we now have at least one request
+      if (!this._requestTimeoutTimer)
+        this._requestTimeoutTimer = Cc["@mozilla.org/timer;1"]
+                                      .createInstance(Ci.nsITimer);
+      this._requestTimeoutTimer.init(this,
+                                     this._requestTimeout,
+                                     Ci.nsITimer.TYPE_REPEATING_SLACK);
+    }
+
+    this._pendingRequests[data.channelID] = { deferred: deferred,
+                                              ctime: Date.now() };
+
+    this._send(action, data);
+    return deferred.promise;
+  },
+
+  _send: function(action, data) {
+    debug("send()");
+    this._requestQueue.push([action, data]);
+    debug("Queued " + action);
+    this._processNextRequestInQueue();
+  },
+
+  _processNextRequestInQueue: function() {
+    debug("_processNextRequestInQueue()");
+
+    if (this._requestQueue.length == 0) {
+      debug("Request queue empty");
+      return;
+    }
+
+    if (this._currentState != STATE_READY) {
+      if (!this._ws) {
+        // This will end up calling processNextRequestInQueue().
+        this._beginWSSetup();
+      }
+      else {
+        // We have a socket open so we are just waiting for hello to finish.
+        // That will call processNextRequestInQueue().
+      }
+      return;
+    }
+
+    [action, data] = this._requestQueue.shift();
+    data.messageType = action;
+    if (!this._ws) {
+      // If our websocket is not ready and our state is STATE_READY we may as
+      // well give up all assumptions about the world and start from scratch
+      // again.  Discard the message itself, let the timeout notify error to
+      // the app.
+      debug("This should never happen!");
+      this._shutdownWS();
+    }
+
+    this._ws.sendMsg(JSON.stringify(data));
+    // Process the next one as soon as possible.
+    setTimeout(this._processNextRequestInQueue.bind(this), 0);
+  },
+
+  _receivedUpdate: function(aChannelID, aLatestVersion) {
+    debug("Updating: " + aChannelID + " -> " + aLatestVersion);
+
+    var compareRecordVersionAndNotify = function(aPushRecord) {
+      debug("compareRecordVersionAndNotify()");
+      if (!aPushRecord) {
+        debug("No record for channel ID " + aChannelID);
+        return;
+      }
+
+      if (aPushRecord.version == null ||
+          aPushRecord.version < aLatestVersion) {
+        debug("Version changed, notifying app and updating DB");
+        aPushRecord.version = aLatestVersion;
+        this._notifyApp(aPushRecord);
+        this._updatePushRecord(aPushRecord)
+          .then(
+            null,
+            function(e) {
+              debug("Error updating push record");
+            }
+          );
+      }
+      else {
+        debug("No significant version change: " + aLatestVersion);
+      }
+    }
+
+    var recoverNoSuchChannelID = function(aChannelIDFromServer) {
+      debug("Could not get channelID " + aChannelIDFromServer + " from DB");
+    }
+
+    this._db.getByChannelID(aChannelID,
+                            compareRecordVersionAndNotify.bind(this),
+                            recoverNoSuchChannelID.bind(this));
+  },
+
+  _notifyAllAppsRegister: function() {
+    debug("notifyAllAppsRegister()");
+    let messenger = Cc["@mozilla.org/system-message-internal;1"]
+                      .getService(Ci.nsISystemMessagesInternal);
+    messenger.broadcastMessage('push-register', {});
+  },
+
+  _notifyApp: function(aPushRecord) {
+    if (!aPushRecord || !aPushRecord.pageURL || !aPushRecord.manifestURL) {
+      debug("notifyApp() something is undefined.  Dropping notification");
+      return;
+    }
+
+    debug("notifyApp() " + aPushRecord.pageURL +
+          "  " + aPushRecord.manifestURL);
+    var pageURI = Services.io.newURI(aPushRecord.pageURL, null, null);
+    var manifestURI = Services.io.newURI(aPushRecord.manifestURL, null, null);
+    var message = {
+      pushEndpoint: aPushRecord.pushEndpoint,
+      version: aPushRecord.version
+    };
+    let messenger = Cc["@mozilla.org/system-message-internal;1"]
+                      .getService(Ci.nsISystemMessagesInternal);
+    messenger.sendMessage('push', message, pageURI, manifestURI);
+  },
+
+  _updatePushRecord: function(aPushRecord) {
+    debug("updatePushRecord()");
+    var deferred = Promise.defer();
+    this._db.put(aPushRecord, deferred.resolve, deferred.reject);
+    return deferred.promise;
+  },
+
+  _dropRegistrations: function() {
+    var deferred = Promise.defer();
+    this._db.drop(deferred.resolve, deferred.reject);
+    return deferred.promise;
+  },
+
+  receiveMessage: function(aMessage) {
+    debug("receiveMessage(): " + aMessage.name);
+
+    if (kCHILD_PROCESS_MESSAGES.indexOf(aMessage.name) == -1) {
+      debug("Invalid message from child " + aMessage.name);
+      return;
+    }
+
+    let mm = aMessage.target.QueryInterface(Ci.nsIMessageSender);
+    let json = aMessage.data;
+    this[aMessage.name.slice("Push:".length).toLowerCase()](json, mm);
+  },
+
+  /**
+   * Called on message from the child process. aPageRecord is an object sent by
+   * navigator.push, identifying the sending page and other fields.
+   */
+  register: function(aPageRecord, aMessageManager) {
+    debug("register()");
+
+    let uuidGenerator = Cc["@mozilla.org/uuid-generator;1"]
+                          .getService(Ci.nsIUUIDGenerator);
+    // generateUUID() gives a UUID surrounded by {...}, slice them off.
+    var channelID = uuidGenerator.generateUUID().toString()
+                      .slice(1)
+                      .slice(0, -1);
+
+    this._sendRequest("register", {channelID: channelID})
+      .then(
+        this._onRegisterSuccess.bind(this, aPageRecord, channelID),
+        this._onRegisterError.bind(this, aPageRecord, aMessageManager)
+      )
+      .then(
+        function(message) {
+          aMessageManager.sendAsyncMessage("PushService:Register:OK", message);
+        },
+        function() {
+          aMessageManager.sendAsyncMessage("PushService:Register:KO", message);
+      });
+  },
+
+  /**
+   * Exceptions thrown in _onRegisterSuccess are caught by the promise obtained
+   * from _sendRequest, causing the promise to be rejected instead.
+   */
+  _onRegisterSuccess: function(aPageRecord, generatedChannelID, data) {
+    debug("_onRegisterSuccess()");
+    var deferred = Promise.defer();
+    var message = { requestID: aPageRecord.requestID };
+
+    if (typeof data.channelID !== "string") {
+      debug("Invalid channelID " + message);
+      message["error"] = "Invalid channelID received";
+      throw message;
+    }
+    else if (data.channelID != generatedChannelID) {
+      debug("Server replied with different channelID " + data.channelID +
+            " than what UA generated " + generatedChannelID);
+      message["error"] = "Server sent 200 status code but different channelID";
+      throw message;
+    }
+
+    try {
+      Services.io.newURI(data.pushEndpoint, null, null);
+    }
+    catch (e) {
+      debug("Invalid pushEndpoint " + data.pushEndpoint);
+      message["error"] = "Invalid pushEndpoint " + data.pushEndpoint;
+      throw message;
+    }
+
+    var record = {
+      channelID: data.channelID,
+      pushEndpoint: data.pushEndpoint,
+      pageURL: aPageRecord.pageURL,
+      manifestURL: aPageRecord.manifestURL,
+      version: null
+    };
+
+    this._updatePushRecord(record)
+      .then(
+        function() {
+          message["pushEndpoint"] = data.pushEndpoint;
+          deferred.resolve(message);
+        },
+        function(error) {
+          // Unable to save.
+          this._sendRequest("unregister", {channelID: record.channelID});
+          message["error"] = error;
+          deferred.reject(message);
+        }
+      );
+
+    return deferred.promise;
+  },
+
+  /**
+   * Exceptions thrown in _onRegisterError are caught by the promise obtained
+   * from _sendRequest, causing the promise to be rejected instead.
+   */
+  _onRegisterError: function(aPageRecord, aMessageManager, reply) {
+    debug("_onRegisterError()");
+    switch (reply.status) {
+      case kERROR_CHID_CONFLICT:
+        if (typeof aPageRecord._attempts !== "number")
+          aPageRecord._attempts = 0;
+
+        if (aPageRecord._attempts < kCONFLICT_RETRY_ATTEMPTS) {
+          aPageRecord._attempts++;
+          // Since register is async, it's OK to launch it in a callback.
+          debug("CONFLICT: trying again");
+          this.register(aPageRecord, aMessageManager);
+          return;
+        }
+        throw { requestID: aPageRecord.requestID, error: "conflict" };
+      default:
+        debug("General failure " + reply.status);
+        throw { requestID: aPageRecord.requestID, error: reply.error };
+    }
+  },
+
+  /**
+   * Called on message from the child process.
+   *
+   * Why is the record being deleted from the local database before the server
+   * is told?
+   *
+   * Unregistration is for the benefit of the app and the AppServer
+   * so that the AppServer does not keep pinging a channel the UserAgent isn't
+   * watching The important part of the transaction in this case is left to the
+   * app, to tell its server of the unregistration.  Even if the request to the
+   * PushServer were to fail, it would not affect correctness of the protocol,
+   * and the server GC would just clean up the channelID eventually.  Since the
+   * appserver doesn't ping it, no data is lost.
+   *
+   * If rather we were to unregister at the server and update the database only
+   * on success: If the server receives the unregister, and deletes the
+   * channelID, but the response is lost because of network failure, the
+   * application is never informed. In addition the application may retry the
+   * unregister when it fails due to timeout at which point the server will say
+   * it does not know of this unregistration.  We'll have to make the
+   * registration/unregistration phases have retries and attempts to resend
+   * messages from the server, and have the client acknowledge. On a server,
+   * data is cheap, reliable notification is not.
+   */
+  unregister: function(aPageRecord, aMessageManager) {
+    debug("unregister()");
+
+    var fail = function(error) {
+      debug("unregister() fail() error " + error);
+      var message = {requestID: aPageRecord.requestID, error: error};
+      aMessageManager.sendAsyncMessage("PushService:Unregister:KO", message);
+    }
+
+    this._db.getByPushEndpoint(aPageRecord.pushEndpoint, function(record) {
+      // Non-owner tried to unregister, say success, but don't do anything.
+      if (record.manifestURL !== aPageRecord.manifestURL) {
+        aMessageManager.sendAsyncMessage("PushService:Unregister:OK", {
+          requestID: aPageRecord.requestID,
+          pushEndpoint: aPageRecord.pushEndpoint
+        });
+        return;
+      }
+
+      this._db.delete(record.channelID, function() {
+        // Let's be nice to the server and try to inform it, but we don't care
+        // about the reply.
+        this._sendRequest("unregister", {channelID: record.channelID});
+        aMessageManager.sendAsyncMessage("PushService:Unregister:OK", {
+          requestID: aPageRecord.requestID,
+          pushEndpoint: aPageRecord.pushEndpoint
+        });
+      }.bind(this), fail);
+    }.bind(this), fail);
+  },
+
+  /**
+   * Called on message from the child process
+   */
+  registrations: function(aPageRecord, aMessageManager) {
+    debug("registrations()");
+
+    if (aPageRecord.manifestURL) {
+      this._db.getAllByManifestURL(aPageRecord.manifestURL,
+        this._onRegistrationsSuccess.bind(this, aPageRecord, aMessageManager),
+        this._onRegistrationsError.bind(this, aPageRecord, aMessageManager));
+    }
+    else {
+      this._onRegistrationsError(aPageRecord, aMessageManager);
+    }
+  },
+
+  _onRegistrationsSuccess: function(aPageRecord,
+                                    aMessageManager,
+                                    pushRecords) {
+    var registrations = [];
+    pushRecords.forEach(function(pushRecord) {
+      registrations.push({
+          __exposedProps__: { pushEndpoint: 'r', version: 'r' },
+          pushEndpoint: pushRecord.pushEndpoint,
+          version: pushRecord.version
+      });
+    });
+    aMessageManager.sendAsyncMessage("PushService:Registrations:OK", {
+      requestID: aPageRecord.requestID,
+      registrations: registrations
+    });
+  },
+
+  _onRegistrationsError: function(aPageRecord, aMessageManager) {
+    aMessageManager.sendAsyncMessage("PushService:Registrations:KO", {
+      requestID: aPageRecord.requestID,
+      error: "Database error"
+    });
+  },
+
+  // begin Push protocol handshake
+  _wsOnStart: function(context) {
+    debug("wsOnStart()");
+    if (this._currentState != STATE_WAITING_FOR_WS_START) {
+      debug("NOT in STATE_WAITING_FOR_WS_START. Current state " +
+            this._currentState + ". Skipping");
+      return;
+    }
+
+    if (this._retryTimeoutTimer)
+      this._retryTimeoutTimer.cancel();
+
+    // Since we've had a successful connection reset the retry fail count.
+    this._retryFailCount = 0;
+
+    var data = {
+      messageType: "hello",
+    }
+
+    if (this._UAID)
+      data["uaid"] = this._UAID;
+
+    function sendHelloMessage(ids) {
+      // On success, ids is an array, on error its not.
+      data["channelIDs"] = ids.map ?
+                           ids.map(function(el) { return el.channelID; }) : [];
+      this._ws.sendMsg(JSON.stringify(data));
+      this._currentState = STATE_WAITING_FOR_HELLO;
+    }
+
+    this._db.getAllChannelIDs(sendHelloMessage.bind(this),
+                              sendHelloMessage.bind(this));
+  },
+
+  /**
+   * This statusCode is not the websocket protocol status code, but the TCP
+   * connection close status code.
+   */
+  _wsOnStop: function(context, statusCode) {
+    debug("wsOnStop()");
+    if (statusCode != Components.results.NS_OK) {
+      debug("Socket error " + statusCode);
+      this._socketError(statusCode);
+    }
+
+    this._shutdownWS();
+  },
+
+  _wsOnMessageAvailable: function(context, message) {
+    debug("wsOnMessageAvailable() " + message);
+    var reply = undefined;
+    try {
+      reply = JSON.parse(message);
+    } catch(e) {
+      debug("Parsing JSON failed. text : " + message);
+      return;
+    }
+
+    if (typeof reply.messageType != "string") {
+      debug("messageType not a string " + reply.messageType);
+      return;
+    }
+
+    // A whitelist of protocol handlers. Add to these if new messages are added
+    // in the protocol.
+    var handlers = ["Hello", "Register", "Unregister", "Notification"];
+
+    // Build up the handler name to call from messageType.
+    // e.g. messageType == "register" -> _handleRegisterReply.
+    var handlerName = reply.messageType[0].toUpperCase() +
+                      reply.messageType.slice(1).toLowerCase();
+
+    if (handlers.indexOf(handlerName) == -1) {
+      debug("No whitelisted handler " + handlerName + ". messageType: " +
+            reply.messageType);
+      return;
+    }
+
+    var handler = "_handle" + handlerName + "Reply";
+
+    if (typeof this[handler] !== "function") {
+      debug("Handler whitelisted but not implemented! " + handler);
+      return;
+    }
+
+    this[handler](reply);
+  },
+
+  _wsOnServerClose: function(context, aStatusCode, aReason) {
+    debug("wsOnServerClose() " + aStatusCode + " " + aReason);
+    // 1000 is the normal close
+    if (aStatusCode == 1000 && Object.keys(this._pendingRequests).length > 0) {
+      // This should never happen. A successful close cannot have pending
+      // requests since the server should've responded to them before the
+      // connection was closed.
+      this._shutdownWS();
+      this._beginWSSetup();
+    }
+  }
+}
+
+this.NSGetFactory = XPCOMUtils.generateNSGetFactory([PushService]);
new file mode 100644
--- /dev/null
+++ b/dom/push/src/PushService.manifest
@@ -0,0 +1,4 @@
+component {0ACE8D15-9B15-41F4-992F-C88820421DBF} PushService.js
+contract @mozilla.org/PushService;1 {0ACE8D15-9B15-41F4-992F-C88820421DBF}
+category app-startup PushService service,@mozilla.org/PushService;1
+
new file mode 100644
--- /dev/null
+++ b/dom/push/src/moz.build
@@ -0,0 +1,5 @@
+# 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/.
+
new file mode 100644
--- /dev/null
+++ b/dom/push/tests/moz.build
@@ -0,0 +1,4 @@
+# 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/.
--- a/extensions/cookie/Permission.txt
+++ b/extensions/cookie/Permission.txt
@@ -63,16 +63,18 @@ This exposes information about the curre
 
 power
 {}
 PowerManagement API
 Turn on/off screen, cpu, device power, etc. Listen and inspect resource lock events.
 
 push
 {}
+Push Notifications, 822712
+Notify application about updates to things it registers interest in.
 
 settings
 {read, write}
 Settings API, 678695
 API to configure device settings
 
 sms
 {}
--- a/mobile/android/installer/package-manifest.in
+++ b/mobile/android/installer/package-manifest.in
@@ -123,16 +123,17 @@
 @BINPATH@/components/dom_geolocation.xpt
 @BINPATH@/components/dom_media.xpt
 @BINPATH@/components/dom_network.xpt
 @BINPATH@/components/dom_notification.xpt
 @BINPATH@/components/dom_html.xpt
 @BINPATH@/components/dom_indexeddb.xpt
 @BINPATH@/components/dom_offline.xpt
 @BINPATH@/components/dom_json.xpt
+@BINPATH@/components/dom_push.xpt
 @BINPATH@/components/dom_browserelement.xpt
 @BINPATH@/components/dom_power.xpt
 @BINPATH@/components/dom_quota.xpt
 @BINPATH@/components/dom_range.xpt
 @BINPATH@/components/dom_settings.xpt
 @BINPATH@/components/dom_sidebar.xpt
 @BINPATH@/components/dom_mobilemessage.xpt
 @BINPATH@/components/dom_storage.xpt
@@ -360,16 +361,20 @@
 @BINPATH@/components/nsPrompter.manifest
 @BINPATH@/components/nsPrompter.js
 @BINPATH@/components/TelemetryPing.js
 @BINPATH@/components/TelemetryPing.manifest
 @BINPATH@/components/Webapps.js
 @BINPATH@/components/Webapps.manifest
 @BINPATH@/components/AppsService.js
 @BINPATH@/components/AppsService.manifest
+@BINPATH@/components/Push.js
+@BINPATH@/components/Push.manifest
+@BINPATH@/components/PushService.js
+@BINPATH@/components/PushService.manifest
 @BINPATH@/components/AppProtocolHandler.js
 @BINPATH@/components/AppProtocolHandler.manifest
 
 @BINPATH@/components/TCPSocket.js
 @BINPATH@/components/TCPSocketParentIntermediary.js
 @BINPATH@/components/TCPSocket.manifest
 
 #ifdef MOZ_WEBRTC