Bug 899574 - Part 2, add Notification.get() with notification storage. r=bent
authorMichael Henretty <mhenretty@mozilla.com>
Wed, 02 Oct 2013 18:27:53 -0700
changeset 166596 119d46e5cec3ddbcc53ddaa89ecffae2605c5aad
parent 166595 ae1c322dab45a0db0ca5c741e57c9d761635ced5
child 166597 1b1567b456a53fdc64c21048629d92a8c8ec72a2
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)
reviewersbent
bugs899574
milestone27.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 899574 - Part 2, add Notification.get() with notification storage. r=bent
b2g/chrome/content/shell.js
b2g/installer/package-manifest.in
browser/base/content/browser.js
browser/installer/package-manifest.in
dom/interfaces/notification/moz.build
dom/interfaces/notification/nsINotificationStorage.idl
dom/src/notification/Notification.cpp
dom/src/notification/Notification.h
dom/src/notification/NotificationDB.jsm
dom/src/notification/NotificationStorage.js
dom/src/notification/NotificationStorage.manifest
dom/src/notification/moz.build
dom/tests/mochitest/moz.build
dom/tests/mochitest/notification/MockServices.js
dom/tests/mochitest/notification/NotificationTest.js
dom/tests/mochitest/notification/create_notification.html
dom/tests/mochitest/notification/desktop-notification/create_notification.html
dom/tests/mochitest/notification/desktop-notification/moz.build
dom/tests/mochitest/notification/desktop-notification/notification_common.js
dom/tests/mochitest/notification/desktop-notification/test_basic_notification.html
dom/tests/mochitest/notification/desktop-notification/test_basic_notification_click.html
dom/tests/mochitest/notification/desktop-notification/test_leak_windowClose.html
dom/tests/mochitest/notification/desktop-notification/test_notification_tag.html
dom/tests/mochitest/notification/desktop-notification/test_system_principal.xul
dom/tests/mochitest/notification/mochitest.ini
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_basics.html
dom/tests/mochitest/notification/test_notification_storage.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
dom/webidl/Notification.webidl
mobile/android/chrome/content/browser.js
mobile/android/installer/package-manifest.in
--- 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/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
--- 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,16 +504,18 @@
 @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
--- 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/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>
--- 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/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