Bug 871445 - patch 1 - DataStoreService and getDataStores(), r=mounir, r=fabrice, r=ehsan
authorAndrea Marchesini <amarchesini@mozilla.com>
Wed, 02 Oct 2013 13:27:07 -0400
changeset 163546 12d670c20afc7e3279c486e1d2de90764544e429
parent 163545 ce0fb1912d01a56e17c5eece461dba1ac6398fbb
child 163547 6e29aa59b5a6181ea6478176c377f7b5442a51de
push id3066
push userakeybl@mozilla.com
push dateMon, 09 Dec 2013 19:58:46 +0000
treeherdermozilla-beta@a31a0dce83aa [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersmounir, fabrice, ehsan
bugs871445
milestone27.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 871445 - patch 1 - DataStoreService and getDataStores(), r=mounir, r=fabrice, r=ehsan
CLOBBER
b2g/app/b2g.js
b2g/installer/package-manifest.in
browser/installer/package-manifest.in
dom/apps/src/Webapps.jsm
dom/base/Navigator.cpp
dom/base/Navigator.h
dom/datastore/DataStore.jsm
dom/datastore/DataStore.manifest
dom/datastore/DataStoreService.js
dom/datastore/moz.build
dom/datastore/nsIDataStoreService.idl
dom/datastore/tests/Makefile.in
dom/datastore/tests/file_app.sjs
dom/datastore/tests/file_app.template.webapp
dom/datastore/tests/moz.build
dom/datastore/tests/test_app_install.html
dom/moz.build
dom/webidl/Navigator.webidl
mobile/android/installer/package-manifest.in
modules/libpref/src/init/all.js
--- a/CLOBBER
+++ b/CLOBBER
@@ -13,9 +13,9 @@
 #          |               |
 #          O <-- Clobber   O  <-- Clobber
 #
 # Note: The description below will be part of the error message shown to users.
 #
 # Modifying this file will now automatically clobber the buildbot machines \o/
 #
 
-Bug 921563 needs a clobber due to apparent ipdl regeneration bugs.
+Bug 871445 needs a clobber.
--- a/b2g/app/b2g.js
+++ b/b2g/app/b2g.js
@@ -790,16 +790,18 @@ pref("network.sntp.refreshPeriod", 86400
 pref("network.sntp.pools", // Servers separated by ';'.
      "0.pool.ntp.org;1.pool.ntp.org;2.pool.ntp.org;3.pool.ntp.org");
 pref("network.sntp.port", 123);
 pref("network.sntp.timeout", 30); // In seconds.
 
 // Enable promise
 pref("dom.promise.enabled", false);
 
+pref("dom.datastore.enabled", true);
+
 // DOM Inter-App Communication API.
 #ifdef MOZ_WIDGET_GONK
 // Enable this only for gonk-specific build but not for desktop build.
 pref("dom.inter-app-communication-api.enabled", true);
 #endif
 
 // Allow ADB to run for this many hours before disabling
 // (only applies when marionette is disabled)
--- a/b2g/installer/package-manifest.in
+++ b/b2g/installer/package-manifest.in
@@ -761,16 +761,20 @@ bin/components/@DLL_PREFIX@nkgnomevfs@DL
 @BINPATH@/components/YoutubeProtocolHandler.js
 @BINPATH@/components/RecoveryService.js
 @BINPATH@/components/MailtoProtocolHandler.js
 @BINPATH@/components/SmsProtocolHandler.js
 @BINPATH@/components/TelProtocolHandler.js
 @BINPATH@/components/B2GAboutRedirector.js
 @BINPATH@/components/FilePicker.js
 
+@BINPATH@/components/DataStore.manifest
+@BINPATH@/components/DataStoreService.js
+@BINPATH@/components/dom_datastore.xpt
+
 #ifdef MOZ_WEBSPEECH
 @BINPATH@/components/dom_webspeechsynth.xpt
 #endif
 
 #ifdef XP_MACOSX
 @BINPATH@/@DLL_PREFIX@plugin_child_interpose@DLL_SUFFIX@
 #endif
 
--- a/browser/installer/package-manifest.in
+++ b/browser/installer/package-manifest.in
@@ -811,11 +811,16 @@ bin/libfreebl_32int64_3.so
 @BINPATH@/metro/chrome/@AB_CD@.manifest
 @BINPATH@/metro/chrome/pdfjs.manifest
 @BINPATH@/metro/chrome/pdfjs/*
 @BINPATH@/metro/components
 @BINPATH@/metro/defaults
 @BINPATH@/metro/modules
 #endif
 
+@BINPATH@/components/DataStore.manifest
+@BINPATH@/components/DataStoreService.js
+@BINPATH@/components/dom_datastore.xpt
+
+
 #ifdef MOZ_ASAN
 @BINPATH@/llvm-symbolizer
 #endif
--- a/dom/apps/src/Webapps.jsm
+++ b/dom/apps/src/Webapps.jsm
@@ -60,16 +60,20 @@ XPCOMUtils.defineLazyServiceGetter(this,
                                    "@mozilla.org/childprocessmessagemanager;1",
                                    "nsIMessageSender");
 
 XPCOMUtils.defineLazyGetter(this, "interAppCommService", function() {
   return Cc["@mozilla.org/inter-app-communication-service;1"]
          .getService(Ci.nsIInterAppCommService);
 });
 
+XPCOMUtils.defineLazyServiceGetter(this, "dataStoreService",
+                                   "@mozilla.org/datastore-service;1",
+                                   "nsIDataStoreService");
+
 XPCOMUtils.defineLazyGetter(this, "msgmgr", function() {
   return Cc["@mozilla.org/system-message-internal;1"]
          .getService(Ci.nsISystemMessagesInternal);
 });
 
 XPCOMUtils.defineLazyGetter(this, "updateSvc", function() {
   return Cc["@mozilla.org/offlinecacheupdate-service;1"]
            .getService(Ci.nsIOfflineCacheUpdateService);
@@ -267,16 +271,29 @@ this.DOMApplicationRegistry = {
         }, this);
       }).bind(this));
 
       // Nothing else to do but notifying we're ready.
       this.notifyAppsRegistryReady();
     }
   },
 
+  updateDataStoreForApp: function(aId) {
+    if (!this.webapps[aId]) {
+      return;
+    }
+
+    // Create or Update the DataStore for this app
+    this._readManifests([{ id: aId }], (function(aResult) {
+      this.updateDataStore(this.webapps[aId].localId,
+                           this.webapps[aId].manifestURL,
+                           aResult[0].manifest);
+    }).bind(this));
+  },
+
   updatePermissionsForApp: function updatePermissionsForApp(aId) {
     if (!this.webapps[aId]) {
       return;
     }
 
     // Install the permissions for this app, as if we were updating
     // to cleanup the old ones if needed.
     // TODO It's not clear what this should do when there are multiple profiles.
@@ -508,16 +525,17 @@ this.DOMApplicationRegistry = {
         // At first run, install preloaded apps and set up their permissions.
         for (let id in this.webapps) {
           this.installPreinstalledApp(id);
           if (!this.webapps[id]) {
             continue;
           }
           this.updateOfflineCacheForApp(id);
           this.updatePermissionsForApp(id);
+          this.updateDataStoreForApp(id);
         }
         // Need to update the persisted list of apps since
         // installPreinstalledApp() removes the ones failing to install.
         this._saveApps();
       }
       this.registerAppsHandlers(runUpdate);
     }).bind(this);
 
@@ -529,16 +547,30 @@ this.DOMApplicationRegistry = {
       else
         onAppsLoaded();
 #else
       onAppsLoaded();
 #endif
     }).bind(this));
   },
 
+  updateDataStore: function(aId, aManifestURL, aManifest) {
+    if (!("datastores" in aManifest)) {
+      return;
+    }
+
+    for (let name in aManifest.datastores) {
+      let readonly = ("readonly" in aManifest.datastores[name]) &&
+                     !aManifest.datastores[name].readonly
+                       ? false : true;
+
+      dataStoreService.installDataStore(aId, name, aManifestURL, readonly);
+    }
+  },
+
   // |aEntryPoint| is either the entry_point name or the null in which case we
   // use the root of the manifest.
   //
   // TODO Bug 908094 Refine _registerSystemMessagesForEntryPoint(...).
   _registerSystemMessagesForEntryPoint: function(aManifest, aApp, aEntryPoint) {
     let root = aManifest;
     if (aEntryPoint && aManifest.entry_points[aEntryPoint]) {
       root = aManifest.entry_points[aEntryPoint];
@@ -1387,17 +1419,17 @@ this.DOMApplicationRegistry = {
           this.updateAppHandlers(aOldManifest, aData, app);
           if (supportUseCurrentProfile()) {
             PermissionsInstaller.installPermissions(
               { manifest: aData,
                 origin: app.origin,
                 manifestURL: app.manifestURL },
               true);
           }
-
+          this.updateDataStore(this.webapps[id].localId, app.manifestURL, aData);
           this.broadcastMessage("Webapps:PackageEvent",
                                 { type: "applied",
                                   manifestURL: app.manifestURL,
                                   app: app,
                                   manifest: aData });
         }).bind(this));
       }).bind(this));
     }).bind(this));
@@ -1593,16 +1625,18 @@ this.DOMApplicationRegistry = {
           // Update the permissions for this app.
           PermissionsInstaller.installPermissions({
             manifest: app.manifest,
             origin: app.origin,
             manifestURL: aData.manifestURL
           }, true);
         }
 
+        this.updateDataStore(this.webapps[id].localId, app.manifestURL, app.manifest);
+
         app.name = manifest.name;
         app.csp = manifest.csp || "";
         app.role = manifest.role || "";
         app.updateTime = Date.now();
       } else {
         manifest = new ManifestHelper(aOldManifest, app.origin);
       }
 
@@ -2092,16 +2126,20 @@ this.DOMApplicationRegistry = {
 
           if (supportUseCurrentProfile()) {
             // Update the permissions for this app.
             PermissionsInstaller.installPermissions({ manifest: aManifest,
                                                       origin: appObject.origin,
                                                       manifestURL: appObject.manifestURL },
                                                     true);
           }
+
+          this.updateDataStore(this.webapps[aId].localId, appObject.manifestURL,
+                               aManifest);
+
           debug("About to fire Webapps:PackageEvent 'installed'");
           this.broadcastMessage("Webapps:PackageEvent",
                                 { type: "installed",
                                   manifestURL: appObject.manifestURL,
                                   app: app,
                                   manifest: aManifest });
           if (installSuccessCallback) {
             installSuccessCallback(aManifest, zipFile.path);
@@ -2194,16 +2232,19 @@ this.DOMApplicationRegistry = {
       if (supportUseCurrentProfile()) {
         PermissionsInstaller.installPermissions({ origin: appObject.origin,
                                                   manifestURL: appObject.manifestURL,
                                                   manifest: jsonManifest },
                                                 isReinstall, (function() {
           this.uninstall(aData, aData.mm);
         }).bind(this));
       }
+
+      this.updateDataStore(this.webapps[id].localId, this.webapps[id].manifestURL,
+                           jsonManifest);
     }
 
     ["installState", "downloadAvailable",
      "downloading", "downloadSize", "readyToApplyDownload"].forEach(function(aProp) {
       aData.app[aProp] = appObject[aProp];
      });
 
     if (manifest.appcache_path) {
--- a/dom/base/Navigator.cpp
+++ b/dom/base/Navigator.cpp
@@ -65,21 +65,23 @@
 #include "AudioChannelManager.h"
 #endif
 
 #ifdef MOZ_B2G_FM
 #include "mozilla/dom/FMRadio.h"
 #endif
 
 #include "nsIDOMGlobalPropertyInitializer.h"
+#include "nsIDataStoreService.h"
 #include "nsJSUtils.h"
 
 #include "nsScriptNameSpaceManager.h"
 
 #include "mozilla/dom/NavigatorBinding.h"
+#include "mozilla/dom/Promise.h"
 
 namespace mozilla {
 namespace dom {
 
 static bool sDoNotTrackEnabled = false;
 static bool sVibratorEnabled   = false;
 static uint32_t sMaxVibrateMS  = 0;
 static uint32_t sMaxVibrateListLen = 0;
@@ -1090,16 +1092,38 @@ Navigator::GetBattery(ErrorResult& aRv)
 
     mBatteryManager = new battery::BatteryManager();
     mBatteryManager->Init(mWindow);
   }
 
   return mBatteryManager;
 }
 
+already_AddRefed<Promise>
+Navigator::GetDataStores(const nsAString& aName, ErrorResult& aRv)
+{
+  if (!mWindow || !mWindow->GetDocShell()) {
+    aRv.Throw(NS_ERROR_UNEXPECTED);
+    return nullptr;
+  }
+
+  nsCOMPtr<nsIDataStoreService> service =
+    do_GetService("@mozilla.org/datastore-service;1");
+  if (!service) {
+    aRv.Throw(NS_ERROR_FAILURE);
+    return nullptr;
+  }
+
+  nsCOMPtr<nsISupports> promise;
+  aRv = service->GetDataStores(mWindow, aName, getter_AddRefs(promise));
+
+  nsRefPtr<Promise> p = static_cast<Promise*>(promise.get());
+  return p.forget();
+}
+
 PowerManager*
 Navigator::GetMozPower(ErrorResult& aRv)
 {
   if (!mPowerManager) {
     if (!mWindow) {
       aRv.Throw(NS_ERROR_UNEXPECTED);
       return nullptr;
     }
--- a/dom/base/Navigator.h
+++ b/dom/base/Navigator.h
@@ -51,16 +51,18 @@ namespace dom {
 namespace battery {
 class BatteryManager;
 } // namespace battery
 
 #ifdef MOZ_B2G_FM
 class FMRadio;
 #endif
 
+class Promise;
+
 class DesktopNotificationCenter;
 class MobileMessageManager;
 class MozIdleObserver;
 #ifdef MOZ_GAMEPAD
 class Gamepad;
 #endif // MOZ_GAMEPAD
 #ifdef MOZ_MEDIA_NAVIGATOR
 class NavigatorUserMediaSuccessCallback;
@@ -170,16 +172,18 @@ public:
                                const nsAString& aTitle, ErrorResult& aRv);
   void RegisterContentHandler(const nsAString& aMIMEType, const nsAString& aURL,
                               const nsAString& aTitle, ErrorResult& aRv);
   nsMimeTypeArray* GetMimeTypes(ErrorResult& aRv);
   nsPluginArray* GetPlugins(ErrorResult& aRv);
   // The XPCOM GetDoNotTrack is ok
   Geolocation* GetGeolocation(ErrorResult& aRv);
   battery::BatteryManager* GetBattery(ErrorResult& aRv);
+  already_AddRefed<Promise> GetDataStores(const nsAString &aName,
+                                          ErrorResult& aRv);
   bool Vibrate(uint32_t aDuration);
   bool Vibrate(const nsTArray<uint32_t>& aDuration);
   void GetAppCodeName(nsString& aAppCodeName, ErrorResult& aRv)
   {
     aRv = GetAppCodeName(aAppCodeName);
   }
   void GetOscpu(nsString& aOscpu, ErrorResult& aRv)
   {
new file mode 100644
--- /dev/null
+++ b/dom/datastore/DataStore.jsm
@@ -0,0 +1,63 @@
+/* -*- Mode: js2; js2-basic-offset: 2; indent-tabs-mode: nil; -*- */
+/* vim: set ft=javascript ts=2 et sw=2 tw=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'
+
+var EXPORTED_SYMBOLS = ["DataStore"];
+
+/* DataStore object */
+
+function DataStore(aAppId, aName, aOwner, aReadOnly) {
+  this.appId = aAppId;
+  this.name = aName;
+  this.owner = aOwner;
+  this.readOnly = aReadOnly;
+}
+
+DataStore.prototype = {
+  appId: null,
+  name: null,
+  owner: null,
+  readOnly: null,
+
+  exposeObject: function(aWindow) {
+    let self = this;
+    let chromeObject = {
+      get name() {
+        return self.name;
+      },
+
+      get owner() {
+        return self.owner;
+      },
+
+      get readOnly() {
+        return self.readOnly;
+      },
+
+      /* TODO:
+         Promise<Object> get(unsigned long id);
+         Promise<void> update(unsigned long id, any obj);
+         Promise<int> add(any obj)
+         Promise<boolean> remove(unsigned long id)
+         Promise<void> clear();
+
+         readonly attribute DOMString revisionId
+         attribute EventHandler onchange;
+         Promise<DataStoreChanges> getChanges(DOMString revisionId)
+         getAll(), getLength()
+       */
+
+      __exposedProps__: {
+        name: 'r',
+        owner: 'r',
+        readOnly: 'r'
+      }
+    };
+
+    return chromeObject;
+  }
+};
new file mode 100644
--- /dev/null
+++ b/dom/datastore/DataStore.manifest
@@ -0,0 +1,2 @@
+component {d193d0e2-c677-4a7b-bb0a-19155b470f2e} DataStoreService.js
+contract @mozilla.org/datastore-service;1 {d193d0e2-c677-4a7b-bb0a-19155b470f2e}
new file mode 100644
--- /dev/null
+++ b/dom/datastore/DataStoreService.js
@@ -0,0 +1,115 @@
+/* -*- Mode: js2; js2-basic-offset: 2; indent-tabs-mode: nil; -*- */
+/* vim: set ft=javascript ts=2 et sw=2 tw=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'
+
+/* static functions */
+
+let DEBUG = 0;
+let debug;
+if (DEBUG)
+  debug = function (s) { dump('DEBUG DataStore: ' + s + '\n'); }
+else
+  debug = function (s) {}
+
+const {classes: Cc, interfaces: Ci, utils: Cu, results: Cr} = Components;
+
+Cu.import('resource://gre/modules/XPCOMUtils.jsm');
+Cu.import('resource://gre/modules/Services.jsm');
+Cu.import('resource://gre/modules/DataStore.jsm');
+
+/* DataStoreService */
+
+const DATASTORESERVICE_CID = Components.ID('{d193d0e2-c677-4a7b-bb0a-19155b470f2e}');
+
+function DataStoreService() {
+  debug('DataStoreService Constructor');
+
+  let obs = Services.obs;
+  if (!obs) {
+    debug("DataStore Error: observer-service is null!");
+    return;
+  }
+
+  obs.addObserver(this, 'webapps-clear-data', false);
+}
+
+DataStoreService.prototype = {
+  // Hash of DataStores
+  stores: {},
+
+  installDataStore: function(aAppId, aName, aOwner, aReadOnly) {
+    debug('installDataStore - appId: ' + aAppId + ', aName: ' + aName +
+          ', aOwner:' + aOwner + ', aReadOnly: ' + aReadOnly);
+
+    if (aName in this.stores && aAppId in this.stores[aName]) {
+      debug('This should not happen');
+      return;
+    }
+
+    let store = new DataStore(aAppId, aName, aOwner, aReadOnly);
+
+    if (!(aName in this.stores)) {
+      this.stores[aName] = {};
+    }
+
+    this.stores[aName][aAppId] = store;
+  },
+
+  getDataStores: function(aWindow, aName) {
+    debug('getDataStores - aName: ' + aName);
+    let self = this;
+    return new aWindow.Promise(function(resolve, reject) {
+      let results = [];
+
+      if (aName in self.stores) {
+        for (let appId in self.stores[aName]) {
+          let obj = self.stores[aName][appId].exposeObject(aWindow);
+          results.push(obj);
+        }
+      }
+
+      resolve(results);
+    });
+  },
+
+  observe: function observe(aSubject, aTopic, aData) {
+    debug('getDataStores - aTopic: ' + aTopic);
+    if (aTopic != 'webapps-clear-data') {
+      return;
+    }
+
+    let params =
+      aSubject.QueryInterface(Ci.mozIApplicationClearPrivateDataParams);
+
+    // DataStore is explosed to apps, not browser content.
+    if (params.browserOnly) {
+      return;
+    }
+
+    for (let key in this.stores) {
+      if (params.appId in this.stores[key]) {
+        delete this.stores[key][params.appId];
+      }
+
+      if (!this.stores[key].length) {
+        delete this.stores[key];
+      }
+    }
+  },
+
+  classID : DATASTORESERVICE_CID,
+  QueryInterface: XPCOMUtils.generateQI([Ci.nsIDataStoreService,
+                                         Ci.nsIObserver]),
+  classInfo: XPCOMUtils.generateCI({
+    classID: DATASTORESERVICE_CID,
+    contractID: '@mozilla.org/datastore-service;1',
+    interfaces: [Ci.nsIDataStoreService, Ci.nsIObserver],
+    flags: Ci.nsIClassInfo.SINGLETON
+  })
+};
+
+this.NSGetFactory = XPCOMUtils.generateNSGetFactory([DataStoreService]);
new file mode 100644
--- /dev/null
+++ b/dom/datastore/moz.build
@@ -0,0 +1,24 @@
+# -*- Mode: python; c-basic-offset: 4; indent-tabs-mode: nil; tab-width: 40 -*-
+# vim: set filetype=python:
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+TEST_DIRS += ['tests']
+
+XPIDL_SOURCES += [
+    'nsIDataStoreService.idl',
+]
+
+XPIDL_MODULE = 'dom_datastore'
+
+MODULE = 'dom'
+
+EXTRA_COMPONENTS += [
+    'DataStore.manifest',
+    'DataStoreService.js',
+]
+
+EXTRA_JS_MODULES += [
+    'DataStore.jsm',
+]
new file mode 100644
--- /dev/null
+++ b/dom/datastore/nsIDataStoreService.idl
@@ -0,0 +1,20 @@
+/* -*- Mode: C++; 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/. */
+
+#include "nsISupports.idl"
+
+interface nsIDOMWindow;
+
+[scriptable, uuid(d193d0e2-c677-4a7b-bb0a-19155b470f2e)]
+interface nsIDataStoreService : nsISupports
+{
+  void installDataStore(in unsigned long appId,
+                        in DOMString name,
+                        in DOMString manifestURL,
+                        in boolean readOnly);
+
+  nsISupports getDataStores(in nsIDOMWindow window,
+                            in DOMString name);
+};
new file mode 100644
--- /dev/null
+++ b/dom/datastore/tests/Makefile.in
@@ -0,0 +1,20 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this file,
+# You can obtain one at http://mozilla.org/MPL/2.0/.
+
+DEPTH            = @DEPTH@
+topsrcdir        = @top_srcdir@
+srcdir           = @srcdir@
+VPATH            = @srcdir@
+
+relativesrcdir   = @relativesrcdir@
+
+include $(DEPTH)/config/autoconf.mk
+
+MOCHITEST_FILES = \
+  test_app_install.html \
+  file_app.sjs \
+  file_app.template.webapp \
+  $(NULL)
+
+include $(topsrcdir)/config/rules.mk
copy from dom/apps/tests/file_app.sjs
copy to dom/datastore/tests/file_app.sjs
--- a/dom/apps/tests/file_app.sjs
+++ b/dom/datastore/tests/file_app.sjs
@@ -1,114 +1,15 @@
-var gBasePath = "tests/dom/apps/tests/";
-var gAppTemplatePath = "tests/dom/apps/tests/file_app.template.html";
-var gAppcacheTemplatePath = "tests/dom/apps/tests/file_cached_app.template.appcache";
-var gDefaultIcon = "default_icon";
-
-function makeResource(templatePath, version, apptype) {
-  let icon = getState('icon') || gDefaultIcon;
-  var res = readTemplate(templatePath).replace(/VERSIONTOKEN/g, version)
-                                      .replace(/APPTYPETOKEN/g, apptype)
-                                      .replace(/ICONTOKEN/g, icon);
-
-  // Hack - This is necessary to make the tests pass, but hbambas says it
-  // shouldn't be necessary. Comment it out and watch the tests fail.
-  if (templatePath == gAppTemplatePath && apptype == 'cached') {
-    res = res.replace('<html>', '<html manifest="file_app.sjs?apptype=cached&getappcache=true">');
-  }
-
-  return res;
-}
+var gBasePath = "tests/dom/datastore/tests/";
+var gAppTemplatePath = "tests/dom/datastore/tests/file_app.template.webapp";
 
 function handleRequest(request, response) {
-  var query = getQuery(request);
-
-  // If this is a version update, update state and return.
-  if ("setVersion" in query) {
-    setState('version', query.setVersion);
-    response.setHeader("Content-Type", "text/html", false);
-    response.setHeader("Access-Control-Allow-Origin", "*", false);
-    response.write('OK');
-    return;
-  }
-
-  if ("setIcon" in query) {
-    let icon = query.setIcon;
-    if (icon === 'DEFAULT') {
-      icon = null;
-    }
-
-    setState('icon', icon);
-
-    response.setHeader("Content-Type", "text/html", false);
-    response.setHeader("Access-Control-Allow-Origin", "*", false);
-    response.write('OK');
-    return;
-  }
-
-  // Get the app type.
-  var apptype = query.apptype;
-  if (apptype != 'hosted' && apptype != 'cached')
-    throw "Invalid app type: " + apptype;
-
-  // Get the version from server state and handle the etag.
-  var version = Number(getState('version'));
-  var etag = getEtag(request, version);
-  dump("Server Etag: " + etag + "\n");
-
-  if (etagMatches(request, etag)) {
-    dump("Etags Match. Sending 304\n");
-    response.setStatusLine(request.httpVersion, "304", "Not Modified");
-    return;
-  }
-
-  response.setHeader("Etag", etag, false);
-  if (request.hasHeader("If-None-Match"))
-    dump("Client Etag: " + request.getHeader("If-None-Match") + "\n");
-  else
-    dump("No Client Etag\n");
-
-  // Check if we're generating a webapp manifest.
-  if ('getmanifest' in query) {
-    var template = gBasePath + 'file_' + apptype + '_app.template.webapp';
-    response.setHeader("Content-Type", "application/x-web-app-manifest+json", false);
-    response.write(makeResource(template, version, apptype));
-    return;
-  }
-
-  // If apptype==cached, we might be generating the appcache manifest.
-  //
-  // NB: Among other reasons, we use the same sjs file here so that the version
-  //     state is shared.
-  if (apptype == 'cached' && 'getappcache' in query) {
-    response.setHeader("Content-Type", "text/cache-manifest", false);
-    response.write(makeResource(gAppcacheTemplatePath, version, apptype));
-    return;
-  }
-
-  // Generate the app.
-  response.setHeader("Content-Type", "text/html", false);
-  response.write(makeResource(gAppTemplatePath, version, apptype));
-}
-
-function getEtag(request, version) {
-  return request.queryString.replace(/&/g, '-').replace(/=/g, '-') + '-' + version;
-}
-
-function etagMatches(request, etag) {
-  return request.hasHeader("If-None-Match") && request.getHeader("If-None-Match") == etag;
-}
-
-function getQuery(request) {
-  var query = {};
-  request.queryString.split('&').forEach(function (val) {
-    var [name, value] = val.split('=');
-    query[name] = unescape(value);
-  });
-  return query;
+  var template = gBasePath + 'file_app.template.webapp';
+  response.setHeader("Content-Type", "application/x-web-app-manifest+json", false);
+  response.write(readTemplate(template));
 }
 
 // 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);
copy from dom/apps/tests/file_hosted_app.template.webapp
copy to dom/datastore/tests/file_app.template.webapp
--- a/dom/apps/tests/file_hosted_app.template.webapp
+++ b/dom/datastore/tests/file_app.template.webapp
@@ -1,8 +1,10 @@
 {
   "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/apps/tests/file_app.sjs?apptype=hosted",
-  "icons": {
-    "128": "ICONTOKEN"
+  "launch_path": "/tests/dom/datastore/tests/file_app.sjs",
+  "icons": { "128": "default_icon" },
+  "datastores" : {
+    "foo" : { "readonly": false, "description" : "This store is called foo" },
+    "bar" : { "readonly": true, "description" : "This store is called bar" }
   }
 }
new file mode 100644
--- /dev/null
+++ b/dom/datastore/tests/moz.build
@@ -0,0 +1,5 @@
+# -*- 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/.
new file mode 100644
--- /dev/null
+++ b/dom/datastore/tests/test_app_install.html
@@ -0,0 +1,105 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+  <meta charset="utf-8">
+  <title>Test for DataStore - install/uninstall apps</title>
+  <script type="application/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+  <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+  <script type="application/javascript;version=1.7">
+
+  SimpleTest.waitForExplicitFinish();
+
+  var gBaseURL = 'http://test/tests/dom/datastore/tests/';
+  var gHostedManifestURL = gBaseURL + 'file_app.sjs';
+  var gGenerator = runTest();
+
+  function go() {
+    SpecialPowers.pushPermissions(
+      [{ "type": "browser", "allow": 1, "context": document },
+       { "type": "embed-apps", "allow": 1, "context": document },
+       { "type": "webapps-manage", "allow": 1, "context": document }],
+      function() { gGenerator.next() });
+  }
+
+  function continueTest() {
+    gGenerator.next();
+  }
+
+  function cbError() {
+    ok(false, "Error callback invoked");
+    finish();
+  }
+
+  function runTest() {
+    ok("getDataStores" in navigator, "getDataStores exists");
+    is(typeof navigator.getDataStores, "function", "getDataStores exists and it's a function");
+    navigator.getDataStores('foo').then(function(stores) {
+      is(stores.length, 0, "getDataStores('foo') returns 0 elements");
+      continueTest();
+    }, cbError);
+    yield undefined;
+
+    SpecialPowers.autoConfirmAppInstall(continueTest);
+    yield undefined;
+
+    var request = navigator.mozApps.install(gHostedManifestURL);
+    request.onerror = cbError;
+    request.onsuccess = continueTest;
+    yield undefined;
+
+    var app = request.result;
+    isnot(app, null, "App is non-null");
+    is(app.manifest.description, "Updated even faster than Firefox, just to annoy slashdotters.",
+       "Manifest is HTML-sanitized");
+
+    navigator.getDataStores('foo').then(function(stores) {
+      is(stores.length, 1, "getDataStores('foo') returns 1 element");
+      is(stores[0].name, 'foo', 'The dataStore.name is foo');
+      is(stores[0].owner, 'http://test/tests/dom/datastore/tests/file_app.sjs', 'The dataStore.owner exists');
+      is(stores[0].readOnly, false, 'The dataStore foo is not in readonly');
+      continueTest();
+    }, cbError);
+    yield undefined;
+
+    navigator.getDataStores('bar').then(function(stores) {
+      is(stores.length, 1, "getDataStores('bar') returns 1 element");
+      is(stores[0].name, 'bar', 'The dataStore.name is bar');
+      is(stores[0].owner, 'http://test/tests/dom/datastore/tests/file_app.sjs', 'The dataStore.owner exists');
+      is(stores[0].readOnly, true, 'The dataStore bar is in readonly');
+      continueTest();
+    }, cbError);
+    yield undefined;
+
+    // Uninstall the app.
+    request = navigator.mozApps.mgmt.uninstall(app);
+    request.onerror = cbError;
+    request.onsuccess = continueTest;
+    yield undefined;
+
+    navigator.getDataStores('foo').then(function(stores) {
+      is(stores.length, 0, "getDataStores('foo') returns 0 elements");
+      continueTest();
+    }, cbError);
+    yield undefined;
+
+    // All done.
+    info("All done");
+    finish();
+  }
+
+  function finish() {
+    SimpleTest.finish();
+  }
+
+  </script>
+</head>
+<body onload="go()">
+<p id="display"></p>
+<div id="content" style="display: none">
+
+</div>
+<pre id="test">
+</pre>
+<div id="container"></div>
+</body>
+</html>
--- a/dom/moz.build
+++ b/dom/moz.build
@@ -41,16 +41,17 @@ PARALLEL_DIRS += [
     'activities',
     'bindings',
     'battery',
     'bluetooth',
     'browser-element',
     'contacts',
     'phonenumberutils',
     'alarm',
+    'datastore',
     'devicestorage',
     'encoding',
     'file',
     'fmradio',
     'media',
     'messages',
     'power',
     'push',
--- a/dom/webidl/Navigator.webidl
+++ b/dom/webidl/Navigator.webidl
@@ -103,16 +103,24 @@ Navigator implements NavigatorGeolocatio
 interface NavigatorBattery {
     // XXXbz Per spec this should be non-nullable, but we return null in
     // torn-down windows.  See bug 884925.
     [Throws, Func="Navigator::HasBatterySupport"]
     readonly attribute BatteryManager? battery;
 };
 Navigator implements NavigatorBattery;
 
+// https://wiki.mozilla.org/WebAPI/DataStore
+[NoInterfaceObject]
+interface NavigatorDataStore {
+    [Throws, Creator, Pref="dom.datastore.enabled"]
+    Promise getDataStores(DOMString name);
+};
+Navigator implements NavigatorDataStore;
+
 // http://www.w3.org/TR/vibration/#vibration-interface
 partial interface Navigator {
     // We don't support sequences in unions yet
     //boolean vibrate ((unsigned long or sequence<unsigned long>) pattern);
     boolean vibrate(unsigned long duration);
     boolean vibrate(sequence<unsigned long> pattern);
 };
 
--- a/mobile/android/installer/package-manifest.in
+++ b/mobile/android/installer/package-manifest.in
@@ -586,8 +586,12 @@ bin/components/@DLL_PREFIX@nkgnomevfs@DL
 @BINPATH@/components/browsercomps.xpt
 
 #ifdef ENABLE_MARIONETTE
 @BINPATH@/chrome/marionette@JAREXT@ 
 @BINPATH@/chrome/marionette.manifest
 @BINPATH@/components/MarionetteComponents.manifest
 @BINPATH@/components/marionettecomponent.js
 #endif
+
+@BINPATH@/components/DataStore.manifest
+@BINPATH@/components/DataStoreService.js
+@BINPATH@/components/dom_datastore.xpt
--- a/modules/libpref/src/init/all.js
+++ b/modules/libpref/src/init/all.js
@@ -4429,16 +4429,22 @@ pref("captivedetect.maxRetryCount", 5);
 pref("dom.forms.inputmode", false);
 #else
 pref("dom.forms.inputmode", true);
 #endif
 
 // InputMethods for soft keyboards in B2G
 pref("dom.mozInputMethod.enabled", false);
 
+#ifdef RELEASE_BUILD
+pref("dom.datastore.enabled", false);
+#else
+pref("dom.datastore.enabled", true);
+#endif
+
 // Telephony API
 pref("dom.telephony.enabled", false);
 
 // DOM Inter-App Communication API.
 pref("dom.inter-app-communication-api.enabled", false);
 
 // The tables used for Safebrowsing phishing and malware checks.
 pref("urlclassifier.malware_table", "goog-malware-shavar");