Bug 1018320 - RequestSync API - patch 1 - webIDL and basic logic, r=ehsan
☠☠ backed out by 636498d041b5 ☠ ☠
authorAndrea Marchesini <amarchesini@mozilla.com>
Sun, 04 Jan 2015 10:36:46 +0100
changeset 221953 5e26604cc6e03a3e0bfd49efddeffffc4b7dc442
parent 221952 1a5309ae312591a21ef0139123fb84a7d5e14d61
child 221954 c6fcdd1c681f960d875496b6bef09b7c7eab507c
push id53474
push useramarchesini@mozilla.com
push dateSun, 04 Jan 2015 09:38:35 +0000
treeherdermozilla-inbound@2ef1c26d77d3 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersehsan
bugs1018320
milestone37.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 1018320 - RequestSync API - patch 1 - webIDL and basic logic, r=ehsan
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
--- 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
@@ -337,16 +337,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
@@ -540,16 +540,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,86 @@
+<!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 = [
+    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
@@ -1205,16 +1205,20 @@ 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", premission: "requestsync-manager" },
+// IMPORTANT: Do not change this list without review from a DOM peer!
+    {name: "RequestSyncScheduler", b2g: true, pref: "dom.requestSync.enabled" },
+// 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
@@ -336,16 +336,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
@@ -281,16 +281,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