Source code

Revision control

Copy as Markdown

Other Tools

/* 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 ToolkitModules = {};
ChromeUtils.defineESModuleGetters(ToolkitModules, {
EventEmitter: "resource://gre/modules/EventEmitter.sys.mjs",
NetUtil: "resource://gre/modules/NetUtil.sys.mjs",
});
const AlertNotification = Components.Constructor(
"@mozilla.org/alert-notification;1",
"nsIAlertNotification",
"initWithObject"
);
var { ignoreEvent } = ExtensionCommon;
// Manages a notification popup (notifications API) created by the extension.
function Notification(context, notificationsMap, id, options) {
this.notificationsMap = notificationsMap;
this.id = id;
this.options = options;
// Set before calling into nsIAlertsService, because the notification may be
// closed during the call.
notificationsMap.set(id, this);
this.showAlert(context);
}
Notification.prototype = {
clear() {
try {
let svc = Cc["@mozilla.org/alerts-service;1"].getService(
Ci.nsIAlertsService
);
svc.closeAlert(this.id);
} catch (e) {
// This will fail if the OS doesn't support this function.
}
this.notificationsMap.delete(this.id);
},
async showAlert(context) {
let imageURL;
if (this.options.iconUrl) {
imageURL = context.extension.baseURI.resolve(this.options.iconUrl);
}
let image;
try {
if (imageURL) {
let uri = ToolkitModules.NetUtil.newURI(imageURL);
let channel = ToolkitModules.NetUtil.newChannel({
uri,
securityFlags:
Ci.nsILoadInfo.SEC_ALLOW_CROSS_ORIGIN_SEC_CONTEXT_IS_NULL,
contentPolicyType: Ci.nsIContentPolicy.TYPE_IMAGE,
loadingPrincipal: context.principal,
});
image = await ChromeUtils.fetchDecodedImage(uri, channel);
}
} catch (e) {
console.error(`Loading image ${imageURL} for Notification failed: ${e}`);
imageURL = undefined;
}
// Return if the notification was explicitly closed while waiting for the image.
if (!this.notificationsMap.has(this.id)) {
this.observe(null, "alertfinished");
return;
}
try {
let svc = Cc["@mozilla.org/alerts-service;1"].getService(
Ci.nsIAlertsService
);
// Principal is not set because doing so reveals buttons to control
// notification preferences, which are currently not implemented for
// notifications triggered via this extension API (bug 1589693).
let alert = new AlertNotification({
// The imageURL and image should be in sync. Only Android still needs
// the imageURL instead of image.
imageURL,
image,
title: this.options.title,
text: this.options.message,
textClickable: true,
name: this.id,
inPrivateBrowsing: context.incognito,
});
svc.showAlert(alert, this);
} catch (e) {
// This will fail if alerts aren't available on the system.
this.observe(null, "alertfinished");
}
},
observe(subject, topic) {
switch (topic) {
case "alertclickcallback":
this.notificationsMap.emit("clicked", this.id);
break;
case "alertfinished":
this.notificationsMap.emit("closed", this.id);
this.notificationsMap.delete(this.id);
break;
case "alertshow":
this.notificationsMap.emit("shown", this.id);
break;
}
},
};
this.notifications = class extends ExtensionAPIPersistent {
constructor(extension) {
super(extension);
this.nextId = 0;
this.notificationsMap = new Map();
ToolkitModules.EventEmitter.decorate(this.notificationsMap);
}
onShutdown() {
for (let notification of this.notificationsMap.values()) {
notification.clear();
}
}
PERSISTENT_EVENTS = {
onClosed({ fire }) {
let listener = (event, notificationId) => {
// TODO Bug 1413188, Support the byUser argument.
fire.async(notificationId, true);
};
this.notificationsMap.on("closed", listener);
return {
unregister: () => {
this.notificationsMap.off("closed", listener);
},
convert(_fire) {
fire = _fire;
},
};
},
onClicked({ fire }) {
let listener = (event, notificationId) => {
fire.async(notificationId);
};
this.notificationsMap.on("clicked", listener);
return {
unregister: () => {
this.notificationsMap.off("clicked", listener);
},
convert(_fire) {
fire = _fire;
},
};
},
onShown({ fire }) {
let listener = (event, notificationId) => {
fire.async(notificationId);
};
this.notificationsMap.on("shown", listener);
return {
unregister: () => {
this.notificationsMap.off("shown", listener);
},
convert(_fire) {
fire = _fire;
},
};
},
};
getAPI(context) {
let notificationsMap = this.notificationsMap;
return {
notifications: {
create: (notificationId, options) => {
if (!notificationId) {
notificationId = String(this.nextId++);
}
if (notificationsMap.has(notificationId)) {
notificationsMap.get(notificationId).clear();
}
new Notification(context, notificationsMap, notificationId, options);
return Promise.resolve(notificationId);
},
clear: function (notificationId) {
if (notificationsMap.has(notificationId)) {
notificationsMap.get(notificationId).clear();
return Promise.resolve(true);
}
return Promise.resolve(false);
},
getAll: function () {
let result = {};
notificationsMap.forEach((value, key) => {
result[key] = value.options;
});
return Promise.resolve(result);
},
onClosed: new EventManager({
context,
module: "notifications",
event: "onClosed",
extensionApi: this,
}).api(),
onClicked: new EventManager({
context,
module: "notifications",
event: "onClicked",
extensionApi: this,
}).api(),
onShown: new EventManager({
context,
module: "notifications",
event: "onShown",
extensionApi: this,
}).api(),
// TODO Bug 1190681, implement button support.
onButtonClicked: ignoreEvent(context, "notifications.onButtonClicked"),
},
};
}
};