Bug 909932 - Implemented a Notifications.jsm module to expose rich notification api to addons; r=wesj

This commit is contained in:
Federico Paolinelli 2013-11-01 12:06:07 -07:00
parent 143c06354d
commit 5ed99b8b74
4 changed files with 413 additions and 147 deletions

View File

@ -44,18 +44,20 @@ public final class NotificationHelper implements GeckoEventListener {
private static final String LIGHT_ATTR = "light";
private static final String ONGOING_ATTR = "ongoing";
private static final String WHEN_ATTR = "when";
private static final String PRIORITY_ATTR = "priority";
private static final String LARGE_ICON_ATTR = "largeIcon";
private static final String COOKIE_ATTR = "cookie";
private static final String EVENT_TYPE_ATTR = "eventType";
private static final String ACTIONS_ATTR = "actions";
private static final String ACTION_ATTR = "actionKind";
private static final String ACTION_ID_ATTR = "buttonId";
private static final String ACTION_TITLE_ATTR = "title";
private static final String ACTION_ICON_ATTR = "icon";
private static final String NOTIFICATION_SCHEME = "moz-notification";
private static final String BUTTON_EVENT = "notification-button-clicked";
private static final String CLICK_EVENT = "notification-clicked";
private static final String CLEARED_EVENT = "notification-cleared";
private static final String CLOSED_EVENT = "notification-closed";
private static Context mContext;
private static Set<String> mShowing;
@ -127,50 +129,78 @@ public final class NotificationHelper implements GeckoEventListener {
mShowing.remove(id);
}
// If the notification was clicked, we are closing it.
if (CLICK_EVENT.equals(notificationType) && !i.getBooleanExtra(ONGOING_ATTR, false)) {
hideNotification(id);
}
String cookie = data.getQueryParameter(COOKIE_ATTR);
if (GeckoThread.checkLaunchState(GeckoThread.LaunchState.GeckoRunning)) {
JSONObject args = new JSONObject();
try {
args.put(NOTIFICATION_ID, id);
if (cookie != null) {
args.put(COOKIE_ATTR, cookie);
}
args.put(ID_ATTR, id);
args.put(EVENT_TYPE_ATTR, notificationType);
if (BUTTON_EVENT.equals(notificationType)) {
final String actionName = data.getQueryParameter(ACTION_ID_ATTR);
args.put(ACTION_ID_ATTR, actionName);
}
GeckoAppShell.sendEventToGecko(GeckoEvent.createBroadcastEvent("Notification:Event", args.toString()));
} catch (JSONException e) {
Log.w(LOGTAG, "Error building JSON notification arguments.", e);
}
}
// If the notification was clicked, we are closing it. This must be executed after
// sending the event to js side because when the notification is canceled no event can be
// handled.
if (CLICK_EVENT.equals(notificationType) && !i.getBooleanExtra(ONGOING_ATTR, false)) {
hideNotification(id);
}
}
private PendingIntent buildNotificationPendingIntent(JSONObject message, String type) {
Intent notificationIntent = new Intent(HELPER_BROADCAST_ACTION);
final boolean ongoing = message.optBoolean(ONGOING_ATTR);
notificationIntent.putExtra(ONGOING_ATTR, ongoing);
private Uri.Builder getNotificationBuilder(JSONObject message, String type) {
Uri.Builder b = new Uri.Builder();
b.scheme(NOTIFICATION_SCHEME).appendQueryParameter(EVENT_TYPE_ATTR, type);
try {
final String id = message.getString(ID_ATTR);
b.appendQueryParameter(ID_ATTR, id);
if (message.has(COOKIE_ATTR)) {
b.appendQueryParameter(COOKIE_ATTR,
message.getString(COOKIE_ATTR));
} catch (JSONException ex) {
Log.i(LOGTAG, "buildNotificationPendingIntent, error parsing", ex);
}
return b;
}
private Intent buildNotificationIntent(JSONObject message, Uri.Builder builder) {
Intent notificationIntent = new Intent(HELPER_BROADCAST_ACTION);
final boolean ongoing = message.optBoolean(ONGOING_ATTR);
notificationIntent.putExtra(ONGOING_ATTR, ongoing);
final Uri dataUri = builder.build();
notificationIntent.setData(dataUri);
notificationIntent.putExtra(HELPER_NOTIFICATION, true);
return notificationIntent;
}
private PendingIntent buildNotificationPendingIntent(JSONObject message, String type) {
Uri.Builder builder = getNotificationBuilder(message, type);
final Intent notificationIntent = buildNotificationIntent(message, builder);
PendingIntent pi = PendingIntent.getBroadcast(mContext, 0, notificationIntent, PendingIntent.FLAG_UPDATE_CURRENT);
return pi;
}
private PendingIntent buildButtonClickPendingIntent(JSONObject message, JSONObject action) {
Uri.Builder builder = getNotificationBuilder(message, BUTTON_EVENT);
try {
// Action name must be in query uri, otherwise buttons pending intents
// would be collapsed.
if(action.has(ACTION_ID_ATTR)) {
builder.appendQueryParameter(ACTION_ID_ATTR, action.getString(ACTION_ID_ATTR));
} else {
Log.i(LOGTAG, "button event with no name");
}
} catch (JSONException ex) {
Log.i(LOGTAG, "buildNotificationPendingIntent, error parsing", ex);
}
final Uri dataUri = b.build();
notificationIntent.setData(dataUri);
notificationIntent.putExtra(HELPER_NOTIFICATION, true);
PendingIntent pi = PendingIntent.getBroadcast(mContext, 0, notificationIntent, PendingIntent.FLAG_UPDATE_CURRENT);
return pi;
final Intent notificationIntent = buildNotificationIntent(message, builder);
PendingIntent res = PendingIntent.getBroadcast(mContext, 0, notificationIntent, PendingIntent.FLAG_UPDATE_CURRENT);
return res;
}
private void showNotification(JSONObject message) {
@ -209,6 +239,11 @@ public final class NotificationHelper implements GeckoEventListener {
builder.setWhen(when);
}
if (message.has(PRIORITY_ATTR)) {
int priority = message.optInt(PRIORITY_ATTR);
builder.setPriority(priority);
}
if (message.has(LARGE_ICON_ATTR)) {
Bitmap b = BitmapUtils.getBitmapFromDataURI(message.optString(LARGE_ICON_ATTR));
builder.setLargeIcon(b);
@ -230,7 +265,7 @@ public final class NotificationHelper implements GeckoEventListener {
try {
for (int i = 0; i < actions.length(); i++) {
JSONObject action = actions.getJSONObject(i);
final PendingIntent pending = buildNotificationPendingIntent(message, action.getString(ACTION_ATTR));
final PendingIntent pending = buildButtonClickPendingIntent(message, action);
final String actionTitle = action.getString(ACTION_TITLE_ATTR);
final Uri actionImage = Uri.parse(action.optString(ACTION_ICON_ATTR));
builder.addAction(BitmapUtils.getResource(actionImage, R.drawable.ic_status_logo),
@ -265,9 +300,24 @@ public final class NotificationHelper implements GeckoEventListener {
hideNotification(id);
}
private void sendNotificationWasClosed(String id) {
if (!GeckoThread.checkLaunchState(GeckoThread.LaunchState.GeckoRunning)) {
return;
}
JSONObject args = new JSONObject();
try {
args.put(ID_ATTR, id);
args.put(EVENT_TYPE_ATTR, CLOSED_EVENT);
GeckoAppShell.sendEventToGecko(GeckoEvent.createBroadcastEvent("Notification:Event", args.toString()));
} catch (JSONException ex) {
Log.w(LOGTAG, "sendNotificationWasClosed: error building JSON notification arguments.", ex);
}
}
public void hideNotification(String id) {
GeckoAppShell.sNotificationClient.remove(id.hashCode());
mShowing.remove(id);
sendNotificationWasClosed(id);
}
private void clearAll() {

View File

@ -5,32 +5,21 @@
"use strict";
let Cu = Components.utils;
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
function dump(a) {
Cc["@mozilla.org/consoleservice;1"].getService(Ci.nsIConsoleService).logStringMessage(a);
}
XPCOMUtils.defineLazyModuleGetter(this, "Notifications",
"resource://gre/modules/Notifications.jsm");
const URI_GENERIC_ICON_DOWNLOAD = "drawable://alert_download";
const URI_PAUSE_ICON = "drawable://pause";
const URI_CANCEL_ICON = "drawable://close";
const URI_RESUME_ICON = "drawable://play";
const PAUSE_ACTION = {
actionKind : "pause",
title : Strings.browser.GetStringFromName("alertDownloadsPause"),
icon : URI_PAUSE_ICON
};
const CANCEL_ACTION = {
actionKind : "cancel",
title : Strings.browser.GetStringFromName("alertDownloadsCancel"),
icon : URI_CANCEL_ICON
};
const RESUME_ACTION = {
actionKind : "resume",
title : Strings.browser.GetStringFromName("alertDownloadsResume"),
icon : URI_RESUME_ICON
};
XPCOMUtils.defineLazyModuleGetter(this, "OS", "resource://gre/modules/osfile.jsm");
@ -40,6 +29,7 @@ var Downloads = {
_progressAlert: null,
_privateDownloads: [],
_showingPrompt: false,
_downloadsIdMap: {},
_getLocalFile: function dl__getLocalFile(aFileURI) {
// if this is a URL, get the file from that
@ -58,7 +48,6 @@ var Downloads = {
this._progressAlert = new AlertDownloadProgressListener();
this._dlmgr.addPrivacyAwareListener(this._progressAlert);
Services.obs.addObserver(this, "last-pb-context-exited", true);
Services.obs.addObserver(this, "Notification:Event", true);
},
openDownload: function dl_openDownload(aDownload) {
@ -76,54 +65,12 @@ var Downloads = {
cancelDownload: function dl_cancelDownload(aDownload) {
aDownload.cancel();
let fileURI = aDownload.target.spec;
let f = this._getLocalFile(fileURI);
OS.File.remove(f.path);
},
getNotificationIdFromDownload: function dl_getNotificationIdFromDownload(aDownload) {
return aDownload.target.spec.replace("file:", "download:");
},
showNotification: function dl_showNotification(aDownload, aMessage, aTitle, aOptions) {
let msg = {
type: "Notification:Show",
id: this.getNotificationIdFromDownload(aDownload),
title: aTitle,
smallIcon: URI_GENERIC_ICON_DOWNLOAD,
text: aMessage,
ongoing: false,
cookie: aDownload.guid,
when: aDownload.startTime
};
if (aOptions && aOptions.icon) {
msg.smallIcon = aOptions.icon;
}
if (aOptions && aOptions.percentComplete) {
msg.progress_value = aOptions.percentComplete;
msg.progress_max = 100;
}
if (aOptions && aOptions.actions) {
msg.actions = aOptions.actions;
}
if (aOptions && aOptions.ongoing) {
msg.ongoing = aOptions.ongoing;
}
this._bridge.handleGeckoMessage(JSON.stringify(msg));
},
removeNotification: function dl_removeNotification(aDownload) {
let msg = {
type: "Notification:Hide",
id: this.getNotificationIdFromDownload(aDownload)
};
this._bridge.handleGeckoMessage(JSON.stringify(msg));
},
showCancelConfirmPrompt: function dl_showCancelConfirmPrompt(aDownload) {
if (this._showingPrompt)
return;
@ -149,48 +96,66 @@ var Downloads = {
this.showCancelConfirmPrompt(aDownload);
},
handleNotificationEvent: function dl_handleNotificationEvent(aNotifData, aDownload) {
switch (aNotifData.eventType) {
case "notification-clicked":
this.handleClickEvent(aDownload);
break;
case "cancel":
this.cancelDownload(aDownload);
break;
case "pause":
aDownload.pause();
break;
case "resume":
aDownload.resume();
break;
case "notification-cleared":
// notification cleared by the user
break;
}
clickCallback: function dl_clickCallback(aDownloadId) {
this._dlmgr.getDownloadByGUID(aDownloadId, (function(status, download) {
if (Components.isSuccessCode(status))
this.handleClickEvent(download);
}).bind(this));
},
pauseClickCallback: function dl_buttonPauseCallback(aDownloadId) {
this._dlmgr.getDownloadByGUID(aDownloadId, (function(status, download) {
if (Components.isSuccessCode(status))
download.pause();
}).bind(this));
},
resumeClickCallback: function dl_buttonPauseCallback(aDownloadId) {
this._dlmgr.getDownloadByGUID(aDownloadId, (function(status, download) {
if (Components.isSuccessCode(status))
download.resume();
}).bind(this));
},
cancelClickCallback: function dl_buttonPauseCallback(aDownloadId) {
this._dlmgr.getDownloadByGUID(aDownloadId, (function(status, download) {
if (Components.isSuccessCode(status))
this.cancelDownload(download);
}).bind(this));
},
notificationCanceledCallback: function dl_notifCancelCallback(aId, aDownloadId) {
let notificationId = this._downloadsIdMap[aDownloadId];
if (notificationId && notificationId == aId)
delete this._downloadsIdMap[aDownloadId];
},
createNotification: function dl_createNotif(aDownload, aOptions) {
let notificationId = Notifications.create(aOptions);
this._downloadsIdMap[aDownload.guid] = notificationId;
},
updateNotification: function dl_updateNotif(aDownload, aOptions) {
let notificationId = this._downloadsIdMap[aDownload.guid];
if (notificationId)
Notifications.update(notificationId, aOptions);
},
cancelNotification: function dl_cleanNotif(aDownload) {
Notifications.cancel(this._downloadsIdMap[aDownload.guid]);
delete this._downloadsIdMap[aDownload.guid];
},
// observer for last-pb-context-exited
observe: function dl_observe(aSubject, aTopic, aData) {
switch (aTopic) {
case "Notification:Event": {
let data = JSON.parse(aData);
let guid = data.cookie;
this._dlmgr.getDownloadByGUID(guid, (function(status, download) {
if (Components.isSuccessCode(status))
this.handleNotificationEvent(data, download);
}).bind(this));
break;
}
case "last-pb-context-exited": {
let download;
while ((download = this._privateDownloads.pop())) {
try {
Downloads.removeNotification(download);
} catch (e) {
dump("Error removing private download: " + e);
}
}
break;
let download;
while ((download = this._privateDownloads.pop())) {
try {
let notificationId = aDownload.guid;
Notifications.clear(notificationId);
Downloads.removeNotification(download);
} catch (e) {
dump("Error removing private download: " + e);
}
}
},
@ -201,15 +166,57 @@ var Downloads = {
!aIID.equals(Ci.nsISupportsWeakReference))
throw Components.results.NS_ERROR_NO_INTERFACE;
return this;
},
get _bridge() {
delete this._bridge;
return this._bridge = Cc["@mozilla.org/android/bridge;1"].getService(Ci.nsIAndroidBridge);
}
};
const PAUSE_BUTTON = {
buttonId: "pause",
title : Strings.browser.GetStringFromName("alertDownloadsPause"),
icon : URI_PAUSE_ICON,
onClicked: function (aId, aCookie) {
Downloads.pauseClickCallback(aCookie);
}
};
const CANCEL_BUTTON = {
buttonId: "cancel",
title : Strings.browser.GetStringFromName("alertDownloadsCancel"),
icon : URI_CANCEL_ICON,
onClicked: function (aId, aCookie) {
Downloads.cancelClickCallback(aCookie);
}
};
const RESUME_BUTTON = {
buttonId: "resume",
title : Strings.browser.GetStringFromName("alertDownloadsResume"),
icon: URI_RESUME_ICON,
onClicked: function (aId, aCookie) {
Downloads.resumeClickCallback(aCookie);
}
};
function DownloadNotifOptions (aDownload, aTitle, aMessage) {
this.icon = URI_GENERIC_ICON_DOWNLOAD;
this.onCancel = function (aId, aCookie) {
Downloads.notificationCanceledCallback(aId, aCookie);
}
this.onClick = function (aId, aCookie) {
Downloads.clickCallback(aCookie);
}
this.title = aTitle;
this.message = aMessage;
this.buttons = null;
this.cookie = aDownload.guid;
}
function DownloadProgressNotifOptions (aDownload, aButtons) {
DownloadNotifOptions.apply(this, [aDownload, aDownload.displayName, aDownload.percentComplete + "%"]);
this.ongoing = true;
this.progress = aDownload.percentComplete;
this.buttons = aButtons;
}
// AlertDownloadProgressListener is used to display progress in the alert notifications.
function AlertDownloadProgressListener() { }
@ -225,8 +232,9 @@ AlertDownloadProgressListener.prototype = {
} catch(ex) { }
let contentLength = aDownload.size;
if (availableSpace > 0 && contentLength > 0 && contentLength > availableSpace) {
Downloads.showNotification(aDownload, strings.GetStringFromName("alertDownloadsNoSpace"),
strings.GetStringFromName("alertDownloadsSize"));
Downloads.updateNotification(aDownload, new DownloadNotifOptions(aDownload,
strings.GetStringFromName("alertDownloadsNoSpace"),
strings.GetStringFromName("alertDownloadsSize")));
aDownload.cancel();
}
@ -234,33 +242,30 @@ AlertDownloadProgressListener.prototype = {
// Undetermined progress is not supported yet
return;
}
Downloads.showNotification(aDownload, aDownload.percentComplete + "%",
aDownload.displayName, { percentComplete: aDownload.percentComplete,
ongoing: true,
actions: [PAUSE_ACTION, CANCEL_ACTION] });
Downloads.updateNotification(aDownload, new DownloadProgressNotifOptions(aDownload, [PAUSE_BUTTON, CANCEL_BUTTON]));
},
onDownloadStateChange: function(aState, aDownload) {
let state = aDownload.state;
switch (state) {
case Ci.nsIDownloadManager.DOWNLOAD_QUEUED:
case Ci.nsIDownloadManager.DOWNLOAD_QUEUED: {
NativeWindow.toast.show(Strings.browser.GetStringFromName("alertDownloadsToast"), "long");
Downloads.showNotification(aDownload, Strings.browser.GetStringFromName("alertDownloadsStart2"),
aDownload.displayName);
Downloads.createNotification(aDownload, new DownloadNotifOptions(aDownload,
Strings.browser.GetStringFromName("alertDownloadsStart2"),
aDownload.displayName));
break;
case Ci.nsIDownloadManager.DOWNLOAD_PAUSED:
Downloads.showNotification(aDownload, aDownload.percentComplete + "%",
aDownload.displayName, { percentComplete: aDownload.percentComplete,
ongoing: true,
actions: [RESUME_ACTION, CANCEL_ACTION] });
}
case Ci.nsIDownloadManager.DOWNLOAD_PAUSED: {
Downloads.updateNotification(aDownload, new DownloadProgressNotifOptions(aDownload, [RESUME_BUTTON, CANCEL_BUTTON]));
break;
}
case Ci.nsIDownloadManager.DOWNLOAD_FAILED:
case Ci.nsIDownloadManager.DOWNLOAD_CANCELED:
case Ci.nsIDownloadManager.DOWNLOAD_BLOCKED_PARENTAL:
case Ci.nsIDownloadManager.DOWNLOAD_DIRTY:
case Ci.nsIDownloadManager.DOWNLOAD_FINISHED: {
Downloads.removeNotification(aDownload);
Downloads.cancelNotification(aDownload);
if (aDownload.isPrivate) {
let index = Downloads._privateDownloads.indexOf(aDownload);
if (index != -1) {
@ -269,8 +274,9 @@ AlertDownloadProgressListener.prototype = {
}
if (state == Ci.nsIDownloadManager.DOWNLOAD_FINISHED) {
Downloads.showNotification(aDownload, Strings.browser.GetStringFromName("alertDownloadsDone2"),
aDownload.displayName);
Downloads.createNotification(aDownload, new DownloadNotifOptions(aDownload,
Strings.browser.GetStringFromName("alertDownloadsDone2"),
aDownload.displayName));
}
break;
}

View File

@ -0,0 +1,209 @@
/* 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"
let Cc = Components.classes;
let Ci = Components.interfaces;
Components.utils.import("resource://gre/modules/Services.jsm");
this.EXPORTED_SYMBOLS = ["Notifications"];
function log(msg) {
// Services.console.logStringMessage(msg);
}
var _notificationsMap = {};
function Notification(aId, aOptions) {
this._id = aId;
this._when = (new Date).getTime();
this.fillWithOptions(aOptions);
}
Notification.prototype = {
fillWithOptions: function(aOptions) {
if ("icon" in aOptions && aOptions.icon != null)
this._icon = aOptions.icon;
else
throw "Notification icon is mandatory";
if ("title" in aOptions && aOptions.title != null)
this._title = aOptions.title;
else
throw "Notification title is mandatory";
if ("message" in aOptions && aOptions.message != null)
this._message = aOptions.message;
else
this._message = null;
if ("priority" in aOptions && aOptions.priority != null)
this._priority = aOptions.priority;
if ("buttons" in aOptions && aOptions.buttons != null) {
if (aOptions.buttons.length > 3)
throw "Too many buttons provided. The max number is 3";
this._buttons = {};
for (let i = 0; i < aOptions.buttons.length; i++) {
let button_id = aOptions.buttons[i].buttonId;
this._buttons[button_id] = aOptions.buttons[i];
}
} else {
this._buttons = null;
}
if ("ongoing" in aOptions && aOptions.ongoing != null)
this._ongoing = aOptions.ongoing;
else
this._ongoing = false;
if ("progress" in aOptions && aOptions.progress != null)
this._progress = aOptions.progress;
else
this._progress = null;
if ("onCancel" in aOptions && aOptions.onCancel != null)
this._onCancel = aOptions.onCancel;
else
this._onCancel = null;
if ("onClick" in aOptions && aOptions.onClick != null)
this._onClick = aOptions.onClick;
else
this._onClick = null;
if ("cookie" in aOptions && aOptions.cookie != null)
this._cookie = aOptions.cookie;
else
this._cookie = null;
},
show: function() {
let msg = {
type: "Notification:Show",
id: this._id,
title: this._title,
smallIcon: this._icon,
ongoing: this._ongoing,
when: this._when
};
if (this._message)
msg.text = this._message;
if (this._progress) {
msg.progress_value = this._progress;
msg.progress_max = 100;
}
if (this._priority)
msg.priority = this._priority;
if (this._buttons) {
msg.actions = [];
let buttonName;
for (buttonName in this._buttons) {
let button = this._buttons[buttonName];
let obj = {
buttonId: button.buttonId,
title : button.title,
icon : button.icon
};
msg.actions.push(obj);
}
}
Services.androidBridge.handleGeckoMessage(JSON.stringify(msg));
return this;
},
cancel: function() {
let msg = {
type: "Notification:Hide",
id: this._id
};
Services.androidBridge.handleGeckoMessage(JSON.stringify(msg));
}
}
var Notifications = {
_initObserver: function() {
if (!this._observerAdded) {
Services.obs.addObserver(this, "Notification:Event", true);
this._observerAdded = true;
}
},
get idService() {
delete this.idService;
return this.idService = Cc["@mozilla.org/uuid-generator;1"].getService(Ci.nsIUUIDGenerator);
},
create: function notif_notify(aOptions) {
this._initObserver();
let id = this.idService.generateUUID().toString();
let notification = new Notification(id, aOptions);
_notificationsMap[id] = notification;
notification.show();
return id;
},
update: function notif_update(aId, aOptions) {
let notification = _notificationsMap[aId];
if (!notification)
throw "Unknown notification id";
notification.fillWithOptions(aOptions);
notification.show();
},
cancel: function notif_cancel(aId) {
let notification = _notificationsMap[aId];
if (notification)
notification.cancel();
},
observe: function notif_observe(aSubject, aTopic, aData) {
let data = JSON.parse(aData);
let id = data.id;
let notification = _notificationsMap[id];
if (!notification) {
Services.console.logStringMessage("Notifications.jsm observe: received unknown event id " + id);
return;
}
switch (data.eventType) {
case "notification-clicked":
if (notification._onClick)
notification._onClick(id, notification._cookie);
break;
case "notification-button-clicked": {
if (!notification._buttons) {
Services.console.logStringMessage("Notifications.jsm: received button clicked event but no buttons are available");
break;
}
let button = notification._buttons[data.buttonId];
if (button)
button.onClicked(id, notification._cookie);
}
break;
case "notification-cleared":
case "notification-closed":
if (notification._onCancel)
notification._onCancel(id, notification._cookie);
delete _notificationsMap[id]; // since the notification was dismissed, we no longer need to hold a reference.
break;
}
},
QueryInterface: function (aIID) {
if (!aIID.equals(Ci.nsISupports) &&
!aIID.equals(Ci.nsIObserver) &&
!aIID.equals(Ci.nsISupportsWeakReference))
throw Components.results.NS_ERROR_NO_INTERFACE;
return this;
}
}

View File

@ -10,6 +10,7 @@ EXTRA_JS_MODULES += [
'Home.jsm',
'JNI.jsm',
'LightweightThemeConsumer.jsm',
'Notifications.jsm',
'OrderedBroadcast.jsm',
'Prompt.jsm',
'Sanitizer.jsm',