Merge b2g-inbound to Mozilla-Central
authorCarsten "Tomcat" Book <cbook@mozilla.com>
Thu, 24 Oct 2013 06:50:15 +0200
changeset 166603 ea2c865af416e353db131bbfc0a898af7f571af1
parent 166581 4ceaf2926e5bb78116055ce1b66d88ff41295330 (current diff)
parent 166602 845f75c5a16cb2e1e47cd887d94ef15bcb076d0c (diff)
child 166684 19fd3388c3729fbc8dd4a52376c964aa495a2a25
push id428
push userbbajaj@mozilla.com
push dateTue, 28 Jan 2014 00:16:25 +0000
treeherdermozilla-release@cd72a7ff3a75 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
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
Merge b2g-inbound to Mozilla-Central
configure.in
dom/tests/mochitest/notification/create_notification.html
dom/tests/mochitest/notification/notification_common.js
dom/tests/mochitest/notification/test_basic_notification.html
dom/tests/mochitest/notification/test_basic_notification_click.html
dom/tests/mochitest/notification/test_leak_windowClose.html
dom/tests/mochitest/notification/test_notification_tag.html
dom/tests/mochitest/notification/test_system_principal.xul
dom/tests/mochitest/notification/test_web_notifications.html
netwerk/protocol/app/AppProtocolHandler.js
netwerk/protocol/app/AppProtocolHandler.manifest
--- 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 918207 needed a clobber on every platform because we can't have nice things
+Bug 899574 needed a clobber on every platform because we can't have nice things
--- a/b2g/chrome/content/shell.js
+++ b/b2g/chrome/content/shell.js
@@ -6,16 +6,17 @@
 
 Cu.import('resource://gre/modules/ContactService.jsm');
 Cu.import('resource://gre/modules/SettingsChangeNotifier.jsm');
 Cu.import('resource://gre/modules/DataStoreChangeNotifier.jsm');
 Cu.import('resource://gre/modules/AlarmService.jsm');
 Cu.import('resource://gre/modules/ActivitiesService.jsm');
 Cu.import('resource://gre/modules/PermissionPromptHelper.jsm');
 Cu.import('resource://gre/modules/ObjectWrapper.jsm');
+Cu.import('resource://gre/modules/NotificationDB.jsm');
 Cu.import('resource://gre/modules/accessibility/AccessFu.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');
 #ifdef MOZ_B2G_RIL
 Cu.import('resource://gre/modules/NetworkStatsService.jsm');
--- a/b2g/config/gaia.json
+++ b/b2g/config/gaia.json
@@ -1,4 +1,4 @@
 {
-    "revision": "a57a913f1dd723afa191124f27b8d9fc4b0cb1c0", 
+    "revision": "20e3f42ccb6073c6d9bc9741de3a19a939a8a7d8", 
     "repo_path": "/integration/gaia-central"
 }
--- a/b2g/installer/package-manifest.in
+++ b/b2g/installer/package-manifest.in
@@ -347,16 +347,18 @@
 @BINPATH@/components/ConsoleAPI.manifest
 @BINPATH@/components/ConsoleAPI.js
 @BINPATH@/components/BrowserElementParent.manifest
 @BINPATH@/components/BrowserElementParent.js
 @BINPATH@/components/ContactManager.js
 @BINPATH@/components/ContactManager.manifest
 @BINPATH@/components/PhoneNumberService.js
 @BINPATH@/components/PhoneNumberService.manifest
+@BINPATH@/components/NotificationStorage.js
+@BINPATH@/components/NotificationStorage.manifest
 @BINPATH@/components/PermissionSettings.js
 @BINPATH@/components/PermissionSettings.manifest
 @BINPATH@/components/PermissionPromptService.js
 @BINPATH@/components/PermissionPromptService.manifest
 @BINPATH@/components/AlarmsManager.js
 @BINPATH@/components/AlarmsManager.manifest
 @BINPATH@/components/FeedProcessor.manifest
 @BINPATH@/components/FeedProcessor.js
@@ -538,19 +540,16 @@
 @BINPATH@/components/ActivityWrapper.js
 @BINPATH@/components/ActivityMessageConfigurator.js
 
 @BINPATH@/components/TCPSocket.js
 @BINPATH@/components/TCPServerSocket.js
 @BINPATH@/components/TCPSocketParentIntermediary.js
 @BINPATH@/components/TCPSocket.manifest
 
-@BINPATH@/components/AppProtocolHandler.js
-@BINPATH@/components/AppProtocolHandler.manifest
-
 @BINPATH@/components/Payment.js
 @BINPATH@/components/PaymentFlowInfo.js
 @BINPATH@/components/PaymentRequestInfo.js
 @BINPATH@/components/Payment.manifest
 
 ; InputMethod API
 @BINPATH@/components/MozKeyboard.js
 @BINPATH@/components/InputMethod.manifest
--- a/browser/base/content/browser.js
+++ b/browser/base/content/browser.js
@@ -2,16 +2,17 @@
 # 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/.
 
 let Ci = Components.interfaces;
 let Cu = Components.utils;
 
 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+Cu.import("resource://gre/modules/NotificationDB.jsm");
 Cu.import("resource:///modules/RecentWindow.jsm");
 
 XPCOMUtils.defineLazyModuleGetter(this, "Task",
                                   "resource://gre/modules/Task.jsm");
 
 const nsIWebNavigation = Ci.nsIWebNavigation;
 
 var gCharsetMenu = null;
--- a/browser/installer/package-manifest.in
+++ b/browser/installer/package-manifest.in
@@ -504,29 +504,28 @@
 @BINPATH@/components/recording-cmdline.manifest
 
 @BINPATH@/components/PermissionSettings.js
 @BINPATH@/components/PermissionSettings.manifest
 @BINPATH@/components/ContactManager.js
 @BINPATH@/components/ContactManager.manifest
 @BINPATH@/components/PhoneNumberService.js
 @BINPATH@/components/PhoneNumberService.manifest
+@BINPATH@/components/NotificationStorage.js
+@BINPATH@/components/NotificationStorage.manifest
 @BINPATH@/components/AlarmsManager.js
 @BINPATH@/components/AlarmsManager.manifest
 @BINPATH@/components/Push.js
 @BINPATH@/components/Push.manifest
 @BINPATH@/components/PushServiceLauncher.js
 @BINPATH@/components/TCPSocket.js
 @BINPATH@/components/TCPServerSocket.js
 @BINPATH@/components/TCPSocketParentIntermediary.js
 @BINPATH@/components/TCPSocket.manifest
 
-@BINPATH@/components/AppProtocolHandler.js
-@BINPATH@/components/AppProtocolHandler.manifest
-
 @BINPATH@/components/Payment.js
 @BINPATH@/components/PaymentFlowInfo.js
 @BINPATH@/components/PaymentRequestInfo.js
 @BINPATH@/components/Payment.manifest
 
 #ifdef MOZ_WEBRTC
 @BINPATH@/components/PeerConnection.js
 @BINPATH@/components/PeerConnection.manifest
