diff options
Diffstat (limited to 'mobile/android/modules/DownloadNotifications.jsm')
-rw-r--r-- | mobile/android/modules/DownloadNotifications.jsm | 291 |
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; +} |