Bug 1018320 - RequestSync API - Patch 1 - webIDL and basic logic. r=ehsan, a=bajaj
authorAndrea Marchesini <amarchesini@mozilla.com>
Tue, 13 Jan 2015 09:53:14 +0000
changeset 237135 7ea79cc34e617b0ad23d7a2de5b45041d5946389
parent 237134 42663c5e20c2cad4bc504e40e34e2938a6cb806b
child 237136 32d904a7d04c836568a1e915198f449ae1ea5306
push id213
push userryanvm@gmail.com
push dateTue, 24 Feb 2015 00:59:48 +0000
treeherdermozilla-b2g37_v2_2@b5a532c7f606 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersehsan, bajaj
bugs1018320
milestone37.0
Bug 1018320 - RequestSync API - Patch 1 - webIDL and basic logic. r=ehsan, a=bajaj
b2g/app/b2g.js
b2g/chrome/content/shell.js
b2g/installer/package-manifest.in
browser/installer/package-manifest.in
dom/messages/SystemMessagePermissionsChecker.jsm
dom/moz.build
dom/requestsync/RequestSync.manifest
dom/requestsync/RequestSyncManager.js
dom/requestsync/RequestSyncScheduler.js
dom/requestsync/RequestSyncService.jsm
dom/requestsync/moz.build
dom/requestsync/tests/common_app.js
dom/requestsync/tests/common_basic.js
dom/requestsync/tests/file_app.sjs
dom/requestsync/tests/file_app.template.webapp
dom/requestsync/tests/file_basic_app.html
dom/requestsync/tests/file_interface.html
dom/requestsync/tests/mochitest.ini
dom/requestsync/tests/test_basic.html
dom/requestsync/tests/test_basic_app.html
dom/requestsync/tests/test_minInterval.html
dom/requestsync/tests/test_wakeUp.html
dom/requestsync/tests/test_webidl.html
dom/tests/mochitest/general/test_interfaces.html
dom/webidl/RequestSyncManager.webidl
dom/webidl/RequestSyncScheduler.webidl
dom/webidl/moz.build
mobile/android/installer/package-manifest.in
modules/libpref/init/all.js
--- a/b2g/app/b2g.js
+++ b/b2g/app/b2g.js
@@ -1090,16 +1090,19 @@ pref("dom.mozSettings.SettingsManager.ve
 pref("dom.mozSettings.SettingsRequestManager.verbose.enabled", false);
 pref("dom.mozSettings.SettingsService.verbose.enabled", false);
 
 // Controlling whether we want to allow forcing some Settings
 // IndexedDB transactions to be opened as readonly or keep everything as
 // readwrite.
 pref("dom.mozSettings.allowForceReadOnly", false);
 
+// RequestSync API is enabled by default on B2G.
+pref("dom.requestSync.enabled", true);
+
 // Only enable for kit kat and above devices
 // kit kat == 19, L = 21, 20 is kit-kat for wearables
 // 15 is for the ICS emulators which will fallback to software vsync
 #if ANDROID_VERSION == 19 || ANDROID_VERSION == 21 || ANDROID_VERSION == 15
 pref("gfx.vsync.hw-vsync.enabled", true);
 pref("gfx.vsync.compositor", true);
 pref("gfx.touch.resample", true);
 #else
--- a/b2g/chrome/content/shell.js
+++ b/b2g/chrome/content/shell.js
@@ -10,16 +10,17 @@ Cu.import('resource://gre/modules/AlarmS
 Cu.import('resource://gre/modules/ActivitiesService.jsm');
 Cu.import('resource://gre/modules/NotificationDB.jsm');
 Cu.import('resource://gre/modules/Payment.jsm');
 Cu.import("resource://gre/modules/AppsUtils.jsm");
 Cu.import('resource://gre/modules/UserAgentOverrides.jsm');
 Cu.import('resource://gre/modules/Keyboard.jsm');
 Cu.import('resource://gre/modules/ErrorPage.jsm');
 Cu.import('resource://gre/modules/AlertsHelper.jsm');
+Cu.import('resource://gre/modules/RequestSyncService.jsm');
 #ifdef MOZ_WIDGET_GONK
 Cu.import('resource://gre/modules/NetworkStatsService.jsm');
 Cu.import('resource://gre/modules/ResourceStatsService.jsm');
 #endif
 
 // Identity
 Cu.import('resource://gre/modules/SignInToWebsite.jsm');
 SignInToWebsiteController.init();
--- a/b2g/installer/package-manifest.in
+++ b/b2g/installer/package-manifest.in
@@ -340,16 +340,19 @@
 @BINPATH@/components/xpcom_xpti.xpt
 @BINPATH@/components/xpconnect.xpt
 @BINPATH@/components/xulapp.xpt
 @BINPATH@/components/xul.xpt
 @BINPATH@/components/xultmpl.xpt
 @BINPATH@/components/zipwriter.xpt
 
 ; JavaScript components
+@BINPATH@/components/RequestSync.manifest
+@BINPATH@/components/RequestSyncManager.js
+@BINPATH@/components/RequestSyncScheduler.js
 @BINPATH@/components/ChromeNotifications.js
 @BINPATH@/components/ChromeNotifications.manifest
 @BINPATH@/components/ConsoleAPI.manifest
 @BINPATH@/components/ConsoleAPIStorage.js
 @BINPATH@/components/BrowserElementParent.manifest
 @BINPATH@/components/BrowserElementParent.js
 @BINPATH@/components/ContactManager.js
 @BINPATH@/components/ContactManager.manifest
--- a/browser/installer/package-manifest.in
+++ b/browser/installer/package-manifest.in
@@ -537,16 +537,20 @@
 @RESPATH@/components/nsDOMIdentity.js
 @RESPATH@/components/nsIDService.js
 @RESPATH@/components/Identity.manifest
 @RESPATH@/components/recording-cmdline.js
 @RESPATH@/components/recording-cmdline.manifest
 @RESPATH@/components/htmlMenuBuilder.js
 @RESPATH@/components/htmlMenuBuilder.manifest
 
+@RESPATH@/components/RequestSync.manifest
+@RESPATH@/components/RequestSyncManager.js
+@RESPATH@/components/RequestSyncScheduler.js
+
 @RESPATH@/components/PermissionSettings.js
 @RESPATH@/components/PermissionSettings.manifest
 @RESPATH@/components/ContactManager.js
 @RESPATH@/components/ContactManager.manifest
 @RESPATH@/components/PhoneNumberService.js
 @RESPATH@/components/PhoneNumberService.manifest
 @RESPATH@/components/NotificationStorage.js
 @RESPATH@/components/NotificationStorage.manifest
--- a/dom/messages/SystemMessagePermissionsChecker.jsm
+++ b/dom/messages/SystemMessagePermissionsChecker.jsm
@@ -79,16 +79,17 @@ this.SystemMessagePermissionsTable = {
     "desktop-notification": []
   },
   "push": {
   	"push": []
   },
   "push-register": {
   	"push": []
   },
+  "request-sync": { },
   "sms-delivery-success": {
     "sms": []
   },
   "sms-read-success": {
     "sms": []
   },
   "sms-received": {
     "sms": []
--- a/dom/moz.build
+++ b/dom/moz.build
@@ -38,16 +38,17 @@ interfaces = [
 DIRS += ['interfaces/' + i for i in interfaces]
 
 DIRS += [
     'animation',
     'apps',
     'base',
     'activities',
     'archivereader',
+    'requestsync',
     'bindings',
     'battery',
     'browser-element',
     'canvas',
     'cellbroadcast',
     'contacts',
     'crypto',
     'phonenumberutils',
new file mode 100644
--- /dev/null
+++ b/dom/requestsync/RequestSync.manifest
@@ -0,0 +1,5 @@
+component {8ee5ab74-15c4-478f-9d32-67627b9f0f1a} RequestSyncScheduler.js
+contract @mozilla.org/dom/request-sync-scheduler;1 {8ee5ab74-15c4-478f-9d32-67627b9f0f1a}
+
+component {e6f55080-e549-4e30-9d00-15f240fb763c} RequestSyncManager.js
+contract @mozilla.org/dom/request-sync-manager;1 {e6f55080-e549-4e30-9d00-15f240fb763c}
new file mode 100644
--- /dev/null
+++ b/dom/requestsync/RequestSyncManager.js
@@ -0,0 +1,80 @@
+/* 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('DEBUG RequestSyncManager: ' + s + '\n');
+}
+
+const {classes: Cc, interfaces: Ci, utils: Cu, results: Cr} = Components;
+
+Cu.import("resource://gre/modules/DOMRequestHelper.jsm");
+Cu.import('resource://gre/modules/XPCOMUtils.jsm');
+
+XPCOMUtils.defineLazyServiceGetter(this, "cpmm",
+                                   "@mozilla.org/childprocessmessagemanager;1",
+                                   "nsIMessageSender");
+
+function RequestSyncManager() {
+  debug('created');
+}
+
+RequestSyncManager.prototype = {
+  __proto__: DOMRequestIpcHelper.prototype,
+
+  classDescription: 'RequestSyncManager XPCOM Component',
+  classID: Components.ID('{e6f55080-e549-4e30-9d00-15f240fb763c}'),
+  contractID: '@mozilla.org/dom/request-sync-manager;1',
+  QueryInterface: XPCOMUtils.generateQI([Ci.nsISupportsWeakReference,
+                                         Ci.nsIObserver,
+                                         Ci.nsIDOMGlobalPropertyInitializer]),
+
+  _messages: [ "RequestSyncManager:Registrations:Return" ],
+
+  init: function(aWindow) {
+    debug("init");
+
+    // DOMRequestIpcHelper.initHelper sets this._window
+    this.initDOMRequestHelper(aWindow, this._messages);
+  },
+
+  sendMessage: function(aMsg, aObj) {
+    let self = this;
+    return this.createPromise(function(aResolve, aReject) {
+      aObj.requestID =
+        self.getPromiseResolverId({ resolve: aResolve, reject: aReject });
+      cpmm.sendAsyncMessage(aMsg, aObj, null,
+                            self._window.document.nodePrincipal);
+    });
+  },
+
+  registrations: function() {
+    debug('registrations');
+    return this.sendMessage("RequestSyncManager:Registrations", {});
+  },
+
+  receiveMessage: function(aMessage) {
+    debug('receiveMessage');
+
+    let req = this.getPromiseResolver(aMessage.data.requestID);
+    if (!req) {
+      return;
+    }
+
+    if ('error' in aMessage.data) {
+      req.reject(Cu.cloneInto(aMessage.data.error, this._window));
+      return;
+    }
+
+    if ('results' in aMessage.data) {
+      req.resolve(Cu.cloneInto(aMessage.data.results, this._window));
+      return;
+    }
+
+    req.resolve();
+  }
+};
+
+this.NSGetFactory = XPCOMUtils.generateNSGetFactory([RequestSyncManager]);
new file mode 100644
--- /dev/null
+++ b/dom/requestsync/RequestSyncScheduler.js
@@ -0,0 +1,101 @@
+/* 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('DEBUG RequestSyncScheduler: ' + s + '\n');
+}
+
+const {classes: Cc, interfaces: Ci, utils: Cu, results: Cr} = Components;
+
+Cu.import('resource://gre/modules/DOMRequestHelper.jsm');
+Cu.import('resource://gre/modules/XPCOMUtils.jsm');
+
+XPCOMUtils.defineLazyServiceGetter(this, 'cpmm',
+                                   '@mozilla.org/childprocessmessagemanager;1',
+                                   'nsIMessageSender');
+
+function RequestSyncScheduler() {
+  debug('created');
+}
+
+RequestSyncScheduler.prototype = {
+  __proto__: DOMRequestIpcHelper.prototype,
+
+  classDescription: 'RequestSyncScheduler XPCOM Component',
+  classID: Components.ID('{8ee5ab74-15c4-478f-9d32-67627b9f0f1a}'),
+  contractID: '@mozilla.org/dom/request-sync-scheduler;1',
+  QueryInterface: XPCOMUtils.generateQI([Ci.nsISupportsWeakReference,
+                                         Ci.nsIObserver,
+                                         Ci.nsIDOMGlobalPropertyInitializer]),
+
+  _messages: [ 'RequestSync:Register:Return',
+               'RequestSync:Unregister:Return',
+               'RequestSync:Registrations:Return',
+               'RequestSync:Registration:Return' ],
+
+  init: function(aWindow) {
+    debug('init');
+
+    // DOMRequestIpcHelper.initHelper sets this._window
+    this.initDOMRequestHelper(aWindow, this._messages);
+  },
+
+  register: function(aTask, aParams) {
+    debug('register');
+    return this.sendMessage('RequestSync:Register',
+                            { task: aTask, params: aParams });
+  },
+
+  unregister: function(aTask) {
+    debug('unregister');
+    return this.sendMessage('RequestSync:Unregister',
+                            { task: aTask });
+  },
+
+  registrations: function() {
+    debug('registrations');
+    return this.sendMessage('RequestSync:Registrations', {});
+  },
+
+  registration: function(aTask) {
+    debug('registration');
+    return this.sendMessage('RequestSync:Registration',
+                            { task: aTask });
+  },
+
+  sendMessage: function(aMsg, aObj) {
+    let self = this;
+    return this.createPromise(function(aResolve, aReject) {
+      aObj.requestID =
+        self.getPromiseResolverId({ resolve: aResolve, reject: aReject });
+      cpmm.sendAsyncMessage(aMsg, aObj, null,
+                            self._window.document.nodePrincipal);
+    });
+  },
+
+  receiveMessage: function(aMessage) {
+    debug('receiveMessage');
+
+    let req = this.getPromiseResolver(aMessage.data.requestID);
+    if (!req) {
+      return;
+    }
+
+    if ('error' in aMessage.data) {
+      req.reject(Cu.cloneInto(aMessage.data.error, this._window));
+      return;
+    }
+
+    if ('results' in aMessage.data) {
+      req.resolve(Cu.cloneInto(aMessage.data.results, this._window));
+      return;
+    }
+
+    req.resolve();
+  }
+};
+
+this.NSGetFactory = XPCOMUtils.generateNSGetFactory([RequestSyncScheduler]);
new file mode 100644
--- /dev/null
+++ b/dom/requestsync/RequestSyncService.jsm
@@ -0,0 +1,625 @@
+/* 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'
+
+/* TODO:
+ - wifi
+*/
+
+const {classes: Cc, interfaces: Ci, utils: Cu, results: Cr} = Components;
+
+function debug(s) {
+  //dump('DEBUG RequestSyncService: ' + s + '\n');
+}
+
+const RSYNCDB_VERSION = 1;
+const RSYNCDB_NAME = "requestSync";
+const RSYNC_MIN_INTERVAL = 100;
+
+Cu.import('resource://gre/modules/IndexedDBHelper.jsm');
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+Cu.import("resource://gre/modules/Services.jsm");
+Cu.importGlobalProperties(["indexedDB"]);
+
+
+XPCOMUtils.defineLazyServiceGetter(this, "appsService",
+                                   "@mozilla.org/AppsService;1",
+                                   "nsIAppsService");
+
+XPCOMUtils.defineLazyServiceGetter(this, "cpmm",
+                                   "@mozilla.org/childprocessmessagemanager;1",
+                                   "nsISyncMessageSender");
+
+XPCOMUtils.defineLazyServiceGetter(this, "ppmm",
+                                   "@mozilla.org/parentprocessmessagemanager;1",
+                                   "nsIMessageBroadcaster");
+
+XPCOMUtils.defineLazyServiceGetter(this, "systemMessenger",
+                                   "@mozilla.org/system-message-internal;1",
+                                   "nsISystemMessagesInternal");
+
+XPCOMUtils.defineLazyServiceGetter(this, "secMan",
+                                   "@mozilla.org/scriptsecuritymanager;1",
+                                   "nsIScriptSecurityManager");
+
+this.RequestSyncService = {
+  __proto__: IndexedDBHelper.prototype,
+
+  children: [],
+
+  _messages: [ "RequestSync:Register", "RequestSync:Unregister",
+               "RequestSync:Registrations", "RequestSync:Registration",
+               "RequestSyncManager:Registrations" ],
+
+  _pendingOperation: false,
+  _pendingMessages: [],
+
+  _registrations: {},
+
+  // Initialization of the RequestSyncService.
+  init: function() {
+    debug("init");
+
+    this._messages.forEach((function(msgName) {
+      ppmm.addMessageListener(msgName, this);
+    }).bind(this));
+
+    Services.obs.addObserver(this, 'xpcom-shutdown', false);
+    Services.obs.addObserver(this, 'webapps-clear-data', false);
+
+    this.initDBHelper("requestSync", RSYNCDB_VERSION, [RSYNCDB_NAME]);
+
+    // Loading all the data from the database into the _registrations map.
+    // Any incoming message will be stored and processed when the async
+    // operation is completed.
+
+    let self = this;
+    this.dbTxn("readonly", function(aStore) {
+      aStore.openCursor().onsuccess = function(event) {
+        let cursor = event.target.result;
+        if (cursor) {
+          self.addRegistration(cursor.value);
+          cursor.continue();
+        }
+      }
+    },
+    function() {
+      debug("initialization done");
+    },
+    function() {
+      dump("ERROR!! RequestSyncService - Failed to retrieve data from the database.\n");
+    });
+  },
+
+  // Shutdown the RequestSyncService.
+  shutdown: function() {
+    debug("shutdown");
+
+    this._messages.forEach((function(msgName) {
+      ppmm.removeMessageListener(msgName, this);
+    }).bind(this));
+
+    Services.obs.removeObserver(this, 'xpcom-shutdown');
+    Services.obs.removeObserver(this, 'webapps-clear-data');
+
+    this.close();
+
+    // Removing all the registrations will delete the pending timers.
+    for (let key  in this._registrations) {
+      for (let task in this._registrations[key]) {
+        this.removeRegistrationInternal(task, key);
+      }
+    }
+  },
+
+  observe: function(aSubject, aTopic, aData) {
+    debug("observe");
+
+    switch (aTopic) {
+      case 'xpcom-shutdown':
+        this.shutdown();
+        break;
+
+      case 'webapps-clear-data':
+       this.clearData(aSubject);
+       break;
+
+      default:
+        debug("Wrong observer topic: " + aTopic);
+        break;
+    }
+  },
+
+  // When an app is uninstalled, we have to clean all its tasks.
+  clearData: function(aData) {
+    debug('clearData');
+
+    if (!aData) {
+      return;
+    }
+
+    let params =
+      aData.QueryInterface(Ci.mozIApplicationClearPrivateDataParams);
+    if (!params) {
+      return;
+    }
+
+    // At this point we don't have the origin, so we cannot create the full
+    // key. Using the partial one is enough to detect the uninstalled app.
+    var partialKey = params.appId + '|' + params.browserOnly + '|';
+    var dbKeys = [];
+
+    for (let key  in this._registrations) {
+      if (key.indexOf(partialKey) != 0) {
+        continue;
+      }
+
+      for (let task in this._registrations[key]) {
+        dbKeys = this._registrations[key][task].dbKey;
+        this.removeRegistrationInternal(task, key);
+      }
+    }
+
+    if (dbKeys.length == 0) {
+      return;
+    }
+
+    // Remove the tasks from the database.
+    this.dbTxn('readwrite', function(aStore) {
+      for (let i = 0; i < dbKeys.length; ++i) {
+        aStore.delete(dbKeys[i]);
+      }
+    },
+    function() {
+      debug("ClearData completed");
+    }, function() {
+      debug("ClearData failed");
+    });
+  },
+
+  // Creation of the schema for the database.
+  upgradeSchema: function(aTransaction, aDb, aOldVersion, aNewVersion) {
+    debug('updateSchema');
+    aDb.createObjectStore(RSYNCDB_NAME, { autoIncrement: true });
+  },
+
+  // This method generates the key for the indexedDB object storage.
+  principalToKey: function(aPrincipal) {
+    return aPrincipal.appId + '|' +
+           aPrincipal.isInBrowserElement + '|' +
+           aPrincipal.origin;
+  },
+
+  // Add a task to the _registrations map and create the timer if it's needed.
+  addRegistration: function(aObj) {
+    debug('addRegistration');
+
+    let key = this.principalToKey(aObj.principal);
+    if (!(key in this._registrations)) {
+      this._registrations[key] = {};
+    }
+
+    this.scheduleTimer(aObj);
+    this._registrations[key][aObj.data.task] = aObj;
+  },
+
+  // Remove a task from the _registrations map and delete the timer if it's
+  // needed. It also checks if the principal is correct before doing the real
+  // operation.
+  removeRegistration: function(aTaskName, aKey, aPrincipal) {
+    debug('removeRegistration');
+
+    if (!(aKey in this._registrations) ||
+        !(aTaskName in this._registrations[aKey])) {
+      return false;
+    }
+
+    // Additional security check.
+    if (!aPrincipal.equals(this._registrations[aKey][aTaskName].principal)) {
+      return false;
+    }
+
+    this.removeRegistrationInternal(aTaskName, aKey);
+    return true;
+  },
+
+  removeRegistrationInternal: function(aTaskName, aKey) {
+    debug('removeRegistrationInternal');
+
+    if (this._registrations[aKey][aTaskName].timer) {
+      this._registrations[aKey][aTaskName].timer.cancel();
+    }
+
+    delete this._registrations[aKey][aTaskName];
+
+    // Lets remove the key in case there are not tasks registered.
+    for (var key in this._registrations[aKey]) {
+      return;
+    }
+    delete this._registrations[aKey];
+  },
+
+  // The communication from the exposed objects and the service is done using
+  // messages. This function receives and processes them.
+  receiveMessage: function(aMessage) {
+    debug("receiveMessage");
+
+    // We cannot process this request now.
+    if (this._pendingOperation) {
+      this._pendingMessages.push(aMessage);
+      return;
+    }
+
+    // The principal is used to validate the message.
+    if (!aMessage.principal) {
+      return;
+    }
+
+    let uri = Services.io.newURI(aMessage.principal.origin, null, null);
+
+    let principal;
+    try {
+      principal = secMan.getAppCodebasePrincipal(uri,
+        aMessage.principal.appId, aMessage.principal.isInBrowserElement);
+    } catch(e) {
+      return;
+    }
+
+    if (!principal) {
+      return;
+    }
+
+    switch (aMessage.name) {
+      case "RequestSync:Register":
+        this.register(aMessage.target, aMessage.data, principal);
+        break;
+
+      case "RequestSync:Unregister":
+        this.unregister(aMessage.target, aMessage.data, principal);
+        break;
+
+      case "RequestSync:Registrations":
+        this.registrations(aMessage.target, aMessage.data, principal);
+        break;
+
+      case "RequestSync:Registration":
+        this.registration(aMessage.target, aMessage.data, principal);
+        break;
+
+      case "RequestSyncManager:Registrations":
+        this.managerRegistrations(aMessage.target, aMessage.data, principal);
+        break;
+
+      default:
+        debug("Wrong message: " + aMessage.name);
+        break;
+    }
+  },
+
+  // Basic validation.
+  validateRegistrationParams: function(aParams) {
+    if (aParams === null) {
+      return false;
+    }
+
+    // We must have a page.
+    if (!("wakeUpPage" in aParams) ||
+        aParams.wakeUpPage.length == 0) {
+      return false;
+    }
+
+    let minInterval = RSYNC_MIN_INTERVAL;
+    try {
+      minInterval = Services.prefs.getIntPref("dom.requestSync.minInterval");
+    } catch(e) {}
+
+    if (!("minInterval" in aParams) ||
+        aParams.minInterval < minInterval) {
+      return false;
+    }
+
+    return true;
+  },
+
+  // Registration of a new task.
+  register: function(aTarget, aData, aPrincipal) {
+    debug("register");
+
+    if (!this.validateRegistrationParams(aData.params)) {
+      aTarget.sendAsyncMessage("RequestSync:Register:Return",
+                               { requestID: aData.requestID,
+                                 error: "ParamsError" } );
+      return;
+    }
+
+    let key = this.principalToKey(aPrincipal);
+    if (key in this._registrations &&
+        aData.task in this._registrations[key]) {
+      // if this task already exists we overwrite it.
+      this.removeRegistrationInternal(aData.task, key);
+    }
+
+    // This creates a RequestTaskFull object.
+    aData.params.task = aData.task;
+    aData.params.lastSync = 0;
+    aData.params.principal = aPrincipal;
+
+    let dbKey = aData.task + "|" +
+                aPrincipal.appId + '|' +
+                aPrincipal.isInBrowserElement + '|' +
+                aPrincipal.origin;
+
+    let data = { principal: aPrincipal,
+                 dbKey: dbKey,
+                 data: aData.params,
+                 active: true,
+                 timer: null };
+
+    let self = this;
+    this.dbTxn('readwrite', function(aStore) {
+      aStore.put(data, data.dbKey);
+    },
+    function() {
+      self.addRegistration(data);
+      aTarget.sendAsyncMessage("RequestSync:Register:Return",
+                               { requestID: aData.requestID });
+    },
+    function() {
+      aTarget.sendAsyncMessage("RequestSync:Register:Return",
+                               { requestID: aData.requestID,
+                                 error: "IndexDBError" } );
+    });
+  },
+
+  // Unregister a task.
+  unregister: function(aTarget, aData, aPrincipal) {
+    debug("unregister");
+
+    let key = this.principalToKey(aPrincipal);
+    if (!(key in this._registrations) ||
+        !(aData.task in this._registrations[key])) {
+      aTarget.sendAsyncMessage("RequestSync:Unregister:Return",
+                               { requestID: aData.requestID,
+                                 error: "UnknownTaskError" });
+      return;
+    }
+
+    let dbKey = this._registrations[key][aData.task].dbKey;
+    this.removeRegistration(aData.task, key, aPrincipal);
+
+    let self = this;
+    this.dbTxn('readwrite', function(aStore) {
+      aStore.delete(dbKey);
+    },
+    function() {
+      aTarget.sendAsyncMessage("RequestSync:Unregister:Return",
+                               { requestID: aData.requestID });
+    },
+    function() {
+      aTarget.sendAsyncMessage("RequestSync:Unregister:Return",
+                               { requestID: aData.requestID,
+                                 error: "IndexDBError" } );
+    });
+  },
+
+  // Get the list of registered tasks for this principal.
+  registrations: function(aTarget, aData, aPrincipal) {
+    debug("registrations");
+
+    let results = [];
+    let key = this.principalToKey(aPrincipal);
+    if (key in this._registrations) {
+      for (let i in this._registrations[key]) {
+        results.push(this.createPartialTaskObject(
+          this._registrations[key][i].data));
+      }
+    }
+
+    aTarget.sendAsyncMessage("RequestSync:Registrations:Return",
+                             { requestID: aData.requestID,
+                               results: results });
+  },
+
+  // Get a particular registered task for this principal.
+  registration: function(aTarget, aData, aPrincipal) {
+    debug("registration");
+
+    let results = null;
+    let key = this.principalToKey(aPrincipal);
+    if (key in this._registrations &&
+        aData.task in this._registrations[key]) {
+      results = this.createPartialTaskObject(
+        this._registrations[key][aData.task].data);
+    }
+
+    aTarget.sendAsyncMessage("RequestSync:Registration:Return",
+                             { requestID: aData.requestID,
+                               results: results });
+  },
+
+  // Get the list of the registered tasks.
+  managerRegistrations: function(aTarget, aData, aPrincipal) {
+    debug("managerRegistrations");
+
+    let results = [];
+    for (var key in this._registrations) {
+      for (var task in this._registrations[key]) {
+        results.push(
+          this.createFullTaskObject(this._registrations[key][task].data));
+      }
+    }
+
+    aTarget.sendAsyncMessage("RequestSyncManager:Registrations:Return",
+                             { requestID: aData.requestID,
+                               results: results });
+  },
+
+  // We cannot expose the full internal object to content but just a subset.
+  // This method creates this subset.
+  createPartialTaskObject: function(aObj) {
+    return { task: aObj.task,
+             lastSync: aObj.lastSync,
+             oneShot: aObj.oneShot,
+             minInterval: aObj.minInterval,
+             wakeUpPage: aObj.wakeUpPage,
+             wifiOnly: aObj.wifiOnly,
+             data: aObj.data };
+  },
+
+  createFullTaskObject: function(aObj) {
+    let obj = this.createPartialTaskObject(aObj);
+
+    obj.app = { manifestURL: '',
+                origin: aObj.principal.origin,
+                isInBrowserElement: aObj.principal.isInBrowserElement };
+
+    let app = appsService.getAppByLocalId(aObj.principal.appId);
+    if (app) {
+      obj.app.manifestURL = app.manifestURL;
+    }
+
+    return obj;
+  },
+
+  // Creation of the timer for a particular task object.
+  scheduleTimer: function(aObj) {
+    debug("scheduleTimer");
+
+    // A  registration can be already inactive if it was 1 shot.
+    if (aObj.active) {
+      aObj.timer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer);
+
+      let self = this;
+      aObj.timer.initWithCallback(function() { self.timeout(aObj); },
+                                  aObj.data.minInterval * 1000,
+                                  Ci.nsITimer.TYPE_ONE_SHOT);
+    }
+  },
+
+  timeout: function(aObj) {
+    debug("timeout");
+
+    let app = appsService.getAppByLocalId(aObj.principal.appId);
+    if (!app) {
+      dump("ERROR!! RequestSyncService - Failed to retrieve app data from a principal.\n");
+      aObj.active = false;
+      this.updateObjectInDB(aObj);
+      return;
+    }
+
+    let manifestURL = Services.io.newURI(app.manifestURL, null, null);
+    let pageURL = Services.io.newURI(aObj.data.wakeUpPage, null, aObj.principal.URI);
+
+    // Maybe need to be rescheduled?
+    if (this.needRescheduling('request-sync', manifestURL, pageURL)) {
+      this.scheduleTimer(aObj);
+      return;
+    }
+
+    aObj.timer = null;
+
+    if (!manifestURL || !pageURL) {
+      dump("ERROR!! RequestSyncService - Failed to create URI for the page or the manifest\n");
+      aObj.active = false;
+      this.updateObjectInDB(aObj);
+      return;
+    }
+
+    // Sending the message.
+    systemMessenger.sendMessage('request-sync',
+                                this.createPartialTaskObject(aObj.data),
+                                pageURL, manifestURL);
+
+    // One shot? Then this is not active.
+    aObj.active = !aObj.data.oneShot;
+    aObj.data.lastSync = new Date();
+
+    let self = this;
+    this.updateObjectInDB(aObj, function() {
+      // SchedulerTimer creates a timer and a nsITimer cannot be cloned. This
+      // is the reason why this operation has to be done after storing the aObj
+      // into IDB.
+      if (!aObj.data.oneShot) {
+        self.scheduleTimer(aObj);
+      }
+    });
+  },
+
+  needRescheduling: function(aMessageName, aManifestURL, aPageURL) {
+    let hasPendingMessages =
+      cpmm.sendSyncMessage("SystemMessageManager:HasPendingMessages",
+                           { type: aMessageName,
+                             pageURL: aPageURL.spec,
+                             manifestURL: aManifestURL.spec })[0];
+
+    debug("Pending messages: " + hasPendingMessages);
+
+    if (hasPendingMessages) {
+      return true;
+    }
+
+    // FIXME: other reasons?
+
+    return false;
+  },
+
+  // Update the object into the database.
+  updateObjectInDB: function(aObj, aCb) {
+    debug("updateObjectInDB");
+
+    this.dbTxn('readwrite', function(aStore) {
+      aStore.put(aObj, aObj.dbKey);
+    },
+    function() {
+      if (aCb) {
+        aCb();
+      }
+      debug("UpdateObjectInDB completed");
+    }, function() {
+      debug("UpdateObjectInDB failed");
+    });
+  },
+
+  pendingOperationStarted: function() {
+    debug('pendingOperationStarted');
+    this._pendingOperation = true;
+  },
+
+  pendingOperationDone: function() {
+    debug('pendingOperationDone');
+
+    this._pendingOperation = false;
+
+    // managing the pending messages now that the initialization is completed.
+    while (this._pendingMessages.length) {
+      this.receiveMessage(this._pendingMessages.shift());
+    }
+  },
+
+  // This method creates a transaction and runs callbacks. Plus it manages the
+  // pending operations system.
+  dbTxn: function(aType, aCb, aSuccessCb, aErrorCb) {
+    debug('dbTxn');
+
+    this.pendingOperationStarted();
+
+    let self = this;
+    this.newTxn(aType, RSYNCDB_NAME, function(aTxn, aStore) {
+      aCb(aStore);
+    },
+    function() {
+      self.pendingOperationDone();
+      aSuccessCb();
+    },
+    function() {
+      self.pendingOperationDone();
+      aErrorCb();
+    });
+  }
+}
+
+RequestSyncService.init();
+
+this.EXPORTED_SYMBOLS = [""];
new file mode 100644
--- /dev/null
+++ b/dom/requestsync/moz.build
@@ -0,0 +1,17 @@
+# -*- Mode: python; c-basic-offset: 4; indent-tabs-mode: nil; tab-width: 40 -*-
+# vim: set filetype=python:
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+MOCHITEST_MANIFESTS += ['tests/mochitest.ini']
+
+EXTRA_COMPONENTS += [
+    'RequestSync.manifest',
+    'RequestSyncManager.js',
+    'RequestSyncScheduler.js',
+]
+
+EXTRA_JS_MODULES += [
+    'RequestSyncService.jsm',
+]
new file mode 100644
--- /dev/null
+++ b/dom/requestsync/tests/common_app.js
@@ -0,0 +1,15 @@
+function is(a, b, msg) {
+  alert((a === b ? 'OK' : 'KO') + ' ' + msg)
+}
+
+function ok(a, msg) {
+  alert((a ? 'OK' : 'KO')+ ' ' + msg)
+}
+
+function cbError() {
+  alert('KO error');
+}
+
+function finish() {
+  alert('DONE');
+}
new file mode 100644
--- /dev/null
+++ b/dom/requestsync/tests/common_basic.js
@@ -0,0 +1,172 @@
+function test_registerFailure() {
+  ok("sync" in navigator, "navigator.sync exists");
+
+  navigator.sync.register().then(
+  function() {
+    ok(false, "navigator.sync.register() throws without a task name");
+  }, function() {
+    ok(true, "navigator.sync.register() throws without a task name");
+  })
+
+  .then(function() {
+    return navigator.sync.register(42);
+  }).then(function() {
+    ok(false, "navigator.sync.register() throws without a string task name");
+  }, function() {
+    ok(true, "navigator.sync.register() throws without a string task name");
+  })
+
+  .then(function() {
+    return navigator.sync.register('foobar');
+  }).then(function() {
+    ok(false, "navigator.sync.register() throws without a param dictionary");
+  }, function() {
+    ok(true, "navigator.sync.register() throws without a param dictionary");
+  })
+
+  .then(function() {
+    return navigator.sync.register('foobar', 42);
+  }).then(function() {
+    ok(false, "navigator.sync.register() throws without a real dictionary");
+  }, function() {
+    ok(true, "navigator.sync.register() throws without a real dictionary");
+  })
+
+  .then(function() {
+    return navigator.sync.register('foobar', {});
+  }).then(function() {
+    ok(false, "navigator.sync.register() throws without a minInterval and wakeUpPage");
+  }, function() {
+    ok(true, "navigator.sync.register() throws without a minInterval and wakeUpPage");
+  })
+
+  .then(function() {
+    return navigator.sync.register('foobar', { minInterval: 100 });
+  }).then(function() {
+    ok(false, "navigator.sync.register() throws without a wakeUpPage");
+  }, function() {
+    ok(true, "navigator.sync.register() throws without a wakeUpPage");
+  })
+
+  .then(function() {
+    return navigator.sync.register('foobar', { wakeUpPage: 100 });
+  }).then(function() {
+    ok(false, "navigator.sync.register() throws without a minInterval");
+  }, function() {
+    ok(true, "navigator.sync.register() throws without a minInterval");
+  })
+
+  .then(function() {
+    runTests();
+  });
+}
+
+function genericError() {
+  ok(false, "Some promise failed");
+}
+
+function test_register() {
+  navigator.sync.register('foobar', { minInterval: 5, wakeUpPage:'/' }).then(
+  function() {
+    ok(true, "navigator.sync.register() worked!");
+    runTests();
+  }, genericError);
+}
+
+function test_unregister() {
+  navigator.sync.unregister('foobar').then(
+  function() {
+    ok(true, "navigator.sync.unregister() worked!");
+    runTests();
+  }, genericError);
+}
+
+function test_unregisterDuplicate() {
+  navigator.sync.unregister('foobar').then(
+  genericError,
+  function(error) {
+    ok(true, "navigator.sync.unregister() should throw if the task doesn't exist.");
+    ok(error, "UnknownTaskError", "Duplicate unregistration error is correct");
+    runTests();
+  });
+}
+
+function test_registrationEmpty() {
+  navigator.sync.registration('bar').then(
+  function(results) {
+    is(results, null, "navigator.sync.registration() should return null.");
+    runTests();
+  },
+  genericError);
+}
+
+function test_registration() {
+  navigator.sync.registration('foobar').then(
+  function(results) {
+    is(results.task, 'foobar', "navigator.sync.registration().task is correct");
+    ok("lastSync" in results, "navigator.sync.registration().lastSync is correct");
+    is(results.oneShot, true, "navigator.sync.registration().oneShot is correct");
+    is(results.minInterval, 5, "navigator.sync.registration().minInterval is correct");
+    ok("wakeUpPage" in results, "navigator.sync.registration().wakeUpPage is correct");
+    ok("wifiOnly" in results, "navigator.sync.registration().wifiOnly is correct");
+    ok("data" in results, "navigator.sync.registration().data is correct");
+    ok(!("app" in results), "navigator.sync.registrations().app is correct");
+    runTests();
+  },
+  genericError);
+}
+
+function test_registrationsEmpty() {
+  navigator.sync.registrations().then(
+  function(results) {
+    is(results.length, 0, "navigator.sync.registrations() should return an empty array.");
+    runTests();
+  },
+  genericError);
+}
+
+function test_registrations() {
+  navigator.sync.registrations().then(
+  function(results) {
+    is(results.length, 1, "navigator.sync.registrations() should not return an empty array.");
+    is(results[0].task, 'foobar', "navigator.sync.registrations()[0].task is correct");
+    ok("lastSync" in results[0], "navigator.sync.registrations()[0].lastSync is correct");
+    is(results[0].oneShot, true, "navigator.sync.registrations()[0].oneShot is correct");
+    is(results[0].minInterval, 5, "navigator.sync.registrations()[0].minInterval is correct");
+    ok("wakeUpPage" in results[0], "navigator.sync.registration()[0].wakeUpPage is correct");
+    ok("wifiOnly" in results[0], "navigator.sync.registrations()[0].wifiOnly is correct");
+    ok("data" in results[0], "navigator.sync.registrations()[0].data is correct");
+    ok(!("app" in results[0]), "navigator.sync.registrations()[0].app is correct");
+    runTests();
+  },
+  genericError);
+}
+
+function test_managerRegistrationsEmpty() {
+  navigator.syncManager.registrations().then(
+  function(results) {
+    is(results.length, 0, "navigator.syncManager.registrations() should return an empty array.");
+    runTests();
+  },
+  genericError);
+}
+
+function test_managerRegistrations() {
+  navigator.syncManager.registrations().then(
+  function(results) {
+    is(results.length, 1, "navigator.sync.registrations() should not return an empty array.");
+    is(results[0].task, 'foobar', "navigator.sync.registrations()[0].task is correct");
+    ok("lastSync" in results[0], "navigator.sync.registrations()[0].lastSync is correct");
+    is(results[0].oneShot, true, "navigator.sync.registrations()[0].oneShot is correct");
+    is(results[0].minInterval, 5, "navigator.sync.registrations()[0].minInterval is correct");
+    ok("wakeUpPage" in results[0], "navigator.sync.registration()[0].wakeUpPage is correct");
+    ok("wifiOnly" in results[0], "navigator.sync.registrations()[0].wifiOnly is correct");
+    ok("data" in results[0], "navigator.sync.registrations()[0].data is correct");
+    ok("app" in results[0], "navigator.sync.registrations()[0].app is correct");
+    ok("manifestURL" in results[0].app, "navigator.sync.registrations()[0].app.manifestURL is correct");
+    is(results[0].app.origin, 'http://mochi.test:8888', "navigator.sync.registrations()[0].app.origin is correct");
+    is(results[0].app.isInBrowserElement, false, "navigator.sync.registrations()[0].app.isInBrowserElement is correct");
+    runTests();
+  },
+  genericError);
+}
new file mode 100644
--- /dev/null
+++ b/dom/requestsync/tests/file_app.sjs
@@ -0,0 +1,54 @@
+var gBasePath = "tests/dom/requestsync/tests/";
+var gTemplate = "file_app.template.webapp";
+
+function handleRequest(request, response) {
+  var query = getQuery(request);
+
+  var testToken = '';
+  if ('testToken' in query) {
+    testToken = query.testToken;
+  }
+
+  var template = gBasePath + gTemplate;
+  response.setHeader("Content-Type", "application/x-web-app-manifest+json", false);
+  response.write(readTemplate(template).replace(/TESTTOKEN/g, testToken));
+}
+
+// Copy-pasted incantations. There ought to be a better way to synchronously read
+// a file into a string, but I guess we're trying to discourage that.
+function readTemplate(path) {
+  var file = Components.classes["@mozilla.org/file/directory_service;1"].
+                        getService(Components.interfaces.nsIProperties).
+                        get("CurWorkD", Components.interfaces.nsILocalFile);
+  var fis  = Components.classes['@mozilla.org/network/file-input-stream;1'].
+                        createInstance(Components.interfaces.nsIFileInputStream);
+  var cis = Components.classes["@mozilla.org/intl/converter-input-stream;1"].
+                       createInstance(Components.interfaces.nsIConverterInputStream);
+  var split = path.split("/");
+  for(var i = 0; i < split.length; ++i) {
+    file.append(split[i]);
+  }
+  fis.init(file, -1, -1, false);
+  cis.init(fis, "UTF-8", 0, 0);
+
+  var data = "";
+  let (str = {}) {
+    let read = 0;
+    do {
+      read = cis.readString(0xffffffff, str); // read as much as we can and put it in str.value
+      data += str.value;
+    } while (read != 0);
+  }
+  cis.close();
+  return data;
+}
+
+function getQuery(request) {
+  var query = {};
+  request.queryString.split('&').forEach(function (val) {
+    var [name, value] = val.split('=');
+    query[name] = unescape(value);
+  });
+  return query;
+}
+
new file mode 100644
--- /dev/null
+++ b/dom/requestsync/tests/file_app.template.webapp
@@ -0,0 +1,6 @@
+{
+  "name": "Really Rapid Release (hosted)",
+  "description": "Updated even faster than <a href='http://mozilla.org'>Firefox</a>, just to annoy slashdotters.",
+  "launch_path": "/tests/dom/requestsync/tests/TESTTOKEN",
+  "icons": { "128": "default_icon" }
+}
new file mode 100644
--- /dev/null
+++ b/dom/requestsync/tests/file_basic_app.html
@@ -0,0 +1,62 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+  <script type="application/javascript" src="common_app.js"></script>
+  <script type="application/javascript" src="common_basic.js"></script>
+  <meta charset="utf-8">
+</head>
+<body>
+<div id="container"></div>
+  <script type="application/javascript;version=1.7">
+
+  function test_sync_interface() {
+    ok("sync" in navigator, "navigator.sync should exist with permissions");
+    ok(!("syncManager" in navigator), "navigator.syncManager should not exist without permissions");
+
+    ok("register" in navigator.sync, "navigator.sync.register exists");
+    ok("unregister" in navigator.sync, "navigator.sync.unregister exists");
+    ok("registrations" in navigator.sync, "navigator.sync.registrations exists");
+    ok("registration" in navigator.sync, "navigator.sync.registration exists");
+
+    runTests();
+  }
+
+  var tests = [
+    test_sync_interface,
+
+    test_registrationsEmpty,
+
+    test_registerFailure,
+    test_register,
+    // overwrite the same registration.
+    test_register,
+
+    test_registrations,
+
+    test_registrationEmpty,
+    test_registration,
+
+    test_unregister,
+    test_unregisterDuplicate,
+
+    test_registrationsEmpty,
+
+    // Let's keep a registration active when the app is uninstall...
+    test_register,
+    test_registrations
+  ];
+
+  function runTests() {
+    if (!tests.length) {
+      finish();
+      return;
+    }
+
+    var test = tests.shift();
+    test();
+  }
+
+  runTests();
+  </script>
+</body>
+</html>
new file mode 100644
--- /dev/null
+++ b/dom/requestsync/tests/file_interface.html
@@ -0,0 +1,21 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+  <script type="application/javascript" src="common_app.js"></script>
+  <meta charset="utf-8">
+</head>
+<body>
+<div id="container"></div>
+  <script type="application/javascript;version=1.7">
+
+  ok("sync" in navigator, "navigator.sync should exist with permissions");
+  ok("register" in navigator.sync, "navigator.sync.register exists");
+  ok("unregister" in navigator.sync, "navigator.sync.unregister exists");
+  ok("registrations" in navigator.sync, "navigator.sync.registrations exists");
+  ok("registration" in navigator.sync, "navigator.sync.registration exists");
+
+  finish();
+
+  </script>
+</body>
+</html>
new file mode 100644
--- /dev/null
+++ b/dom/requestsync/tests/mochitest.ini
@@ -0,0 +1,16 @@
+[DEFAULT]
+skip-if = e10s
+support-files =
+  file_app.template.webapp
+  file_app.sjs
+  file_basic_app.html
+  common_app.js
+  common_basic.js
+
+[test_webidl.html]
+[test_minInterval.html]
+[test_basic.html]
+[test_basic_app.html]
+run-if = buildapp != 'b2g'
+[test_wakeUp.html]
+run-if = buildapp == 'b2g' && toolkit == 'gonk'
new file mode 100644
--- /dev/null
+++ b/dom/requestsync/tests/test_basic.html
@@ -0,0 +1,73 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+  <meta charset="utf-8">
+  <title>Test for RequestSync basic use</title>
+  <script type="application/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+  <script type="application/javascript" src="common_basic.js"></script>
+  <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+  <script type="application/javascript;version=1.7">
+
+
+  var tests = [
+    function() {
+      SpecialPowers.pushPrefEnv({"set": [["dom.requestSync.enabled", true],
+                                         ["dom.requestSync.minInterval", 1],
+                                         ["dom.ignore_webidl_scope_checks", true]]}, runTests);
+    },
+
+    function() {
+      SpecialPowers.pushPermissions(
+        [{ "type": "requestsync-manager", "allow": 1, "context": document } ], runTests);
+    },
+
+    function() {
+      if (SpecialPowers.isMainProcess()) {
+        SpecialPowers.Cu.import("resource://gre/modules/RequestSyncService.jsm");
+      }
+      runTests();
+    },
+
+    test_managerRegistrationsEmpty,
+    test_registrationsEmpty,
+
+    test_registerFailure,
+    test_register,
+    // overwrite the same registration.
+    test_register,
+
+    test_managerRegistrations,
+    test_registrations,
+
+    test_registrationEmpty,
+    test_registration,
+
+    test_unregister,
+    test_unregisterDuplicate,
+
+    test_managerRegistrationsEmpty,
+    test_registrationsEmpty,
+  ];
+
+  function runTests() {
+    if (!tests.length) {
+      finish();
+      return;
+    }
+
+    var test = tests.shift();
+    test();
+  }
+
+  function finish() {
+    SimpleTest.finish();
+  }
+
+  SimpleTest.waitForExplicitFinish();
+  runTests();
+
+  </script>
+</body>
+</html>
new file mode 100644
--- /dev/null
+++ b/dom/requestsync/tests/test_basic_app.html
@@ -0,0 +1,135 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+  <meta charset="utf-8">
+  <title>Test for requestSync - basic operations in app</title>
+  <script type="application/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+  <script type="application/javascript" src="common_basic.js"></script>
+  <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+<div id="container"></div>
+  <script type="application/javascript;version=1.7">
+
+  var gHostedManifestURL = 'http://test/tests/dom/requestsync/tests/file_app.sjs?testToken=file_basic_app.html';
+  var gApp;
+
+  function cbError() {
+    ok(false, "Error callback invoked");
+    finish();
+  }
+
+  function installApp() {
+    var request = navigator.mozApps.install(gHostedManifestURL);
+    request.onerror = cbError;
+    request.onsuccess = function() {
+      gApp = request.result;
+      runTests();
+    }
+  }
+
+  function uninstallApp() {
+    // Uninstall the app.
+    var request = navigator.mozApps.mgmt.uninstall(gApp);
+    request.onerror = cbError;
+    request.onsuccess = function() {
+      // All done.
+      info("All done");
+      runTests();
+    }
+  }
+
+  function testApp() {
+    var ifr = document.createElement('iframe');
+    ifr.setAttribute('mozbrowser', 'true');
+    ifr.setAttribute('mozapp', gApp.manifestURL);
+    ifr.setAttribute('src', gApp.manifest.launch_path);
+    var domParent = document.getElementById('container');
+
+    // Set us up to listen for messages from the app.
+    var listener = function(e) {
+      var message = e.detail.message;
+      if (/^OK/.exec(message)) {
+        ok(true, "Message from app: " + message);
+      } else if (/KO/.exec(message)) {
+        ok(false, "Message from app: " + message);
+      } else if (/DONE/.exec(message)) {
+        ok(true, "Messaging from app complete");
+        ifr.removeEventListener('mozbrowsershowmodalprompt', listener);
+        domParent.removeChild(ifr);
+        runTests();
+      }
+    }
+
+    // This event is triggered when the app calls "alert".
+    ifr.addEventListener('mozbrowsershowmodalprompt', listener, false);
+    domParent.appendChild(ifr);
+  }
+
+  var tests = [
+    // Permissions
+    function() {
+      SpecialPowers.pushPermissions(
+        [{ "type": "browser", "allow": 1, "context": document },
+         { "type": "embed-apps", "allow": 1, "context": document },
+         {"type": "requestsync-manager", "allow": 1, "context": document },
+         { "type": "webapps-manage", "allow": 1, "context": document }], runTests);
+    },
+
+    // Preferences
+    function() {
+      SpecialPowers.pushPrefEnv({"set": [["dom.requestSync.enabled", true],
+                                         ["dom.requestSync.minInterval", 1],
+                                         ["dom.ignore_webidl_scope_checks", true],
+                                         ["dom.testing.ignore_ipc_principal", true]]}, runTests);
+    },
+
+    function() {
+      if (SpecialPowers.isMainProcess()) {
+        SpecialPowers.Cu.import("resource://gre/modules/RequestSyncService.jsm");
+      }
+
+      SpecialPowers.setAllAppsLaunchable(true);
+      SpecialPowers.setBoolPref("dom.mozBrowserFramesEnabled", true);
+      runTests();
+    },
+
+    // No confirmation needed when an app is installed
+    function() {
+      SpecialPowers.autoConfirmAppInstall(() =>
+        SpecialPowers.autoConfirmAppUninstall(runTests));
+    },
+
+    test_managerRegistrationsEmpty,
+
+    // Installing the app
+    installApp,
+
+    // Run tests in app
+    testApp,
+
+    // Uninstall the app
+    uninstallApp,
+
+    test_managerRegistrationsEmpty
+  ];
+
+  function runTests() {
+    if (!tests.length) {
+      finish();
+      return;
+    }
+
+    var test = tests.shift();
+    test();
+  }
+
+  function finish() {
+    SimpleTest.finish();
+  }
+
+  SimpleTest.waitForExplicitFinish();
+  runTests();
+  </script>
+</body>
+</html>
new file mode 100644
--- /dev/null
+++ b/dom/requestsync/tests/test_minInterval.html
@@ -0,0 +1,65 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+  <meta charset="utf-8">
+  <title>Test for RequestSync minInterval pref</title>
+  <script type="application/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+  <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+  <script type="application/javascript;version=1.7">
+
+  function test_minInterval(expected) {
+    navigator.sync.register('foobar', { minInterval: 1, wakeUpPage: '/' }).then(
+    function() {
+      ok(expected, "MinInterval succeeded");
+    },
+    function(e) {
+      ok(!expected, "MinInterval failed");
+      is(e, "ParamsError", "Correct error received");
+    })
+
+    .then(runTests);
+  }
+
+  var tests = [
+    function() {
+      if (SpecialPowers.isMainProcess()) {
+        SpecialPowers.Cu.import("resource://gre/modules/RequestSyncService.jsm");
+      }
+      runTests();
+    },
+
+    function() {
+      SpecialPowers.pushPrefEnv({"set": [["dom.ignore_webidl_scope_checks", true],
+                                         ["dom.requestSync.enabled", true]]}, runTests);
+    },
+
+    function() { test_minInterval(false); },
+
+    function() {
+      SpecialPowers.pushPrefEnv({"set": [["dom.requestSync.minInterval", 1]]}, runTests);
+    },
+
+    function() { test_minInterval(true); },
+  ];
+
+  function runTests() {
+    if (!tests.length) {
+      finish();
+      return;
+    }
+
+    var test = tests.shift();
+    test();
+  }
+
+  function finish() {
+    SimpleTest.finish();
+  }
+
+  SimpleTest.waitForExplicitFinish();
+  runTests();
+  </script>
+</body>
+</html>
new file mode 100644
--- /dev/null
+++ b/dom/requestsync/tests/test_wakeUp.html
@@ -0,0 +1,132 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+  <meta charset="utf-8">
+  <title>Test for requestSync - wakeUp</title>
+  <script type="application/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+  <script type="application/javascript" src="common_basic.js"></script>
+  <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+<div id="container"></div>
+  <script type="application/javascript;version=1.7">
+
+  var oneShotCounter = 0;
+  var multiShotCounter = 0;
+
+  function maybeDone() {
+    if (oneShotCounter == 1 && multiShotCounter == 3) {
+      runTests();
+    }
+  }
+
+  function setMessageHandler() {
+    navigator.mozSetMessageHandler('request-sync', function(e) {
+      ok(true, "One event has been received!");
+
+      if (e.task == "oneShot") {
+        is(e.data, 42, "e.data is correct");
+        is(e.lastSync, 0, "e.lastSync is correct");
+        is(e.oneShot, true, "e.oneShot is correct");
+        is(e.minInterval, 2, "e.minInterval is correct");
+        is(e.wifiOnly, false, "e.wifiOnly is correct");
+
+        is(++oneShotCounter, 1, "Only 1 shot should be received here");
+        maybeDone();
+      }
+
+      else if (e.task == "multiShots") {
+        is(e.data, 'hello world!', "e.data is correct");
+
+        if (multiShotCounter == 0) {
+          is(e.lastSync, 0, "e.lastSync is correct");
+        } else {
+          isnot(e.lastSync, 0, "e.lastSync is correct");
+        }
+
+        is(e.oneShot, false, "e.oneShot is correct");
+        is(e.minInterval, 3, "e.minInterval is correct");
+        is(e.wifiOnly, false, "e.wifiOnly is correct");
+
+        ++multiShotCounter;
+        maybeDone();
+      }
+
+      else {
+        ok(false, "Unknown event has been received!");
+      }
+    });
+
+    runTests();
+  }
+
+  function test_register_oneShot() {
+    navigator.sync.register('oneShot', { minInterval: 2,
+                                         oneShot: true,
+                                         data: 42,
+                                         wifiOnly: false,
+                                         wakeUpPage: location.href }).then(
+    function() {
+      ok(true, "navigator.sync.register() oneShot done");
+      runTests();
+    }, genericError);
+  }
+
+  function test_register_multiShots() {
+    navigator.sync.register('multiShots', { minInterval: 3,
+                                            oneShot: false,
+                                            data: 'hello world!',
+                                            wifiOnly: false,
+                                            wakeUpPage: location.href }).then(
+    function() {
+      ok(true, "navigator.sync.register() multiShots done");
+      runTests();
+    }, genericError);
+  }
+
+  function test_wait() {
+    // nothing to do here.
+  }
+
+  var tests = [
+    function() {
+      SpecialPowers.pushPrefEnv({"set": [["dom.requestSync.enabled", true],
+                                         ["dom.requestSync.minInterval", 1],
+                                         ["dom.ignore_webidl_scope_checks", true]]}, runTests);
+    },
+
+    function() {
+      SpecialPowers.pushPermissions(
+        [{ "type": "requestsync-manager", "allow": 1, "context": document } ], runTests);
+    },
+
+    function() {
+      if (SpecialPowers.isMainProcess()) {
+        SpecialPowers.Cu.import("resource://gre/modules/RequestSyncService.jsm");
+      }
+      runTests();
+    },
+
+    setMessageHandler,
+
+    test_register_oneShot,
+    test_register_multiShots,
+
+    test_wait,
+  ];
+
+  function runTests() {
+    if (!tests.length) {
+      SimpleTest.finish();
+      return;
+    }
+
+    var test = tests.shift();
+    test();
+  }
+
+  SimpleTest.waitForExplicitFinish();
+  runTests();
+  </script>
+</body>
+</html>
new file mode 100644
--- /dev/null
+++ b/dom/requestsync/tests/test_webidl.html
@@ -0,0 +1,90 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+  <meta charset="utf-8">
+  <title>Test for RequestSync interfaces</title>
+  <script type="application/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+  <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+  <script type="application/javascript;version=1.7">
+
+  function test_no_interface() {
+    ok(!("sync" in navigator), "navigator.sync should not exist without permissions");
+    ok(!("syncManager" in navigator), "navigator.syncManager should not exist without permissions");
+    runTests();
+  }
+
+  function test_sync() {
+    ok("register" in navigator.sync, "navigator.sync.register exists");
+    ok("unregister" in navigator.sync, "navigator.sync.unregister exists");
+    ok("registrations" in navigator.sync, "navigator.sync.registrations exists");
+    ok("registration" in navigator.sync, "navigator.sync.registration exists");
+  }
+
+  function test_sync_interface() {
+    ok("sync" in navigator, "navigator.sync should exist with permissions");
+    ok(!("syncManager" in navigator), "navigator.syncManager should not exist without permissions");
+
+    test_sync();
+    runTests();
+  }
+
+  function test_sync_manager_interface() {
+    ok("sync" in navigator, "navigator.sync should exist with permissions");
+    ok("syncManager" in navigator, "navigator.syncManager should exist with permissions");
+
+    test_sync();
+
+    ok("registrations" in navigator.syncManager, "navigator.syncManager.registrations exists");
+    runTests();
+  }
+
+  var tests = [
+    function() {
+      SpecialPowers.pushPrefEnv({"set": [["dom.requestSync.enabled", false]]}, runTests);
+    },
+
+    test_no_interface,
+
+    function() {
+      SpecialPowers.pushPrefEnv({"set": [["dom.ignore_webidl_scope_checks", true]]}, runTests);
+    },
+
+    test_no_interface,
+
+    function() {
+      SpecialPowers.pushPrefEnv({"set": [["dom.requestSync.enabled", true],
+                                         ["dom.requestSync.minInterval", 1]]}, runTests);
+    },
+
+    test_sync_interface,
+
+    // Permissions
+    function() {
+      SpecialPowers.pushPermissions(
+        [{ "type": "requestsync-manager", "allow": 1, "context": document } ], runTests);
+    },
+
+    test_sync_manager_interface,
+  ];
+
+  function runTests() {
+    if (!tests.length) {
+      finish();
+      return;
+    }
+
+    var test = tests.shift();
+    test();
+  }
+
+  function finish() {
+    SimpleTest.finish();
+  }
+
+  SimpleTest.waitForExplicitFinish();
+  runTests();
+  </script>
+</body>
+</html>
--- a/dom/tests/mochitest/general/test_interfaces.html
+++ b/dom/tests/mochitest/general/test_interfaces.html
@@ -1203,16 +1203,18 @@ var interfaceNamesInGlobalScope =
     "SVGUseElement",
 // IMPORTANT: Do not change this list without review from a DOM peer!
     "SVGViewElement",
 // IMPORTANT: Do not change this list without review from a DOM peer!
     "SVGZoomAndPan",
 // IMPORTANT: Do not change this list without review from a DOM peer!
     "SVGZoomEvent",
 // IMPORTANT: Do not change this list without review from a DOM peer!
+    {name: "RequestSyncManager", b2g: true, pref: "dom.requestSync.enabled", permission: ["requestsync-manager"] },
+// IMPORTANT: Do not change this list without review from a DOM peer!
     {name: "Telephony", b2g: true, pref: "dom.telephony.enabled"},
 // IMPORTANT: Do not change this list without review from a DOM peer!
     {name: "TelephonyCall", b2g: true, pref: "dom.telephony.enabled"},
 // IMPORTANT: Do not change this list without review from a DOM peer!
     {name: "TelephonyCallGroup", b2g: true, pref: "dom.telephony.enabled"},
 // IMPORTANT: Do not change this list without review from a DOM peer!
     {name: "TelephonyCallId", b2g: true, pref: "dom.telephony.enabled"},
 // IMPORTANT: Do not change this list without review from a DOM peer!
new file mode 100644
--- /dev/null
+++ b/dom/webidl/RequestSyncManager.webidl
@@ -0,0 +1,27 @@
+/* -*- Mode: IDL; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* 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/.
+ */
+
+// Rappresentation of the app in the RequestTaskFull.
+dictionary RequestTaskApp {
+  USVString origin;
+  USVString manifestURL;
+  boolean isInBrowserElement;
+};
+
+// Like a normal task, but with info about the app.
+dictionary RequestTaskFull : RequestTask {
+  RequestTaskApp app;
+};
+
+[NavigatorProperty="syncManager",
+ AvailableIn=CertifiedApps,
+ Pref="dom.requestSync.enabled",
+ CheckPermissions="requestsync-manager",
+ JSImplementation="@mozilla.org/dom/request-sync-manager;1"]
+// This interface will be used only by the B2G SystemApp
+interface RequestSyncManager {
+    Promise<sequence<RequestTaskFull>> registrations();
+};
new file mode 100644
--- /dev/null
+++ b/dom/webidl/RequestSyncScheduler.webidl
@@ -0,0 +1,38 @@
+/* -*- Mode: IDL; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* 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/.
+ */
+
+// This is the dictionary for the creation of a new task.
+dictionary RequestTaskParams {
+  required USVString wakeUpPage;
+  boolean oneShot = true;
+  required long minInterval; // in seconds >= dom.requestSync.minInterval or 100 secs
+  boolean wifiOnly = true;
+  any data = null;
+};
+
+
+// This is the dictionary you can have back from registration{s}().
+dictionary RequestTask : RequestTaskParams {
+  USVString task = "";
+
+  // Last synchonization date.. maybe it's useful to know.
+  DOMTimeStamp lastSync;
+};
+
+[NavigatorProperty="sync",
+ AvailableIn=CertifiedApps,
+ Pref="dom.requestSync.enabled",
+ JSImplementation="@mozilla.org/dom/request-sync-scheduler;1"]
+interface RequestSyncScheduler {
+
+  Promise<void> register(USVString task,
+                         optional RequestTaskParams params);
+  Promise<void> unregister(USVString task);
+
+  // Useful methods to get registrations
+  Promise<sequence<RequestTask>> registrations();
+  Promise<RequestTask> registration(USVString task);
+};
--- a/dom/webidl/moz.build
+++ b/dom/webidl/moz.build
@@ -335,16 +335,18 @@ WEBIDL_FILES = [
     'ProfileTimelineMarker.webidl',
     'Promise.webidl',
     'PromiseDebugging.webidl',
     'PushManager.webidl',
     'RadioNodeList.webidl',
     'Range.webidl',
     'Rect.webidl',
     'Request.webidl',
+    'RequestSyncManager.webidl',
+    'RequestSyncScheduler.webidl',
     'ResourceStats.webidl',
     'ResourceStatsManager.webidl',
     'Response.webidl',
     'RGBColor.webidl',
     'RTCConfiguration.webidl',
     'RTCIceCandidate.webidl',
     'RTCIdentityAssertion.webidl',
     'RTCPeerConnection.webidl',
--- a/mobile/android/installer/package-manifest.in
+++ b/mobile/android/installer/package-manifest.in
@@ -282,16 +282,19 @@
 @BINPATH@/components/xpcom_xpti.xpt
 @BINPATH@/components/xpconnect.xpt
 @BINPATH@/components/xulapp.xpt
 @BINPATH@/components/xul.xpt
 @BINPATH@/components/xultmpl.xpt
 @BINPATH@/components/zipwriter.xpt
 
 ; JavaScript components
+@BINPATH@/components/RequestSync.manifest
+@BINPATH@/components/RequestSyncManager.js
+@BINPATH@/components/RequestSyncScheduler.js
 @BINPATH@/components/ChromeNotifications.js
 @BINPATH@/components/ChromeNotifications.manifest
 @BINPATH@/components/ConsoleAPI.manifest
 @BINPATH@/components/ConsoleAPIStorage.js
 @BINPATH@/components/ContactManager.js
 @BINPATH@/components/ContactManager.manifest
 @BINPATH@/components/PhoneNumberService.js
 @BINPATH@/components/PhoneNumberService.manifest
--- a/modules/libpref/init/all.js
+++ b/modules/libpref/init/all.js
@@ -4437,16 +4437,19 @@ pref("dom.mozSettings.SettingsManager.ve
 pref("dom.mozSettings.SettingsRequestManager.verbose.enabled", false);
 pref("dom.mozSettings.SettingsService.verbose.enabled", false);
 
 // Controlling whether we want to allow forcing some Settings
 // IndexedDB transactions to be opened as readonly or keep everything as
 // readwrite.
 pref("dom.mozSettings.allowForceReadOnly", false);
 
+// RequestSync API is disabled by default.
+pref("dom.requestSync.enabled", false);
+
 // Search service settings
 pref("browser.search.log", false);
 pref("browser.search.update", true);
 pref("browser.search.update.log", false);
 pref("browser.search.update.interval", 21600);
 pref("browser.search.suggest.enabled", true);
 pref("browser.search.geoSpecificDefaults", false);
 pref("browser.search.geoip.url", "https://location.services.mozilla.com/v1/country?key=%MOZILLA_API_KEY%");