--- a/browser/metro/profile/metro.js
+++ b/browser/metro/profile/metro.js
@@ -31,25 +31,27 @@ pref("metro.debug.selection.dumpEvents",
 // Enable tab-modal prompts
 pref("prompts.tab_modal.enabled", true);
 
 
 // Enable off main thread compositing
 pref("layers.offmainthreadcomposition.enabled", true);
 pref("layers.async-pan-zoom.enabled", true);
 pref("layers.componentalpha.enabled", false);
-pref("gfx.azpc.touch_start_tolerance", "0.1"); // dpi * tolerance = pixel threshold
-pref("gfx.azpc.pan_repaint_interval", "50");   // prefer 20 fps
-pref("gfx.azpc.fling_repaint_interval", "50"); // prefer 20 fps
-pref("gfx.axis.fling_friction", "0.002");
-pref("gfx.axis.fling_stopped_threshold", "0.2");
+
+// Prefs to control the async pan/zoom behaviour
+pref("apz.touch_start_tolerance", "0.1"); // dpi * tolerance = pixel threshold
+pref("apz.pan_repaint_interval", "50");   // prefer 20 fps
+pref("apz.fling_repaint_interval", "50"); // prefer 20 fps
+pref("apz.fling_friction", "0.002");
+pref("apz.fling_stopped_threshold", "0.2");
 
 // 0 = free, 1 = standard, 2 = sticky
-pref("apzc.axis_lock_mode", 2);
-pref("apzc.cross_slide.enabled", true);
+pref("apz.axis_lock_mode", 2);
+pref("apz.cross_slide.enabled", true);
 
 // Enable Microsoft TSF support by default for imes.
 pref("intl.enable_tsf_support", true);
 
 pref("general.autoScroll", true);
 pref("general.smoothScroll", true);
 pref("general.smoothScroll.durationToIntervalRatio", 200);
 pref("mousewheel.enable_pixel_scrolling", true);
--- a/configure.in
+++ b/configure.in
@@ -3981,17 +3981,17 @@ MOZ_UNIVERSALCHARDET=1
 MOZ_URL_CLASSIFIER=
 MOZ_XUL=1
 MOZ_ZIPWRITER=1
 NS_PRINTING=1
 MOZ_PDF_PRINTING=
 MOZ_DISABLE_CRYPTOLEGACY=
 NSS_DISABLE_DBM=
 NECKO_COOKIES=1
-NECKO_PROTOCOLS_DEFAULT="about data file ftp http res viewsource websocket wyciwyg device"
+NECKO_PROTOCOLS_DEFAULT="about app data file ftp http res viewsource websocket wyciwyg device"
 USE_ARM_KUSER=
 BUILD_CTYPES=1
 MOZ_USE_NATIVE_POPUP_WINDOWS=
 MOZ_ANDROID_HISTORY=
 MOZ_WEBSMS_BACKEND=
 MOZ_ANDROID_BEAM=
 ACCESSIBILITY=1
 MOZ_TIME_MANAGER=
--- a/dom/apps/src/OperatorApps.jsm
+++ b/dom/apps/src/OperatorApps.jsm
@@ -161,17 +161,18 @@ this.OperatorAppsRegistry = {
       app: {
         installOrigin: aMetadata.installOrigin,
         origin: aMetadata.origin,
         manifestURL: aMetadata.manifestURL,
         manifestHash: AppsUtils.computeHash(JSON.stringify(aManifest))
       },
       appId: undefined,
       isBrowser: false,
-      isPackage: isPackage
+      isPackage: isPackage,
+      forceSuccessAck: true
     };
 
     if (isPackage) {
       debug("aId:" + aId + ". Installing as packaged app.");
       let installPack = OS.Path.join(this.appsDir.path, aId, APPLICATION_ZIP);
       OS.File.exists(installPack).then(
         function(aExists) {
           if (!aExists) {
--- a/dom/apps/src/Webapps.jsm
+++ b/dom/apps/src/Webapps.jsm
@@ -2150,16 +2150,30 @@ this.DOMApplicationRegistry = {
 
   // This function is called after we called the onsuccess callback on the
   // content side. This let the webpage the opportunity to set event handlers
   // on the app before we start firing progress events.
   queuedDownload: {},
   queuedPackageDownload: {},
 
   onInstallSuccessAck: function onInstallSuccessAck(aManifestURL) {
+    // If we are offline, register to run when we'll be online.
+    if (Services.io.offline) {
+      let onlineWrapper = {
+        observe: function(aSubject, aTopic, aData) {
+          Services.obs.removeObserver(onlineWrapper,
+                                      "network:offline-status-changed");
+          DOMApplicationRegistry.onInstallSuccessAck(aManifestURL);
+        }
+      }
+      Services.obs.addObserver(onlineWrapper,
+                               "network:offline-status-changed", false);
+      return;
+    }
+
     let cacheDownload = this.queuedDownload[aManifestURL];
     if (cacheDownload) {
       this.startOfflineCacheDownload(cacheDownload.manifest,
                                      cacheDownload.app,
                                      cacheDownload.profileDir);
       delete this.queuedDownload[aManifestURL];
 
       return;
@@ -2366,22 +2380,22 @@ this.DOMApplicationRegistry = {
       // can't be used to resolve package paths.
       manifest = new ManifestHelper(jsonManifest, app.manifestURL);
 
       this.queuedPackageDownload[app.manifestURL] = {
         manifest: manifest,
         app: appObject,
         callback: aInstallSuccessCallback
       };
-
-      if (aData.app.localInstallPath) {
-        // if it's a local install, there's no content process so just
-        // ack the install
-        this.onInstallSuccessAck(app.manifestURL);
-      }
+    }
+
+    if (aData.forceSuccessAck) {
+      // If it's a local install, there's no content process so just
+      // ack the install.
+      this.onInstallSuccessAck(app.manifestURL);
     }
   },
 
   _nextLocalId: function() {
     let id = Services.prefs.getIntPref("dom.mozApps.maxLocalId") + 1;
 
     while (this.getManifestURLByLocalId(id)) {
       id++;
--- a/dom/devicestorage/nsDeviceStorage.cpp
+++ b/dom/devicestorage/nsDeviceStorage.cpp
@@ -757,20 +757,26 @@ DeviceStorageFile::GetRootDirectoryForTy
 #else
      f = sDirs->sdcard;
 #endif
   }
 
   // crash reports directory.
   else if (aStorageType.EqualsLiteral(DEVICESTORAGE_CRASHES)) {
     f = sDirs->crashes;
+  } else {
+    // Not a storage type that we recognize. Return null
+    return;
   }
 
-  // in testing, we default all device storage types to a temp directory
-  if (f && sDirs->temp) {
+  // In testing, we default all device storage types to a temp directory.
+  // sDirs->temp will only have been initialized (in InitDirs) if the
+  // preference device.storage.testing was set to true. We can't test the
+  // preference directly here, since we may not be on the main thread.
+  if (sDirs->temp) {
     f = sDirs->temp;
   }
 
   if (f) {
     f->Clone(aFile);
   }
 }
 
--- a/dom/interfaces/notification/moz.build
+++ b/dom/interfaces/notification/moz.build
@@ -1,14 +1,15 @@
 # -*- 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/.
 
 XPIDL_SOURCES += [
     'nsIDOMDesktopNotification.idl',
+    'nsINotificationStorage.idl',
 ]
 
 XPIDL_MODULE = 'dom_notification'
 
 MODULE = 'dom'
 
new file mode 100644
--- /dev/null
+++ b/dom/interfaces/notification/nsINotificationStorage.idl
@@ -0,0 +1,92 @@
+/* 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 "domstubs.idl"
+
+[scriptable, uuid(fb089720-1c5c-11e3-b773-0800200c9a66)]
+interface nsINotificationStorageCallback : nsISupports
+{
+  /**
+   * Callback function used to pass single notification back
+   * into C++ land for Notification.get return data.
+   *
+   * @param id: a uuid for this notification
+   * @param title: the notification title
+   * @param dir: the notification direction,
+   *             possible values are "ltr", "rtl", "auto"
+   * @param lang: the notification language
+   * @param body: the notification body
+   * @param tag: the notification tag
+   */
+  [implicit_jscontext]
+  void handle(in DOMString id,
+              in DOMString title,
+              in DOMString dir,
+              in DOMString lang,
+              in DOMString body,
+              in DOMString tag,
+              in DOMString icon);
+
+  /**
+   * Callback function used to notify C++ the we have returned
+   * all notification objects for this Notification.get call.
+   */
+  [implicit_jscontext]
+  void done();
+};
+
+/**
+ * Interface for notification persistence layer.
+ */
+[scriptable, uuid(b177b080-2a23-11e3-8224-0800200c9a66)]
+interface nsINotificationStorage : nsISupports
+{
+
+  /**
+   * Add/replace a notification to the persistence layer.
+   *
+   * @param origin: the origin/app of this notification
+   * @param id: a uuid for this notification
+   * @param title: the notification title
+   * @param dir: the notification direction,
+   *             possible values are "ltr", "rtl", "auto"
+   * @param lang: the notification language
+   * @param body: the notification body
+   * @param tag: notification tag, will replace any existing
+   *             notifications with same origin/tag pair
+   */
+  void put(in DOMString origin,
+           in DOMString id,
+           in DOMString title,
+           in DOMString dir,
+           in DOMString lang,
+           in DOMString body,
+           in DOMString tag,
+           in DOMString icon);
+
+  /**
+   * Retrieve a list of notifications.
+   *
+   * @param origin: the origin/app for which to fetch notifications from
+   * @param tag: used to fetch only a specific tag
+   * @param callback: nsINotificationStorageCallback, used for
+   *                  returning notifications objects
+   */
+  void get(in DOMString origin,
+           in DOMString tag,
+           in nsINotificationStorageCallback aCallback);
+
+  /**
+   * Remove a notification from storage.
+   *
+   * @param origin: the origin/app to delete the notification from
+   * @param id: the uuid for the notification to delete
+   */
+  void delete(in DOMString origin,
+              in DOMString id);
+};
+
+%{C++
+#define NS_NOTIFICATION_STORAGE_CONTRACTID "@mozilla.org/notificationStorage;1"
+%}
--- a/dom/promise/Promise.cpp
+++ b/dom/promise/Promise.cpp
@@ -184,16 +184,30 @@ Promise::EnabledForScope(JSContext* aCx,
     return workers::GetWorkerPrivateFromContext(aCx)->IsChromeWorker();
   }
 
   nsIPrincipal* prin = nsContentUtils::GetSubjectPrincipal();
   return nsContentUtils::IsSystemPrincipal(prin) ||
     prin->GetAppStatus() == nsIPrincipal::APP_STATUS_CERTIFIED;
 }
 
+void
+Promise::MaybeResolve(JSContext* aCx,
+                      const Optional<JS::Handle<JS::Value> >& aValue)
+{
+  MaybeResolveInternal(aCx, aValue);
+}
+
+void
+Promise::MaybeReject(JSContext* aCx,
+                     const Optional<JS::Handle<JS::Value> >& aValue)
+{
+  MaybeRejectInternal(aCx, aValue);
+}
+
 static void
 EnterCompartment(Maybe<JSAutoCompartment>& aAc, JSContext* aCx,
                  const Optional<JS::Handle<JS::Value> >& aValue)
 {
   // FIXME Bug 878849
   if (aValue.WasPassed() && aValue.Value().isObject()) {
     JS::Rooted<JSObject*> rooted(aCx, &aValue.Value().toObject());
     aAc.construct(aCx, rooted);
@@ -224,19 +238,19 @@ Promise::JSCallback(JSContext *aCx, unsi
   if (aArgc) {
     value.Value() = args[0];
   }
 
   v = js::GetFunctionNativeReserved(&args.callee(), SLOT_TASK);
   PromiseCallback::Task task = static_cast<PromiseCallback::Task>(v.toInt32());
 
   if (task == PromiseCallback::Resolve) {
-    promise->MaybeResolve(aCx, value);
+    promise->MaybeResolveInternal(aCx, value);
   } else {
-    promise->MaybeReject(aCx, value);
+    promise->MaybeRejectInternal(aCx, value);
   }
 
   return true;
 }
 
 /* static */ JSObject*
 Promise::CreateFunction(JSContext* aCx, JSObject* aParent, Promise* aPromise,
                         int32_t aTask)
@@ -295,17 +309,17 @@ Promise::Constructor(const GlobalObject&
   aRv.WouldReportJSException();
 
   if (aRv.IsJSException()) {
     Optional<JS::Handle<JS::Value> > value(cx);
     aRv.StealJSException(cx, &value.Value());
 
     Maybe<JSAutoCompartment> ac;
     EnterCompartment(ac, cx, value);
-    promise->MaybeReject(cx, value);
+    promise->MaybeRejectInternal(cx, value);
   }
 
   return promise.forget();
 }
 
 /* static */ already_AddRefed<Promise>
 Promise::Resolve(const GlobalObject& aGlobal, JSContext* aCx,
                  JS::Handle<JS::Value> aValue, ErrorResult& aRv)
@@ -314,34 +328,34 @@ Promise::Resolve(const GlobalObject& aGl
   if (!window) {
     aRv.Throw(NS_ERROR_UNEXPECTED);
     return nullptr;
   }
 
   nsRefPtr<Promise> promise = new Promise(window);
 
   Optional<JS::Handle<JS::Value> > value(aCx, aValue);
-  promise->MaybeResolve(aCx, value);
+  promise->MaybeResolveInternal(aCx, value);
   return promise.forget();
 }
 
 /* static */ already_AddRefed<Promise>
 Promise::Reject(const GlobalObject& aGlobal, JSContext* aCx,
                 JS::Handle<JS::Value> aValue, ErrorResult& aRv)
 {
   nsCOMPtr<nsPIDOMWindow> window = do_QueryInterface(aGlobal.GetAsSupports());
   if (!window) {
     aRv.Throw(NS_ERROR_UNEXPECTED);
     return nullptr;
   }
 
   nsRefPtr<Promise> promise = new Promise(window);
 
   Optional<JS::Handle<JS::Value> > value(aCx, aValue);
-  promise->MaybeReject(aCx, value);
+  promise->MaybeRejectInternal(aCx, value);
   return promise.forget();
 }
 
 already_AddRefed<Promise>
 Promise::Then(const Optional<OwningNonNull<AnyCallback> >& aResolveCallback,
               const Optional<OwningNonNull<AnyCallback> >& aRejectCallback)
 {
   nsRefPtr<Promise> promise = new Promise(GetParentObject());
@@ -436,31 +450,31 @@ Promise::MaybeReportRejected()
     new AsyncErrorReporter(JS_GetObjectRuntime(&mResult.toObject()),
                            report,
                            nullptr,
                            nsContentUtils::GetObjectPrincipal(&mResult.toObject()),
                            win));
 }
 
 void
-Promise::MaybeResolve(JSContext* aCx,
-                      const Optional<JS::Handle<JS::Value> >& aValue,
-                      PromiseTaskSync aAsynchronous)
+Promise::MaybeResolveInternal(JSContext* aCx,
+                              const Optional<JS::Handle<JS::Value> >& aValue,
+                              PromiseTaskSync aAsynchronous)
 {
   if (mResolvePending) {
     return;
   }
 
   ResolveInternal(aCx, aValue, aAsynchronous);
 }
 
 void
-Promise::MaybeReject(JSContext* aCx,
-                     const Optional<JS::Handle<JS::Value> >& aValue,
-                     PromiseTaskSync aAsynchronous)
+Promise::MaybeRejectInternal(JSContext* aCx,
+                             const Optional<JS::Handle<JS::Value> >& aValue,
+                             PromiseTaskSync aAsynchronous)
 {
   if (mResolvePending) {
     return;
   }
 
   RejectInternal(aCx, aValue, aAsynchronous);
 }
 
--- a/dom/promise/Promise.h
+++ b/dom/promise/Promise.h
@@ -38,16 +38,21 @@ public:
   NS_DECL_CYCLE_COLLECTION_SCRIPT_HOLDER_CLASS(Promise)
 
   Promise(nsPIDOMWindow* aWindow);
   ~Promise();
 
   static bool PrefEnabled();
   static bool EnabledForScope(JSContext* aCx, JSObject* /* unused */);
 
+  void MaybeResolve(JSContext* aCx,
+                    const Optional<JS::Handle<JS::Value> >& aValue);
+  void MaybeReject(JSContext* aCx,
+                   const Optional<JS::Handle<JS::Value> >& aValue);
+
   // WebIDL
 
   nsPIDOMWindow* GetParentObject() const
   {
     return mWindow;
   }
 
   virtual JSObject*
@@ -109,22 +114,22 @@ private:
 
   void AppendCallbacks(PromiseCallback* aResolveCallback,
                        PromiseCallback* aRejectCallback);
 
   // If we have been rejected and our mResult is a JS exception,
   // report it to the error console.
   void MaybeReportRejected();
 
-  void MaybeResolve(JSContext* aCx,
-                    const Optional<JS::Handle<JS::Value> >& aValue,
-                    PromiseTaskSync aSync = AsyncTask);
-  void MaybeReject(JSContext* aCx,
-                   const Optional<JS::Handle<JS::Value> >& aValue,
-                   PromiseTaskSync aSync = AsyncTask);
+  void MaybeResolveInternal(JSContext* aCx,
+                            const Optional<JS::Handle<JS::Value> >& aValue,
+                            PromiseTaskSync aSync = AsyncTask);
+  void MaybeRejectInternal(JSContext* aCx,
+                           const Optional<JS::Handle<JS::Value> >& aValue,
+                           PromiseTaskSync aSync = AsyncTask);
 
   void ResolveInternal(JSContext* aCx,
                        const Optional<JS::Handle<JS::Value> >& aValue,
                        PromiseTaskSync aSync = AsyncTask);
 
   void RejectInternal(JSContext* aCx,
                       const Optional<JS::Handle<JS::Value> >& aValue,
                       PromiseTaskSync aSync = AsyncTask);
--- a/dom/src/notification/Notification.cpp
+++ b/dom/src/notification/Notification.cpp
@@ -1,37 +1,153 @@
 /* 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 "PCOMContentPermissionRequestChild.h"
 #include "mozilla/dom/Notification.h"
 #include "mozilla/dom/AppNotificationServiceOptionsBinding.h"
 #include "mozilla/dom/OwningNonNull.h"
+#include "mozilla/dom/Promise.h"
 #include "mozilla/Preferences.h"
 #include "TabChild.h"
 #include "nsContentUtils.h"
 #include "nsDOMEvent.h"
 #include "nsIAlertsService.h"
+#include "nsIAppsService.h"
 #include "nsIContentPermissionPrompt.h"
 #include "nsIDocument.h"
+#include "nsINotificationStorage.h"
 #include "nsIPermissionManager.h"
+#include "nsIUUIDGenerator.h"
 #include "nsServiceManagerUtils.h"
 #include "nsToolkitCompsCID.h"
 #include "nsGlobalWindow.h"
 #include "nsDOMJSUtils.h"
 #include "nsIScriptSecurityManager.h"
 #ifdef MOZ_B2G
 #include "nsIDOMDesktopNotification.h"
-#include "nsIAppsService.h"
 #endif
 
 namespace mozilla {
 namespace dom {
 
+class NotificationStorageCallback MOZ_FINAL : public nsINotificationStorageCallback
+{
+public:
+  NS_DECL_CYCLE_COLLECTING_ISUPPORTS
+  NS_DECL_CYCLE_COLLECTION_SCRIPT_HOLDER_CLASS(NotificationStorageCallback)
+
+  NotificationStorageCallback(const GlobalObject& aGlobal, nsPIDOMWindow* aWindow, Promise* aPromise)
+    : mCount(0),
+      mGlobal(aGlobal.Get()),
+      mWindow(aWindow),
+      mPromise(aPromise)
+  {
+    MOZ_ASSERT(aWindow);
+    MOZ_ASSERT(aPromise);
+    JSContext* cx = aGlobal.GetContext();
+    JSAutoCompartment ac(cx, mGlobal);
+    mNotifications = JS_NewArrayObject(cx, 0, nullptr);
+    HoldData();
+  }
+
+  NS_IMETHOD Handle(const nsAString& aID,
+                    const nsAString& aTitle,
+                    const nsAString& aDir,
+                    const nsAString& aLang,
+                    const nsAString& aBody,
+                    const nsAString& aTag,
+                    const nsAString& aIcon,
+                    JSContext* aCx)
+  {
+    MOZ_ASSERT(!aID.IsEmpty());
+    MOZ_ASSERT(!aTitle.IsEmpty());
+
+    NotificationOptions options;
+    options.mDir = Notification::StringToDirection(nsString(aDir));
+    options.mLang = aLang;
+    options.mBody = aBody;
+    options.mTag = aTag;
+    options.mIcon = aIcon;
+    nsRefPtr<Notification> notification = Notification::CreateInternal(mWindow,
+                                                                       aID,
+                                                                       aTitle,
+                                                                       options);
+    JSAutoCompartment ac(aCx, mGlobal);
+    JS::RootedObject scope(aCx, mGlobal);
+    JS::RootedObject element(aCx, notification->WrapObject(aCx, scope));
+    NS_ENSURE_TRUE(element, NS_ERROR_FAILURE);
+
+    if (!JS_DefineElement(aCx, mNotifications, mCount++,
+                          JS::ObjectValue(*element), nullptr, nullptr, 0)) {
+      return NS_ERROR_FAILURE;
+    }
+    return NS_OK;
+  }
+
+  NS_IMETHOD Done(JSContext* aCx)
+  {
+    JSAutoCompartment ac(aCx, mGlobal);
+    Optional<JS::HandleValue> result(aCx, JS::ObjectValue(*mNotifications));
+    mPromise->MaybeResolve(aCx, result);
+    return NS_OK;
+  }
+
+private:
+  ~NotificationStorageCallback()
+  {
+    DropData();
+  }
+
+  void HoldData()
+  {
+    mozilla::HoldJSObjects(this);
+  }
+
+  void DropData()
+  {
+    mGlobal = nullptr;
+    mNotifications = nullptr;
+    mozilla::DropJSObjects(this);
+  }
+
+  uint32_t  mCount;
+  JS::Heap<JSObject *> mGlobal;
+  nsCOMPtr<nsPIDOMWindow> mWindow;
+  nsRefPtr<Promise> mPromise;
+  JS::Heap<JSObject *> mNotifications;
+};
+
+NS_IMPL_CYCLE_COLLECTING_ADDREF(NotificationStorageCallback)
+NS_IMPL_CYCLE_COLLECTING_RELEASE(NotificationStorageCallback)
+NS_IMPL_CYCLE_COLLECTION_CLASS(NotificationStorageCallback)
+
+NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(NotificationStorageCallback)
+  NS_INTERFACE_MAP_ENTRY(nsINotificationStorageCallback)
+  NS_INTERFACE_MAP_ENTRY(nsISupports)
+NS_INTERFACE_MAP_END
+
+NS_IMPL_CYCLE_COLLECTION_TRACE_BEGIN(NotificationStorageCallback)
+  NS_IMPL_CYCLE_COLLECTION_TRACE_JS_MEMBER_CALLBACK(mGlobal)
+  NS_IMPL_CYCLE_COLLECTION_TRACE_JS_MEMBER_CALLBACK(mNotifications)
+NS_IMPL_CYCLE_COLLECTION_TRACE_END
+
+NS_IMPL_CYCLE_COLLECTION_TRAVERSE_BEGIN(NotificationStorageCallback)
+  NS_IMPL_CYCLE_COLLECTION_TRAVERSE_SCRIPT_OBJECTS
+  NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mWindow)
+  NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mPromise)
+NS_IMPL_CYCLE_COLLECTION_TRAVERSE_END
+
+NS_IMPL_CYCLE_COLLECTION_UNLINK_BEGIN(NotificationStorageCallback)
+  NS_IMPL_CYCLE_COLLECTION_UNLINK(mWindow)
+  NS_IMPL_CYCLE_COLLECTION_UNLINK(mPromise)
+  tmp->DropData();
+NS_IMPL_CYCLE_COLLECTION_UNLINK_END
+
 class NotificationPermissionRequest : public nsIContentPermissionRequest,
                                       public PCOMContentPermissionRequestChild,
                                       public nsIRunnable
 {
 public:
   NS_DECL_CYCLE_COLLECTING_ISUPPORTS
   NS_DECL_NSICONTENTPERMISSIONREQUEST
   NS_DECL_NSIRUNNABLE
@@ -252,22 +368,25 @@ NotificationPermissionRequest::Recv__del
 
 NS_IMPL_ISUPPORTS1(NotificationTask, nsIRunnable)
 
 NS_IMETHODIMP
 NotificationTask::Run()
 {
   switch (mAction) {
   case eShow:
-    return mNotification->ShowInternal();
+    mNotification->ShowInternal();
+    break;
   case eClose:
-    return mNotification->CloseInternal();
+    mNotification->CloseInternal();
+    break;
   default:
     MOZ_CRASH("Unexpected action for NotificationTask.");
   }
+  return NS_OK;
 }
 
 NS_IMPL_ISUPPORTS1(NotificationObserver, nsIObserver)
 
 NS_IMETHODIMP
 NotificationObserver::Observe(nsISupports* aSubject, const char* aTopic,
                               const PRUnichar* aData)
 {
@@ -278,136 +397,195 @@ NotificationObserver::Observe(nsISupport
     mNotification->DispatchTrustedEvent(NS_LITERAL_STRING("close"));
   } else if (!strcmp("alertshow", aTopic)) {
     mNotification->DispatchTrustedEvent(NS_LITERAL_STRING("show"));
   }
 
   return NS_OK;
 }
 
-Notification::Notification(const nsAString& aTitle, const nsAString& aBody,
+Notification::Notification(const nsAString& aID, const nsAString& aTitle, const nsAString& aBody,
                            NotificationDirection aDir, const nsAString& aLang,
                            const nsAString& aTag, const nsAString& aIconUrl)
-  : mTitle(aTitle), mBody(aBody), mDir(aDir), mLang(aLang),
+  : mID(aID), mTitle(aTitle), mBody(aBody), mDir(aDir), mLang(aLang),
     mTag(aTag), mIconUrl(aIconUrl), mIsClosed(false)
 {
   SetIsDOMBinding();
 }
 
+// static
 already_AddRefed<Notification>
 Notification::Constructor(const GlobalObject& aGlobal,
                           const nsAString& aTitle,
                           const NotificationOptions& aOptions,
                           ErrorResult& aRv)
 {
-  nsString tag;
-  if (aOptions.mTag.WasPassed()) {
-    tag.Append(NS_LITERAL_STRING("tag:"));
-    tag.Append(aOptions.mTag.Value());
-  } else {
-    tag.Append(NS_LITERAL_STRING("notag:"));
-    tag.AppendInt(sCount++);
-  }
-
-  nsRefPtr<Notification> notification = new Notification(aTitle,
-                                                         aOptions.mBody,
-                                                         aOptions.mDir,
-                                                         aOptions.mLang,
-                                                         tag,
-                                                         aOptions.mIcon);
-
+  MOZ_ASSERT(NS_IsMainThread());
   nsCOMPtr<nsPIDOMWindow> window = do_QueryInterface(aGlobal.GetAsSupports());
   MOZ_ASSERT(window, "Window should not be null.");
-  notification->BindToOwner(window);
+  nsRefPtr<Notification> notification = CreateInternal(window,
+                                                       EmptyString(),
+                                                       aTitle,
+                                                       aOptions);
 
   // Queue a task to show the notification.
   nsCOMPtr<nsIRunnable> showNotificationTask =
     new NotificationTask(notification, NotificationTask::eShow);
-  NS_DispatchToMainThread(showNotificationTask);
+  NS_DispatchToCurrentThread(showNotificationTask);
+
+  // Persist the notification.
+  nsresult rv;
+  nsCOMPtr<nsINotificationStorage> notificationStorage =
+    do_GetService(NS_NOTIFICATION_STORAGE_CONTRACTID, &rv);
+  if (NS_FAILED(rv)) {
+    aRv.Throw(rv);
+    return nullptr;
+  }
+
+  nsString origin;
+  aRv = GetOrigin(window, origin);
+  if (aRv.Failed()) {
+    return nullptr;
+  }
+
+  nsString id;
+  notification->GetID(id);
+  aRv = notificationStorage->Put(origin,
+                                 id,
+                                 aTitle,
+                                 DirectionToString(aOptions.mDir),
+                                 aOptions.mLang,
+                                 aOptions.mBody,
+                                 aOptions.mTag,
+                                 aOptions.mIcon);
+  if (aRv.Failed()) {
+    return nullptr;
+  }
 
   return notification.forget();
 }
 
-nsresult
+already_AddRefed<Notification>
+Notification::CreateInternal(nsPIDOMWindow* aWindow,
+                             const nsAString& aID,
+                             const nsAString& aTitle,
+                             const NotificationOptions& aOptions)
+{
+  nsString id;
+  if (!aID.IsEmpty()) {
+    id = aID;
+  } else {
+    nsCOMPtr<nsIUUIDGenerator> uuidgen =
+      do_GetService("@mozilla.org/uuid-generator;1");
+    NS_ENSURE_TRUE(uuidgen, nullptr);
+    nsID uuid;
+    nsresult rv = uuidgen->GenerateUUIDInPlace(&uuid);
+    NS_ENSURE_SUCCESS(rv, nullptr);
+
+    char buffer[NSID_LENGTH];
+    uuid.ToProvidedString(buffer);
+    NS_ConvertASCIItoUTF16 convertedID(buffer);
+    id = convertedID;
+  }
+
+  nsRefPtr<Notification> notification = new Notification(id,
+                                                         aTitle,
+                                                         aOptions.mBody,
+                                                         aOptions.mDir,
+                                                         aOptions.mLang,
+                                                         aOptions.mTag,
+                                                         aOptions.mIcon);
+
+  notification->BindToOwner(aWindow);
+  return notification.forget();
+}
+
+void
 Notification::ShowInternal()
 {
   nsCOMPtr<nsIAlertsService> alertService =
     do_GetService(NS_ALERTSERVICE_CONTRACTID);
 
   ErrorResult result;
   if (GetPermissionInternal(GetOwner(), result) !=
     NotificationPermission::Granted || !alertService) {
     // We do not have permission to show a notification or alert service
     // is not available.
-    return DispatchTrustedEvent(NS_LITERAL_STRING("error"));
+    DispatchTrustedEvent(NS_LITERAL_STRING("error"));
+    return;
   }
 
   nsresult rv;
   nsAutoString absoluteUrl;
   if (mIconUrl.Length() > 0) {
     // Resolve image URL against document base URI.
     nsIDocument* doc = GetOwner()->GetExtantDoc();
-    NS_ENSURE_TRUE(doc, NS_ERROR_UNEXPECTED);
-    nsCOMPtr<nsIURI> baseUri = doc->GetBaseURI();
-    NS_ENSURE_TRUE(baseUri, NS_ERROR_UNEXPECTED);
-    nsCOMPtr<nsIURI> srcUri;
-    rv = nsContentUtils::NewURIWithDocumentCharset(getter_AddRefs(srcUri),
-                                                   mIconUrl, doc, baseUri);
-    NS_ENSURE_SUCCESS(rv, rv);
-    if (srcUri) {
-      nsAutoCString src;
-      srcUri->GetSpec(src);
-      absoluteUrl = NS_ConvertUTF8toUTF16(src);
+    if (doc) {
+      nsCOMPtr<nsIURI> baseUri = doc->GetBaseURI();
+      if (baseUri) {
+        nsCOMPtr<nsIURI> srcUri;
+        rv = nsContentUtils::NewURIWithDocumentCharset(getter_AddRefs(srcUri),
+                                                       mIconUrl, doc, baseUri);
+        if (NS_SUCCEEDED(rv)) {
+          nsAutoCString src;
+          srcUri->GetSpec(src);
+          absoluteUrl = NS_ConvertUTF8toUTF16(src);
+        }
+      }
+
     }
   }
 
   nsCOMPtr<nsIObserver> observer = new NotificationObserver(this);
 
   nsString alertName;
   rv = GetAlertName(alertName);
-  NS_ENSURE_SUCCESS(rv, rv);
+  NS_ENSURE_SUCCESS_VOID(rv);
 
 #ifdef MOZ_B2G
   nsCOMPtr<nsIAppNotificationService> appNotifier =
     do_GetService("@mozilla.org/system-alerts-service;1");
   if (appNotifier) {
     nsCOMPtr<nsPIDOMWindow> window = GetOwner();
     uint32_t appId = (window.get())->GetDoc()->NodePrincipal()->GetAppId();
 
     if (appId != nsIScriptSecurityManager::UNKNOWN_APP_ID) {
       nsCOMPtr<nsIAppsService> appsService = do_GetService("@mozilla.org/AppsService;1");
       nsString manifestUrl = EmptyString();
-      appsService->GetManifestURLByLocalId(appId, manifestUrl);
-      mozilla::AutoSafeJSContext cx;
-      JS::RootedValue val(cx);
-      AppNotificationServiceOptions ops;
-      ops.mTextClickable = true;
-      ops.mManifestURL = manifestUrl;
-      ops.mId = alertName;
-      ops.mDir = DirectionToString(mDir);
-      ops.mLang = mLang;
+      rv = appsService->GetManifestURLByLocalId(appId, manifestUrl);
+      if (NS_SUCCEEDED(rv)) {
+        mozilla::AutoSafeJSContext cx;
+        JS::RootedValue val(cx);
+        AppNotificationServiceOptions ops;
+        ops.mTextClickable = true;
+        ops.mManifestURL = manifestUrl;
+        ops.mId = alertName;
+        ops.mDir = DirectionToString(mDir);
+        ops.mLang = mLang;
 
-      if (!ops.ToObject(cx, JS::NullPtr(), &val)) {
-        NS_WARNING("Converting dict to object failed!");
-        return NS_ERROR_FAILURE;
+        if (!ops.ToObject(cx, JS::NullPtr(), &val)) {
+          NS_WARNING("Converting dict to object failed!");
+          return;
+        }
+
+        appNotifier->ShowAppNotification(mIconUrl, mTitle, mBody,
+                                         observer, val);
+        return;
       }
-
-      return appNotifier->ShowAppNotification(mIconUrl, mTitle, mBody,
-                                              observer, val);
     }
   }
 #endif
 
   // In the case of IPC, the parent process uses the cookie to map to
   // nsIObserver. Thus the cookie must be unique to differentiate observers.
   nsString uniqueCookie = NS_LITERAL_STRING("notification:");
   uniqueCookie.AppendInt(sCount++);
-  return alertService->ShowAlertNotification(absoluteUrl, mTitle, mBody, true,
-                                             uniqueCookie, observer, alertName,
-                                             DirectionToString(mDir), mLang);
+  alertService->ShowAlertNotification(absoluteUrl, mTitle, mBody, true,
+                                      uniqueCookie, observer, alertName,
+                                      DirectionToString(mDir), mLang);
 }
 
 void
 Notification::RequestPermission(const GlobalObject& aGlobal,
                                 const Optional<OwningNonNull<NotificationPermissionCallback> >& aCallback,
                                 ErrorResult& aRv)
 {
   // Get principal from global to make permission request for notifications.
@@ -485,16 +663,57 @@ Notification::GetPermissionInternal(nsIS
     return NotificationPermission::Granted;
   case nsIPermissionManager::DENY_ACTION:
     return NotificationPermission::Denied;
   default:
     return NotificationPermission::Default;
   }
 }
 
+already_AddRefed<Promise>
+Notification::Get(const GlobalObject& aGlobal,
+                  const GetNotificationOptions& aFilter,
+                  ErrorResult& aRv)
+{
+  nsCOMPtr<nsPIDOMWindow> window = do_QueryInterface(aGlobal.GetAsSupports());
+  MOZ_ASSERT(window);
+  nsIDocument* doc = window->GetExtantDoc();
+  if (!doc) {
+    aRv.Throw(NS_ERROR_UNEXPECTED);
+    return nullptr;
+  }
+
+  nsString origin;
+  aRv = GetOrigin(window, origin);
+  if (aRv.Failed()) {
+    return nullptr;
+  }
+
+  nsresult rv;
+  nsCOMPtr<nsINotificationStorage> notificationStorage =
+    do_GetService(NS_NOTIFICATION_STORAGE_CONTRACTID, &rv);
+  if (NS_FAILED(rv)) {
+    aRv.Throw(rv);
+    return nullptr;
+  }
+
+  nsRefPtr<Promise> promise = new Promise(window);
+  nsCOMPtr<nsINotificationStorageCallback> callback =
+    new NotificationStorageCallback(aGlobal, window, promise);
+  nsString tag = aFilter.mTag.WasPassed() ?
+                 aFilter.mTag.Value() :
+                 EmptyString();
+  aRv = notificationStorage->Get(origin, tag, callback);
+  if (aRv.Failed()) {
+    return nullptr;
+  }
+
+  return promise.forget();
+}
+
 bool
 Notification::PrefEnabled()
 {
   return Preferences::GetBool("dom.webnotifications.enabled", false);
 }
 
 JSObject*
 Notification::WrapObject(JSContext* aCx, JS::Handle<JSObject*> aScope)
@@ -506,51 +725,88 @@ void
 Notification::Close()
 {
   // Queue a task to close the notification.
   nsCOMPtr<nsIRunnable> showNotificationTask =
     new NotificationTask(this, NotificationTask::eClose);
   NS_DispatchToMainThread(showNotificationTask);
 }
 
-nsresult
+void
 Notification::CloseInternal()
 {
   if (!mIsClosed) {
+    nsresult rv;
+    // Don't bail out if notification storage fails, since we still
+    // want to send the close event through the alert service.
+    nsCOMPtr<nsINotificationStorage> notificationStorage =
+      do_GetService(NS_NOTIFICATION_STORAGE_CONTRACTID);
+    if (notificationStorage) {
+      nsString origin;
+      rv = GetOrigin(GetOwner(), origin);
+      if (NS_SUCCEEDED(rv)) {
+        notificationStorage->Delete(origin, mID);
+      }
+    }
+
     nsCOMPtr<nsIAlertsService> alertService =
       do_GetService(NS_ALERTSERVICE_CONTRACTID);
-
     if (alertService) {
       nsString alertName;
-      nsresult rv = GetAlertName(alertName);
-      NS_ENSURE_SUCCESS(rv, rv);
+      rv = GetAlertName(alertName);
+      if (NS_SUCCEEDED(rv)) {
+        alertService->CloseAlert(alertName);
+      }
+    }
+  }
+}
 
-      rv = alertService->CloseAlert(alertName);
-      NS_ENSURE_SUCCESS(rv, rv);
-    }
+nsresult
+Notification::GetOrigin(nsPIDOMWindow* aWindow, nsString& aOrigin)
+{
+  MOZ_ASSERT(aWindow);
+  nsresult rv;
+  nsIDocument* doc = aWindow->GetExtantDoc();
+  NS_ENSURE_TRUE(doc, NS_ERROR_UNEXPECTED);
+  nsIPrincipal* principal = doc->NodePrincipal();
+  NS_ENSURE_TRUE(principal, NS_ERROR_UNEXPECTED);
+
+  uint16_t appStatus = principal->GetAppStatus();
+  uint32_t appId = principal->GetAppId();
+
+  if (appStatus == nsIPrincipal::APP_STATUS_NOT_INSTALLED ||
+      appId == nsIScriptSecurityManager::NO_APP_ID ||
+      appId == nsIScriptSecurityManager::UNKNOWN_APP_ID) {
+    rv = nsContentUtils::GetUTFOrigin(principal, aOrigin);
+    NS_ENSURE_SUCCESS(rv, rv);
+  } else {
+    // If we are in "app code", use manifest URL as unique origin since
+    // multiple apps can share the same origin but not same notifications.
+    nsCOMPtr<nsIAppsService> appsService =
+      do_GetService("@mozilla.org/AppsService;1", &rv);
+    NS_ENSURE_SUCCESS(rv, rv);
+    appsService->GetManifestURLByLocalId(appId, aOrigin);
   }
 
   return NS_OK;
 }
 
 nsresult
 Notification::GetAlertName(nsString& aAlertName)
 {
-  // Get the notification name that is unique per origin + tag.
-  // The name of the alert is of the form origin#tag
-
-  nsPIDOMWindow* owner = GetOwner();
-  NS_ENSURE_TRUE(owner, NS_ERROR_UNEXPECTED);
-
-  nsIDocument* doc = owner->GetExtantDoc();
-  NS_ENSURE_TRUE(doc, NS_ERROR_UNEXPECTED);
-
-  nsresult rv = nsContentUtils::GetUTFOrigin(doc->NodePrincipal(),
-                                             aAlertName);
+  // Get the notification name that is unique per origin + tag/ID.
+  // The name of the alert is of the form origin#tag/ID.
+  nsresult rv = GetOrigin(GetOwner(), aAlertName);
   NS_ENSURE_SUCCESS(rv, rv);
   aAlertName.AppendLiteral("#");
-  aAlertName.Append(mTag);
+  if (!mTag.IsEmpty()) {
+    aAlertName.Append(NS_LITERAL_STRING("tag:"));
+    aAlertName.Append(mTag);
+  } else {
+    aAlertName.Append(NS_LITERAL_STRING("notag:"));
+    aAlertName.Append(mID);
+  }
   return NS_OK;
 }
 
 } // namespace dom
 } // namespace mozilla
 
--- a/dom/src/notification/Notification.h
+++ b/dom/src/notification/Notification.h
@@ -5,111 +5,142 @@
 #ifndef mozilla_dom_notification_h__
 #define mozilla_dom_notification_h__
 
 #include "mozilla/dom/NotificationBinding.h"
 
 #include "nsDOMEventTargetHelper.h"
 #include "nsIObserver.h"
 
+#include "nsCycleCollectionParticipant.h"
+
 namespace mozilla {
 namespace dom {
 
+
 class NotificationObserver;
+class Promise;
 
 class Notification : public nsDOMEventTargetHelper
 {
   friend class NotificationTask;
   friend class NotificationPermissionRequest;
   friend class NotificationObserver;
+  friend class NotificationStorageCallback;
+
 public:
   IMPL_EVENT_HANDLER(click)
   IMPL_EVENT_HANDLER(show)
   IMPL_EVENT_HANDLER(error)
   IMPL_EVENT_HANDLER(close)
 
-  Notification(const nsAString& aTitle, const nsAString& aBody,
-               NotificationDirection aDir, const nsAString& aLang,
-               const nsAString& aTag, const nsAString& aIconUrl);
-
   static already_AddRefed<Notification> Constructor(const GlobalObject& aGlobal,
                                                     const nsAString& aTitle,
                                                     const NotificationOptions& aOption,
                                                     ErrorResult& aRv);
-  void GetTitle(nsString& aRetval)
+  void GetID(nsAString& aRetval) {
+    aRetval = mID;
+  }
+
+  void GetTitle(nsAString& aRetval)
   {
     aRetval = mTitle;
   }
 
   NotificationDirection Dir()
   {
     return mDir;
   }
 
-  void GetLang(nsString& aRetval)
+  void GetLang(nsAString& aRetval)
   {
     aRetval = mLang;
   }
 
-  void GetBody(nsString& aRetval)
+  void GetBody(nsAString& aRetval)
   {
     aRetval = mBody;
   }
 
-  void GetTag(nsString& aRetval)
+  void GetTag(nsAString& aRetval)
   {
-    if (StringBeginsWith(mTag, NS_LITERAL_STRING("tag:"))) {
-      aRetval = Substring(mTag, 4);
-    }
+    aRetval = mTag;
   }
 
-  void GetIcon(nsString& aRetval)
+  void GetIcon(nsAString& aRetval)
   {
     aRetval = mIconUrl;
   }
 
   static void RequestPermission(const GlobalObject& aGlobal,
                                 const Optional<OwningNonNull<NotificationPermissionCallback> >& aCallback,
                                 ErrorResult& aRv);
 
   static NotificationPermission GetPermission(const GlobalObject& aGlobal,
                                               ErrorResult& aRv);
 
+  static already_AddRefed<Promise> Get(const GlobalObject& aGlobal,
+                                       const GetNotificationOptions& aFilter,
+                                       ErrorResult& aRv);
+
   void Close();
 
   static bool PrefEnabled();
 
   nsPIDOMWindow* GetParentObject()
   {
     return GetOwner();
   }
 
   virtual JSObject* WrapObject(JSContext* aCx,
                                JS::Handle<JSObject*> aScope) MOZ_OVERRIDE;
 protected:
-  nsresult ShowInternal();
-  nsresult CloseInternal();
+  Notification(const nsAString& aID, const nsAString& aTitle, const nsAString& aBody,
+               NotificationDirection aDir, const nsAString& aLang,
+               const nsAString& aTag, const nsAString& aIconUrl);
+
+  static already_AddRefed<Notification> CreateInternal(nsPIDOMWindow* aWindow,
+                                                       const nsAString& aID,
+                                                       const nsAString& aTitle,
+                                                       const NotificationOptions& aOptions);
+
+  void ShowInternal();
+  void CloseInternal();
 
   static NotificationPermission GetPermissionInternal(nsISupports* aGlobal,
                                                       ErrorResult& rv);
 
   static const nsString DirectionToString(NotificationDirection aDirection)
   {
     switch (aDirection) {
     case NotificationDirection::Ltr:
       return NS_LITERAL_STRING("ltr");
     case NotificationDirection::Rtl:
       return NS_LITERAL_STRING("rtl");
     default:
       return NS_LITERAL_STRING("auto");
     }
   }
 
+  static const NotificationDirection StringToDirection(const nsAString& aDirection)
+  {
+    if (aDirection.EqualsLiteral("ltr")) {
+      return NotificationDirection::Ltr;
+    }
+    if (aDirection.EqualsLiteral("rtl")) {
+      return NotificationDirection::Rtl;
+    }
+    return NotificationDirection::Auto;
+  }
+
+  static nsresult GetOrigin(nsPIDOMWindow* aWindow, nsString& aOrigin);
+
   nsresult GetAlertName(nsString& aAlertName);
 
+  nsString mID;
   nsString mTitle;
   nsString mBody;
   NotificationDirection mDir;
   nsString mLang;
   nsString mTag;
   nsString mIconUrl;
 
   bool mIsClosed;
new file mode 100644
--- /dev/null
+++ b/dom/src/notification/NotificationDB.jsm
@@ -0,0 +1,270 @@
+/* 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";
+
+this.EXPORTED_SYMBOLS = [];
+
+const DEBUG = false;
+function debug(s) { dump("-*- NotificationDB component: " + s + "\n"); }
+
+const Cu = Components.utils;
+const Cc = Components.classes;
+const Ci = Components.interfaces;
+
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+Cu.import("resource://gre/modules/osfile.jsm");
+
+XPCOMUtils.defineLazyServiceGetter(this, "ppmm",
+                                   "@mozilla.org/parentprocessmessagemanager;1",
+                                   "nsIMessageListenerManager");
+
+XPCOMUtils.defineLazyGetter(this, "gEncoder", function() {
+  return new TextEncoder();
+});
+
+XPCOMUtils.defineLazyGetter(this, "gDecoder", function() {
+  return new TextDecoder();
+});
+
+
+const NOTIFICATION_STORE_DIR = OS.Constants.Path.profileDir;
+const NOTIFICATION_STORE_PATH =
+        OS.Path.join(NOTIFICATION_STORE_DIR, "notificationstore.json");
+
+let NotificationDB = {
+  init: function() {
+    this.notifications = {};
+    this.byTag = {};
+    this.loaded = false;
+
+    this.tasks = []; // read/write operation queue
+    this.runningTask = false;
+
+    ppmm.addMessageListener("Notification:Save", this);
+    ppmm.addMessageListener("Notification:Delete", this);
+    ppmm.addMessageListener("Notification:GetAll", this);
+  },
+
+  // Attempt to read notification file, if it's not there we will create it.
+  load: function(callback) {
+    var promise = OS.File.read(NOTIFICATION_STORE_PATH);
+    promise.then(
+      function onSuccess(data) {
+        try {
+          this.notifications = JSON.parse(gDecoder.decode(data));
+        } catch (e) {
+          if (DEBUG) { debug("Unable to parse file data " + e); }
+        }
+        this.loaded = true;
+        callback && callback();
+      }.bind(this),
+
+      // If read failed, we assume we have no notifications to load.
+      function onFailure(reason) {
+        this.loaded = true;
+        this.createStore(callback);
+      }.bind(this)
+    );
+  },
+
+  // Creates the notification directory.
+  createStore: function(callback) {
+    var promise = OS.File.makeDir(NOTIFICATION_STORE_DIR, {
+      ignoreExisting: true
+    });
+    promise.then(
+      function onSuccess() {
+        this.createFile(callback);
+      }.bind(this),
+
+      function onFailure(reason) {
+        if (DEBUG) { debug("Directory creation failed:" + reason); }
+        callback && callback();
+      }
+    );
+  },
+
+  // Creates the notification file once the directory is created.
+  createFile: function(callback) {
+    var promise = OS.File.open(NOTIFICATION_STORE_PATH, {create: true});
+    promise.then(
+      function onSuccess(handle) {
+        callback && callback();
+      },
+      function onFailure(reason) {
+        if (DEBUG) { debug("File creation failed:" + reason); }
+        callback && callback();
+      }
+    );
+  },
+
+  // Save current notifications to the file.
+  save: function(callback) {
+    var data = gEncoder.encode(JSON.stringify(this.notifications));
+    var promise = OS.File.writeAtomic(NOTIFICATION_STORE_PATH, data);
+    promise.then(
+      function onSuccess() {
+        callback && callback();
+      },
+      function onFailure(reason) {
+        if (DEBUG) { debug("Save failed:" + reason); }
+        callback && callback();
+      }
+    );
+  },
+
+  // Helper function: callback will be called once file exists and/or is loaded.
+  ensureLoaded: function(callback) {
+    if (!this.loaded) {
+      this.load(callback);
+    } else {
+      callback();
+    }
+  },
+
+  receiveMessage: function(message) {
+    if (DEBUG) { debug("Received message:" + message.name); }
+
+    switch (message.name) {
+      case "Notification:GetAll":
+        this.queueTask("getall", message.data, function(notifications) {
+          message.target.sendAsyncMessage("Notification:GetAll:Return:OK", {
+            requestID: message.data.requestID,
+            notifications: notifications
+          });
+        });
+        break;
+
+      case "Notification:Save":
+        this.queueTask("save", message.data, function() {
+          message.target.sendAsyncMessage("Notification:Save:Return:OK", {
+            requestID: message.data.requestID
+          });
+        });
+        break;
+
+      case "Notification:Delete":
+        this.queueTask("delete", message.data, function() {
+          message.target.sendAsyncMessage("Notification:Delete:Return:OK", {
+            requestID: message.data.requestID
+          });
+        });
+        break;
+
+      default:
+        if (DEBUG) { debug("Invalid message name" + message.name); }
+    }
+  },
+
+  // We need to make sure any read/write operations are atomic,
+  // so use a queue to run each operation sequentially.
+  queueTask: function(operation, data, callback) {
+    if (DEBUG) { debug("Queueing task: " + operation); }
+    this.tasks.push({
+      operation: operation,
+      data: data,
+      callback: callback
+    });
+
+    // Only run immediately if we aren't currently running another task.
+    if (!this.runningTask) {
+      if (DEBUG) { dump("Task queue was not running, starting now..."); }
+      this.runNextTask();
+    }
+  },
+
+  runNextTask: function() {
+    if (this.tasks.length === 0) {
+      if (DEBUG) { dump("No more tasks to run, queue depleted"); }
+      this.runningTask = false;
+      return;
+    }
+    this.runningTask = true;
+
+    // Always make sure we are loaded before performing any read/write tasks.
+    this.ensureLoaded(function() {
+      var task = this.tasks.shift();
+
+      // Wrap the task callback to make sure we immediately
+      // run the next task after running the original callback.
+      var wrappedCallback = function() {
+        if (DEBUG) { debug("Finishing task: " + task.operation); }
+        task.callback.apply(this, arguments);
+        this.runNextTask();
+      }.bind(this);
+
+      switch (task.operation) {
+        case "getall":
+          this.taskGetAll(task.data, wrappedCallback);
+          break;
+
+        case "save":
+          this.taskSave(task.data, wrappedCallback);
+          break;
+
+        case "delete":
+          this.taskDelete(task.data, wrappedCallback);
+          break;
+      }
+    }.bind(this));
+  },
+
+  taskGetAll: function(data, callback) {
+    if (DEBUG) { debug("Task, getting all"); }
+    var origin = data.origin;
+    var notifications = [];
+    // Grab only the notifications for specified origin.
+    for (var i in this.notifications[origin]) {
+      notifications.push(this.notifications[origin][i]);
+    }
+    callback(notifications);
+  },
+
+  taskSave: function(data, callback) {
+    if (DEBUG) { debug("Task, saving"); }
+    var origin = data.origin;
+    var notification = data.notification;
+    if (!this.notifications[origin]) {
+      this.notifications[origin] = {};
+      this.byTag[origin] = {};
+    }
+
+    // We might have existing notification with this tag,
+    // if so we need to remove it before saving the new one.
+    if (notification.tag && this.byTag[origin][notification.tag]) {
+      var oldNotification = this.byTag[origin][notification.tag];
+      delete this.notifications[origin][oldNotification.id];
+      this.byTag[origin][notification.tag] = notification;
+    }
+
+    this.notifications[origin][notification.id] = notification;
+    this.save(callback);
+  },
+
+  taskDelete: function(data, callback) {
+    if (DEBUG) { debug("Task, deleting"); }
+    var origin = data.origin;
+    var id = data.id;
+    if (!this.notifications[origin]) {
+      if (DEBUG) { debug("No notifications found for origin: " + origin); }
+      return;
+    }
+
+    // Make sure we can find the notification to delete.
+    var oldNotification = this.notifications[origin][id];
+    if (!oldNotification) {
+      if (DEBUG) { debug("No notification found with id: " + id); }
+      return;
+    }
+
+    if (oldNotification.tag) {
+      delete this.byTag[origin][oldNotification.tag];
+    }
+    delete this.notifications[origin][id];
+    this.save(callback);
+  }
+};
+
+NotificationDB.init();
new file mode 100644
--- /dev/null
+++ b/dom/src/notification/NotificationStorage.js
@@ -0,0 +1,174 @@
+/* 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";
+
+const DEBUG = false;
+function debug(s) { dump("-*- NotificationStorage.js: " + s + "\n"); }
+
+const Cc = Components.classes;
+const Ci = Components.interfaces;
+const Cu = Components.utils;
+
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+
+const NOTIFICATIONSTORAGE_CID = "{37f819b0-0b5c-11e3-8ffd-0800200c9a66}";
+const NOTIFICATIONSTORAGE_CONTRACTID = "@mozilla.org/notificationStorage;1";
+
+XPCOMUtils.defineLazyServiceGetter(this, "cpmm",
+                                   "@mozilla.org/childprocessmessagemanager;1",
+                                   "nsIMessageSender");
+
+
+function NotificationStorage() {
+  // cache objects
+  this._notifications = {};
+  this._byTag = {};
+  this._cached = false;
+
+  this._requests = {};
+  this._requestCount = 0;
+
+  // Register for message listeners.
+  cpmm.addMessageListener("Notification:GetAll:Return:OK", this);
+}
+
+NotificationStorage.prototype = {
+
+  put: function(origin, id, title, dir, lang, body, tag, icon) {
+    if (DEBUG) { debug("PUT: " + id + ": " + title); }
+    var notification = {
+      id: id,
+      title: title,
+      dir: dir,
+      lang: lang,
+      body: body,
+      tag: tag,
+      icon: icon
+    };
+
+    this._notifications[id] = notification;
+    if (tag) {
+      // We might have existing notification with this tag,
+      // if so we need to remove it from our cache.
+      if (this._byTag[tag]) {
+        var oldNotification = this._byTag[tag];
+        delete this._notifications[oldNotification.id];
+      }
+
+      this._byTag[tag] = notification;
+    };
+
+    cpmm.sendAsyncMessage("Notification:Save", {
+      origin: origin,
+      notification: notification
+    });
+  },
+
+  get: function(origin, tag, callback) {
+    if (DEBUG) { debug("GET: " + tag); }
+    if (this._cached) {
+      this._fetchFromCache(tag, callback);
+    } else {
+      this._fetchFromDB(origin, tag, callback);
+    }
+  },
+
+  delete: function(origin, id) {
+    if (DEBUG) { debug("DELETE: " + id); }
+    var notification = this._notifications[id];
+    if (notification) {
+      if (notification.tag) {
+        delete this._byTag[notification.tag];
+      }
+      delete this._notifications[id];
+    }
+
+    cpmm.sendAsyncMessage("Notification:Delete", {
+      origin: origin,
+      id: id
+    });
+  },
+
+  receiveMessage: function(message) {
+    switch (message.name) {
+      case "Notification:GetAll:Return:OK":
+        var request = this._requests[message.data.requestID];
+        delete this._requests[message.data.requestID];
+        this._populateCache(message.data.notifications);
+        this._fetchFromCache(request.tag, request.callback);
+        break;
+
+      default:
+        if (DEBUG) debug("Unrecognized message: " + message.name);
+        break;
+    }
+  },
+
+  _fetchFromDB: function(origin, tag, callback) {
+    var request = {
+      origin: origin,
+      tag: tag,
+      callback: callback
+    };
+    var requestID = this._requestCount++;
+    this._requests[requestID] = request;
+    cpmm.sendAsyncMessage("Notification:GetAll", {
+      origin: origin,
+      requestID: requestID
+    });
+  },
+
+  _fetchFromCache: function(tag, callback) {
+    var notifications = [];
+    // If a tag was specified and we have a notification
+    // with this tag, return that. If no tag was specified
+    // simple return all stored notifications.
+    if (tag && this._byTag[tag]) {
+      notifications.push(this._byTag[tag]);
+    } else if (!tag) {
+      for (var id in this._notifications) {
+        notifications.push(this._notifications[id]);
+      }
+    }
+
+    // Pass each notification back separately.
+    notifications.forEach(function(notification) {
+      try {
+        callback.handle(notification.id,
+                        notification.title,
+                        notification.dir,
+                        notification.lang,
+                        notification.body,
+                        notification.tag,
+                        notification.icon);
+      } catch (e) {
+        if (DEBUG) { debug("Error calling callback handle: " + e); }
+      }
+    });
+    try {
+      callback.done();
+    } catch (e) {
+      if (DEBUG) { debug("Error calling callback done: " + e); }
+    }
+  },
+
+  _populateCache: function(notifications) {
+    notifications.forEach(function(notification) {
+      this._notifications[notification.id] = notification;
+      if (notification.tag) {
+        this._byTag[notification.tag] = notification;
+      }
+    }.bind(this));
+    this._cached = true;
+  },
+
+  classID : Components.ID(NOTIFICATIONSTORAGE_CID),
+  contractID : NOTIFICATIONSTORAGE_CONTRACTID,
+  QueryInterface: XPCOMUtils.generateQI([Ci.nsINotificationStorage,
+                                         Ci.nsIMessageListener]),
+};
+
+
+this.NSGetFactory = XPCOMUtils.generateNSGetFactory([NotificationStorage]);
new file mode 100644
--- /dev/null
+++ b/dom/src/notification/NotificationStorage.manifest
@@ -0,0 +1,3 @@
+# NotificationStorage.js
+component {37f819b0-0b5c-11e3-8ffd-0800200c9a66} NotificationStorage.js
+contract @mozilla.org/notificationStorage;1 {37f819b0-0b5c-11e3-8ffd-0800200c9a66}
--- a/dom/src/notification/moz.build
+++ b/dom/src/notification/moz.build
@@ -1,16 +1,25 @@
 # -*- 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/.
 
 MODULE = 'dom'
 
+EXTRA_COMPONENTS += [
+    'NotificationStorage.js',
+    'NotificationStorage.manifest',
+]
+
+EXTRA_JS_MODULES += [
+    'NotificationDB.jsm'
+]
+
 EXPORTS.mozilla.dom += [
     'DesktopNotification.h',
     'Notification.h',
 ]
 
 CPP_SOURCES += [
     'DesktopNotification.cpp',
     'Notification.cpp',
--- a/dom/tests/mochitest/moz.build
+++ b/dom/tests/mochitest/moz.build
@@ -24,12 +24,8 @@ DIRS += [
     'notification',
     'webapps',
     'webcomponents',
 ]
 
 if CONFIG['MOZ_GAMEPAD']:
    DIRS += ['gamepad']
 
-#needs IPC support, also tests do not run successfully in Firefox for now
-#if CONFIG['MOZ_BUILD_APP'] != 'mobile':
-#    DIRS += ['notification']
-
new file mode 100644
--- /dev/null
+++ b/dom/tests/mochitest/notification/MockServices.js
@@ -0,0 +1,81 @@
+var MockServices = (function () {
+  "use strict";
+
+  const MOCK_ALERTS_CID = SpecialPowers.wrap(SpecialPowers.Components)
+                          .ID("{48068bc2-40ab-4904-8afd-4cdfb3a385f3}");
+  const ALERTS_SERVICE_CONTRACT_ID = "@mozilla.org/alerts-service;1";
+
+  const MOCK_SYSTEM_ALERTS_CID = SpecialPowers.wrap(SpecialPowers.Components)
+                                 .ID("{e86d888c-e41b-4b78-9104-2f2742a532de}");
+  const SYSTEM_ALERTS_SERVICE_CONTRACT_ID = "@mozilla.org/system-alerts-service;1";
+
+  var registrar = SpecialPowers.wrap(SpecialPowers.Components).manager
+                  .QueryInterface(SpecialPowers.Ci.nsIComponentRegistrar);
+
+  var activeNotifications = Object.create(null);
+
+  var mockAlertsService = {
+    showAlertNotification: function(imageUrl, title, text, textClickable,
+                                    cookie, alertListener, name) {
+      var listener = SpecialPowers.wrap(alertListener);
+      activeNotifications[name] = {
+        listener: listener,
+        cookie: cookie
+      };
+
+      // fake async alert show event
+      setTimeout(function () {
+        listener.observe(null, "alertshow", cookie);
+      }, 100);
+
+      // ?? SpecialPowers.wrap(alertListener).observe(null, "alertclickcallback", cookie);
+    },
+
+    showAppNotification: function(imageUrl, title, text, textClickable,
+                                  manifestURL, alertListener, name) {
+      this.showAlertNotification(imageUrl, title, text, textClickable, "", alertListener, name);
+    },
+
+    closeAlert: function(name) {
+      var notification = activeNotifications[name];
+      if (notification) {
+        notification.listener.observe(null, "alertfinished", notification.cookie);
+        delete activeNotifications[name];
+      }
+    },
+
+    QueryInterface: function(aIID) {
+      if (SpecialPowers.wrap(aIID).equals(SpecialPowers.Ci.nsISupports) ||
+          SpecialPowers.wrap(aIID).equals(SpecialPowers.Ci.nsIAlertsService)) {
+        return this;
+      }
+      throw SpecialPowers.Components.results.NS_ERROR_NO_INTERFACE;
+    },
+
+    createInstance: function(aOuter, aIID) {
+      if (aOuter != null) {
+        throw SpecialPowers.Components.results.NS_ERROR_NO_AGGREGATION;
+      }
+      return this.QueryInterface(aIID);
+    }
+  };
+  mockAlertsService = SpecialPowers.wrapCallbackObject(mockAlertsService);
+
+  // MockServices API
+  return {
+    register: function () {
+      registrar.registerFactory(MOCK_ALERTS_CID, "alerts service",
+          ALERTS_SERVICE_CONTRACT_ID,
+          mockAlertsService);
+
+      registrar.registerFactory(MOCK_SYSTEM_ALERTS_CID, "system alerts service",
+          SYSTEM_ALERTS_SERVICE_CONTRACT_ID,
+          mockAlertsService);
+    },
+
+    unregister: function () {
+      registrar.unregisterFactory(MOCK_ALERTS_CID, mockAlertsService);
+      registrar.unregisterFactory(MOCK_SYSTEM_ALERTS_CID, mockAlertsService);
+    },
+  };
+})();
new file mode 100644
--- /dev/null
+++ b/dom/tests/mochitest/notification/NotificationTest.js
@@ -0,0 +1,73 @@
+var NotificationTest = (function () {
+  "use strict";
+
+  function info(msg, name) {
+    SimpleTest.info("::Notification Tests::" + (name || ""), msg);
+  }
+
+  function setup_testing_env() {
+    SimpleTest.waitForExplicitFinish();
+    // turn on testing pref (used by notification.cpp, and mock the alerts
+    SpecialPowers.setBoolPref("notification.prompt.testing", true);
+  }
+
+  function teardown_testing_env() {
+    SimpleTest.finish();
+  }
+
+  function executeTests(tests, callback) {
+    // context is `this` object in test functions
+    // it can be used to track data between tests
+    var context = {};
+
+    (function executeRemainingTests(remainingTests) {
+      if (!remainingTests.length) {
+        return callback();
+      }
+
+      var nextTest = remainingTests.shift();
+      var finishTest = executeRemainingTests.bind(null, remainingTests);
+      var startTest = nextTest.call.bind(nextTest, context, finishTest);
+
+      try {
+        startTest();
+        // if no callback was defined for test function,
+        // we must manually invoke finish to continue
+        if (nextTest.length === 0) {
+          finishTest();
+        }
+      } catch (e) {
+        ok(false, "Test threw exception!");
+        finishTest();
+      }
+    })(tests);
+  }
+
+  // NotificationTest API
+  return {
+    run: function (tests, callback) {
+      setup_testing_env();
+
+      addLoadEvent(function () {
+        executeTests(tests, function () {
+          teardown_testing_env();
+          callback && callback();
+        });
+      });
+    },
+
+    allowNotifications: function () {
+      SpecialPowers.setBoolPref("notification.prompt.testing.allow", true);
+    },
+
+    denyNotifications: function () {
+      SpecialPowers.setBoolPref("notification.prompt.testing.allow", false);
+    },
+
+    clickNotification: function (notification) {
+      // TODO: how??
+    },
+
+    info: info
+  };
+})();
rename from dom/tests/mochitest/notification/create_notification.html
rename to dom/tests/mochitest/notification/desktop-notification/create_notification.html
new file mode 100644
--- /dev/null
+++ b/dom/tests/mochitest/notification/desktop-notification/moz.build
@@ -0,0 +1,6 @@
+# -*- 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/.
+
rename from dom/tests/mochitest/notification/notification_common.js
rename to dom/tests/mochitest/notification/desktop-notification/notification_common.js
rename from dom/tests/mochitest/notification/test_basic_notification.html
rename to dom/tests/mochitest/notification/desktop-notification/test_basic_notification.html
rename from dom/tests/mochitest/notification/test_basic_notification_click.html
rename to dom/tests/mochitest/notification/desktop-notification/test_basic_notification_click.html
rename from dom/tests/mochitest/notification/test_leak_windowClose.html
rename to dom/tests/mochitest/notification/desktop-notification/test_leak_windowClose.html
rename from dom/tests/mochitest/notification/test_notification_tag.html
rename to dom/tests/mochitest/notification/desktop-notification/test_notification_tag.html
rename from dom/tests/mochitest/notification/test_system_principal.xul
rename to dom/tests/mochitest/notification/desktop-notification/test_system_principal.xul
--- a/dom/tests/mochitest/notification/mochitest.ini
+++ b/dom/tests/mochitest/notification/mochitest.ini
@@ -1,10 +1,7 @@
 [DEFAULT]
 support-files =
-  create_notification.html
-  notification_common.js
+  MockServices.js
+  NotificationTest.js
 
-[test_basic_notification.html]
-[test_basic_notification_click.html]
-[test_leak_windowClose.html]
-[test_notification_tag.html]
-[test_web_notifications.html]
+[test_notification_basics.html]
+[test_notification_storage.html]
new file mode 100644
--- /dev/null
+++ b/dom/tests/mochitest/notification/test_notification_basics.html
@@ -0,0 +1,115 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+  <title>Notification Basics</title>
+  <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+  <script type="text/javascript" src="MockServices.js"></script>
+  <script type="text/javascript" src="NotificationTest.js"></script>
+  <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<p id="display"></p>
+<div id="content" style="display: none"></div>
+<pre id="test"></pre>
+<script type="text/javascript">
+
+  var info = NotificationTest.info;
+
+  var steps = [
+    function () {
+      info("Test notification spec");
+      ok(Notification, "Notification constructor exists");
+      ok(Notification.permission, "Notification.permission exists");
+      ok(Notification.requestPermission, "Notification.requestPermission exists");
+      ok(Notification.get, "Notification.get exists");
+    },
+
+    function () {
+      info("Test blank requestPermission");
+      Notification.requestPermission();
+    },
+
+    function (done) {
+      info("Test requestPermission deny");
+      NotificationTest.denyNotifications();
+      Notification.requestPermission(function(perm) {
+        is(perm, "denied", "Permission should be denied.");
+        is(Notification.permission, "denied", "Permission should be denied.");
+        done();
+      });
+    },
+
+    function (done) {
+      info("Test requestPermission grant");
+      NotificationTest.allowNotifications();
+      Notification.requestPermission(function (perm) {
+        is(perm, "granted", "Permission should be granted.");
+        is(Notification.permission, "granted", "Permission should be granted");
+        done();
+      });
+    },
+
+    function () {
+      info("Test invalid requestPermission");
+      try {
+        Notification.requestPermission({});
+        ok(false, "Non callable arg to requestPermission should throw");
+      } catch (e) {
+        ok(true, "Non callable arg to requestPermission should throw");
+      }
+    },
+
+    function (done) {
+      info("Test create notification");
+
+      var options = {
+        dir: "auto",
+        lang: "",
+        body: "This is a notification body",
+        tag: "sometag",
+        icon: "icon.png"
+      };
+      var notification = new Notification("This is a title", options);
+
+      ok(notification, "Notification exists");
+      is(notification.onclick, null, "onclick() should be null");
+      is(notification.onshow, null, "onshow() should be null");
+      is(notification.onerror, null, "onerror() should be null");
+      is(notification.onclose, null, "onclose() should be null");
+      is(typeof notification.close, "function", "close() should exist");
+
+      is(notification.dir, options.dir, "auto should get set");
+      is(notification.lang, options.lang, "lang should get set");
+      is(notification.body, options.body, "body should get set");
+      is(notification.tag, options.tag, "tag should get set");
+      is(notification.icon, options.icon, "icon should get set");
+
+      // store notification in test context
+      this.notification = notification;
+
+      notification.onshow = function () {
+        ok(true, "onshow handler should be called");
+        done();
+      };
+    },
+
+    function (done) {
+      info("Test closing a notification");
+      var notification = this.notification;
+
+      notification.onclose = function () {
+        ok(true, "onclose handler should be called");
+        done();
+      };
+
+      notification.close();
+    },
+  ];
+
+  MockServices.register();
+  NotificationTest.run(steps, function () {
+    MockServices.unregister();
+  });
+</script>
+</body>
+</html>
new file mode 100644
--- /dev/null
+++ b/dom/tests/mochitest/notification/test_notification_storage.html
@@ -0,0 +1,132 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+  <title>Notification Basics</title>
+  <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+  <script type="text/javascript" src="MockServices.js"></script>
+  <script type="text/javascript" src="NotificationTest.js"></script>
+  <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<p id="display"></p>
+<div id="content" style="display: none"></div>
+<pre id="test"></pre>
+<script type="text/javascript">
+
+  function deleteAllNotifications() {
+    var promise = Notification.get();
+    promise.then(function (notifications) {
+      notifications.forEach(function(notification) {
+        notification.close();
+      });
+    });
+  }
+
+  var info = NotificationTest.info;
+
+  var steps = [
+    function (done) {
+      info("Test that Notifcation.get fulfills the promise");
+      var promise = Notification.get();
+      ok(promise.then, "should return a promise");
+
+      // Create a new notification to make sure
+      // Notification.get() works while creating
+      var notification = new Notification("this is a test");
+
+      promise.then(function () {
+        ok(true, "promise should be fulfilled");
+        done();
+      });
+    },
+
+    deleteAllNotifications,
+
+    function (done) {
+      info("Test adding a notification, and making sure get returns it");
+      NotificationTest.allowNotifications();
+      var options = {
+        dir: "auto",
+        lang: "",
+        body: "This is a notification body",
+        tag: "sometag",
+        icon: "icon.png"
+      };
+      var notification = new Notification("This is a title", options);
+      var promise = Notification.get();
+      promise.then(function (notifications) {
+        ok(notifications.length, "should return notifications");
+        for (var i = 0; i < notifications.length; i++) {
+          var notification = notifications[i];
+          if (notification.tag === options.tag) {
+            ok(true, "should contain newly created notification");
+            for (var key in options) {
+              is(notification[key], options[key], key + " property should match");
+            }
+            notification.close();
+            return;
+          }
+        }
+        ok(false, "should contain newly created notification");
+        notification.close();
+      });
+      notification.onclose = done;
+    },
+
+    function (done) {
+      info("Testing fetching notification by tag filter");
+      var n1 = new Notification("title1", {tag: "tag1"});
+      var n2 = new Notification("title2", {tag: "tag2"});
+      var n3 = new Notification("title3", {tag: "tag3"});
+      var promise = Notification.get({tag: "tag3"});
+      promise.then(function (notifications) {
+        var notification = notifications[0];
+        is(notifications.length, 1, "should return 1 notification");
+        is(notifications[0].title, "title3", "titles should match");
+        is(notifications[0].tag, "tag3", "tags should match");
+        var closeCount = 0;
+        var waitForAll = function () {
+          if (++closeCount >= 3) {
+            done();
+          }
+        };
+        n1.onclose = waitForAll;
+        n2.onclose = waitForAll;
+        n3.onclose = waitForAll;
+        n1.close();
+        n2.close();
+        n3.close();
+      });
+    },
+
+    deleteAllNotifications,
+
+    function (done) {
+      info("Testing fetching no notifications");
+      var promise = Notification.get();
+      promise.then(function (notifications) {
+        is(notifications.length, 0, "should return 0 notifications");
+        done();
+      });
+    },
+
+    function (done) {
+      info("Testing fetching multiple notifications");
+      var n1 = new Notification("title1");
+      var n2 = new Notification("title2");
+      var n3 = new Notification("title3");
+      var promise = Notification.get();
+      promise.then(function (notifications) {
+        is(notifications.length, 3, "should return 2 notifications");
+        done();
+      });
+    }
+  ];
+
+  MockServices.register();
+  NotificationTest.run(steps, function () {
+    MockServices.unregister();
+  });
+</script>
+</body>
+</html>
deleted file mode 100644
--- a/dom/tests/mochitest/notification/test_web_notifications.html
+++ /dev/null
@@ -1,100 +0,0 @@
-<!DOCTYPE HTML>
-<html>
-<!--
-https://bugzilla.mozilla.org/show_bug.cgi?id=782211
--->
-<head>
-  <title>Bug 782211</title>
-  <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
-  <script type="text/javascript" src="notification_common.js"></script>
-  <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
-</head>
-<body>
-<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=782211">Bug 782211</a>
-<p id="display"></p>
-<div id="content" style="display: none">
-</div>
-<pre id="test">
-</pre>
-<script type="text/javascript">
-  if (window.Notification) {
-    SimpleTest.waitForExplicitFinish();
-
-    function showNotifications() {
-      // Make sure callback is called.
-      Notification.requestPermission(function(perm) {
-        is(perm, "granted", "Permission should be granted.");
-        is(Notification.permission, "granted", "Permission should be granted.");
-        callbackCalled();
-      });
-
-      // Make sure nothing bad happens when requestPermission is called without a callback.
-      Notification.requestPermission();
-
-      try {
-        Notification.requestPermission({});
-        ok(false, "Non callable arugment to request permission should throw exception.");
-      } catch (ex) {
-        ok(true, "Non callable arugment to request permission should throw exception.");
-      }
-
-      var title = "This is a title";
-
-      var notification = new Notification(title);
-
-      is(notification.title, title, "Title should be set");
-      is(notification.dir, "auto", "Dir should default to 'auto'");
-      is(notification.lang, "", "Lang should not be set");
-      is(notification.body, "", "Body should not be set");
-      is(notification.tag, "", "Tag should not be set");
-
-      var options = {
-        dir: "auto",
-        lang: "",
-        body: "This is a notification body",
-        tag: "sometag"
-      };
-
-      var notification = new Notification(title, options);
-
-      is(notification.title, title, "Title should be set");
-      is(notification.dir, options.dir, "Dir should be set");
-      is(notification.lang, options.lang, "Lang should be set");
-      is(notification.body, options.body, "Body should be set");
-      is(notification.tag, options.tag, "Tag should be set");
-
-      notification.onclose = function() {
-        ok(true, "Notification should be closed.");
-        callbackCalled();
-      };
-
-      notification.onshow = function() {
-        ok(true, "Notification should be shown.");
-        notification.close();
-        callbackCalled();
-      };
-
-      notification.onerror = function() {
-        ok(false, "Failed to show notification.");
-        reset_notifications();
-        SimpleTest.finish();
-      };
-
-      var numCallbacksCalled = 0;
-
-      function callbackCalled() {
-        numCallbacksCalled++;
-        if (numCallbacksCalled == 3) {
-          reset_notifications();
-          SimpleTest.finish();
-        }
-      }
-    }
-
-    setup_notifications(true, true, showNotifications);
-  } else {
-    ok(true, "Notifications are not enabled on the platform.");
-  }
-</script>
-</body>
-</html>
new file mode 100644
--- /dev/null
+++ b/dom/webidl/AppInfo.webidl
@@ -0,0 +1,12 @@
+/* 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 dictionnary holds the parameters supporting the app:// protocol.
+  */
+dictionary AppInfo
+{
+  DOMString path = "";
+  boolean   isCoreApp = false;
+};
--- a/dom/webidl/DummyBinding.webidl
+++ b/dom/webidl/DummyBinding.webidl
@@ -23,13 +23,14 @@ interface DummyInterface : EventTarget {
   void MmsParameters(optional MmsParameters arg);
   void MmsAttachment(optional MmsAttachment arg);
   void AsyncScrollEventDetail(optional AsyncScrollEventDetail arg);
   void OpenWindowEventDetail(optional OpenWindowEventDetail arg);
   void DOMWindowResizeEventDetail(optional DOMWindowResizeEventDetail arg);
   void WifiOptions(optional WifiCommandOptions arg1,
                    optional WifiResultOptions arg2);
   void AppNotificationServiceOptions(optional AppNotificationServiceOptions arg);
+  void AppInfo(optional AppInfo arg1);
 };
 
 interface DummyInterfaceWorkers {
   BlobPropertyBag blobBag();
 };
--- a/dom/webidl/Notification.webidl
+++ b/dom/webidl/Notification.webidl
@@ -14,16 +14,19 @@
 [PrefControlled, Constructor(DOMString title, optional NotificationOptions options)]
 interface Notification : EventTarget {
   [GetterThrows]
   static readonly attribute NotificationPermission permission;
 
   [Throws]
   static void requestPermission(optional NotificationPermissionCallback permissionCallback);
 
+  [Throws]
+  static Promise get(optional GetNotificationOptions filter);
+
   attribute EventHandler onclick;
 
   attribute EventHandler onshow;
 
   attribute EventHandler onerror;
 
   attribute EventHandler onclose;
 
@@ -47,18 +50,22 @@ interface Notification : EventTarget {
 
   void close();
 };
 
 dictionary NotificationOptions {
   NotificationDirection dir = "auto";
   DOMString lang = "";
   DOMString body = "";
+  DOMString tag = "";
+  DOMString icon = "";
+};
+
+dictionary GetNotificationOptions {
   DOMString tag;
-  DOMString icon = "";
 };
 
 enum NotificationPermission {
   "default",
   "denied",
   "granted"
 };
 
--- a/dom/webidl/moz.build
+++ b/dom/webidl/moz.build
@@ -14,16 +14,17 @@ PREPROCESSED_WEBIDL_FILES = [
     'Crypto.webidl',
     'Navigator.webidl',
 ]
 
 WEBIDL_FILES = [
     'AbstractWorker.webidl',
     'AnalyserNode.webidl',
     'AnimationEvent.webidl',
+    'AppInfo.webidl',
     'AppNotificationServiceOptions.webidl',
     'ArchiveReader.webidl',
     'ArchiveRequest.webidl',
     'Attr.webidl',
     'AudioBuffer.webidl',
     'AudioBufferSourceNode.webidl',
     'AudioContext.webidl',
     'AudioDestinationNode.webidl',
--- a/gfx/layers/ipc/AsyncPanZoomController.cpp
+++ b/gfx/layers/ipc/AsyncPanZoomController.cpp
@@ -168,24 +168,24 @@ static float gYSkateSizeMultiplier = 3.5
  * case that a user is reading a page that scrolls up/down. Note that one,
  * both or neither of these may be used at any instant.
  */
 static float gXStationarySizeMultiplier = 1.5f;
 static float gYStationarySizeMultiplier = 2.5f;
 
 /**
  * The time period in ms that throttles mozbrowserasyncscroll event.
- * Default is 100ms if there is no "apzc.asyncscroll.throttle" in preference.
+ * Default is 100ms if there is no "apz.asyncscroll.throttle" in preference.
  */
 
 static int gAsyncScrollThrottleTime = 100;
 
 /**
  * The timeout in ms for mAsyncScrollTimeoutTask delay task.
- * Default is 300ms if there is no "apzc.asyncscroll.timeout" in preference.
+ * Default is 300ms if there is no "apz.asyncscroll.timeout" in preference.
  */
 static int gAsyncScrollTimeout = 300;
 
 /**
  * Temporary pref for disabling zoom in metrofx on aurora.
  */
 static bool gAsyncZoomDisabled = false;
 
@@ -231,31 +231,31 @@ AsyncPanZoomController::InitializeGlobal
 {
   MOZ_ASSERT(NS_IsMainThread());
 
   static bool sInitialized = false;
   if (sInitialized)
     return;
   sInitialized = true;
 
-  Preferences::AddIntVarCache(&gPanRepaintInterval, "gfx.azpc.pan_repaint_interval", gPanRepaintInterval);
-  Preferences::AddIntVarCache(&gFlingRepaintInterval, "gfx.azpc.fling_repaint_interval", gFlingRepaintInterval);
-  Preferences::AddFloatVarCache(&gMinSkateSpeed, "gfx.azpc.min_skate_speed", gMinSkateSpeed);
-  Preferences::AddIntVarCache(&gTouchListenerTimeout, "gfx.azpc.touch_listener_timeout", gTouchListenerTimeout);
-  Preferences::AddIntVarCache(&gNumPaintDurationSamples, "gfx.azpc.num_paint_duration_samples", gNumPaintDurationSamples);
-  Preferences::AddFloatVarCache(&gTouchStartTolerance, "gfx.azpc.touch_start_tolerance", gTouchStartTolerance);
-  Preferences::AddFloatVarCache(&gXSkateSizeMultiplier, "gfx.azpc.x_skate_size_multiplier", gXSkateSizeMultiplier);
-  Preferences::AddFloatVarCache(&gYSkateSizeMultiplier, "gfx.azpc.y_skate_size_multiplier", gYSkateSizeMultiplier);
-  Preferences::AddFloatVarCache(&gXStationarySizeMultiplier, "gfx.azpc.x_stationary_size_multiplier", gXStationarySizeMultiplier);
-  Preferences::AddFloatVarCache(&gYStationarySizeMultiplier, "gfx.azpc.y_stationary_size_multiplier", gYStationarySizeMultiplier);
-  Preferences::AddIntVarCache(&gAsyncScrollThrottleTime, "apzc.asyncscroll.throttle", gAsyncScrollThrottleTime);
-  Preferences::AddIntVarCache(&gAsyncScrollTimeout, "apzc.asyncscroll.timeout", gAsyncScrollTimeout);
-  Preferences::AddBoolVarCache(&gAsyncZoomDisabled, "apzc.asynczoom.disabled", gAsyncZoomDisabled);
-  Preferences::AddBoolVarCache(&gCrossSlideEnabled, "apzc.cross_slide.enabled", gCrossSlideEnabled);
-  Preferences::AddIntVarCache(&gAxisLockMode, "apzc.axis_lock_mode", gAxisLockMode);
+  Preferences::AddIntVarCache(&gPanRepaintInterval, "apz.pan_repaint_interval", gPanRepaintInterval);
+  Preferences::AddIntVarCache(&gFlingRepaintInterval, "apz.fling_repaint_interval", gFlingRepaintInterval);
+  Preferences::AddFloatVarCache(&gMinSkateSpeed, "apz.min_skate_speed", gMinSkateSpeed);
+  Preferences::AddIntVarCache(&gTouchListenerTimeout, "apz.touch_listener_timeout", gTouchListenerTimeout);
+  Preferences::AddIntVarCache(&gNumPaintDurationSamples, "apz.num_paint_duration_samples", gNumPaintDurationSamples);
+  Preferences::AddFloatVarCache(&gTouchStartTolerance, "apz.touch_start_tolerance", gTouchStartTolerance);
+  Preferences::AddFloatVarCache(&gXSkateSizeMultiplier, "apz.x_skate_size_multiplier", gXSkateSizeMultiplier);
+  Preferences::AddFloatVarCache(&gYSkateSizeMultiplier, "apz.y_skate_size_multiplier", gYSkateSizeMultiplier);
+  Preferences::AddFloatVarCache(&gXStationarySizeMultiplier, "apz.x_stationary_size_multiplier", gXStationarySizeMultiplier);
+  Preferences::AddFloatVarCache(&gYStationarySizeMultiplier, "apz.y_stationary_size_multiplier", gYStationarySizeMultiplier);
+  Preferences::AddIntVarCache(&gAsyncScrollThrottleTime, "apz.asyncscroll.throttle", gAsyncScrollThrottleTime);
+  Preferences::AddIntVarCache(&gAsyncScrollTimeout, "apz.asyncscroll.timeout", gAsyncScrollTimeout);
+  Preferences::AddBoolVarCache(&gAsyncZoomDisabled, "apz.asynczoom.disabled", gAsyncZoomDisabled);
+  Preferences::AddBoolVarCache(&gCrossSlideEnabled, "apz.cross_slide.enabled", gCrossSlideEnabled);
+  Preferences::AddIntVarCache(&gAxisLockMode, "apz.axis_lock_mode", gAxisLockMode);
 
   gComputedTimingFunction = new ComputedTimingFunction();
   gComputedTimingFunction->Init(
     nsTimingFunction(NS_STYLE_TRANSITION_TIMING_FUNCTION_EASE));
   ClearOnShutdown(&gComputedTimingFunction);
 }
 
 AsyncPanZoomController::AsyncPanZoomController(uint64_t aLayersId,
--- a/gfx/layers/ipc/Axis.cpp
+++ b/gfx/layers/ipc/Axis.cpp
@@ -62,22 +62,22 @@ static float gFlingStoppedThreshold = 0.
  * Maximum size of velocity queue. The queue contains last N velocity records.
  * On touch end we calculate the average velocity in order to compensate
  * touch/mouse drivers misbehaviour.
  */
 static int gMaxVelocityQueueSize = 5;
 
 static void ReadAxisPrefs()
 {
-  Preferences::AddFloatVarCache(&gMaxEventAcceleration, "gfx.axis.max_event_acceleration", gMaxEventAcceleration);
-  Preferences::AddFloatVarCache(&gFlingFriction, "gfx.axis.fling_friction", gFlingFriction);
-  Preferences::AddFloatVarCache(&gVelocityThreshold, "gfx.axis.velocity_threshold", gVelocityThreshold);
-  Preferences::AddFloatVarCache(&gAccelerationMultiplier, "gfx.axis.acceleration_multiplier", gAccelerationMultiplier);
-  Preferences::AddFloatVarCache(&gFlingStoppedThreshold, "gfx.axis.fling_stopped_threshold", gFlingStoppedThreshold);
-  Preferences::AddIntVarCache(&gMaxVelocityQueueSize, "gfx.axis.max_velocity_queue_size", gMaxVelocityQueueSize);
+  Preferences::AddFloatVarCache(&gMaxEventAcceleration, "apz.max_event_acceleration", gMaxEventAcceleration);
+  Preferences::AddFloatVarCache(&gFlingFriction, "apz.fling_friction", gFlingFriction);
+  Preferences::AddFloatVarCache(&gVelocityThreshold, "apz.velocity_threshold", gVelocityThreshold);
+  Preferences::AddFloatVarCache(&gAccelerationMultiplier, "apz.acceleration_multiplier", gAccelerationMultiplier);
+  Preferences::AddFloatVarCache(&gFlingStoppedThreshold, "apz.fling_stopped_threshold", gFlingStoppedThreshold);
+  Preferences::AddIntVarCache(&gMaxVelocityQueueSize, "apz.max_velocity_queue_size", gMaxVelocityQueueSize);
 }
 
 class ReadAxisPref MOZ_FINAL : public nsRunnable {
 public:
   NS_IMETHOD Run()
   {
     ReadAxisPrefs();
     return NS_OK;
--- a/mobile/android/chrome/content/browser.js
+++ b/mobile/android/chrome/content/browser.js
@@ -13,16 +13,17 @@ let Cr = Components.results;
 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
 Cu.import("resource://gre/modules/Services.jsm");
 Cu.import("resource://gre/modules/AddonManager.jsm");
 Cu.import("resource://gre/modules/FileUtils.jsm");
 Cu.import("resource://gre/modules/JNI.jsm");
 Cu.import('resource://gre/modules/Payment.jsm');
 Cu.import("resource://gre/modules/PermissionPromptHelper.jsm");
 Cu.import("resource://gre/modules/ContactService.jsm");
+Cu.import("resource://gre/modules/NotificationDB.jsm");
 Cu.import("resource://gre/modules/SpatialNavigation.jsm");
 
 #ifdef ACCESSIBILITY
 Cu.import("resource://gre/modules/accessibility/AccessFu.jsm");
 #endif
 
 XPCOMUtils.defineLazyModuleGetter(this, "PluralForm",
                                   "resource://gre/modules/PluralForm.jsm");
--- a/mobile/android/installer/package-manifest.in
+++ b/mobile/android/installer/package-manifest.in
@@ -277,16 +277,18 @@
 
 ; JavaScript components
 @BINPATH@/components/ConsoleAPI.manifest
 @BINPATH@/components/ConsoleAPI.js
 @BINPATH@/components/ContactManager.js
 @BINPATH@/components/ContactManager.manifest
 @BINPATH@/components/PhoneNumberService.js
 @BINPATH@/components/PhoneNumberService.manifest
+@BINPATH@/components/NotificationStorage.js
+@BINPATH@/components/NotificationStorage.manifest
 @BINPATH@/components/SettingsManager.js
 @BINPATH@/components/SettingsManager.manifest
 @BINPATH@/components/SettingsService.js
 @BINPATH@/components/SettingsService.manifest
 @BINPATH@/components/BrowserElementParent.manifest
 @BINPATH@/components/BrowserElementParent.js
 @BINPATH@/components/FeedProcessor.manifest
 @BINPATH@/components/FeedProcessor.js
@@ -380,18 +382,16 @@
 @BINPATH@/components/nsPrompter.manifest
 @BINPATH@/components/nsPrompter.js
 @BINPATH@/components/TelemetryPing.js
 @BINPATH@/components/TelemetryPing.manifest
 @BINPATH@/components/Webapps.js
 @BINPATH@/components/Webapps.manifest
 @BINPATH@/components/AppsService.js
 @BINPATH@/components/AppsService.manifest
-@BINPATH@/components/AppProtocolHandler.js
-@BINPATH@/components/AppProtocolHandler.manifest
 
 @BINPATH@/components/Push.js
 @BINPATH@/components/Push.manifest
 
 @BINPATH@/components/SystemMessageInternal.js
 @BINPATH@/components/SystemMessageManager.js
 @BINPATH@/components/SystemMessageManager.manifest
 
--- a/modules/libpref/src/init/all.js
+++ b/modules/libpref/src/init/all.js
@@ -282,17 +282,17 @@ pref("media.video_stats.enabled", true);
 
 // Whether to enable the audio writing APIs on the audio element
 pref("media.audio_data.enabled", true);
 
 // Whether to lock touch scrolling to one axis at a time
 // 0 = FREE (No locking at all)
 // 1 = STANDARD (Once locked, remain locked until scrolling ends)
 // 2 = STICKY (Allow lock to be broken, with hysteresis)
-pref("apzc.axis_lock_mode", 0);
+pref("apz.axis_lock_mode", 0);
 
 #ifdef XP_MACOSX
 // Whether to run in native HiDPI mode on machines with "Retina"/HiDPI display;
 //   <= 0 : hidpi mode disabled, display will just use pixel-based upscaling
 //   == 1 : hidpi supported if all screens share the same backingScaleFactor
 //   >= 2 : hidpi supported even with mixed backingScaleFactors (somewhat broken)
 pref("gfx.hidpi.enabled", 2);
 #endif
--- a/netwerk/build/Makefile.in
+++ b/netwerk/build/Makefile.in
@@ -64,16 +64,17 @@ LOCAL_INCLUDES = \
   -I$(srcdir)/../base/src \
   -I$(srcdir)/../dns \
   -I$(srcdir)/../socket \
   -I$(srcdir)/../streamconv/src \
   -I$(srcdir)/../streamconv/converters \
   -I$(srcdir)/../mime \
   -I$(srcdir)/../cache \
   -I$(srcdir)/../protocol/about \
+  -I$(srcdir)/../protocol/app \
   -I../dns \
   $(foreach d,$(filter-out about,$(NECKO_PROTOCOLS)), \
     -I$(srcdir)/../protocol/$(d)) \
   $(NULL)
 
 ifeq ($(OS_ARCH),WINNT)
     LOCAL_INCLUDES += -I$(srcdir)/../system/win32
 endif
--- a/netwerk/build/nsNetCID.h
+++ b/netwerk/build/nsNetCID.h
@@ -598,16 +598,28 @@
 { /* fbc81170-1f69-11d3-9344-00104ba0fd40 */         \
     0xfbc81170,                                      \
     0x1f69,                                          \
     0x11d3,                                          \
     {0x93, 0x44, 0x00, 0x10, 0x4b, 0xa0, 0xfd, 0x40} \
 }
 
 /******************************************************************************
+ * netwerk/protocol/app/ classes
+ */
+
+#define NS_APPPROTOCOLHANDLER_CID                    \
+{ /* {B6ED3030-9999-11d3-A178-0050041CAF44} */       \
+    0xb6ed3030,                                      \
+    0x9999,                                          \
+    0x11d3,                                          \
+    {0xa1, 0x78, 0x00, 0x50, 0x04, 0x1c, 0xaf, 0x44} \
+}
+
+/******************************************************************************
  * netwerk/protocol/data/ classes
  */
 
 #define NS_DATAPROTOCOLHANDLER_CID                   \
 { /* {B6ED3030-6183-11d3-A178-0050041CAF44} */       \
     0xb6ed3030,                                      \
     0x6183,                                          \
     0x11d3,                                          \
--- a/netwerk/build/nsNetModule.cpp
+++ b/netwerk/build/nsNetModule.cpp
@@ -238,16 +238,17 @@ NS_GENERIC_FACTORY_CONSTRUCTOR(nsHttpDig
 #endif // !NECKO_PROTOCOL_http
 
 #include "mozilla/net/Dashboard.h"
 namespace mozilla {
 namespace net {
   NS_GENERIC_FACTORY_CONSTRUCTOR(Dashboard)
 }
 }
+#include "AppProtocolHandler.h"
 
 #ifdef NECKO_PROTOCOL_res
 // resource
 #include "nsResProtocolHandler.h"
 NS_GENERIC_FACTORY_CONSTRUCTOR_INIT(nsResProtocolHandler, Init)
 NS_GENERIC_FACTORY_CONSTRUCTOR(nsResURL)
 #endif
 
@@ -767,16 +768,17 @@ NS_DEFINE_NAMED_CID(NS_ABOUT_CACHE_ENTRY
 #endif
 NS_DEFINE_NAMED_CID(NS_SOCKSSOCKETPROVIDER_CID);
 NS_DEFINE_NAMED_CID(NS_SOCKS4SOCKETPROVIDER_CID);
 NS_DEFINE_NAMED_CID(NS_UDPSOCKETPROVIDER_CID);
 NS_DEFINE_NAMED_CID(NS_CACHESERVICE_CID);
 NS_DEFINE_NAMED_CID(NS_APPLICATIONCACHESERVICE_CID);
 NS_DEFINE_NAMED_CID(NS_APPLICATIONCACHENAMESPACE_CID);
 NS_DEFINE_NAMED_CID(NS_APPLICATIONCACHE_CID);
+NS_DEFINE_NAMED_CID(NS_APPPROTOCOLHANDLER_CID);
 #ifdef NECKO_COOKIES
 NS_DEFINE_NAMED_CID(NS_COOKIEMANAGER_CID);
 NS_DEFINE_NAMED_CID(NS_COOKIESERVICE_CID);
 #endif
 #ifdef NECKO_WIFI
 NS_DEFINE_NAMED_CID(NS_WIFI_MONITOR_COMPONENT_CID);
 #endif
 #ifdef NECKO_PROTOCOL_data
@@ -905,16 +907,17 @@ static const mozilla::Module::CIDEntry k
 #endif
     { &kNS_SOCKSSOCKETPROVIDER_CID, false, nullptr, nsSOCKSSocketProvider::CreateV5 },
     { &kNS_SOCKS4SOCKETPROVIDER_CID, false, nullptr, nsSOCKSSocketProvider::CreateV4 },
     { &kNS_UDPSOCKETPROVIDER_CID, false, nullptr, nsUDPSocketProviderConstructor },
     { &kNS_CACHESERVICE_CID, false, nullptr, nsCacheService::Create },
     { &kNS_APPLICATIONCACHESERVICE_CID, false, nullptr, nsApplicationCacheServiceConstructor },
     { &kNS_APPLICATIONCACHENAMESPACE_CID, false, nullptr, nsApplicationCacheNamespaceConstructor },
     { &kNS_APPLICATIONCACHE_CID, false, nullptr, nsApplicationCacheConstructor },
+    { &kNS_APPPROTOCOLHANDLER_CID, false, nullptr, AppProtocolHandler::Create },
 #ifdef NECKO_COOKIES
     { &kNS_COOKIEMANAGER_CID, false, nullptr, nsICookieServiceConstructor },
     { &kNS_COOKIESERVICE_CID, false, nullptr, nsICookieServiceConstructor },
 #endif
 #ifdef NECKO_WIFI
     { &kNS_WIFI_MONITOR_COMPONENT_CID, false, nullptr, nsWifiMonitorConstructor },
 #endif
 #ifdef NECKO_PROTOCOL_data
@@ -1050,16 +1053,17 @@ static const mozilla::Module::ContractID
 #endif
     { NS_NETWORK_SOCKET_CONTRACTID_PREFIX "socks", &kNS_SOCKSSOCKETPROVIDER_CID },
     { NS_NETWORK_SOCKET_CONTRACTID_PREFIX "socks4", &kNS_SOCKS4SOCKETPROVIDER_CID },
     { NS_NETWORK_SOCKET_CONTRACTID_PREFIX "udp", &kNS_UDPSOCKETPROVIDER_CID },
     { NS_CACHESERVICE_CONTRACTID, &kNS_CACHESERVICE_CID },
     { NS_APPLICATIONCACHESERVICE_CONTRACTID, &kNS_APPLICATIONCACHESERVICE_CID },
     { NS_APPLICATIONCACHENAMESPACE_CONTRACTID, &kNS_APPLICATIONCACHENAMESPACE_CID },
     { NS_APPLICATIONCACHE_CONTRACTID, &kNS_APPLICATIONCACHE_CID },
+    { NS_NETWORK_PROTOCOL_CONTRACTID_PREFIX "app", &kNS_APPPROTOCOLHANDLER_CID },
 #ifdef NECKO_COOKIES
     { NS_COOKIEMANAGER_CONTRACTID, &kNS_COOKIEMANAGER_CID },
     { NS_COOKIESERVICE_CONTRACTID, &kNS_COOKIESERVICE_CID },
 #endif
 #ifdef NECKO_WIFI
     { NS_WIFI_MONITOR_CONTRACTID, &kNS_WIFI_MONITOR_COMPONENT_CID },
 #endif
 #ifdef NECKO_PROTOCOL_data
new file mode 100644
--- /dev/null
+++ b/netwerk/protocol/app/AppProtocolHandler.cpp
@@ -0,0 +1,424 @@
+/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */
+/* vim:set expandtab ts=4 sw=4 sts=4 cin: */
+/* 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 "AppProtocolHandler.h"
+#include "nsBaseChannel.h"
+#include "nsJARChannel.h"
+#include "nsNetCID.h"
+#include "nsIAppsService.h"
+#include "nsCxPusher.h"
+#include "nsXULAppAPI.h"
+
+/**
+  * This dummy channel implementation only provides enough functionality
+  * to return a fake 404 error when the caller asks for an app:// URL
+  * containing an unknown appId.
+  */
+class DummyChannel : public nsIJARChannel
+                          , nsRunnable
+{
+public:
+  NS_DECL_ISUPPORTS
+  NS_DECL_NSIREQUEST
+  NS_DECL_NSICHANNEL
+  NS_DECL_NSIJARCHANNEL
+
+  DummyChannel();
+
+  NS_IMETHODIMP Run();
+
+private:
+  bool                        mPending;
+  uint32_t                    mSuspendCount;
+  nsCOMPtr<nsISupports>       mListenerContext;
+  nsCOMPtr<nsIStreamListener> mListener;
+  nsCOMPtr<nsILoadGroup>      mLoadGroup;
+  nsLoadFlags                 mLoadFlags;
+};
+
+NS_IMPL_ISUPPORTS3(DummyChannel, nsIRequest, nsIChannel, nsIJARChannel)
+
+DummyChannel::DummyChannel() : mPending(false)
+                             , mSuspendCount(0)
+                             , mLoadFlags(LOAD_NORMAL)
+{
+}
+
+NS_IMETHODIMP DummyChannel::GetName(nsACString &result)
+{
+  result = "dummy_app_channel";
+  return NS_OK;
+}
+
+NS_IMETHODIMP DummyChannel::GetStatus(nsresult *aStatus)
+{
+  *aStatus = NS_ERROR_FILE_NOT_FOUND;
+  return NS_OK;
+}
+
+NS_IMETHODIMP DummyChannel::IsPending(bool *aResult)
+{
+  *aResult = mPending;
+  return NS_OK;
+}
+
+NS_IMETHODIMP DummyChannel::Suspend()
+{
+  mSuspendCount++;
+  return NS_OK;
+}
+
+NS_IMETHODIMP DummyChannel::Resume()
+{
+  if (mSuspendCount <= 0) {
+    return NS_ERROR_UNEXPECTED;
+  }
+
+  if (--mSuspendCount == 0) {
+    NS_DispatchToMainThread(this, NS_DISPATCH_NORMAL);
+  }
+  return NS_OK;
+}
+
+NS_IMETHODIMP DummyChannel::Open(nsIInputStream**)
+{
+  return NS_ERROR_NOT_IMPLEMENTED;
+}
+
+NS_IMETHODIMP DummyChannel::AsyncOpen(nsIStreamListener* aListener, nsISupports* aContext)
+{
+  mListener = aListener;
+  mListenerContext = aContext;
+  mPending = true;
+
+  if (mLoadGroup) {
+    mLoadGroup->AddRequest(this, aContext);
+  }
+
+  if (mSuspendCount == 0) {
+    NS_DispatchToMainThread(this, NS_DISPATCH_NORMAL);
+  }
+
+  return NS_OK;
+}
+
+// nsIJarChannel, needed for XHR to turn NS_ERROR_FILE_NOT_FOUND into
+// a 404 error.
+NS_IMETHODIMP DummyChannel::GetIsUnsafe(bool *aResult)
+{
+  *aResult = false;
+  return NS_OK;
+}
+
+NS_IMETHODIMP DummyChannel::SetAppURI(nsIURI *aURI)
+{
+  return NS_ERROR_NOT_IMPLEMENTED;
+}
+
+NS_IMETHODIMP DummyChannel::Run()
+{
+  nsresult rv = mListener->OnStartRequest(this, mListenerContext);
+  NS_ENSURE_SUCCESS(rv, rv);
+  mPending = false;
+  rv = mListener->OnStopRequest(this, mListenerContext, NS_ERROR_FILE_NOT_FOUND);
+  NS_ENSURE_SUCCESS(rv, rv);
+  if (mLoadGroup) {
+    mLoadGroup->RemoveRequest(this, mListenerContext, NS_ERROR_FILE_NOT_FOUND);
+  }
+
+  mListener = nullptr;
+  mListenerContext = nullptr;
+  rv = SetNotificationCallbacks(nullptr);
+  NS_ENSURE_SUCCESS(rv, rv);
+
+  return NS_OK;
+}
+
+NS_IMETHODIMP DummyChannel::Cancel(nsresult)
+{
+  return NS_ERROR_NOT_IMPLEMENTED;
+}
+
+NS_IMETHODIMP DummyChannel::GetLoadGroup(nsILoadGroup* *aLoadGroup)
+{
+  *aLoadGroup = mLoadGroup;
+  NS_IF_ADDREF(*aLoadGroup);
+  return NS_OK;
+}
+
+NS_IMETHODIMP DummyChannel::SetLoadGroup(nsILoadGroup* aLoadGroup)
+{
+  mLoadGroup = aLoadGroup;
+  return NS_OK;
+}
+
+NS_IMETHODIMP DummyChannel::GetLoadFlags(nsLoadFlags *aLoadFlags)
+{
+  *aLoadFlags = mLoadFlags;
+  return NS_OK;
+}
+
+NS_IMETHODIMP DummyChannel::SetLoadFlags(nsLoadFlags aLoadFlags)
+{
+  mLoadFlags = aLoadFlags;
+  return NS_OK;
+}
+
+NS_IMETHODIMP DummyChannel::GetOriginalURI(nsIURI**)
+{
+  return NS_ERROR_NOT_IMPLEMENTED;
+}
+
+NS_IMETHODIMP DummyChannel::SetOriginalURI(nsIURI*)
+{
+  return NS_ERROR_NOT_IMPLEMENTED;
+}
+
+NS_IMETHODIMP DummyChannel::GetOwner(nsISupports**)
+{
+  return NS_ERROR_NOT_IMPLEMENTED;
+}
+
+NS_IMETHODIMP DummyChannel::SetOwner(nsISupports*)
+{
+  return NS_ERROR_NOT_IMPLEMENTED;
+}
+
+NS_IMETHODIMP DummyChannel::GetNotificationCallbacks(nsIInterfaceRequestor**)
+{
+  return NS_ERROR_NOT_IMPLEMENTED;
+}
+
+NS_IMETHODIMP DummyChannel::SetNotificationCallbacks(nsIInterfaceRequestor*)
+{
+  return NS_ERROR_NOT_IMPLEMENTED;
+}
+
+NS_IMETHODIMP DummyChannel::GetSecurityInfo(nsISupports**)
+{
+  return NS_ERROR_NOT_IMPLEMENTED;
+}
+
+NS_IMETHODIMP DummyChannel::GetContentType(nsACString&)
+{
+  return NS_ERROR_NOT_IMPLEMENTED;
+}
+
+NS_IMETHODIMP DummyChannel::SetContentType(const nsACString&)
+{
+  return NS_ERROR_NOT_IMPLEMENTED;
+}
+
+NS_IMETHODIMP DummyChannel::GetContentCharset(nsACString&)
+{
+  return NS_ERROR_NOT_IMPLEMENTED;
+}
+
+NS_IMETHODIMP DummyChannel::SetContentCharset(const nsACString&)
+{
+  return NS_ERROR_NOT_IMPLEMENTED;
+}
+
+NS_IMETHODIMP DummyChannel::GetContentLength(int64_t*)
+{
+  return NS_ERROR_NOT_IMPLEMENTED;
+}
+
+NS_IMETHODIMP DummyChannel::SetContentLength(int64_t)
+{
+  return NS_ERROR_NOT_IMPLEMENTED;
+}
+
+NS_IMETHODIMP DummyChannel::GetContentDisposition(uint32_t*)
+{
+  return NS_ERROR_NOT_IMPLEMENTED;
+}
+
+NS_IMETHODIMP DummyChannel::SetContentDisposition(uint32_t)
+{
+  return NS_ERROR_NOT_IMPLEMENTED;
+}
+
+NS_IMETHODIMP DummyChannel::GetURI(nsIURI**)
+{
+  return NS_ERROR_NOT_IMPLEMENTED;
+}
+
+NS_IMETHODIMP DummyChannel::GetContentDispositionFilename(nsAString&)
+{
+  return NS_ERROR_NOT_IMPLEMENTED;
+}
+
+NS_IMETHODIMP DummyChannel::SetContentDispositionFilename(nsAString const &)
+{
+  return NS_ERROR_NOT_IMPLEMENTED;
+}
+
+NS_IMETHODIMP DummyChannel::GetContentDispositionHeader(nsACString&)
+{
+  return NS_ERROR_NOT_IMPLEMENTED;
+}
+
+/**
+  * app:// protocol implementation.
+  */
+
+AppProtocolHandler::AppProtocolHandler() {
+}
+
+AppProtocolHandler::~AppProtocolHandler() {
+  mAppInfoCache.Clear();
+}
+
+NS_IMPL_ISUPPORTS1(AppProtocolHandler, nsIProtocolHandler)
+
+/* static */
+nsresult
+AppProtocolHandler::Create(nsISupports* aOuter,
+                           const nsIID& aIID,
+                           void* *aResult)
+{
+  AppProtocolHandler* ph = new AppProtocolHandler();
+  if (ph == nullptr) {
+    return NS_ERROR_OUT_OF_MEMORY;
+  }
+  NS_ADDREF(ph);
+  nsresult rv = ph->QueryInterface(aIID, aResult);
+  NS_RELEASE(ph);
+  return rv;
+}
+
+NS_IMETHODIMP
+AppProtocolHandler::GetScheme(nsACString &aResult)
+{
+  aResult.AssignLiteral("app");
+  return NS_OK;
+}
+
+NS_IMETHODIMP
+AppProtocolHandler::GetDefaultPort(int32_t *aResult)
+{
+  // No ports for the app protocol.
+  *aResult = -1;
+  return NS_OK;
+}
+
+NS_IMETHODIMP
+AppProtocolHandler::GetProtocolFlags(uint32_t *aResult)
+{
+  *aResult = URI_NOAUTH |
+             URI_DANGEROUS_TO_LOAD |
+             URI_CROSS_ORIGIN_NEEDS_WEBAPPS_PERM;
+  return NS_OK;
+}
+
+NS_IMETHODIMP
+AppProtocolHandler::NewURI(const nsACString &aSpec,
+                           const char *aCharset, // ignore charset info
+                           nsIURI *aBaseURI,
+                           nsIURI **result)
+{
+  nsresult rv;
+  nsCOMPtr<nsIStandardURL> surl(do_CreateInstance(NS_STANDARDURL_CONTRACTID, &rv));
+  NS_ENSURE_SUCCESS(rv, rv);
+
+  rv = surl->Init(nsIStandardURL::URLTYPE_STANDARD, -1, aSpec, aCharset, aBaseURI);
+  NS_ENSURE_SUCCESS(rv, rv);
+
+  nsCOMPtr<nsIURL> url(do_QueryInterface(surl, &rv));
+  NS_ENSURE_SUCCESS(rv, rv);
+
+  surl->SetMutable(false);
+  NS_ADDREF(*result = url);
+  return NS_OK;
+}
+
+// We map app://ABCDEF/path/to/file.ext to
+// jar:file:///path/to/profile/webapps/ABCDEF/application.zip!/path/to/file.ext
+NS_IMETHODIMP
+AppProtocolHandler::NewChannel(nsIURI* aUri, nsIChannel* *aResult)
+{
+  NS_ENSURE_ARG_POINTER(aUri);
+  nsJARChannel* channel = new nsJARChannel();
+  if (!channel) {
+    return NS_ERROR_OUT_OF_MEMORY;
+  }
+
+  nsAutoCString host;
+  nsresult rv = aUri->GetHost(host);
+  NS_ENSURE_SUCCESS(rv, rv);
+
+  nsAutoCString fileSpec;
+  nsCOMPtr<nsIURL> url = do_QueryInterface(aUri);
+  rv = url->GetFilePath(fileSpec);
+  NS_ENSURE_SUCCESS(rv, rv);
+
+  mozilla::dom::AppInfo appInfo;
+
+  if (!mAppInfoCache.Get(host, &appInfo)) {
+    nsCOMPtr<nsIAppsService> appsService = do_GetService(APPS_SERVICE_CONTRACTID);
+    if (!appsService) {
+      return NS_ERROR_FAILURE;
+    }
+
+    JS::Value jsInfo;
+    rv = appsService->GetAppInfo(NS_ConvertUTF8toUTF16(host), &jsInfo);
+    if (NS_FAILED(rv)) {
+      // Return a DummyChannel.
+      delete channel;
+      NS_IF_ADDREF(*aResult = new DummyChannel());
+      return NS_OK;
+    }
+
+    mozilla::AutoSafeJSContext cx;
+    if (!appInfo.Init(cx, JS::Handle<JS::Value>::fromMarkedLocation(&jsInfo)) ||
+        appInfo.mPath.IsEmpty()) {
+      printf_stderr("!! No appInfo for %s\n", host.get());
+      // Return a DummyChannel.
+      delete channel;
+      NS_IF_ADDREF(*aResult = new DummyChannel());
+      return NS_OK;
+    }
+
+    mAppInfoCache.Put(host, appInfo);
+  }
+
+  bool noRemote = (appInfo.mIsCoreApp ||
+                   XRE_GetProcessType() == GeckoProcessType_Default);
+
+  // In-parent and CoreApps can directly access files, so use jar:file://
+  nsAutoCString jarSpec(noRemote ? "jar:file://"
+                                 : "jar:remoteopenfile://");
+  jarSpec += NS_ConvertUTF16toUTF8(appInfo.mPath) +
+             NS_LITERAL_CSTRING("/application.zip!") +
+             fileSpec;
+
+  nsCOMPtr<nsIURI> jarURI;
+  rv = NS_NewURI(getter_AddRefs(jarURI),
+                 jarSpec, nullptr, nullptr);
+  NS_ENSURE_SUCCESS(rv, rv);
+
+  rv = channel->Init(jarURI);
+  NS_ENSURE_SUCCESS(rv, rv);
+
+  rv = channel->SetAppURI(aUri);
+  NS_ENSURE_SUCCESS(rv, rv);
+
+  rv = channel->SetOriginalURI(aUri);
+  NS_ENSURE_SUCCESS(rv, rv);
+
+  NS_ADDREF(*aResult = channel);
+  return NS_OK;
+}
+
+NS_IMETHODIMP
+AppProtocolHandler::AllowPort(int32_t aPort, const char *aScheme, bool *aRetval)
+{
+  // No port allowed for this scheme.
+  *aRetval = false;
+  return NS_OK;
+}
+
new file mode 100644
--- /dev/null
+++ b/netwerk/protocol/app/AppProtocolHandler.h
@@ -0,0 +1,35 @@
+/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */
+/* vim:set expandtab ts=4 sw=4 sts=4 cin: */
+/* 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/. */
+
+#ifndef AppProtocolHandler_
+#define AppProtocolHandler_
+
+#include "nsIProtocolHandler.h"
+#include "nsDataHashtable.h"
+#include "mozilla/dom/AppInfoBinding.h"
+
+class AppProtocolHandler : public nsIProtocolHandler
+{
+public:
+  NS_DECL_ISUPPORTS
+
+  // nsIProtocolHandler methods:
+  NS_DECL_NSIPROTOCOLHANDLER
+
+  // AppProtocolHandler methods:
+  AppProtocolHandler();
+  virtual ~AppProtocolHandler();
+
+  // Define a Create method to be used with a factory:
+  static nsresult Create(nsISupports* aOuter,
+                         const nsIID& aIID,
+                         void* *aResult);
+
+private:
+  nsDataHashtable<nsCStringHashKey, mozilla::dom::AppInfo> mAppInfoCache;
+};
+
+#endif /* AppProtocolHandler_ */
deleted file mode 100644
--- a/netwerk/protocol/app/AppProtocolHandler.js
+++ /dev/null
@@ -1,197 +0,0 @@
-/* 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";
-
-const Cc = Components.classes;
-const Ci = Components.interfaces;
-const Cu = Components.utils;
-const Cr = Components.results;
-
-Cu.import("resource://gre/modules/XPCOMUtils.jsm");
-Cu.import("resource://gre/modules/Services.jsm");
-
-XPCOMUtils.defineLazyServiceGetter(this, "appsService",
-                                   "@mozilla.org/AppsService;1",
-                                   "nsIAppsService");
-
-function AppProtocolHandler() {
-  this._appInfo = [];
-  this._runningInParent = Cc["@mozilla.org/xre/runtime;1"]
-                            .getService(Ci.nsIXULRuntime)
-                            .processType == Ci.nsIXULRuntime.PROCESS_TYPE_DEFAULT;
-}
-
-AppProtocolHandler.prototype = {
-  classID: Components.ID("{b7ad6144-d344-4687-b2d0-b6b9dce1f07f}"),
-  QueryInterface: XPCOMUtils.generateQI([Ci.nsIProtocolHandler]),
-
-  scheme: "app",
-  defaultPort: -1,
-  // Don't allow loading from other protocols, and only from app:// if webapps is granted
-  protocolFlags: Ci.nsIProtocolHandler.URI_NOAUTH |
-                 Ci.nsIProtocolHandler.URI_DANGEROUS_TO_LOAD |
-                 Ci.nsIProtocolHandler.URI_CROSS_ORIGIN_NEEDS_WEBAPPS_PERM,
-
-  getAppInfo: function app_phGetAppInfo(aId) {
-
-    if (!this._appInfo[aId]) {
-      this._appInfo[aId] = appsService.getAppInfo(aId);
-    }
-    return this._appInfo[aId];
-  },
-
-  newURI: function app_phNewURI(aSpec, aOriginCharset, aBaseURI) {
-    let uri = Cc["@mozilla.org/network/standard-url;1"]
-              .createInstance(Ci.nsIStandardURL);
-    uri.init(Ci.nsIStandardURL.URLTYPE_STANDARD, -1, aSpec, aOriginCharset,
-             aBaseURI);
-    return uri.QueryInterface(Ci.nsIURI);
-  },
-
-  newChannel: function app_phNewChannel(aURI) {
-    // We map app://ABCDEF/path/to/file.ext to
-    // jar:file:///path/to/profile/webapps/ABCDEF/application.zip!/path/to/file.ext
-    let url = aURI.QueryInterface(Ci.nsIURL);
-    let appId = aURI.host;
-    let fileSpec = url.filePath;
-
-    // Build a jar channel and masquerade as an app:// URI.
-    let appInfo = this.getAppInfo(appId);
-    if (!appInfo) {
-      // That should not happen, so dump() inconditionnally.
-      // We create a dummy channel instead of throwing to let the
-      // downstream user get a 404 error.
-      dump("!! got no appInfo for " + appId + "\n");
-      return new DummyChannel();
-    }
-
-    let uri;
-    if (this._runningInParent || appInfo.isCoreApp) {
-      // In-parent and CoreApps can directly access files, so use jar:file://
-      uri = "jar:file://" + appInfo.path + "/application.zip!" + fileSpec;
-    } else {
-      // non-CoreApps in child need to ask parent for file handle, use jar:ipcfile://
-      uri = "jar:remoteopenfile://" + appInfo.path + "/application.zip!" + fileSpec;
-    }
-    let channel = Services.io.newChannel(uri, null, null);
-    channel.QueryInterface(Ci.nsIJARChannel).setAppURI(aURI);
-    channel.QueryInterface(Ci.nsIChannel).originalURI = aURI;
-
-    return channel;
-  },
-
-  allowPort: function app_phAllowPort(aPort, aScheme) {
-    return false;
-  }
-};
-
-/**
-  * This dummy channel implementation only provides enough functionality
-  * to return a fake 404 error when the caller asks for an app:// URL
-  * containing an unknown appId.
-  */
-function DummyChannel() {
-  this.originalURI = Services.io.newURI("app://unknown/nothing.html", null, null);
-  this.URI = Services.io.newURI("app://unknown/nothing.html", null, null);
-}
-
-DummyChannel.prototype = {
-  QueryInterface: XPCOMUtils.generateQI([Ci.nsIRequest,
-                                         Ci.nsIChannel,
-                                         Ci.nsIJARChannel]),
-
-  // nsIRequest
-  name: "dummy_app_channel",
-
-  isPending: function dc_isPending() {
-    return this._pending;
-  },
-
-  status: Cr.NS_ERROR_FILE_NOT_FOUND,
-
-  cancel: function dc_cancel() {
-  },
-
-  suspend: function dc_suspend() {
-    this._suspendCount++;
-  },
-
-  resume: function dc_resume() {
-    if (this._suspendCount <= 0)
-      throw Cr.NS_ERROR_UNEXPECTED;
-
-    if (--this._suspendCount == 0 && this._pending) {
-      this._dispatch();
-    }
-  },
-
-  loadGroup: null,
-  loadFlags: Ci.nsIRequest.LOAD_NORMAL,
-
-  // nsIChannel
-  owner: null,
-  notificationCallbacks: null,
-  securityInfo: null,
-  contentType: null,
-  contentCharset: null,
-  contentLength: 0,
-  contentDisposition: Ci.nsIChannel.DISPOSITION_INLINE,
-  contentDispositionFilename: "",
-
-  _pending: false,
-  _suspendCount: 0,
-  _listener: null,
-  _context: null,
-
-  open: function dc_open() {
-    return Cr.NS_ERROR_NOT_IMPLEMENTED;
-  },
-
-  _dispatch: function dc_dispatch() {
-    let request = this;
-
-    Services.tm.currentThread.dispatch(
-    {
-      run: function dc_run() {
-        request._listener.onStartRequest(request, request._context);
-        request._listener.onStopRequest(request, request._context,
-                                        Cr.NS_ERROR_FILE_NOT_FOUND);
-        if (request.loadGroup) {
-          request.loadGroup.removeRequest(request, request._context,
-                                          Cr.NS_ERROR_FILE_NOT_FOUND);
-        }
-        request._pending = false;
-        request.notificationCallbacks = null;
-        request._listener = null;
-        request._context = null;
-      }
-    },
-    Ci.nsIThread.DISPATCH_NORMAL);
-  },
-
-  asyncOpen: function dc_asyncopenfunction(aListener, aContext) {
-    if (this.loadGroup) {
-      this.loadGroup.addRequest(this, aContext);
-    }
-
-    this._listener = aListener;
-    this._context = aContext;
-    this._pending = true;
-
-    if (!this._suspended) {
-      this._dispatch();
-    }
-  },
-
-  // nsIJarChannel, needed for XHR to turn NS_ERROR_FILE_NOT_FOUND into
-  // a 404 error.
-  isUnsafe: false,
-
-  setAppURI: function(aURI) {
-    throw Cr.NS_ERROR_NOT_IMPLEMENTED;
-  }
-};
-
-this.NSGetFactory = XPCOMUtils.generateNSGetFactory([AppProtocolHandler]);
deleted file mode 100644
--- a/netwerk/protocol/app/AppProtocolHandler.manifest
+++ /dev/null
@@ -1,3 +0,0 @@
-# AppProtocolHander.js
-component {b7ad6144-d344-4687-b2d0-b6b9dce1f07f} AppProtocolHandler.js
-contract @mozilla.org/network/protocol;1?name=app {b7ad6144-d344-4687-b2d0-b6b9dce1f07f}
new file mode 100644
--- /dev/null
+++ b/netwerk/protocol/app/Makefile.in
@@ -0,0 +1,9 @@
+#
+# 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/.
+
+LOCAL_INCLUDES = \
+  -I$(srcdir)/../../base/src \
+  -I$(srcdir)/../../../modules/libjar \
+  $(NULL)
--- a/netwerk/protocol/app/moz.build
+++ b/netwerk/protocol/app/moz.build
@@ -1,10 +1,18 @@
 # -*- 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/.
 
-EXTRA_COMPONENTS += [
-    'AppProtocolHandler.js',
-    'AppProtocolHandler.manifest',
+MODULE = 'necko'
+
+CPP_SOURCES += [
+    'AppProtocolHandler.cpp',
 ]
+
+LIBRARY_NAME = 'nkapp_s'
+
+FAIL_ON_WARNINGS = True
+
+LIBXUL_LIBRARY = True
+