Bug 899574 - Part 2, add Notification.get() with notification storage. r=bent

This commit is contained in:
Michael Henretty 2013-10-02 18:27:53 -07:00
Родитель 8af1bfd73a
Коммит 2ba9443191
16 изменённых файлов: 1072 добавлений и 83 удалений

Просмотреть файл

@ -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");

Просмотреть файл

@ -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

Просмотреть файл

@ -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",

Просмотреть файл

@ -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

Просмотреть файл

@ -118,6 +118,10 @@ DOMInterfaces = {
'resultNotAddRefed': [ 'playbackRate' ],
},
'Notification' : {
'nativeType': 'mozilla::dom::Notification'
},
'AudioNode' : {
'concrete': False,
'binaryNames': {

Просмотреть файл

@ -6,6 +6,7 @@
XPIDL_SOURCES += [
'nsIDOMDesktopNotification.idl',
'nsINotificationStorage.idl',
]
XPIDL_MODULE = 'dom_notification'

Просмотреть файл

@ -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"
%}

Просмотреть файл

@ -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 = 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
@ -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>
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 =
@ -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<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);
}
}
}
}
@ -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<nsIAppNotificationService> appNotifier =
@ -374,12 +548,15 @@ Notification::ShowInternal()
if (appId != nsIScriptSecurityManager::UNKNOWN_APP_ID) {
nsCOMPtr<nsIAppsService> 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<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()
{
@ -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<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 = 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<nsIAppsService> 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;
}

Просмотреть файл

@ -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<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;
}
@ -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<Promise> 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<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);
@ -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;

Просмотреть файл

@ -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();

Просмотреть файл

@ -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]);

Просмотреть файл

@ -0,0 +1,3 @@
# NotificationStorage.js
component {37f819b0-0b5c-11e3-8ffd-0800200c9a66} NotificationStorage.js
contract @mozilla.org/notificationStorage;1 {37f819b0-0b5c-11e3-8ffd-0800200c9a66}

Просмотреть файл

@ -6,6 +6,15 @@
MODULE = 'dom'
EXTRA_COMPONENTS += [
'NotificationStorage.js',
'NotificationStorage.manifest',
]
EXTRA_JS_MODULES += [
'NotificationDB.jsm'
]
EXPORTS.mozilla.dom += [
'DesktopNotification.h',
'Notification.h',

Просмотреть файл

@ -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']

Просмотреть файл

@ -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);
},
};
})();

Просмотреть файл

@ -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
};
})();