summaryrefslogtreecommitdiffstats
path: root/browser/components/downloads/DownloadsCommon.sys.mjs
diff options
context:
space:
mode:
Diffstat (limited to 'browser/components/downloads/DownloadsCommon.sys.mjs')
-rw-r--r--browser/components/downloads/DownloadsCommon.sys.mjs1643
1 files changed, 1643 insertions, 0 deletions
diff --git a/browser/components/downloads/DownloadsCommon.sys.mjs b/browser/components/downloads/DownloadsCommon.sys.mjs
new file mode 100644
index 0000000000..a0cbd2f8d8
--- /dev/null
+++ b/browser/components/downloads/DownloadsCommon.sys.mjs
@@ -0,0 +1,1643 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set ts=2 et sw=2 tw=80 filetype=javascript: */
+/* 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/. */
+
+/**
+ * Handles the Downloads panel shared methods and data access.
+ *
+ * This file includes the following constructors and global objects:
+ *
+ * DownloadsCommon
+ * This object is exposed directly to the consumers of this JavaScript module,
+ * and provides shared methods for all the instances of the user interface.
+ *
+ * DownloadsData
+ * Retrieves the list of past and completed downloads from the underlying
+ * Downloads API data, and provides asynchronous notifications allowing
+ * to build a consistent view of the available data.
+ *
+ * DownloadsIndicatorData
+ * This object registers itself with DownloadsData as a view, and transforms the
+ * notifications it receives into overall status data, that is then broadcast to
+ * the registered download status indicators.
+ */
+
+// Globals
+
+import { XPCOMUtils } from "resource://gre/modules/XPCOMUtils.sys.mjs";
+
+const lazy = {};
+
+ChromeUtils.defineESModuleGetters(lazy, {
+ BrowserWindowTracker: "resource:///modules/BrowserWindowTracker.sys.mjs",
+ DownloadHistory: "resource://gre/modules/DownloadHistory.sys.mjs",
+ DownloadUtils: "resource://gre/modules/DownloadUtils.sys.mjs",
+ Downloads: "resource://gre/modules/Downloads.sys.mjs",
+ NetUtil: "resource://gre/modules/NetUtil.sys.mjs",
+ PlacesUtils: "resource://gre/modules/PlacesUtils.sys.mjs",
+ PrivateBrowsingUtils: "resource://gre/modules/PrivateBrowsingUtils.sys.mjs",
+});
+
+XPCOMUtils.defineLazyServiceGetters(lazy, {
+ gClipboardHelper: [
+ "@mozilla.org/widget/clipboardhelper;1",
+ "nsIClipboardHelper",
+ ],
+ gMIMEService: ["@mozilla.org/mime;1", "nsIMIMEService"],
+});
+
+ChromeUtils.defineLazyGetter(lazy, "DownloadsLogger", () => {
+ let { ConsoleAPI } = ChromeUtils.importESModule(
+ "resource://gre/modules/Console.sys.mjs"
+ );
+ let consoleOptions = {
+ maxLogLevelPref: "browser.download.loglevel",
+ prefix: "Downloads",
+ };
+ return new ConsoleAPI(consoleOptions);
+});
+
+XPCOMUtils.defineLazyPreferenceGetter(
+ lazy,
+ "gAlwaysOpenPanel",
+ "browser.download.alwaysOpenPanel",
+ true
+);
+
+const kDownloadsStringBundleUrl =
+ "chrome://browser/locale/downloads/downloads.properties";
+
+const kDownloadsFluentStrings = new Localization(
+ ["browser/downloads.ftl"],
+ true
+);
+
+const kDownloadsStringsRequiringFormatting = {
+ sizeWithUnits: true,
+ statusSeparator: true,
+ statusSeparatorBeforeNumber: true,
+};
+
+const kMaxHistoryResultsForLimitedView = 42;
+
+const kPrefBranch = Services.prefs.getBranch("browser.download.");
+
+const kGenericContentTypes = [
+ "application/octet-stream",
+ "binary/octet-stream",
+ "application/unknown",
+];
+
+var PrefObserver = {
+ QueryInterface: ChromeUtils.generateQI([
+ "nsIObserver",
+ "nsISupportsWeakReference",
+ ]),
+ getPref(name) {
+ try {
+ switch (typeof this.prefs[name]) {
+ case "boolean":
+ return kPrefBranch.getBoolPref(name);
+ }
+ } catch (ex) {}
+ return this.prefs[name];
+ },
+ observe(aSubject, aTopic, aData) {
+ if (this.prefs.hasOwnProperty(aData)) {
+ delete this[aData];
+ this[aData] = this.getPref(aData);
+ }
+ },
+ register(prefs) {
+ this.prefs = prefs;
+ kPrefBranch.addObserver("", this, true);
+ for (let key in prefs) {
+ let name = key;
+ ChromeUtils.defineLazyGetter(this, name, function () {
+ return PrefObserver.getPref(name);
+ });
+ }
+ },
+};
+
+PrefObserver.register({
+ // prefName: defaultValue
+ openInSystemViewerContextMenuItem: true,
+ alwaysOpenInSystemViewerContextMenuItem: true,
+});
+
+// DownloadsCommon
+
+/**
+ * This object is exposed directly to the consumers of this JavaScript module,
+ * and provides shared methods for all the instances of the user interface.
+ */
+export var DownloadsCommon = {
+ // The following legacy constants are still returned by stateOfDownload, but
+ // individual properties of the Download object should normally be used.
+ DOWNLOAD_NOTSTARTED: -1,
+ DOWNLOAD_DOWNLOADING: 0,
+ DOWNLOAD_FINISHED: 1,
+ DOWNLOAD_FAILED: 2,
+ DOWNLOAD_CANCELED: 3,
+ DOWNLOAD_PAUSED: 4,
+ DOWNLOAD_BLOCKED_PARENTAL: 6,
+ DOWNLOAD_DIRTY: 8,
+ DOWNLOAD_BLOCKED_POLICY: 9,
+
+ // The following are the possible values of the "attention" property.
+ ATTENTION_NONE: "",
+ ATTENTION_SUCCESS: "success",
+ ATTENTION_INFO: "info",
+ ATTENTION_WARNING: "warning",
+ ATTENTION_SEVERE: "severe",
+
+ // Bit flags for the attentionSuppressed property.
+ SUPPRESS_NONE: 0,
+ SUPPRESS_PANEL_OPEN: 1,
+ SUPPRESS_ALL_DOWNLOADS_OPEN: 2,
+ SUPPRESS_CONTENT_AREA_DOWNLOADS_OPEN: 4,
+
+ /**
+ * Returns an object whose keys are the string names from the downloads string
+ * bundle, and whose values are either the translated strings or functions
+ * returning formatted strings.
+ */
+ get strings() {
+ let strings = {};
+ let sb = Services.strings.createBundle(kDownloadsStringBundleUrl);
+ for (let string of sb.getSimpleEnumeration()) {
+ let stringName = string.key;
+ if (stringName in kDownloadsStringsRequiringFormatting) {
+ strings[stringName] = function () {
+ // Convert "arguments" to a real array before calling into XPCOM.
+ return sb.formatStringFromName(stringName, Array.from(arguments));
+ };
+ } else {
+ strings[stringName] = string.value;
+ }
+ }
+ delete this.strings;
+ return (this.strings = strings);
+ },
+
+ /**
+ * Indicates whether or not to show the 'Open in system viewer' context menu item when appropriate
+ */
+ get openInSystemViewerItemEnabled() {
+ return PrefObserver.openInSystemViewerContextMenuItem;
+ },
+
+ /**
+ * Indicates whether or not to show the 'Always open...' context menu item when appropriate
+ */
+ get alwaysOpenInSystemViewerItemEnabled() {
+ return PrefObserver.alwaysOpenInSystemViewerContextMenuItem;
+ },
+
+ /**
+ * Get access to one of the DownloadsData, PrivateDownloadsData, or
+ * HistoryDownloadsData objects, depending on the privacy status of the
+ * specified window and on whether history downloads should be included.
+ *
+ * @param [optional] window
+ * The browser window which owns the download button.
+ * If not given, the privacy status will be assumed as non-private.
+ * @param [optional] history
+ * True to include history downloads when the window is public.
+ * @param [optional] privateAll
+ * Whether to force the public downloads data to be returned together
+ * with the private downloads data for a private window.
+ * @param [optional] limited
+ * True to limit the amount of downloads returned to
+ * `kMaxHistoryResultsForLimitedView`.
+ */
+ getData(window, history = false, privateAll = false, limited = false) {
+ let isPrivate =
+ window && lazy.PrivateBrowsingUtils.isContentWindowPrivate(window);
+ if (isPrivate && !privateAll) {
+ return lazy.PrivateDownloadsData;
+ }
+ if (history) {
+ if (isPrivate && privateAll) {
+ return lazy.LimitedPrivateHistoryDownloadData;
+ }
+ return limited
+ ? lazy.LimitedHistoryDownloadsData
+ : lazy.HistoryDownloadsData;
+ }
+ return lazy.DownloadsData;
+ },
+
+ /**
+ * Initializes the Downloads back-end and starts receiving events for both the
+ * private and non-private downloads data objects.
+ */
+ initializeAllDataLinks() {
+ lazy.DownloadsData.initializeDataLink();
+ lazy.PrivateDownloadsData.initializeDataLink();
+ },
+
+ /**
+ * Get access to one of the DownloadsIndicatorData or
+ * PrivateDownloadsIndicatorData objects, depending on the privacy status of
+ * the window in question.
+ */
+ getIndicatorData(aWindow) {
+ if (lazy.PrivateBrowsingUtils.isContentWindowPrivate(aWindow)) {
+ return lazy.PrivateDownloadsIndicatorData;
+ }
+ return lazy.DownloadsIndicatorData;
+ },
+
+ /**
+ * Returns a reference to the DownloadsSummaryData singleton - creating one
+ * in the process if one hasn't been instantiated yet.
+ *
+ * @param aWindow
+ * The browser window which owns the download button.
+ * @param aNumToExclude
+ * The number of items on the top of the downloads list to exclude
+ * from the summary.
+ */
+ getSummary(aWindow, aNumToExclude) {
+ if (lazy.PrivateBrowsingUtils.isContentWindowPrivate(aWindow)) {
+ if (this._privateSummary) {
+ return this._privateSummary;
+ }
+ return (this._privateSummary = new DownloadsSummaryData(
+ true,
+ aNumToExclude
+ ));
+ }
+ if (this._summary) {
+ return this._summary;
+ }
+ return (this._summary = new DownloadsSummaryData(false, aNumToExclude));
+ },
+ _summary: null,
+ _privateSummary: null,
+
+ /**
+ * Returns the legacy state integer value for the provided Download object.
+ */
+ stateOfDownload(download) {
+ // Collapse state using the correct priority.
+ if (!download.stopped) {
+ return DownloadsCommon.DOWNLOAD_DOWNLOADING;
+ }
+ if (download.succeeded) {
+ return DownloadsCommon.DOWNLOAD_FINISHED;
+ }
+ if (download.error) {
+ if (download.error.becauseBlockedByParentalControls) {
+ return DownloadsCommon.DOWNLOAD_BLOCKED_PARENTAL;
+ }
+ if (download.error.becauseBlockedByReputationCheck) {
+ return DownloadsCommon.DOWNLOAD_DIRTY;
+ }
+ return DownloadsCommon.DOWNLOAD_FAILED;
+ }
+ if (download.canceled) {
+ if (download.hasPartialData) {
+ return DownloadsCommon.DOWNLOAD_PAUSED;
+ }
+ return DownloadsCommon.DOWNLOAD_CANCELED;
+ }
+ return DownloadsCommon.DOWNLOAD_NOTSTARTED;
+ },
+
+ /**
+ * Removes a Download object from both session and history downloads.
+ */
+ async deleteDownload(download) {
+ // Check hasBlockedData to avoid double counting if you click the X button
+ // in the Libarary view and then delete the download from the history.
+ if (
+ download.error?.becauseBlockedByReputationCheck &&
+ download.hasBlockedData
+ ) {
+ Services.telemetry
+ .getKeyedHistogramById("DOWNLOADS_USER_ACTION_ON_BLOCKED_DOWNLOAD")
+ .add(download.error.reputationCheckVerdict, 1); // confirm block
+ }
+
+ // Remove the associated history element first, if any, so that the views
+ // that combine history and session downloads won't resurrect the history
+ // download into the view just before it is deleted permanently.
+ try {
+ await lazy.PlacesUtils.history.remove(download.source.url);
+ } catch (ex) {
+ console.error(ex);
+ }
+ let list = await lazy.Downloads.getList(lazy.Downloads.ALL);
+ await list.remove(download);
+ await download.finalize(true);
+ },
+
+ /**
+ * Deletes all files associated with a download, with or without removing it
+ * from the session downloads list and/or download history.
+ *
+ * @param download
+ * The download to delete and/or forget.
+ * @param clearHistory
+ * Optional. Removes history from session downloads list or history.
+ * 0 - Don't remove the download from session list or history.
+ * 1 - Remove the download from session list, but not history.
+ * 2 - Remove the download from both session list and history.
+ */
+ async deleteDownloadFiles(download, clearHistory = 0) {
+ if (clearHistory > 1) {
+ try {
+ await lazy.PlacesUtils.history.remove(download.source.url);
+ } catch (ex) {
+ console.error(ex);
+ }
+ }
+ if (clearHistory > 0) {
+ let list = await lazy.Downloads.getList(lazy.Downloads.ALL);
+ await list.remove(download);
+ }
+ await download.manuallyRemoveData();
+ if (clearHistory < 2) {
+ lazy.DownloadHistory.updateMetaData(download).catch(console.error);
+ }
+ },
+
+ /**
+ * Get a nsIMIMEInfo object for a download
+ */
+ getMimeInfo(download) {
+ if (!download.succeeded) {
+ return null;
+ }
+ let contentType = download.contentType;
+ let url = Cc["@mozilla.org/network/standard-url-mutator;1"]
+ .createInstance(Ci.nsIURIMutator)
+ .setSpec("http://example.com") // construct the URL
+ .setFilePath(download.target.path)
+ .finalize()
+ .QueryInterface(Ci.nsIURL);
+ let fileExtension = url.fileExtension;
+
+ // look at file extension if there's no contentType or it is generic
+ if (!contentType || kGenericContentTypes.includes(contentType)) {
+ try {
+ contentType = lazy.gMIMEService.getTypeFromExtension(fileExtension);
+ } catch (ex) {
+ DownloadsCommon.log(
+ "Cant get mimeType from file extension: ",
+ fileExtension
+ );
+ }
+ }
+ if (!(contentType || fileExtension)) {
+ return null;
+ }
+ let mimeInfo = null;
+ try {
+ mimeInfo = lazy.gMIMEService.getFromTypeAndExtension(
+ contentType || "",
+ fileExtension || ""
+ );
+ } catch (ex) {
+ DownloadsCommon.log(
+ "Can't get nsIMIMEInfo for contentType: ",
+ contentType,
+ "and fileExtension:",
+ fileExtension
+ );
+ }
+ return mimeInfo;
+ },
+
+ /**
+ * Confirm if the download exists on the filesystem and is a given mime-type
+ */
+ isFileOfType(download, mimeType) {
+ if (!(download.succeeded && download.target?.exists)) {
+ DownloadsCommon.log(
+ `isFileOfType returning false for mimeType: ${mimeType}, succeeded: ${download.succeeded}, exists: ${download.target?.exists}`
+ );
+ return false;
+ }
+ let mimeInfo = DownloadsCommon.getMimeInfo(download);
+ return mimeInfo?.type === mimeType.toLowerCase();
+ },
+
+ /**
+ * Copies the source URI of the given Download object to the clipboard.
+ */
+ copyDownloadLink(download) {
+ lazy.gClipboardHelper.copyString(
+ download.source.originalUrl || download.source.url
+ );
+ },
+
+ /**
+ * Given an iterable collection of Download objects, generates and returns
+ * statistics about that collection.
+ *
+ * @param downloads An iterable collection of Download objects.
+ *
+ * @return Object whose properties are the generated statistics. Currently,
+ * we return the following properties:
+ *
+ * numActive : The total number of downloads.
+ * numPaused : The total number of paused downloads.
+ * numDownloading : The total number of downloads being downloaded.
+ * totalSize : The total size of all downloads once completed.
+ * totalTransferred: The total amount of transferred data for these
+ * downloads.
+ * slowestSpeed : The slowest download rate.
+ * rawTimeLeft : The estimated time left for the downloads to
+ * complete.
+ * percentComplete : The percentage of bytes successfully downloaded.
+ */
+ summarizeDownloads(downloads) {
+ let summary = {
+ numActive: 0,
+ numPaused: 0,
+ numDownloading: 0,
+ totalSize: 0,
+ totalTransferred: 0,
+ // slowestSpeed is Infinity so that we can use Math.min to
+ // find the slowest speed. We'll set this to 0 afterwards if
+ // it's still at Infinity by the time we're done iterating all
+ // download.
+ slowestSpeed: Infinity,
+ rawTimeLeft: -1,
+ percentComplete: -1,
+ };
+
+ for (let download of downloads) {
+ summary.numActive++;
+
+ if (!download.stopped) {
+ summary.numDownloading++;
+ if (download.hasProgress && download.speed > 0) {
+ let sizeLeft = download.totalBytes - download.currentBytes;
+ summary.rawTimeLeft = Math.max(
+ summary.rawTimeLeft,
+ sizeLeft / download.speed
+ );
+ summary.slowestSpeed = Math.min(summary.slowestSpeed, download.speed);
+ }
+ } else if (download.canceled && download.hasPartialData) {
+ summary.numPaused++;
+ }
+
+ // Only add to total values if we actually know the download size.
+ if (download.succeeded) {
+ summary.totalSize += download.target.size;
+ summary.totalTransferred += download.target.size;
+ } else if (download.hasProgress) {
+ summary.totalSize += download.totalBytes;
+ summary.totalTransferred += download.currentBytes;
+ }
+ }
+
+ if (summary.totalSize != 0) {
+ summary.percentComplete = Math.floor(
+ (summary.totalTransferred / summary.totalSize) * 100
+ );
+ }
+
+ if (summary.slowestSpeed == Infinity) {
+ summary.slowestSpeed = 0;
+ }
+
+ return summary;
+ },
+
+ /**
+ * If necessary, smooths the estimated number of seconds remaining for one
+ * or more downloads to complete.
+ *
+ * @param aSeconds
+ * Current raw estimate on number of seconds left for one or more
+ * downloads. This is a floating point value to help get sub-second
+ * accuracy for current and future estimates.
+ */
+ smoothSeconds(aSeconds, aLastSeconds) {
+ // We apply an algorithm similar to the DownloadUtils.getTimeLeft function,
+ // though tailored to a single time estimation for all downloads. We never
+ // apply something if the new value is less than half the previous value.
+ let shouldApplySmoothing = aLastSeconds >= 0 && aSeconds > aLastSeconds / 2;
+ if (shouldApplySmoothing) {
+ // Apply hysteresis to favor downward over upward swings. Trust only 30%
+ // of the new value if lower, and 10% if higher (exponential smoothing).
+ let diff = aSeconds - aLastSeconds;
+ aSeconds = aLastSeconds + (diff < 0 ? 0.3 : 0.1) * diff;
+
+ // If the new time is similar, reuse something close to the last time
+ // left, but subtract a little to provide forward progress.
+ diff = aSeconds - aLastSeconds;
+ let diffPercent = (diff / aLastSeconds) * 100;
+ if (Math.abs(diff) < 5 || Math.abs(diffPercent) < 5) {
+ aSeconds = aLastSeconds - (diff < 0 ? 0.4 : 0.2);
+ }
+ }
+
+ // In the last few seconds of downloading, we are always subtracting and
+ // never adding to the time left. Ensure that we never fall below one
+ // second left until all downloads are actually finished.
+ return (aLastSeconds = Math.max(aSeconds, 1));
+ },
+
+ /**
+ * Opens a downloaded file.
+ *
+ * @param downloadProperties
+ * A Download object or the initial properties of a serialized download
+ * @param options.openWhere
+ * Optional string indicating how to handle opening a download target file URI.
+ * One of "window", "tab", "tabshifted".
+ * @param options.useSystemDefault
+ * Optional value indicating how to handle launching this download,
+ * this call only. Will override the associated mimeInfo.preferredAction
+ * @return {Promise}
+ * @resolves When the instruction to launch the file has been
+ * successfully given to the operating system or handled internally
+ * @rejects JavaScript exception if there was an error trying to launch
+ * the file.
+ */
+ async openDownload(download, options) {
+ // some download objects got serialized and need reconstituting
+ if (typeof download.launch !== "function") {
+ download = await lazy.Downloads.createDownload(download);
+ }
+ return download.launch(options).catch(ex => console.error(ex));
+ },
+
+ /**
+ * Show a downloaded file in the system file manager.
+ *
+ * @param aFile
+ * a downloaded file.
+ */
+ showDownloadedFile(aFile) {
+ if (!(aFile instanceof Ci.nsIFile)) {
+ throw new Error("aFile must be a nsIFile object");
+ }
+ try {
+ // Show the directory containing the file and select the file.
+ aFile.reveal();
+ } catch (ex) {
+ // If reveal fails for some reason (e.g., it's not implemented on unix
+ // or the file doesn't exist), try using the parent if we have it.
+ let parent = aFile.parent;
+ if (parent) {
+ this.showDirectory(parent);
+ }
+ }
+ },
+
+ /**
+ * Show the specified folder in the system file manager.
+ *
+ * @param aDirectory
+ * a directory to be opened with system file manager.
+ */
+ showDirectory(aDirectory) {
+ if (!(aDirectory instanceof Ci.nsIFile)) {
+ throw new Error("aDirectory must be a nsIFile object");
+ }
+ try {
+ aDirectory.launch();
+ } catch (ex) {
+ // If launch fails (probably because it's not implemented), let
+ // the OS handler try to open the directory.
+ Cc["@mozilla.org/uriloader/external-protocol-service;1"]
+ .getService(Ci.nsIExternalProtocolService)
+ .loadURI(
+ lazy.NetUtil.newURI(aDirectory),
+ Services.scriptSecurityManager.getSystemPrincipal()
+ );
+ }
+ },
+
+ /**
+ * Displays an alert message box which asks the user if they want to
+ * unblock the downloaded file or not.
+ *
+ * @param options
+ * An object with the following properties:
+ * {
+ * verdict:
+ * The detailed reason why the download was blocked, according to
+ * the "Downloads.Error.BLOCK_VERDICT_" constants. If an unknown
+ * reason is specified, "Downloads.Error.BLOCK_VERDICT_MALWARE" is
+ * assumed.
+ * window:
+ * The window with which this action is associated.
+ * dialogType:
+ * String that determines which actions are available:
+ * - "unblock" to offer just "unblock".
+ * - "chooseUnblock" to offer "unblock" and "confirmBlock".
+ * - "chooseOpen" to offer "open" and "confirmBlock".
+ * }
+ *
+ * @return {Promise}
+ * @resolves String representing the action that should be executed:
+ * - "open" to allow the download and open the file.
+ * - "unblock" to allow the download without opening the file.
+ * - "confirmBlock" to delete the blocked data permanently.
+ * - "cancel" to do nothing and cancel the operation.
+ */
+ async confirmUnblockDownload({ verdict, window, dialogType }) {
+ let s = DownloadsCommon.strings;
+
+ // All the dialogs have an action button and a cancel button, while only
+ // some of them have an additonal button to remove the file. The cancel
+ // button must always be the one at BUTTON_POS_1 because this is the value
+ // returned by confirmEx when using ESC or closing the dialog (bug 345067).
+ let title = s.unblockHeaderUnblock;
+ let firstButtonText = s.unblockButtonUnblock;
+ let firstButtonAction = "unblock";
+ let buttonFlags =
+ Ci.nsIPrompt.BUTTON_TITLE_IS_STRING * Ci.nsIPrompt.BUTTON_POS_0 +
+ Ci.nsIPrompt.BUTTON_TITLE_CANCEL * Ci.nsIPrompt.BUTTON_POS_1;
+
+ switch (dialogType) {
+ case "unblock":
+ // Use only the unblock action. The default is to cancel.
+ buttonFlags += Ci.nsIPrompt.BUTTON_POS_1_DEFAULT;
+ break;
+ case "chooseUnblock":
+ // Use the unblock and remove file actions. The default is remove file.
+ buttonFlags +=
+ Ci.nsIPrompt.BUTTON_TITLE_IS_STRING * Ci.nsIPrompt.BUTTON_POS_2 +
+ Ci.nsIPrompt.BUTTON_POS_2_DEFAULT;
+ break;
+ case "chooseOpen":
+ // Use the unblock and open file actions. The default is open file.
+ title = s.unblockHeaderOpen;
+ firstButtonText = s.unblockButtonOpen;
+ firstButtonAction = "open";
+ buttonFlags +=
+ Ci.nsIPrompt.BUTTON_TITLE_IS_STRING * Ci.nsIPrompt.BUTTON_POS_2 +
+ Ci.nsIPrompt.BUTTON_POS_0_DEFAULT;
+ break;
+ default:
+ console.error("Unexpected dialog type: " + dialogType);
+ return "cancel";
+ }
+
+ let message;
+ switch (verdict) {
+ case lazy.Downloads.Error.BLOCK_VERDICT_UNCOMMON:
+ message = s.unblockTypeUncommon2;
+ break;
+ case lazy.Downloads.Error.BLOCK_VERDICT_POTENTIALLY_UNWANTED:
+ message = s.unblockTypePotentiallyUnwanted2;
+ break;
+ case lazy.Downloads.Error.BLOCK_VERDICT_INSECURE:
+ message = s.unblockInsecure2;
+ break;
+ default:
+ // Assume Downloads.Error.BLOCK_VERDICT_MALWARE
+ message = s.unblockTypeMalware;
+ break;
+ }
+ message += "\n\n" + s.unblockTip2;
+
+ Services.ww.registerNotification(function onOpen(subj, topic) {
+ if (topic == "domwindowopened" && subj instanceof Ci.nsIDOMWindow) {
+ // Make sure to listen for "DOMContentLoaded" because it is fired
+ // before the "load" event.
+ subj.addEventListener(
+ "DOMContentLoaded",
+ function () {
+ if (
+ subj.document.documentURI ==
+ "chrome://global/content/commonDialog.xhtml"
+ ) {
+ Services.ww.unregisterNotification(onOpen);
+ let dialog = subj.document.getElementById("commonDialog");
+ if (dialog) {
+ // Change the dialog to use a warning icon.
+ dialog.classList.add("alert-dialog");
+ }
+ }
+ },
+ { once: true }
+ );
+ }
+ });
+
+ let rv = Services.prompt.confirmEx(
+ window,
+ title,
+ message,
+ buttonFlags,
+ firstButtonText,
+ null,
+ s.unblockButtonConfirmBlock,
+ null,
+ {}
+ );
+ return [firstButtonAction, "cancel", "confirmBlock"][rv];
+ },
+};
+
+ChromeUtils.defineLazyGetter(DownloadsCommon, "log", () => {
+ return lazy.DownloadsLogger.log.bind(lazy.DownloadsLogger);
+});
+ChromeUtils.defineLazyGetter(DownloadsCommon, "error", () => {
+ return lazy.DownloadsLogger.error.bind(lazy.DownloadsLogger);
+});
+
+// DownloadsData
+
+/**
+ * Retrieves the list of past and completed downloads from the underlying
+ * Downloads API data, and provides asynchronous notifications allowing to
+ * build a consistent view of the available data.
+ *
+ * Note that using this object does not automatically initialize the list of
+ * downloads. This is useful to display a neutral progress indicator in
+ * the main browser window until the autostart timeout elapses.
+ *
+ * This powers the DownloadsData, PrivateDownloadsData, and HistoryDownloadsData
+ * singleton objects.
+ */
+function DownloadsDataCtor({ isPrivate, isHistory, maxHistoryResults } = {}) {
+ this._isPrivate = !!isPrivate;
+
+ // Contains all the available Download objects and their integer state.
+ this._oldDownloadStates = new WeakMap();
+
+ // For the history downloads list we don't need to register this as a view,
+ // but we have to ensure that the DownloadsData object is initialized before
+ // we register more views. This ensures that the view methods of DownloadsData
+ // are invoked before those of views registered on HistoryDownloadsData,
+ // allowing the endTime property to be set correctly.
+ if (isHistory) {
+ if (isPrivate) {
+ lazy.PrivateDownloadsData.initializeDataLink();
+ }
+ lazy.DownloadsData.initializeDataLink();
+ this._promiseList = lazy.DownloadsData._promiseList.then(() => {
+ // For history downloads in Private Browsing mode, we'll fetch the combined
+ // list of public and private downloads.
+ return lazy.DownloadHistory.getList({
+ type: isPrivate ? lazy.Downloads.ALL : lazy.Downloads.PUBLIC,
+ maxHistoryResults,
+ });
+ });
+ return;
+ }
+
+ // This defines "initializeDataLink" and "_promiseList" synchronously, then
+ // continues execution only when "initializeDataLink" is called, allowing the
+ // underlying data to be loaded only when actually needed.
+ this._promiseList = (async () => {
+ await new Promise(resolve => (this.initializeDataLink = resolve));
+ let list = await lazy.Downloads.getList(
+ isPrivate ? lazy.Downloads.PRIVATE : lazy.Downloads.PUBLIC
+ );
+ await list.addView(this);
+ return list;
+ })();
+}
+
+DownloadsDataCtor.prototype = {
+ /**
+ * Starts receiving events for current downloads.
+ */
+ initializeDataLink() {},
+
+ /**
+ * Promise resolved with the underlying DownloadList object once we started
+ * receiving events for current downloads.
+ */
+ _promiseList: null,
+
+ /**
+ * Iterator for all the available Download objects. This is empty until the
+ * data has been loaded using the JavaScript API for downloads.
+ */
+ get _downloads() {
+ return ChromeUtils.nondeterministicGetWeakMapKeys(this._oldDownloadStates);
+ },
+
+ /**
+ * True if there are finished downloads that can be removed from the list.
+ */
+ get canRemoveFinished() {
+ for (let download of this._downloads) {
+ // Stopped, paused, and failed downloads with partial data are removed.
+ if (download.stopped && !(download.canceled && download.hasPartialData)) {
+ return true;
+ }
+ }
+ return false;
+ },
+
+ /**
+ * Asks the back-end to remove finished downloads from the list. This method
+ * is only called after the data link has been initialized.
+ */
+ removeFinished() {
+ lazy.Downloads.getList(
+ this._isPrivate ? lazy.Downloads.PRIVATE : lazy.Downloads.PUBLIC
+ )
+ .then(list => list.removeFinished())
+ .catch(console.error);
+ },
+
+ // Integration with the asynchronous Downloads back-end
+
+ onDownloadAdded(download) {
+ // Download objects do not store the end time of downloads, as the Downloads
+ // API does not need to persist this information for all platforms. Once a
+ // download terminates on a Desktop browser, it becomes a history download,
+ // for which the end time is stored differently, as a Places annotation.
+ download.endTime = Date.now();
+
+ this._oldDownloadStates.set(
+ download,
+ DownloadsCommon.stateOfDownload(download)
+ );
+ if (download.error?.becauseBlockedByReputationCheck) {
+ this._notifyDownloadEvent("error");
+ }
+ },
+
+ onDownloadChanged(download) {
+ let oldState = this._oldDownloadStates.get(download);
+ let newState = DownloadsCommon.stateOfDownload(download);
+ this._oldDownloadStates.set(download, newState);
+
+ if (oldState != newState) {
+ if (
+ download.succeeded ||
+ (download.canceled && !download.hasPartialData) ||
+ download.error
+ ) {
+ // Store the end time that may be displayed by the views.
+ download.endTime = Date.now();
+
+ // This state transition code should actually be located in a Downloads
+ // API module (bug 941009).
+ lazy.DownloadHistory.updateMetaData(download).catch(console.error);
+ }
+
+ if (
+ download.succeeded ||
+ (download.error && download.error.becauseBlocked)
+ ) {
+ this._notifyDownloadEvent("finish");
+ }
+ }
+
+ if (!download.newDownloadNotified) {
+ download.newDownloadNotified = true;
+ this._notifyDownloadEvent("start", {
+ openDownloadsListOnStart: download.openDownloadsListOnStart,
+ });
+ }
+ },
+
+ onDownloadRemoved(download) {
+ this._oldDownloadStates.delete(download);
+ },
+
+ // Registration of views
+
+ /**
+ * Adds an object to be notified when the available download data changes.
+ * The specified object is initialized with the currently available downloads.
+ *
+ * @param aView
+ * DownloadsView object to be added. This reference must be passed to
+ * removeView before termination.
+ */
+ addView(aView) {
+ this._promiseList.then(list => list.addView(aView)).catch(console.error);
+ },
+
+ /**
+ * Removes an object previously added using addView.
+ *
+ * @param aView
+ * DownloadsView object to be removed.
+ */
+ removeView(aView) {
+ this._promiseList.then(list => list.removeView(aView)).catch(console.error);
+ },
+
+ // Notifications sent to the most recent browser window only
+
+ /**
+ * Set to true after the first download causes the downloads panel to be
+ * displayed.
+ */
+ get panelHasShownBefore() {
+ try {
+ return Services.prefs.getBoolPref("browser.download.panel.shown");
+ } catch (ex) {}
+ return false;
+ },
+
+ set panelHasShownBefore(aValue) {
+ Services.prefs.setBoolPref("browser.download.panel.shown", aValue);
+ },
+
+ /**
+ * Displays a new or finished download notification in the most recent browser
+ * window, if one is currently available with the required privacy type.
+ * @param {string} aType
+ * Set to "start" for new downloads, "finish" for completed downloads,
+ * "error" for downloads that failed and need attention
+ * @param {boolean} [openDownloadsListOnStart]
+ * (Only relevant when aType = "start")
+ * true (default) - open the downloads panel.
+ * false - only show an indicator notification.
+ */
+ _notifyDownloadEvent(aType, { openDownloadsListOnStart = true } = {}) {
+ DownloadsCommon.log(
+ "Attempting to notify that a new download has started or finished."
+ );
+
+ // Show the panel in the most recent browser window, if present.
+ let browserWin = lazy.BrowserWindowTracker.getTopWindow({
+ private: this._isPrivate,
+ });
+ if (!browserWin) {
+ return;
+ }
+
+ let shouldOpenDownloadsPanel =
+ aType == "start" &&
+ DownloadsCommon.summarizeDownloads(this._downloads).numDownloading <= 1 &&
+ lazy.gAlwaysOpenPanel;
+
+ // For new downloads after the first one, don't show the panel
+ // automatically, but provide a visible notification in the topmost browser
+ // window, if the status indicator is already visible. Also ensure that if
+ // openDownloadsListOnStart = false is passed, we always skip opening the
+ // panel. That's because this will only be passed if the download is started
+ // without user interaction or if a dialog was previously opened in the
+ // process of the download (e.g. unknown content type dialog).
+ if (
+ aType != "error" &&
+ ((this.panelHasShownBefore && !shouldOpenDownloadsPanel) ||
+ !openDownloadsListOnStart ||
+ browserWin != Services.focus.activeWindow)
+ ) {
+ DownloadsCommon.log("Showing new download notification.");
+ browserWin.DownloadsIndicatorView.showEventNotification(aType);
+ return;
+ }
+ this.panelHasShownBefore = true;
+ browserWin.DownloadsPanel.showPanel();
+ },
+};
+
+ChromeUtils.defineLazyGetter(lazy, "HistoryDownloadsData", function () {
+ return new DownloadsDataCtor({ isHistory: true });
+});
+
+ChromeUtils.defineLazyGetter(lazy, "LimitedHistoryDownloadsData", function () {
+ return new DownloadsDataCtor({
+ isHistory: true,
+ maxHistoryResults: kMaxHistoryResultsForLimitedView,
+ });
+});
+
+ChromeUtils.defineLazyGetter(
+ lazy,
+ "LimitedPrivateHistoryDownloadData",
+ function () {
+ return new DownloadsDataCtor({
+ isPrivate: true,
+ isHistory: true,
+ maxHistoryResults: kMaxHistoryResultsForLimitedView,
+ });
+ }
+);
+
+ChromeUtils.defineLazyGetter(lazy, "PrivateDownloadsData", function () {
+ return new DownloadsDataCtor({ isPrivate: true });
+});
+
+ChromeUtils.defineLazyGetter(lazy, "DownloadsData", function () {
+ return new DownloadsDataCtor();
+});
+
+// DownloadsViewPrototype
+
+/**
+ * A prototype for an object that registers itself with DownloadsData as soon
+ * as a view is registered with it.
+ */
+const DownloadsViewPrototype = {
+ /**
+ * Contains all the available Download objects and their current state value.
+ *
+ * SUBCLASSES MUST OVERRIDE THIS PROPERTY.
+ */
+ _oldDownloadStates: null,
+
+ // Registration of views
+
+ /**
+ * Array of view objects that should be notified when the available status
+ * data changes.
+ *
+ * SUBCLASSES MUST OVERRIDE THIS PROPERTY.
+ */
+ _views: null,
+
+ /**
+ * Determines whether this view object is over the private or non-private
+ * downloads.
+ *
+ * SUBCLASSES MUST OVERRIDE THIS PROPERTY.
+ */
+ _isPrivate: false,
+
+ /**
+ * Adds an object to be notified when the available status data changes.
+ * The specified object is initialized with the currently available status.
+ *
+ * @param aView
+ * View object to be added. This reference must be
+ * passed to removeView before termination.
+ */
+ addView(aView) {
+ // Start receiving events when the first of our views is registered.
+ if (!this._views.length) {
+ if (this._isPrivate) {
+ lazy.PrivateDownloadsData.addView(this);
+ } else {
+ lazy.DownloadsData.addView(this);
+ }
+ }
+
+ this._views.push(aView);
+ this.refreshView(aView);
+ },
+
+ /**
+ * Updates the properties of an object previously added using addView.
+ *
+ * @param aView
+ * View object to be updated.
+ */
+ refreshView(aView) {
+ // Update immediately even if we are still loading data asynchronously.
+ // Subclasses must provide these two functions!
+ this._refreshProperties();
+ this._updateView(aView);
+ },
+
+ /**
+ * Removes an object previously added using addView.
+ *
+ * @param aView
+ * View object to be removed.
+ */
+ removeView(aView) {
+ let index = this._views.indexOf(aView);
+ if (index != -1) {
+ this._views.splice(index, 1);
+ }
+
+ // Stop receiving events when the last of our views is unregistered.
+ if (!this._views.length) {
+ if (this._isPrivate) {
+ lazy.PrivateDownloadsData.removeView(this);
+ } else {
+ lazy.DownloadsData.removeView(this);
+ }
+ }
+ },
+
+ // Callback functions from DownloadList
+
+ /**
+ * Indicates whether we are still loading downloads data asynchronously.
+ */
+ _loading: false,
+
+ /**
+ * Called before multiple downloads are about to be loaded.
+ */
+ onDownloadBatchStarting() {
+ this._loading = true;
+ },
+
+ /**
+ * Called after data loading finished.
+ */
+ onDownloadBatchEnded() {
+ this._loading = false;
+ this._updateViews();
+ },
+
+ /**
+ * Called when a new download data item is available, either during the
+ * asynchronous data load or when a new download is started.
+ *
+ * @param download
+ * Download object that was just added.
+ *
+ * @note Subclasses should override this and still call the base method.
+ */
+ onDownloadAdded(download) {
+ this._oldDownloadStates.set(
+ download,
+ DownloadsCommon.stateOfDownload(download)
+ );
+ },
+
+ /**
+ * Called when the overall state of a Download has changed. In particular,
+ * this is called only once when the download succeeds or is blocked
+ * permanently, and is never called if only the current progress changed.
+ *
+ * The onDownloadChanged notification will always be sent afterwards.
+ *
+ * @note Subclasses should override this.
+ */
+ onDownloadStateChanged(download) {
+ throw Components.Exception("", Cr.NS_ERROR_NOT_IMPLEMENTED);
+ },
+
+ /**
+ * Called every time any state property of a Download may have changed,
+ * including progress properties.
+ *
+ * Note that progress notification changes are throttled at the Downloads.sys.mjs
+ * API level, and there is no throttling mechanism in the front-end.
+ *
+ * @note Subclasses should override this and still call the base method.
+ */
+ onDownloadChanged(download) {
+ let oldState = this._oldDownloadStates.get(download);
+ let newState = DownloadsCommon.stateOfDownload(download);
+ this._oldDownloadStates.set(download, newState);
+
+ if (oldState != newState) {
+ this.onDownloadStateChanged(download);
+ }
+ },
+
+ /**
+ * Called when a data item is removed, ensures that the widget associated with
+ * the view item is removed from the user interface.
+ *
+ * @param download
+ * Download object that is being removed.
+ *
+ * @note Subclasses should override this.
+ */
+ onDownloadRemoved(download) {
+ this._oldDownloadStates.delete(download);
+ },
+
+ /**
+ * Private function used to refresh the internal properties being sent to
+ * each registered view.
+ *
+ * @note Subclasses should override this.
+ */
+ _refreshProperties() {
+ throw Components.Exception("", Cr.NS_ERROR_NOT_IMPLEMENTED);
+ },
+
+ /**
+ * Private function used to refresh an individual view.
+ *
+ * @note Subclasses should override this.
+ */
+ _updateView() {
+ throw Components.Exception("", Cr.NS_ERROR_NOT_IMPLEMENTED);
+ },
+
+ /**
+ * Computes aggregate values and propagates the changes to our views.
+ */
+ _updateViews() {
+ // Do not update the status indicators during batch loads of download items.
+ if (this._loading) {
+ return;
+ }
+
+ this._refreshProperties();
+ this._views.forEach(this._updateView, this);
+ },
+};
+
+// DownloadsIndicatorData
+
+/**
+ * This object registers itself with DownloadsData as a view, and transforms the
+ * notifications it receives into overall status data, that is then broadcast to
+ * the registered download status indicators.
+ *
+ * Note that using this object does not automatically start the Download Manager
+ * service. Consumers will see an empty list of downloads until the service is
+ * actually started. This is useful to display a neutral progress indicator in
+ * the main browser window until the autostart timeout elapses.
+ */
+function DownloadsIndicatorDataCtor(aPrivate) {
+ this._oldDownloadStates = new WeakMap();
+ this._isPrivate = aPrivate;
+ this._views = [];
+}
+DownloadsIndicatorDataCtor.prototype = {
+ /**
+ * Map of the relative severities of different attention states.
+ * Used in sorting the map of active downloads' attention states
+ * to determine the attention state to be displayed.
+ */
+ _attentionPriority: new Map([
+ [DownloadsCommon.ATTENTION_NONE, 0],
+ [DownloadsCommon.ATTENTION_SUCCESS, 1],
+ [DownloadsCommon.ATTENTION_INFO, 2],
+ [DownloadsCommon.ATTENTION_WARNING, 3],
+ [DownloadsCommon.ATTENTION_SEVERE, 4],
+ ]),
+
+ /**
+ * Iterator for all the available Download objects. This is empty until the
+ * data has been loaded using the JavaScript API for downloads.
+ */
+ get _downloads() {
+ return ChromeUtils.nondeterministicGetWeakMapKeys(this._oldDownloadStates);
+ },
+
+ /**
+ * Removes an object previously added using addView.
+ *
+ * @param aView
+ * DownloadsIndicatorView object to be removed.
+ */
+ removeView(aView) {
+ DownloadsViewPrototype.removeView.call(this, aView);
+
+ if (!this._views.length) {
+ this._itemCount = 0;
+ }
+ },
+
+ onDownloadAdded(download) {
+ DownloadsViewPrototype.onDownloadAdded.call(this, download);
+ this._itemCount++;
+ this._updateViews();
+ },
+
+ onDownloadStateChanged(download) {
+ if (this._attentionSuppressed !== DownloadsCommon.SUPPRESS_NONE) {
+ return;
+ }
+ let attention;
+ if (
+ !download.succeeded &&
+ download.error &&
+ download.error.reputationCheckVerdict
+ ) {
+ switch (download.error.reputationCheckVerdict) {
+ case lazy.Downloads.Error.BLOCK_VERDICT_UNCOMMON:
+ attention = DownloadsCommon.ATTENTION_INFO;
+ break;
+ case lazy.Downloads.Error.BLOCK_VERDICT_POTENTIALLY_UNWANTED: // fall-through
+ case lazy.Downloads.Error.BLOCK_VERDICT_INSECURE:
+ case lazy.Downloads.Error.BLOCK_VERDICT_DOWNLOAD_SPAM:
+ attention = DownloadsCommon.ATTENTION_WARNING;
+ break;
+ case lazy.Downloads.Error.BLOCK_VERDICT_MALWARE:
+ attention = DownloadsCommon.ATTENTION_SEVERE;
+ break;
+ default:
+ attention = DownloadsCommon.ATTENTION_SEVERE;
+ console.error(
+ "Unknown reputation verdict: " +
+ download.error.reputationCheckVerdict
+ );
+ }
+ } else if (download.succeeded) {
+ attention = DownloadsCommon.ATTENTION_SUCCESS;
+ } else if (download.error) {
+ attention = DownloadsCommon.ATTENTION_WARNING;
+ }
+ download.attention = attention;
+ this.updateAttention();
+ },
+
+ onDownloadChanged(download) {
+ DownloadsViewPrototype.onDownloadChanged.call(this, download);
+ this._updateViews();
+ },
+
+ onDownloadRemoved(download) {
+ DownloadsViewPrototype.onDownloadRemoved.call(this, download);
+ this._itemCount--;
+ this.updateAttention();
+ this._updateViews();
+ },
+
+ // Propagation of properties to our views
+
+ // The following properties are updated by _refreshProperties and are then
+ // propagated to the views. See _refreshProperties for details.
+ _hasDownloads: false,
+ _percentComplete: -1,
+
+ /**
+ * Indicates whether the download indicators should be highlighted.
+ */
+ set attention(aValue) {
+ this._attention = aValue;
+ this._updateViews();
+ },
+ _attention: DownloadsCommon.ATTENTION_NONE,
+
+ /**
+ * Indicates whether the user is interacting with downloads, thus the
+ * attention indication should not be shown even if requested.
+ */
+ set attentionSuppressed(aFlags) {
+ this._attentionSuppressed = aFlags;
+ if (aFlags !== DownloadsCommon.SUPPRESS_NONE) {
+ for (let download of this._downloads) {
+ download.attention = DownloadsCommon.ATTENTION_NONE;
+ }
+ this.attention = DownloadsCommon.ATTENTION_NONE;
+ }
+ },
+ get attentionSuppressed() {
+ return this._attentionSuppressed;
+ },
+ _attentionSuppressed: DownloadsCommon.SUPPRESS_NONE,
+
+ /**
+ * Set the indicator's attention to the most severe attention state among the
+ * unseen displayed downloads, or DownloadsCommon.ATTENTION_NONE if empty.
+ */
+ updateAttention() {
+ let currentAttention = DownloadsCommon.ATTENTION_NONE;
+ let currentPriority = 0;
+ for (let download of this._downloads) {
+ let { attention } = download;
+ let priority = this._attentionPriority.get(attention);
+ if (priority > currentPriority) {
+ currentPriority = priority;
+ currentAttention = attention;
+ }
+ }
+ this.attention = currentAttention;
+ },
+
+ /**
+ * Updates the specified view with the current aggregate values.
+ *
+ * @param aView
+ * DownloadsIndicatorView object to be updated.
+ */
+ _updateView(aView) {
+ aView.hasDownloads = this._hasDownloads;
+ aView.percentComplete = this._percentComplete;
+ aView.attention =
+ this.attentionSuppressed !== DownloadsCommon.SUPPRESS_NONE
+ ? DownloadsCommon.ATTENTION_NONE
+ : this._attention;
+ },
+
+ // Property updating based on current download status
+
+ /**
+ * Number of download items that are available to be displayed.
+ */
+ _itemCount: 0,
+
+ /**
+ * A generator function for the Download objects this summary is currently
+ * interested in. This generator is passed off to summarizeDownloads in order
+ * to generate statistics about the downloads we care about - in this case,
+ * it's all active downloads.
+ */
+ *_activeDownloads() {
+ let downloads = this._isPrivate
+ ? lazy.PrivateDownloadsData._downloads
+ : lazy.DownloadsData._downloads;
+ for (let download of downloads) {
+ if (!download.stopped || (download.canceled && download.hasPartialData)) {
+ yield download;
+ }
+ }
+ },
+
+ /**
+ * Computes aggregate values based on the current state of downloads.
+ */
+ _refreshProperties() {
+ let summary = DownloadsCommon.summarizeDownloads(this._activeDownloads());
+
+ // Determine if the indicator should be shown or get attention.
+ this._hasDownloads = this._itemCount > 0;
+
+ // Always show a progress bar if there are downloads in progress.
+ if (summary.percentComplete >= 0) {
+ this._percentComplete = summary.percentComplete;
+ } else if (summary.numDownloading > 0) {
+ this._percentComplete = 0;
+ } else {
+ this._percentComplete = -1;
+ }
+ },
+};
+Object.setPrototypeOf(
+ DownloadsIndicatorDataCtor.prototype,
+ DownloadsViewPrototype
+);
+
+ChromeUtils.defineLazyGetter(
+ lazy,
+ "PrivateDownloadsIndicatorData",
+ function () {
+ return new DownloadsIndicatorDataCtor(true);
+ }
+);
+
+ChromeUtils.defineLazyGetter(lazy, "DownloadsIndicatorData", function () {
+ return new DownloadsIndicatorDataCtor(false);
+});
+
+// DownloadsSummaryData
+
+/**
+ * DownloadsSummaryData is a view for DownloadsData that produces a summary
+ * of all downloads after a certain exclusion point aNumToExclude. For example,
+ * if there were 5 downloads in progress, and a DownloadsSummaryData was
+ * constructed with aNumToExclude equal to 3, then that DownloadsSummaryData
+ * would produce a summary of the last 2 downloads.
+ *
+ * @param aIsPrivate
+ * True if the browser window which owns the download button is a private
+ * window.
+ * @param aNumToExclude
+ * The number of items to exclude from the summary, starting from the
+ * top of the list.
+ */
+function DownloadsSummaryData(aIsPrivate, aNumToExclude) {
+ this._numToExclude = aNumToExclude;
+ // Since we can have multiple instances of DownloadsSummaryData, we
+ // override these values from the prototype so that each instance can be
+ // completely separated from one another.
+ this._loading = false;
+
+ this._downloads = [];
+
+ // Floating point value indicating the last number of seconds estimated until
+ // the longest download will finish. We need to store this value so that we
+ // don't continuously apply smoothing if the actual download state has not
+ // changed. This is set to -1 if the previous value is unknown.
+ this._lastRawTimeLeft = -1;
+
+ // Last number of seconds estimated until all in-progress downloads with a
+ // known size and speed will finish. This value is stored to allow smoothing
+ // in case of small variations. This is set to -1 if the previous value is
+ // unknown.
+ this._lastTimeLeft = -1;
+
+ // The following properties are updated by _refreshProperties and are then
+ // propagated to the views.
+ this._showingProgress = false;
+ this._details = "";
+ this._description = "";
+ this._numActive = 0;
+ this._percentComplete = -1;
+
+ this._oldDownloadStates = new WeakMap();
+ this._isPrivate = aIsPrivate;
+ this._views = [];
+}
+
+DownloadsSummaryData.prototype = {
+ /**
+ * Removes an object previously added using addView.
+ *
+ * @param aView
+ * DownloadsSummary view to be removed.
+ */
+ removeView(aView) {
+ DownloadsViewPrototype.removeView.call(this, aView);
+
+ if (!this._views.length) {
+ // Clear out our collection of Download objects. If we ever have
+ // another view registered with us, this will get re-populated.
+ this._downloads = [];
+ }
+ },
+
+ onDownloadAdded(download) {
+ DownloadsViewPrototype.onDownloadAdded.call(this, download);
+ this._downloads.unshift(download);
+ this._updateViews();
+ },
+
+ onDownloadStateChanged() {
+ // Since the state of a download changed, reset the estimated time left.
+ this._lastRawTimeLeft = -1;
+ this._lastTimeLeft = -1;
+ },
+
+ onDownloadChanged(download) {
+ DownloadsViewPrototype.onDownloadChanged.call(this, download);
+ this._updateViews();
+ },
+
+ onDownloadRemoved(download) {
+ DownloadsViewPrototype.onDownloadRemoved.call(this, download);
+ let itemIndex = this._downloads.indexOf(download);
+ this._downloads.splice(itemIndex, 1);
+ this._updateViews();
+ },
+
+ // Propagation of properties to our views
+
+ /**
+ * Updates the specified view with the current aggregate values.
+ *
+ * @param aView
+ * DownloadsIndicatorView object to be updated.
+ */
+ _updateView(aView) {
+ aView.showingProgress = this._showingProgress;
+ aView.percentComplete = this._percentComplete;
+ aView.description = this._description;
+ aView.details = this._details;
+ },
+
+ // Property updating based on current download status
+
+ /**
+ * A generator function for the Download objects this summary is currently
+ * interested in. This generator is passed off to summarizeDownloads in order
+ * to generate statistics about the downloads we care about - in this case,
+ * it's the downloads in this._downloads after the first few to exclude,
+ * which was set when constructing this DownloadsSummaryData instance.
+ */
+ *_downloadsForSummary() {
+ if (this._downloads.length) {
+ for (let i = this._numToExclude; i < this._downloads.length; ++i) {
+ yield this._downloads[i];
+ }
+ }
+ },
+
+ /**
+ * Computes aggregate values based on the current state of downloads.
+ */
+ _refreshProperties() {
+ // Pre-load summary with default values.
+ let summary = DownloadsCommon.summarizeDownloads(
+ this._downloadsForSummary()
+ );
+
+ // Run sync to update view right away and get correct description.
+ // See refreshView for more details.
+ this._description = kDownloadsFluentStrings.formatValueSync(
+ "downloads-more-downloading",
+ {
+ count: summary.numDownloading,
+ }
+ );
+ this._percentComplete = summary.percentComplete;
+
+ // Only show the downloading items.
+ this._showingProgress = summary.numDownloading > 0;
+
+ // Display the estimated time left, if present.
+ if (summary.rawTimeLeft == -1) {
+ // There are no downloads with a known time left.
+ this._lastRawTimeLeft = -1;
+ this._lastTimeLeft = -1;
+ this._details = "";
+ } else {
+ // Compute the new time left only if state actually changed.
+ if (this._lastRawTimeLeft != summary.rawTimeLeft) {
+ this._lastRawTimeLeft = summary.rawTimeLeft;
+ this._lastTimeLeft = DownloadsCommon.smoothSeconds(
+ summary.rawTimeLeft,
+ this._lastTimeLeft
+ );
+ }
+ [this._details] = lazy.DownloadUtils.getDownloadStatusNoRate(
+ summary.totalTransferred,
+ summary.totalSize,
+ summary.slowestSpeed,
+ this._lastTimeLeft
+ );
+ }
+ },
+};
+Object.setPrototypeOf(DownloadsSummaryData.prototype, DownloadsViewPrototype);