summaryrefslogtreecommitdiffstats
path: root/mobile/android/modules/DownloadNotifications.jsm
diff options
context:
space:
mode:
Diffstat (limited to 'mobile/android/modules/DownloadNotifications.jsm')
-rw-r--r--mobile/android/modules/DownloadNotifications.jsm291
1 files changed, 291 insertions, 0 deletions
diff --git a/mobile/android/modules/DownloadNotifications.jsm b/mobile/android/modules/DownloadNotifications.jsm
new file mode 100644
index 000000000..39b520979
--- /dev/null
+++ b/mobile/android/modules/DownloadNotifications.jsm
@@ -0,0 +1,291 @@
+/* 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 = ["DownloadNotifications"];
+
+const { classes: Cc, interfaces: Ci, utils: Cu, results: Cr } = Components;
+
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+
+XPCOMUtils.defineLazyModuleGetter(this, "Downloads", "resource://gre/modules/Downloads.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "FileUtils", "resource://gre/modules/FileUtils.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "Notifications", "resource://gre/modules/Notifications.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "OS", "resource://gre/modules/osfile.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "Services", "resource://gre/modules/Services.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "Snackbars", "resource://gre/modules/Snackbars.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "UITelemetry", "resource://gre/modules/UITelemetry.jsm");
+
+XPCOMUtils.defineLazyServiceGetter(this, "ParentalControls",
+ "@mozilla.org/parental-controls-service;1", "nsIParentalControlsService");
+
+var Log = Cu.import("resource://gre/modules/AndroidLog.jsm", {}).AndroidLog.i.bind(null, "DownloadNotifications");
+
+XPCOMUtils.defineLazyGetter(this, "strings",
+ () => Services.strings.createBundle("chrome://browser/locale/browser.properties"));
+
+Object.defineProperty(this, "window",
+ { get: () => Services.wm.getMostRecentWindow("navigator:browser") });
+
+const kButtons = {
+ PAUSE: new DownloadNotificationButton("pause",
+ "drawable://pause",
+ "alertDownloadsPause"),
+ RESUME: new DownloadNotificationButton("resume",
+ "drawable://play",
+ "alertDownloadsResume"),
+ CANCEL: new DownloadNotificationButton("cancel",
+ "drawable://close",
+ "alertDownloadsCancel")
+};
+
+var notifications = new Map();
+
+var DownloadNotifications = {
+ _notificationKey: "downloads",
+
+ init: function () {
+ Downloads.getList(Downloads.ALL)
+ .then(list => list.addView(this))
+ .then(() => this._viewAdded = true, Cu.reportError);
+
+ // All click, cancel, and button presses will be handled by this handler as part of the Notifications callback API.
+ Notifications.registerHandler(this._notificationKey, this);
+ },
+
+ onDownloadAdded: function (download) {
+ // Don't create notifications for pre-existing succeeded downloads.
+ // We still add notifications for canceled downloads in case the
+ // user decides to retry the download.
+ if (download.succeeded && !this._viewAdded) {
+ return;
+ }
+
+ if (!ParentalControls.isAllowed(ParentalControls.DOWNLOAD)) {
+ download.cancel().catch(Cu.reportError);
+ download.removePartialData().catch(Cu.reportError);
+ Snackbars.show(strings.GetStringFromName("downloads.disabledInGuest"), Snackbars.LENGTH_LONG);
+ return;
+ }
+
+ let notification = new DownloadNotification(download);
+ notifications.set(download, notification);
+ notification.showOrUpdate();
+
+ // If this is a new download, show a snackbar as well.
+ if (this._viewAdded) {
+ Snackbars.show(strings.GetStringFromName("alertDownloadsToast"), Snackbars.LENGTH_LONG);
+ }
+ },
+
+ onDownloadChanged: function (download) {
+ let notification = notifications.get(download);
+
+ if (download.succeeded) {
+ let file = new FileUtils.File(download.target.path);
+
+ Snackbars.show(strings.formatStringFromName("alertDownloadSucceeded", [file.leafName], 1), Snackbars.LENGTH_LONG, {
+ action: {
+ label: strings.GetStringFromName("helperapps.open"),
+ callback: () => {
+ UITelemetry.addEvent("launch.1", "toast", null, "downloads");
+ try {
+ file.launch();
+ } catch (ex) {
+ this.showInAboutDownloads(download);
+ }
+ if (notification) {
+ notification.hide();
+ }
+ }
+ }});
+ }
+
+ if (notification) {
+ notification.showOrUpdate();
+ }
+ },
+
+ onDownloadRemoved: function (download) {
+ let notification = notifications.get(download);
+ if (!notification) {
+ Cu.reportError("Download doesn't have a notification.");
+ return;
+ }
+
+ notification.hide();
+ notifications.delete(download);
+ },
+
+ _findDownloadForCookie: function(cookie) {
+ return Downloads.getList(Downloads.ALL)
+ .then(list => list.getAll())
+ .then((downloads) => {
+ for (let download of downloads) {
+ let cookie2 = getCookieFromDownload(download);
+ if (cookie2 === cookie) {
+ return download;
+ }
+ }
+
+ throw "Couldn't find download for " + cookie;
+ });
+ },
+
+ onCancel: function(cookie) {
+ // TODO: I'm not sure what we do here...
+ },
+
+ showInAboutDownloads: function (download) {
+ let hash = "#" + window.encodeURIComponent(download.target.path);
+
+ // Force using string equality to find a tab
+ window.BrowserApp.selectOrAddTab("about:downloads" + hash, null, { startsWith: true });
+ },
+
+ onClick: function(cookie) {
+ this._findDownloadForCookie(cookie).then((download) => {
+ if (download.succeeded) {
+ // We don't call Download.launch(), because there's (currently) no way to
+ // tell if the file was actually launched or not, and we want to show
+ // about:downloads if the launch failed.
+ let file = new FileUtils.File(download.target.path);
+ try {
+ file.launch();
+ } catch (ex) {
+ this.showInAboutDownloads(download);
+ }
+ } else {
+ ConfirmCancelPrompt.show(download);
+ }
+ }).catch(Cu.reportError);
+ },
+
+ onButtonClick: function(button, cookie) {
+ this._findDownloadForCookie(cookie).then((download) => {
+ if (button === kButtons.PAUSE.buttonId) {
+ download.cancel().catch(Cu.reportError);
+ } else if (button === kButtons.RESUME.buttonId) {
+ download.start().catch(Cu.reportError);
+ } else if (button === kButtons.CANCEL.buttonId) {
+ download.cancel().catch(Cu.reportError);
+ download.removePartialData().catch(Cu.reportError);
+ }
+ }).catch(Cu.reportError);
+ },
+};
+
+function getCookieFromDownload(download) {
+ return download.target.path +
+ download.source.url +
+ download.startTime;
+}
+
+function DownloadNotification(download) {
+ this.download = download;
+ this._fileName = OS.Path.basename(download.target.path);
+
+ this.id = null;
+}
+
+DownloadNotification.prototype = {
+ _updateFromDownload: function () {
+ this._downloading = !this.download.stopped;
+ this._paused = this.download.canceled && this.download.hasPartialData;
+ this._succeeded = this.download.succeeded;
+
+ this._show = this._downloading || this._paused || this._succeeded;
+ },
+
+ get options() {
+ if (!this._show) {
+ return null;
+ }
+
+ let options = {
+ icon: "drawable://alert_download",
+ cookie: getCookieFromDownload(this.download),
+ handlerKey: DownloadNotifications._notificationKey
+ };
+
+ if (this._downloading) {
+ options.icon = "drawable://alert_download_animation";
+ if (this.download.currentBytes == 0) {
+ this._updateOptionsForStatic(options, "alertDownloadsStart2");
+ } else {
+ let buttons = this.download.hasPartialData ? [kButtons.PAUSE, kButtons.CANCEL] :
+ [kButtons.CANCEL]
+ this._updateOptionsForOngoing(options, buttons);
+ }
+ } else if (this._paused) {
+ this._updateOptionsForOngoing(options, [kButtons.RESUME, kButtons.CANCEL]);
+ } else if (this._succeeded) {
+ options.persistent = false;
+ this._updateOptionsForStatic(options, "alertDownloadsDone2");
+ }
+
+ return options;
+ },
+
+ _updateOptionsForStatic : function (options, titleName) {
+ options.title = strings.GetStringFromName(titleName);
+ options.message = this._fileName;
+ },
+
+ _updateOptionsForOngoing: function (options, buttons) {
+ options.title = this._fileName;
+ options.message = this.download.progress + "%";
+ options.buttons = buttons;
+ options.ongoing = true;
+ options.progress = this.download.progress;
+ options.persistent = true;
+ },
+
+ showOrUpdate: function () {
+ this._updateFromDownload();
+
+ if (this._show) {
+ if (!this.id) {
+ this.id = Notifications.create(this.options);
+ } else if (!this.options.ongoing) {
+ // We need to explictly cancel ongoing notifications,
+ // since updating them to be non-ongoing doesn't seem
+ // to work. See bug 1130834.
+ Notifications.cancel(this.id);
+ this.id = Notifications.create(this.options);
+ } else {
+ Notifications.update(this.id, this.options);
+ }
+ } else {
+ this.hide();
+ }
+ },
+
+ hide: function () {
+ if (this.id) {
+ Notifications.cancel(this.id);
+ this.id = null;
+ }
+ },
+};
+
+var ConfirmCancelPrompt = {
+ show: function (download) {
+ // Open a prompt that offers a choice to cancel the download
+ let title = strings.GetStringFromName("downloadCancelPromptTitle1");
+ let message = strings.GetStringFromName("downloadCancelPromptMessage1");
+
+ if (Services.prompt.confirm(null, title, message)) {
+ download.cancel().catch(Cu.reportError);
+ download.removePartialData().catch(Cu.reportError);
+ }
+ }
+};
+
+function DownloadNotificationButton(buttonId, iconUrl, titleStringName, onClicked) {
+ this.buttonId = buttonId;
+ this.title = strings.GetStringFromName(titleStringName);
+ this.icon = iconUrl;
+}