From 2ba94431911a4a557c8a768f6391841eb4cd3ee1 Mon Sep 17 00:00:00 2001 From: Michael Henretty Date: Wed, 2 Oct 2013 18:27:53 -0700 Subject: [PATCH] Bug 899574 - Part 2, add Notification.get() with notification storage. r=bent --- b2g/chrome/content/shell.js | 1 + b2g/installer/package-manifest.in | 2 + browser/base/content/browser.js | 1 + browser/installer/package-manifest.in | 2 + dom/bindings/Bindings.conf | 4 + dom/interfaces/notification/moz.build | 1 + .../notification/nsINotificationStorage.idl | 92 +++++ dom/src/notification/Notification.cpp | 379 +++++++++++++++--- dom/src/notification/Notification.h | 59 ++- dom/src/notification/NotificationDB.jsm | 270 +++++++++++++ dom/src/notification/NotificationStorage.js | 174 ++++++++ .../notification/NotificationStorage.manifest | 3 + dom/src/notification/moz.build | 9 + dom/tests/mochitest/moz.build | 4 - .../mochitest/notification/MockServices.js | 81 ++++ .../notification/NotificationTest.js | 73 ++++ 16 files changed, 1072 insertions(+), 83 deletions(-) create mode 100644 dom/interfaces/notification/nsINotificationStorage.idl create mode 100644 dom/src/notification/NotificationDB.jsm create mode 100644 dom/src/notification/NotificationStorage.js create mode 100644 dom/src/notification/NotificationStorage.manifest create mode 100644 dom/tests/mochitest/notification/MockServices.js create mode 100644 dom/tests/mochitest/notification/NotificationTest.js diff --git a/b2g/chrome/content/shell.js b/b2g/chrome/content/shell.js index 5e202ed62713..8ee85a21b43a 100644 --- a/b2g/chrome/content/shell.js +++ b/b2g/chrome/content/shell.js @@ -11,6 +11,7 @@ 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"); diff --git a/b2g/installer/package-manifest.in b/b2g/installer/package-manifest.in index ab57076707c8..b2d884c822cc 100644 --- a/b2g/installer/package-manifest.in +++ b/b2g/installer/package-manifest.in @@ -348,6 +348,8 @@ @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 diff --git a/browser/base/content/browser.js b/browser/base/content/browser.js index 51c6728c7789..b511658e62a4 100644 --- a/browser/base/content/browser.js +++ b/browser/base/content/browser.js @@ -7,6 +7,7 @@ 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", diff --git a/browser/installer/package-manifest.in b/browser/installer/package-manifest.in index 4c99a6271c8e..7183b5f5666c 100644 --- a/browser/installer/package-manifest.in +++ b/browser/installer/package-manifest.in @@ -528,6 +528,8 @@ @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 diff --git a/dom/bindings/Bindings.conf b/dom/bindings/Bindings.conf index 9a26565157fa..60002e16feb8 100644 --- a/dom/bindings/Bindings.conf +++ b/dom/bindings/Bindings.conf @@ -118,6 +118,10 @@ DOMInterfaces = { 'resultNotAddRefed': [ 'playbackRate' ], }, +'Notification' : { + 'nativeType': 'mozilla::dom::Notification' +}, + 'AudioNode' : { 'concrete': False, 'binaryNames': { diff --git a/dom/interfaces/notification/moz.build b/dom/interfaces/notification/moz.build index 1a5ba8599576..c87af1a9ec74 100644 --- a/dom/interfaces/notification/moz.build +++ b/dom/interfaces/notification/moz.build @@ -6,6 +6,7 @@ XPIDL_SOURCES += [ 'nsIDOMDesktopNotification.idl', + 'nsINotificationStorage.idl', ] XPIDL_MODULE = 'dom_notification' diff --git a/dom/interfaces/notification/nsINotificationStorage.idl b/dom/interfaces/notification/nsINotificationStorage.idl new file mode 100644 index 000000000000..a046989222d4 --- /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" +%} diff --git a/dom/src/notification/Notification.cpp b/dom/src/notification/Notification.cpp index dc91865f68b1..151668205574 100644 --- a/dom/src/notification/Notification.cpp +++ b/dom/src/notification/Notification.cpp @@ -5,6 +5,7 @@ #include "PCOMContentPermissionRequestChild.h" #include "mozilla/dom/Notification.h" #include "mozilla/dom/OwningNonNull.h" +#include "mozilla/dom/Promise.h" #include "mozilla/Preferences.h" #include "TabChild.h" #include "nsContentUtils.h" @@ -12,20 +13,135 @@ #include "nsIAlertsService.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" +#include "nsIAppsService.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(); + 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::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 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 mGlobal; + nsCOMPtr mWindow; + nsRefPtr mPromise; + JS::Heap 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 @@ -256,12 +372,15 @@ 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) @@ -282,50 +401,103 @@ NotificationObserver::Observe(nsISupports* aSubject, const char* aTopic, 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::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 = new Notification(aTitle, - aOptions.mBody, - aOptions.mDir, - aOptions.mLang, - tag, - aOptions.mIcon); - + MOZ_ASSERT(NS_IsMainThread()); nsCOMPtr window = do_QueryInterface(aGlobal.GetAsSupports()); MOZ_ASSERT(window, "Window should not be null."); - notification->BindToOwner(window); + nsRefPtr notification = CreateInternal(window, + EmptyString(), + aTitle, + aOptions); // Queue a task to show the notification. nsCOMPtr showNotificationTask = new NotificationTask(notification, NotificationTask::eShow); - NS_DispatchToMainThread(showNotificationTask); + NS_DispatchToCurrentThread(showNotificationTask); + + // Persist the notification. + nsresult rv; + nsCOMPtr 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::CreateInternal(nsPIDOMWindow* aWindow, + const nsAString& aID, + const nsAString& aTitle, + const NotificationOptions& aOptions) +{ + nsString id; + if (!aID.IsEmpty()) { + id = aID; + } else { + nsCOMPtr 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 = new Notification(id, + aTitle, + aOptions.mBody, + aOptions.mDir, + aOptions.mLang, + aOptions.mTag, + aOptions.mIcon); + + notification->BindToOwner(aWindow); + return notification.forget(); +} + +void Notification::ShowInternal() { nsCOMPtr alertService = @@ -336,7 +508,8 @@ Notification::ShowInternal() 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; @@ -344,17 +517,18 @@ Notification::ShowInternal() if (mIconUrl.Length() > 0) { // Resolve image URL against document base URI. nsIDocument* doc = GetOwner()->GetExtantDoc(); - NS_ENSURE_TRUE(doc, NS_ERROR_UNEXPECTED); - nsCOMPtr baseUri = doc->GetBaseURI(); - NS_ENSURE_TRUE(baseUri, NS_ERROR_UNEXPECTED); - nsCOMPtr 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 baseUri = doc->GetBaseURI(); + if (baseUri) { + nsCOMPtr srcUri; + rv = nsContentUtils::NewURIWithDocumentCharset(getter_AddRefs(srcUri), + mIconUrl, doc, baseUri); + if (NS_SUCCEEDED(rv)) { + nsAutoCString src; + srcUri->GetSpec(src); + absoluteUrl = NS_ConvertUTF8toUTF16(src); + } + } } } @@ -362,7 +536,7 @@ Notification::ShowInternal() nsString alertName; rv = GetAlertName(alertName); - NS_ENSURE_SUCCESS(rv, rv); + NS_ENSURE_SUCCESS_VOID(rv); #ifdef MOZ_B2G nsCOMPtr appNotifier = @@ -374,12 +548,15 @@ Notification::ShowInternal() if (appId != nsIScriptSecurityManager::UNKNOWN_APP_ID) { nsCOMPtr appsService = do_GetService("@mozilla.org/AppsService;1"); nsString manifestUrl = EmptyString(); - appsService->GetManifestURLByLocalId(appId, manifestUrl); - return appNotifier->ShowAppNotification(mIconUrl, mTitle, mBody, - true, - manifestUrl, - observer, - alertName); + rv = appsService->GetManifestURLByLocalId(appId, manifestUrl); + if (NS_SUCCEEDED(rv)) { + appNotifier->ShowAppNotification(mIconUrl, mTitle, mBody, + true, + manifestUrl, + observer, + alertName); + return; + } } } #endif @@ -388,9 +565,9 @@ Notification::ShowInternal() // 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 @@ -478,6 +655,47 @@ Notification::GetPermissionInternal(nsISupports* aGlobal, ErrorResult& aRv) } } +already_AddRefed +Notification::Get(const GlobalObject& aGlobal, + const GetNotificationOptions& aFilter, + ErrorResult& aRv) +{ + nsCOMPtr 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 notificationStorage = + do_GetService(NS_NOTIFICATION_STORAGE_CONTRACTID, &rv); + if (NS_FAILED(rv)) { + aRv.Throw(rv); + return nullptr; + } + + nsRefPtr promise = new Promise(window); + nsCOMPtr 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() { @@ -499,22 +717,61 @@ Notification::Close() 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 notificationStorage = + do_GetService(NS_NOTIFICATION_STORAGE_CONTRACTID); + if (notificationStorage) { + nsString origin; + rv = GetOrigin(GetOwner(), origin); + if (NS_SUCCEEDED(rv)) { + notificationStorage->Delete(origin, mID); + } + } + nsCOMPtr alertService = do_GetService(NS_ALERTSERVICE_CONTRACTID); - if (alertService) { nsString alertName; - nsresult rv = GetAlertName(alertName); - NS_ENSURE_SUCCESS(rv, rv); - - rv = alertService->CloseAlert(alertName); - NS_ENSURE_SUCCESS(rv, rv); + rv = GetAlertName(alertName); + if (NS_SUCCEEDED(rv)) { + alertService->CloseAlert(alertName); + } } } +} + +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 appsService = + do_GetService("@mozilla.org/AppsService;1", &rv); + NS_ENSURE_SUCCESS(rv, rv); + appsService->GetManifestURLByLocalId(appId, aOrigin); + } return NS_OK; } @@ -522,20 +779,12 @@ Notification::CloseInternal() 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 + ID. + // The name of the alert is of the form origin#ID. + nsresult rv = GetOrigin(GetOwner(), aAlertName); NS_ENSURE_SUCCESS(rv, rv); aAlertName.AppendLiteral("#"); - aAlertName.Append(mTag); + aAlertName.Append(mID); return NS_OK; } diff --git a/dom/src/notification/Notification.h b/dom/src/notification/Notification.h index afadfd60eb03..f6a47c91744a 100644 --- a/dom/src/notification/Notification.h +++ b/dom/src/notification/Notification.h @@ -10,31 +10,37 @@ #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 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; } @@ -44,24 +50,22 @@ public: 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; } @@ -73,6 +77,10 @@ public: static NotificationPermission GetPermission(const GlobalObject& aGlobal, ErrorResult& aRv); + static already_AddRefed Get(const GlobalObject& aGlobal, + const GetNotificationOptions& aFilter, + ErrorResult& aRv); + void Close(); static bool PrefEnabled(); @@ -85,8 +93,17 @@ public: virtual JSObject* WrapObject(JSContext* aCx, JS::Handle 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 CreateInternal(nsPIDOMWindow* aWindow, + const nsAString& aID, + const nsAString& aTitle, + const NotificationOptions& aOptions); + + void ShowInternal(); + void CloseInternal(); static NotificationPermission GetPermissionInternal(nsISupports* aGlobal, ErrorResult& rv); @@ -103,8 +120,22 @@ protected: } } + 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; diff --git a/dom/src/notification/NotificationDB.jsm b/dom/src/notification/NotificationDB.jsm new file mode 100644 index 000000000000..dd590c175266 --- /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(); diff --git a/dom/src/notification/NotificationStorage.js b/dom/src/notification/NotificationStorage.js new file mode 100644 index 000000000000..b8b86ea28113 --- /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]); diff --git a/dom/src/notification/NotificationStorage.manifest b/dom/src/notification/NotificationStorage.manifest new file mode 100644 index 000000000000..34c5c51388de --- /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} diff --git a/dom/src/notification/moz.build b/dom/src/notification/moz.build index f4644c861e26..8687cb00cb98 100644 --- a/dom/src/notification/moz.build +++ b/dom/src/notification/moz.build @@ -6,6 +6,15 @@ MODULE = 'dom' +EXTRA_COMPONENTS += [ + 'NotificationStorage.js', + 'NotificationStorage.manifest', +] + +EXTRA_JS_MODULES += [ + 'NotificationDB.jsm' +] + EXPORTS.mozilla.dom += [ 'DesktopNotification.h', 'Notification.h', diff --git a/dom/tests/mochitest/moz.build b/dom/tests/mochitest/moz.build index 28c9c0495046..3dc79b5b1156 100644 --- a/dom/tests/mochitest/moz.build +++ b/dom/tests/mochitest/moz.build @@ -29,7 +29,3 @@ DIRS += [ 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'] - diff --git a/dom/tests/mochitest/notification/MockServices.js b/dom/tests/mochitest/notification/MockServices.js new file mode 100644 index 000000000000..164cc7e9a91f --- /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); + }, + }; +})(); diff --git a/dom/tests/mochitest/notification/NotificationTest.js b/dom/tests/mochitest/notification/NotificationTest.js new file mode 100644 index 000000000000..871ec137a632 --- /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 + }; +})();