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
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