summaryrefslogtreecommitdiffstats
path: root/application/palemoon/components/downloads/DownloadsCommon.jsm
diff options
context:
space:
mode:
Diffstat (limited to 'application/palemoon/components/downloads/DownloadsCommon.jsm')
-rw-r--r--application/palemoon/components/downloads/DownloadsCommon.jsm2401
1 files changed, 2401 insertions, 0 deletions
diff --git a/application/palemoon/components/downloads/DownloadsCommon.jsm b/application/palemoon/components/downloads/DownloadsCommon.jsm
new file mode 100644
index 000000000..b90baaf9c
--- /dev/null
+++ b/application/palemoon/components/downloads/DownloadsCommon.jsm
@@ -0,0 +1,2401 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 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/. */
+
+"use strict";
+
+this.EXPORTED_SYMBOLS = [
+ "DownloadsCommon",
+];
+
+/**
+ * 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
+ * Download Manager data, and provides asynchronous notifications allowing
+ * to build a consistent view of the available data.
+ *
+ * DownloadsDataItem
+ * Represents a single item in the list of downloads. This object either wraps
+ * an existing nsIDownload from the Download Manager, or provides the same
+ * information read directly from the downloads database, with the possibility
+ * of querying the nsIDownload lazily, for performance reasons.
+ *
+ * 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
+
+const Cc = Components.classes;
+const Ci = Components.interfaces;
+const Cu = Components.utils;
+const Cr = Components.results;
+
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+Cu.import("resource://gre/modules/Services.jsm");
+
+XPCOMUtils.defineLazyModuleGetter(this, "NetUtil",
+ "resource://gre/modules/NetUtil.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "PluralForm",
+ "resource://gre/modules/PluralForm.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "Downloads",
+ "resource://gre/modules/Downloads.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "DownloadUIHelper",
+ "resource://gre/modules/DownloadUIHelper.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "DownloadUtils",
+ "resource://gre/modules/DownloadUtils.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "OS",
+ "resource://gre/modules/osfile.jsm")
+XPCOMUtils.defineLazyModuleGetter(this, "PlacesUtils",
+ "resource://gre/modules/PlacesUtils.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "PrivateBrowsingUtils",
+ "resource://gre/modules/PrivateBrowsingUtils.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "RecentWindow",
+ "resource:///modules/RecentWindow.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "PlacesUtils",
+ "resource://gre/modules/PlacesUtils.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "Promise",
+ "resource://gre/modules/Promise.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "DownloadsLogger",
+ "resource:///modules/DownloadsLogger.jsm");
+
+const nsIDM = Ci.nsIDownloadManager;
+
+const kDownloadsStringBundleUrl =
+ "chrome://browser/locale/downloads/downloads.properties";
+
+const kPrefBdmScanWhenDone = "browser.download.manager.scanWhenDone";
+const kPrefBdmAlertOnExeOpen = "browser.download.manager.alertOnEXEOpen";
+
+const kDownloadsStringsRequiringFormatting = {
+ sizeWithUnits: true,
+ shortTimeLeftSeconds: true,
+ shortTimeLeftMinutes: true,
+ shortTimeLeftHours: true,
+ shortTimeLeftDays: true,
+ statusSeparator: true,
+ statusSeparatorBeforeNumber: true,
+ fileExecutableSecurityWarning: true
+};
+
+const kDownloadsStringsRequiringPluralForm = {
+ otherDownloads2: true
+};
+
+XPCOMUtils.defineLazyGetter(this, "DownloadsLocalFileCtor", function () {
+ return Components.Constructor("@mozilla.org/file/local;1",
+ "nsILocalFile", "initWithPath");
+});
+
+const kPartialDownloadSuffix = ".part";
+
+const kPrefBranch = Services.prefs.getBranch("browser.download.");
+
+let PrefObserver = {
+ QueryInterface: XPCOMUtils.generateQI([Ci.nsIObserver,
+ Ci.nsISupportsWeakReference]),
+ getPref: function PO_getPref(name) {
+ try {
+ switch (typeof this.prefs[name]) {
+ case "boolean":
+ return kPrefBranch.getBoolPref(name);
+ }
+ } catch (ex) { }
+ return this.prefs[name];
+ },
+ observe: function PO_observe(aSubject, aTopic, aData) {
+ if (this.prefs.hasOwnProperty(aData)) {
+ return this[aData] = this.getPref(aData);
+ }
+ },
+ register: function PO_register(prefs) {
+ this.prefs = prefs;
+ kPrefBranch.addObserver("", this, true);
+ for (let key in prefs) {
+ let name = key;
+ XPCOMUtils.defineLazyGetter(this, name, function () {
+ return PrefObserver.getPref(name);
+ });
+ }
+ },
+};
+
+PrefObserver.register({
+ // prefName: defaultValue
+ debug: false,
+ animateNotifications: 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.
+ */
+this.DownloadsCommon = {
+ log: function DC_log(...aMessageArgs) {
+ delete this.log;
+ this.log = function DC_log(...aMessageArgs) {
+ if (!PrefObserver.debug) {
+ return;
+ }
+ DownloadsLogger.log.apply(DownloadsLogger, aMessageArgs);
+ }
+ this.log.apply(this, aMessageArgs);
+ },
+
+ error: function DC_error(...aMessageArgs) {
+ delete this.error;
+ this.error = function DC_error(...aMessageArgs) {
+ if (!PrefObserver.debug) {
+ return;
+ }
+ DownloadsLogger.reportError.apply(DownloadsLogger, aMessageArgs);
+ }
+ this.error.apply(this, aMessageArgs);
+ },
+ /**
+ * 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);
+ let enumerator = sb.getSimpleEnumeration();
+ while (enumerator.hasMoreElements()) {
+ let string = enumerator.getNext().QueryInterface(Ci.nsIPropertyElement);
+ 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.slice(arguments, 0),
+ arguments.length);
+ };
+ } else if (stringName in kDownloadsStringsRequiringPluralForm) {
+ strings[stringName] = function (aCount) {
+ // Convert "arguments" to a real array before calling into XPCOM.
+ let formattedString = sb.formatStringFromName(stringName,
+ Array.slice(arguments, 0),
+ arguments.length);
+ return PluralForm.get(aCount, formattedString);
+ };
+ } else {
+ strings[stringName] = string.value;
+ }
+ }
+ delete this.strings;
+ return this.strings = strings;
+ },
+
+ /**
+ * Generates a very short string representing the given time left.
+ *
+ * @param aSeconds
+ * Value to be formatted. It represents the number of seconds, it must
+ * be positive but does not need to be an integer.
+ *
+ * @return Formatted string, for example "30s" or "2h". The returned value is
+ * maximum three characters long, at least in English.
+ */
+ formatTimeLeft: function DC_formatTimeLeft(aSeconds)
+ {
+ // Decide what text to show for the time
+ let seconds = Math.round(aSeconds);
+ if (!seconds) {
+ return "";
+ } else if (seconds <= 30) {
+ return DownloadsCommon.strings["shortTimeLeftSeconds"](seconds);
+ }
+ let minutes = Math.round(aSeconds / 60);
+ if (minutes < 60) {
+ return DownloadsCommon.strings["shortTimeLeftMinutes"](minutes);
+ }
+ let hours = Math.round(minutes / 60);
+ if (hours < 48) { // two days
+ return DownloadsCommon.strings["shortTimeLeftHours"](hours);
+ }
+ let days = Math.round(hours / 24);
+ return DownloadsCommon.strings["shortTimeLeftDays"](Math.min(days, 99));
+ },
+
+ /**
+ * Indicates whether we should show the full Download Manager window interface
+ * instead of the simplified panel interface. The behavior of downloads
+ * across browsing session is consistent with the selected interface.
+ */
+ get useToolkitUI()
+ {
+ /* Toolkit UI is currently incompatible.
+ * FIXME: Either fix the toolkitUI (make DBConnection work) or remove
+ * the unused code altogether
+ */
+ //try {
+ // return Services.prefs.getBoolPref("browser.download.useToolkitUI");
+ //} catch (ex) { }
+ return false;
+ },
+
+ /**
+ * Indicates whether we should show visual notification on the indicator
+ * when a download event is triggered.
+ */
+ get animateNotifications()
+ {
+ return PrefObserver.animateNotifications;
+ },
+
+ /**
+ * Get access to one of the DownloadsData or PrivateDownloadsData objects,
+ * depending on the privacy status of the window in question.
+ *
+ * @param aWindow
+ * The browser window which owns the download button.
+ */
+ getData: function DC_getData(aWindow) {
+ if (PrivateBrowsingUtils.isWindowPrivate(aWindow)) {
+ return PrivateDownloadsData;
+ } else {
+ return DownloadsData;
+ }
+ },
+
+ /**
+ * Initializes the data link for both the private and non-private downloads
+ * data objects.
+ *
+ * @param aDownloadManagerService
+ * Reference to the service implementing nsIDownloadManager. We need
+ * this because getService isn't available for us when this method is
+ * called, and we must ensure to register our listeners before the
+ * getService call for the Download Manager returns.
+ */
+ initializeAllDataLinks: function DC_initializeAllDataLinks(aDownloadManagerService) {
+ DownloadsData.initializeDataLink(aDownloadManagerService);
+ PrivateDownloadsData.initializeDataLink(aDownloadManagerService);
+ },
+
+ /**
+ * Terminates the data link for both the private and non-private downloads
+ * data objects.
+ */
+ terminateAllDataLinks: function DC_terminateAllDataLinks() {
+ DownloadsData.terminateDataLink();
+ PrivateDownloadsData.terminateDataLink();
+ },
+
+ /**
+ * Reloads the specified kind of downloads from the non-private store.
+ * This method must only be called when Private Browsing Mode is disabled.
+ *
+ * @param aActiveOnly
+ * True to load only active downloads from the database.
+ */
+ ensureAllPersistentDataLoaded:
+ function DC_ensureAllPersistentDataLoaded(aActiveOnly) {
+ DownloadsData.ensurePersistentDataLoaded(aActiveOnly);
+ },
+
+ /**
+ * Get access to one of the DownloadsIndicatorData or
+ * PrivateDownloadsIndicatorData objects, depending on the privacy status of
+ * the window in question.
+ */
+ getIndicatorData: function DC_getIndicatorData(aWindow) {
+ if (PrivateBrowsingUtils.isWindowPrivate(aWindow)) {
+ return PrivateDownloadsIndicatorData;
+ } else {
+ return 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: function DC_getSummary(aWindow, aNumToExclude)
+ {
+ if (PrivateBrowsingUtils.isWindowPrivate(aWindow)) {
+ if (this._privateSummary) {
+ return this._privateSummary;
+ }
+ return this._privateSummary = new DownloadsSummaryData(true, aNumToExclude);
+ } else {
+ 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 nsIDM.DOWNLOAD_DOWNLOADING;
+ }
+ if (download.succeeded) {
+ return nsIDM.DOWNLOAD_FINISHED;
+ }
+ if (download.error) {
+ if (download.error.becauseBlockedByParentalControls) {
+ return nsIDM.DOWNLOAD_BLOCKED_PARENTAL;
+ }
+ if (download.error.becauseBlockedByReputationCheck) {
+ return nsIDM.DOWNLOAD_DIRTY;
+ }
+ return nsIDM.DOWNLOAD_FAILED;
+ }
+ if (download.canceled) {
+ if (download.hasPartialData) {
+ return nsIDM.DOWNLOAD_PAUSED;
+ }
+ return nsIDM.DOWNLOAD_CANCELED;
+ }
+ return nsIDM.DOWNLOAD_NOTSTARTED;
+ },
+
+ /**
+ * Given an iterable collection of DownloadDataItems, generates and returns
+ * statistics about that collection.
+ *
+ * @param aDataItems An iterable collection of DownloadDataItems.
+ *
+ * @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.
+ * numScanning : The total number of downloads being scanned.
+ * 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: function DC_summarizeDownloads(aDataItems)
+ {
+ let summary = {
+ numActive: 0,
+ numPaused: 0,
+ numScanning: 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
+ // dataItems.
+ slowestSpeed: Infinity,
+ rawTimeLeft: -1,
+ percentComplete: -1
+ }
+
+ for (let dataItem of aDataItems) {
+ summary.numActive++;
+ switch (dataItem.state) {
+ case nsIDM.DOWNLOAD_PAUSED:
+ summary.numPaused++;
+ break;
+ case nsIDM.DOWNLOAD_SCANNING:
+ summary.numScanning++;
+ break;
+ case nsIDM.DOWNLOAD_DOWNLOADING:
+ summary.numDownloading++;
+ if (dataItem.maxBytes > 0 && dataItem.speed > 0) {
+ let sizeLeft = dataItem.maxBytes - dataItem.currBytes;
+ summary.rawTimeLeft = Math.max(summary.rawTimeLeft,
+ sizeLeft / dataItem.speed);
+ summary.slowestSpeed = Math.min(summary.slowestSpeed,
+ dataItem.speed);
+ }
+ break;
+ }
+ // Only add to total values if we actually know the download size.
+ if (dataItem.maxBytes > 0 &&
+ dataItem.state != nsIDM.DOWNLOAD_CANCELED &&
+ dataItem.state != nsIDM.DOWNLOAD_FAILED) {
+ summary.totalSize += dataItem.maxBytes;
+ summary.totalTransferred += dataItem.currBytes;
+ }
+ }
+
+ if (summary.numActive != 0 && summary.totalSize != 0 &&
+ summary.numActive != summary.numScanning) {
+ summary.percentComplete = (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: function DC_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 ? .3 : .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 ? .4 : .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.
+ * If you've a dataItem, you should call dataItem.openLocalFile.
+ * @param aFile
+ * the downloaded file to be opened.
+ * @param aMimeInfo
+ * the mime type info object. May be null.
+ * @param aOwnerWindow
+ * the window with which this action is associated.
+ */
+ openDownloadedFile: function DC_openDownloadedFile(aFile, aMimeInfo, aOwnerWindow) {
+ if (!(aFile instanceof Ci.nsIFile))
+ throw new Error("aFile must be a nsIFile object");
+ if (aMimeInfo && !(aMimeInfo instanceof Ci.nsIMIMEInfo))
+ throw new Error("Invalid value passed for aMimeInfo");
+ if (!(aOwnerWindow instanceof Ci.nsIDOMWindow))
+ throw new Error("aOwnerWindow must be a dom-window object");
+
+ // Confirm opening executable files if required.
+ if (aFile.isExecutable()) {
+ let showAlert = true;
+ try {
+ showAlert = Services.prefs.getBoolPref(kPrefBdmAlertOnExeOpen);
+ } catch (ex) { }
+
+ // On Vista and above, we rely on native security prompting for
+ // downloaded content unless it's disabled.
+ if (DownloadsCommon.isWinVistaOrHigher) {
+ try {
+ if (Services.prefs.getBoolPref(kPrefBdmScanWhenDone)) {
+ showAlert = false;
+ }
+ } catch (ex) { }
+ }
+
+ if (showAlert) {
+ let name = aFile.leafName;
+ let message =
+ DownloadsCommon.strings.fileExecutableSecurityWarning(name, name);
+ let title =
+ DownloadsCommon.strings.fileExecutableSecurityWarningTitle;
+ let dontAsk =
+ DownloadsCommon.strings.fileExecutableSecurityWarningDontAsk;
+
+ let checkbox = { value: false };
+ let open = Services.prompt.confirmCheck(aOwnerWindow, title, message,
+ dontAsk, checkbox);
+ if (!open) {
+ return;
+ }
+
+ Services.prefs.setBoolPref(kPrefBdmAlertOnExeOpen,
+ !checkbox.value);
+ }
+ }
+
+ // Actually open the file.
+ try {
+ if (aMimeInfo && aMimeInfo.preferredAction == aMimeInfo.useHelperApp) {
+ aMimeInfo.launchWithFile(aFile);
+ return;
+ }
+ }
+ catch(ex) { }
+
+ // If either we don't have the mime info, or the preferred action failed,
+ // attempt to launch the file directly.
+ try {
+ aFile.launch();
+ }
+ catch(ex) {
+ // If launch fails, try sending it through the system's external "file:"
+ // URL handler.
+ Cc["@mozilla.org/uriloader/external-protocol-service;1"]
+ .getService(Ci.nsIExternalProtocolService)
+ .loadUrl(NetUtil.newURI(aFile));
+ }
+ },
+
+ /**
+ * Show a downloaded file in the system file manager.
+ * If you have a dataItem, use dataItem.showLocalFile.
+ *
+ * @param aFile
+ * a downloaded file.
+ */
+ showDownloadedFile: function DC_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) {
+ try {
+ // Open the parent directory to show where the file should be.
+ parent.launch();
+ } catch (ex) {
+ // If launch also fails (probably because it's not implemented), let
+ // the OS handler try to open the parent.
+ Cc["@mozilla.org/uriloader/external-protocol-service;1"]
+ .getService(Ci.nsIExternalProtocolService)
+ .loadUrl(NetUtil.newURI(parent));
+ }
+ }
+ }
+ }
+};
+
+/**
+ * Returns true if we are executing on Windows Vista or a later version.
+ */
+XPCOMUtils.defineLazyGetter(DownloadsCommon, "isWinVistaOrHigher", function () {
+ let os = Cc["@mozilla.org/xre/app-info;1"].getService(Ci.nsIXULRuntime).OS;
+ if (os != "WINNT") {
+ return false;
+ }
+ let sysInfo = Cc["@mozilla.org/system-info;1"].getService(Ci.nsIPropertyBag2);
+ return parseFloat(sysInfo.getProperty("version")) >= 6;
+});
+
+/**
+ * Returns true to indicate that we should hook the panel to the JavaScript API
+ * for downloads instead of the nsIDownloadManager back-end.
+ * This is kept for compatibility/leftovers and should be removed later.
+ */
+XPCOMUtils.defineLazyGetter(DownloadsCommon, "useJSTransfer", function () {
+ return true;
+});
+
+////////////////////////////////////////////////////////////////////////////////
+//// DownloadsData
+
+/**
+ * Retrieves the list of past and completed downloads from the underlying
+ * Download Manager data, and provides asynchronous notifications allowing to
+ * build a consistent view of the available data.
+ *
+ * This object responds to real-time changes in the underlying Download Manager
+ * data. For example, the deletion of one or more downloads is notified through
+ * the nsIObserver interface, while any state or progress change is notified
+ * through the nsIDownloadProgressListener interface.
+ *
+ * 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.
+ *
+ * Note that DownloadsData and PrivateDownloadsData are two equivalent singleton
+ * objects, one accessing non-private downloads, and the other accessing private
+ * ones.
+ */
+function DownloadsDataCtor(aPrivate) {
+ this._isPrivate = aPrivate;
+
+ // This Object contains all the available DownloadsDataItem objects, indexed by
+ // their globally unique identifier. The identifiers of downloads that have
+ // been removed from the Download Manager data are still present, however the
+ // associated objects are replaced with the value "null". This is required to
+ // prevent race conditions when populating the list asynchronously.
+ this.dataItems = {};
+
+ // Array of view objects that should be notified when the available download
+ // data changes.
+ this._views = [];
+
+ // Maps Download objects to DownloadDataItem objects.
+ this._downloadToDataItemMap = new Map();
+}
+
+DownloadsDataCtor.prototype = {
+ /**
+ * Starts receiving events for current downloads.
+ */
+ initializeDataLink() {
+ if (!this._dataLinkInitialized) {
+ let promiseList = Downloads.getList(this._isPrivate ? Downloads.PRIVATE
+ : Downloads.PUBLIC);
+ promiseList.then(list => list.addView(this)).then(null, Cu.reportError);
+ this._dataLinkInitialized = true;
+ }
+ },
+ _dataLinkInitialized: false,
+
+ /**
+ * Stops receiving events for current downloads and cancels any pending read.
+ */
+ terminateDataLink: function DD_terminateDataLink()
+ {
+ Cu.reportError("terminateDataLink not applicable with JS Transfers");
+ return;
+ },
+
+ /**
+ * True if there are finished downloads that can be removed from the list.
+ */
+ get canRemoveFinished()
+ {
+ for (let [, dataItem] of Iterator(this.dataItems)) {
+ if (dataItem && !dataItem.inProgress) {
+ return true;
+ }
+ }
+ return false;
+ },
+
+ /**
+ * Asks the back-end to remove finished downloads from the list.
+ */
+ removeFinished: function DD_removeFinished()
+ {
+ let promiseList = Downloads.getList(this._isPrivate ? Downloads.PRIVATE
+ : Downloads.PUBLIC);
+ promiseList.then(list => list.removeFinished())
+ .then(null, Cu.reportError);
+ },
+
+ //////////////////////////////////////////////////////////////////////////////
+ //// Integration with the asynchronous Downloads back-end
+
+ onDownloadAdded: function (aDownload)
+ {
+ let dataItem = new DownloadsDataItem(aDownload);
+ this._downloadToDataItemMap.set(aDownload, dataItem);
+ this.dataItems[dataItem.downloadGuid] = dataItem;
+
+ for (let view of this._views) {
+ view.onDataItemAdded(dataItem, true);
+ }
+
+ this._updateDataItemState(dataItem);
+ },
+
+ onDownloadChanged: function (aDownload)
+ {
+ let dataItem = this._downloadToDataItemMap.get(aDownload);
+ if (!dataItem) {
+ Cu.reportError("Download doesn't exist.");
+ return;
+ }
+
+ this._updateDataItemState(dataItem);
+ },
+
+ onDownloadRemoved: function (aDownload)
+ {
+ let dataItem = this._downloadToDataItemMap.get(aDownload);
+ if (!dataItem) {
+ Cu.reportError("Download doesn't exist.");
+ return;
+ }
+
+ this._downloadToDataItemMap.delete(aDownload);
+ this.dataItems[dataItem.downloadGuid] = null;
+ for (let view of this._views) {
+ view.onDataItemRemoved(dataItem);
+ }
+ },
+
+ /**
+ * Updates the given data item and sends related notifications.
+ */
+ _updateDataItemState: function (aDataItem)
+ {
+ let oldState = aDataItem.state;
+ let wasInProgress = aDataItem.inProgress;
+ let wasDone = aDataItem.done;
+
+ aDataItem.updateFromJSDownload();
+
+ if (wasInProgress && !aDataItem.inProgress) {
+ aDataItem.endTime = Date.now();
+ }
+
+ if (oldState != aDataItem.state) {
+ for (let view of this._views) {
+ try {
+ view.getViewItem(aDataItem).onStateChange(oldState);
+ } catch (ex) {
+ Cu.reportError(ex);
+ }
+ }
+
+ // This state transition code should actually be located in a Downloads
+ // API module (bug 941009). Moreover, the fact that state is stored as
+ // annotations should be ideally hidden behind methods of
+ // nsIDownloadHistory (bug 830415).
+ if (!this._isPrivate && !aDataItem.inProgress) {
+ try {
+ let downloadMetaData = { state: aDataItem.state,
+ endTime: aDataItem.endTime };
+ if (aDataItem.done) {
+ downloadMetaData.fileSize = aDataItem.maxBytes;
+ }
+
+ // RRR: Annotation service throws here. commented out for now.
+ /*PlacesUtils.annotations.setPageAnnotation(
+ NetUtil.newURI(aDataItem.uri), "downloads/metaData",
+ JSON.stringify(downloadMetaData), 0,
+ PlacesUtils.annotations.EXPIRE_WITH_HISTORY);*/
+ } catch (ex) {
+ Cu.reportError(ex);
+ }
+ }
+ }
+
+ if (!aDataItem.newDownloadNotified) {
+ aDataItem.newDownloadNotified = true;
+ this._notifyDownloadEvent("start");
+ }
+
+ if (!wasDone && aDataItem.done) {
+ this._notifyDownloadEvent("finish");
+ }
+
+ for (let view of this._views) {
+ view.getViewItem(aDataItem).onProgressChange();
+ }
+ },
+
+ //////////////////////////////////////////////////////////////////////////////
+ //// 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: function DD_addView(aView)
+ {
+ this._views.push(aView);
+ this._updateView(aView);
+ },
+
+ /**
+ * Removes an object previously added using addView.
+ *
+ * @param aView
+ * DownloadsView object to be removed.
+ */
+ removeView: function DD_removeView(aView)
+ {
+ let index = this._views.indexOf(aView);
+ if (index != -1) {
+ this._views.splice(index, 1);
+ }
+ },
+
+ /**
+ * Ensures that the currently loaded data is added to the specified view.
+ *
+ * @param aView
+ * DownloadsView object to be initialized.
+ */
+ _updateView: function DD_updateView(aView)
+ {
+ // Indicate to the view that a batch loading operation is in progress.
+ aView.onDataLoadStarting();
+
+ // Sort backwards by start time, ensuring that the most recent
+ // downloads are added first regardless of their state.
+ let loadedItemsArray = [dataItem
+ for each (dataItem in this.dataItems)
+ if (dataItem)];
+ loadedItemsArray.sort(function(a, b) b.startTime - a.startTime);
+ loadedItemsArray.forEach(
+ function (dataItem) aView.onDataItemAdded(dataItem, false)
+ );
+
+ // Notify the view that all data is available unless loading is in progress.
+ if (!this._pendingStatement) {
+ aView.onDataLoadCompleted();
+ }
+ },
+
+ //////////////////////////////////////////////////////////////////////////////
+ //// In-memory downloads data store
+
+ /**
+ * Clears the loaded data.
+ */
+ clear: function DD_clear()
+ {
+ this._terminateDataAccess();
+ this.dataItems = {};
+ },
+
+ /**
+ * Returns the data item associated with the provided source object. The
+ * source can be a download object that we received from the Download Manager
+ * because of a real-time notification, or a row from the downloads database,
+ * during the asynchronous data load.
+ *
+ * In case we receive download status notifications while we are still
+ * populating the list of downloads from the database, we want the real-time
+ * status to take precedence over the state that is read from the database,
+ * which might be older. This is achieved by creating the download item if
+ * it's not already in the list, but never updating the returned object using
+ * the data from the database, if the object already exists.
+ *
+ * @param aSource
+ * Object containing the data with which the item should be initialized
+ * if it doesn't already exist in the list. This should implement
+ * either nsIDownload or mozIStorageRow. If the item exists, this
+ * argument is only used to retrieve the download identifier.
+ * @param aMayReuseGUID
+ * If false, indicates that the download should not be added if a
+ * download with the same identifier was removed in the meantime. This
+ * ensures that, while loading the list asynchronously, downloads that
+ * have been removed in the meantime do no reappear inadvertently.
+ *
+ * @return New or existing data item, or null if the item was deleted from the
+ * list of available downloads.
+ */
+ _getOrAddDataItem: function DD_getOrAddDataItem(aSource, aMayReuseGUID)
+ {
+ let downloadGuid = (aSource instanceof Ci.nsIDownload)
+ ? aSource.guid
+ : aSource.getResultByName("guid");
+ if (downloadGuid in this.dataItems) {
+ let existingItem = this.dataItems[downloadGuid];
+ if (existingItem || !aMayReuseGUID) {
+ // Returns null if the download was removed and we can't reuse the item.
+ return existingItem;
+ }
+ }
+ DownloadsCommon.log("Creating a new DownloadsDataItem with downloadGuid =",
+ downloadGuid);
+ let dataItem = new DownloadsDataItem(aSource);
+ this.dataItems[downloadGuid] = dataItem;
+
+ // Create the view items before returning.
+ let addToStartOfList = aSource instanceof Ci.nsIDownload;
+ this._views.forEach(
+ function (view) view.onDataItemAdded(dataItem, addToStartOfList)
+ );
+ return dataItem;
+ },
+
+ /**
+ * Removes the data item with the specified identifier.
+ *
+ * This method can be called at most once per download identifier.
+ */
+ _removeDataItem: function DD_removeDataItem(aDownloadId)
+ {
+ if (aDownloadId in this.dataItems) {
+ let dataItem = this.dataItems[aDownloadId];
+ this.dataItems[aDownloadId] = null;
+ this._views.forEach(
+ function (view) view.onDataItemRemoved(dataItem)
+ );
+ }
+ },
+
+ //////////////////////////////////////////////////////////////////////////////
+ //// Persistent data loading
+
+ /**
+ * Represents an executing statement, allowing its cancellation.
+ */
+ _pendingStatement: null,
+
+ /**
+ * Indicates which kind of items from the persistent downloads database have
+ * been fully loaded in memory and are available to the views. This can
+ * assume the value of one of the kLoad constants.
+ */
+ _loadState: 0,
+
+ /** No downloads have been fully loaded yet. */
+ get kLoadNone() 0,
+ /** All the active downloads in the database are loaded in memory. */
+ get kLoadActive() 1,
+ /** All the downloads in the database are loaded in memory. */
+ get kLoadAll() 2,
+
+ /**
+ * Reloads the specified kind of downloads from the persistent database. This
+ * method must only be called when Private Browsing Mode is disabled.
+ *
+ * @param aActiveOnly
+ * True to load only active downloads from the database.
+ */
+ ensurePersistentDataLoaded:
+ function DD_ensurePersistentDataLoaded(aActiveOnly)
+ {
+ if (this == PrivateDownloadsData) {
+ Cu.reportError("ensurePersistentDataLoaded should not be called on PrivateDownloadsData");
+ return;
+ }
+
+ if (this._pendingStatement) {
+ // We are already in the process of reloading all downloads.
+ return;
+ }
+
+ if (aActiveOnly) {
+ if (this._loadState == this.kLoadNone) {
+ DownloadsCommon.log("Loading only active downloads from the persistence database");
+ // Indicate to the views that a batch loading operation is in progress.
+ this._views.forEach(
+ function (view) view.onDataLoadStarting()
+ );
+
+ // Reload the list using the Download Manager service. The list is
+ // returned in no particular order.
+ let downloads = Services.downloads.activeDownloads;
+ while (downloads.hasMoreElements()) {
+ let download = downloads.getNext().QueryInterface(Ci.nsIDownload);
+ this._getOrAddDataItem(download, true);
+ }
+ this._loadState = this.kLoadActive;
+
+ // Indicate to the views that the batch loading operation is complete.
+ this._views.forEach(
+ function (view) view.onDataLoadCompleted()
+ );
+ DownloadsCommon.log("Active downloads done loading.");
+ }
+ } else {
+ if (this._loadState != this.kLoadAll) {
+ // Load only the relevant columns from the downloads database. The
+ // columns are read in the _initFromDataRow method of DownloadsDataItem.
+ // Order by descending download identifier so that the most recent
+ // downloads are notified first to the listening views.
+ DownloadsCommon.log("Loading all downloads from the persistence database.");
+ let dbConnection = Services.downloads.DBConnection;
+ let statement = dbConnection.createAsyncStatement(
+ "SELECT guid, target, name, source, referrer, state, "
+ + "startTime, endTime, currBytes, maxBytes "
+ + "FROM moz_downloads "
+ + "ORDER BY startTime DESC"
+ );
+ try {
+ this._pendingStatement = statement.executeAsync(this);
+ } finally {
+ statement.finalize();
+ }
+ }
+ }
+ },
+
+ /**
+ * Cancels any pending data access and ensures views are notified.
+ */
+ _terminateDataAccess: function DD_terminateDataAccess()
+ {
+ if (this._pendingStatement) {
+ this._pendingStatement.cancel();
+ this._pendingStatement = null;
+ }
+
+ // Close all the views on the current data. Create a copy of the array
+ // because some views might unregister while processing this event.
+ Array.slice(this._views, 0).forEach(
+ function (view) view.onDataInvalidated()
+ );
+ },
+
+ //////////////////////////////////////////////////////////////////////////////
+ //// mozIStorageStatementCallback
+
+ handleResult: function DD_handleResult(aResultSet)
+ {
+ for (let row = aResultSet.getNextRow();
+ row;
+ row = aResultSet.getNextRow()) {
+ // Add the download to the list and initialize it with the data we read,
+ // unless we already received a notification providing more reliable
+ // information for this download.
+ this._getOrAddDataItem(row, false);
+ }
+ },
+
+ handleError: function DD_handleError(aError)
+ {
+ DownloadsCommon.error("Database statement execution error (",
+ aError.result, "): ", aError.message);
+ },
+
+ handleCompletion: function DD_handleCompletion(aReason)
+ {
+ DownloadsCommon.log("Loading all downloads from database completed with reason:",
+ aReason);
+ this._pendingStatement = null;
+
+ // To ensure that we don't inadvertently delete more downloads from the
+ // database than needed on shutdown, we should update the load state only if
+ // the operation completed successfully.
+ if (aReason == Ci.mozIStorageStatementCallback.REASON_FINISHED) {
+ this._loadState = this.kLoadAll;
+ }
+
+ // Indicate to the views that the batch loading operation is complete, even
+ // if the lookup failed or was canceled. The only possible glitch happens
+ // in case the database backend changes while loading data, when the views
+ // would open and immediately close. This case is rare enough not to need a
+ // special treatment.
+ this._views.forEach(
+ function (view) view.onDataLoadCompleted()
+ );
+ },
+
+ //////////////////////////////////////////////////////////////////////////////
+ //// nsIObserver
+
+ observe: function DD_observe(aSubject, aTopic, aData)
+ {
+ switch (aTopic) {
+ case "download-manager-remove-download-guid":
+ // If a single download was removed, remove the corresponding data item.
+ if (aSubject) {
+ let downloadGuid = aSubject.QueryInterface(Ci.nsISupportsCString).data;
+ DownloadsCommon.log("A single download with id",
+ downloadGuid, "was removed.");
+ this._removeDataItem(downloadGuid);
+ break;
+ }
+
+ // Multiple downloads have been removed. Iterate over known downloads
+ // and remove those that don't exist anymore.
+ DownloadsCommon.log("Multiple downloads were removed.");
+ for each (let dataItem in this.dataItems) {
+ if (dataItem) {
+ // Bug 449811 - We have to bind to the dataItem because Javascript
+ // doesn't do fresh let-bindings per loop iteration.
+ let dataItemBinding = dataItem;
+ Services.downloads.getDownloadByGUID(dataItemBinding.downloadGuid,
+ function(aStatus, aResult) {
+ if (aStatus == Components.results.NS_ERROR_NOT_AVAILABLE) {
+ DownloadsCommon.log("Removing download with id",
+ dataItemBinding.downloadGuid);
+ this._removeDataItem(dataItemBinding.downloadGuid);
+ }
+ }.bind(this));
+ }
+ }
+ break;
+ }
+ },
+
+ //////////////////////////////////////////////////////////////////////////////
+ //// nsIDownloadProgressListener
+
+ onDownloadStateChange: function DD_onDownloadStateChange(aOldState, aDownload)
+ {
+ if (aDownload.isPrivate != this._isPrivate) {
+ // Ignore the downloads with a privacy status other than what we are
+ // tracking.
+ return;
+ }
+
+ // When a new download is added, it may have the same identifier of a
+ // download that we previously deleted during this session, and we also
+ // want to provide a visible indication that the download started.
+ let isNew = aOldState == nsIDM.DOWNLOAD_NOTSTARTED ||
+ aOldState == nsIDM.DOWNLOAD_QUEUED;
+
+ let dataItem = this._getOrAddDataItem(aDownload, isNew);
+ if (!dataItem) {
+ return;
+ }
+
+ let wasInProgress = dataItem.inProgress;
+
+ DownloadsCommon.log("A download changed its state to:", aDownload.state);
+ dataItem.state = aDownload.state;
+ dataItem.referrer = aDownload.referrer && aDownload.referrer.spec;
+ dataItem.resumable = aDownload.resumable;
+ dataItem.startTime = Math.round(aDownload.startTime / 1000);
+ dataItem.currBytes = aDownload.amountTransferred;
+ dataItem.maxBytes = aDownload.size;
+
+ if (wasInProgress && !dataItem.inProgress) {
+ dataItem.endTime = Date.now();
+ }
+
+ // When a download is retried, we create a different download object from
+ // the database with the same ID as before. This means that the nsIDownload
+ // that the dataItem holds might now need updating.
+ //
+ // We only overwrite this in the event that _download exists, because if it
+ // doesn't, that means that no caller ever tried to get the nsIDownload,
+ // which means it was never retrieved and doesn't need to be overwritten.
+ if (dataItem._download) {
+ dataItem._download = aDownload;
+ }
+
+ for (let view of this._views) {
+ try {
+ view.getViewItem(dataItem).onStateChange(aOldState);
+ } catch (ex) {
+ Cu.reportError(ex);
+ }
+ }
+
+ if (isNew && !dataItem.newDownloadNotified) {
+ dataItem.newDownloadNotified = true;
+ this._notifyDownloadEvent("start");
+ }
+
+ // This is a final state of which we are only notified once.
+ if (dataItem.done) {
+ this._notifyDownloadEvent("finish");
+ }
+
+ // TODO Bug 830415: this isn't the right place to set these annotation.
+ // It should be set it in places' nsIDownloadHistory implementation.
+ if (!this._isPrivate && !dataItem.inProgress) {
+ let downloadMetaData = { state: dataItem.state,
+ endTime: dataItem.endTime };
+ if (dataItem.done)
+ downloadMetaData.fileSize = dataItem.maxBytes;
+
+ try {
+ PlacesUtils.annotations.setPageAnnotation(
+ NetUtil.newURI(dataItem.uri), "downloads/metaData", JSON.stringify(downloadMetaData), 0,
+ PlacesUtils.annotations.EXPIRE_WITH_HISTORY);
+ }
+ catch(ex) {
+ Cu.reportError(ex);
+ }
+ }
+ },
+
+ onProgressChange: function DD_onProgressChange(aWebProgress, aRequest,
+ aCurSelfProgress,
+ aMaxSelfProgress,
+ aCurTotalProgress,
+ aMaxTotalProgress, aDownload)
+ {
+ if (aDownload.isPrivate != this._isPrivate) {
+ // Ignore the downloads with a privacy status other than what we are
+ // tracking.
+ return;
+ }
+
+ let dataItem = this._getOrAddDataItem(aDownload, false);
+ if (!dataItem) {
+ return;
+ }
+
+ dataItem.currBytes = aDownload.amountTransferred;
+ dataItem.maxBytes = aDownload.size;
+ dataItem.speed = aDownload.speed;
+ dataItem.percentComplete = aDownload.percentComplete;
+
+ this._views.forEach(
+ function (view) view.getViewItem(dataItem).onProgressChange()
+ );
+ },
+
+ onStateChange: function () { },
+
+ onSecurityChange: function () { },
+
+ //////////////////////////////////////////////////////////////////////////////
+ //// 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);
+ return 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 aType
+ * Set to "start" for new downloads, "finish" for completed downloads.
+ */
+ _notifyDownloadEvent: function DD_notifyDownloadEvent(aType)
+ {
+ DownloadsCommon.log("Attempting to notify that a new download has started or finished.");
+ if (DownloadsCommon.useToolkitUI) {
+ DownloadsCommon.log("Cancelling notification - we're using the toolkit downloads manager.");
+ return;
+ }
+
+ // Show the panel in the most recent browser window, if present.
+ let browserWin = RecentWindow.getMostRecentBrowserWindow({ private: this._isPrivate });
+ if (!browserWin) {
+ return;
+ }
+
+ if (this.panelHasShownBefore) {
+ // 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.
+ DownloadsCommon.log("Showing new download notification.");
+ browserWin.DownloadsIndicatorView.showEventNotification(aType);
+ return;
+ }
+ this.panelHasShownBefore = true;
+ browserWin.DownloadsPanel.showPanel();
+ }
+};
+
+XPCOMUtils.defineLazyGetter(this, "PrivateDownloadsData", function() {
+ return new DownloadsDataCtor(true);
+});
+
+XPCOMUtils.defineLazyGetter(this, "DownloadsData", function() {
+ return new DownloadsDataCtor(false);
+});
+
+////////////////////////////////////////////////////////////////////////////////
+//// DownloadsDataItem
+
+/**
+ * Represents a single item in the list of downloads. This object either wraps
+ * an existing nsIDownload from the Download Manager, or provides the same
+ * information read directly from the downloads database, with the possibility
+ * of querying the nsIDownload lazily, for performance reasons.
+ *
+ * @param aSource
+ * Object containing the data with which the item should be initialized.
+ * This should implement either nsIDownload or mozIStorageRow. If the
+ * JavaScript API for downloads is enabled, this is a Download object.
+ */
+function DownloadsDataItem(aSource)
+{
+ this._initFromJSDownload(aSource);
+}
+
+DownloadsDataItem.prototype = {
+ /**
+ * The JavaScript API does not need identifiers for Download objects, so they
+ * are generated sequentially for the corresponding DownloadDataItem.
+ */
+ get _autoIncrementId() ++DownloadsDataItem.prototype.__lastId,
+ __lastId: 0,
+
+ /**
+ * Initializes this object from the JavaScript API for downloads.
+ *
+ * The endTime property is initialized to the current date and time.
+ *
+ * @param aDownload
+ * The Download object with the current state.
+ */
+ _initFromJSDownload: function (aDownload)
+ {
+ this._download = aDownload;
+
+ this.downloadGuid = "id:" + this._autoIncrementId;
+ this.file = aDownload.target.path;
+ this.target = OS.Path.basename(aDownload.target.path);
+ this.uri = aDownload.source.url;
+ this.endTime = Date.now();
+
+ this.updateFromJSDownload();
+ },
+
+ /**
+ * Updates this object from the JavaScript API for downloads.
+ */
+ updateFromJSDownload: function ()
+ {
+ // Collapse state using the correct priority.
+ if (this._download.succeeded) {
+ this.state = nsIDM.DOWNLOAD_FINISHED;
+ } else if (this._download.error &&
+ this._download.error.becauseBlockedByParentalControls) {
+ this.state = nsIDM.DOWNLOAD_BLOCKED_PARENTAL;
+ } else if (this._download.error) {
+ this.state = nsIDM.DOWNLOAD_FAILED;
+ } else if (this._download.canceled && this._download.hasPartialData) {
+ this.state = nsIDM.DOWNLOAD_PAUSED;
+ } else if (this._download.canceled) {
+ this.state = nsIDM.DOWNLOAD_CANCELED;
+ } else if (this._download.stopped) {
+ this.state = nsIDM.DOWNLOAD_NOTSTARTED;
+ } else {
+ this.state = nsIDM.DOWNLOAD_DOWNLOADING;
+ }
+
+ this.referrer = this._download.source.referrer;
+ this.startTime = this._download.startTime;
+ this.currBytes = this._download.currentBytes;
+ this.resumable = this._download.hasPartialData;
+ this.speed = this._download.speed;
+
+ if (this._download.succeeded) {
+ // If the download succeeded, show the final size if available, otherwise
+ // use the last known number of bytes transferred. The final size on disk
+ // will be available when bug 941063 is resolved.
+ this.maxBytes = this._download.hasProgress ?
+ this._download.totalBytes :
+ this._download.currentBytes;
+ this.percentComplete = 100;
+ } else if (this._download.hasProgress) {
+ // If the final size and progress are known, use them.
+ this.maxBytes = this._download.totalBytes;
+ this.percentComplete = this._download.progress;
+ } else {
+ // The download final size and progress percentage is unknown.
+ this.maxBytes = -1;
+ this.percentComplete = -1;
+ }
+ },
+
+ /**
+ * Initializes this object from a download object of the Download Manager.
+ *
+ * The endTime property is initialized to the current date and time.
+ *
+ * @param aDownload
+ * The nsIDownload with the current state.
+ */
+ _initFromDownload: function DDI_initFromDownload(aDownload)
+ {
+ this._download = aDownload;
+
+ // Fetch all the download properties eagerly.
+ this.downloadGuid = aDownload.guid;
+ this.file = aDownload.target.spec;
+ this.target = aDownload.displayName;
+ this.uri = aDownload.source.spec;
+ this.referrer = aDownload.referrer && aDownload.referrer.spec;
+ this.state = aDownload.state;
+ this.startTime = Math.round(aDownload.startTime / 1000);
+ this.endTime = Date.now();
+ this.currBytes = aDownload.amountTransferred;
+ this.maxBytes = aDownload.size;
+ this.resumable = aDownload.resumable;
+ this.speed = aDownload.speed;
+ this.percentComplete = aDownload.percentComplete;
+ },
+
+ /**
+ * Initializes this object from a data row in the downloads database, without
+ * querying the associated nsIDownload object, to improve performance when
+ * loading the list of downloads asynchronously.
+ *
+ * When this object is initialized in this way, accessing the "download"
+ * property loads the underlying nsIDownload object synchronously, and should
+ * be avoided unless the object is really required.
+ *
+ * @param aStorageRow
+ * The mozIStorageRow from the downloads database.
+ */
+ _initFromDataRow: function DDI_initFromDataRow(aStorageRow)
+ {
+ // Get the download properties from the data row.
+ this._download = null;
+ this.downloadGuid = aStorageRow.getResultByName("guid");
+ this.file = aStorageRow.getResultByName("target");
+ this.target = aStorageRow.getResultByName("name");
+ this.uri = aStorageRow.getResultByName("source");
+ this.referrer = aStorageRow.getResultByName("referrer");
+ this.state = aStorageRow.getResultByName("state");
+ this.startTime = Math.round(aStorageRow.getResultByName("startTime") / 1000);
+ this.endTime = Math.round(aStorageRow.getResultByName("endTime") / 1000);
+ this.currBytes = aStorageRow.getResultByName("currBytes");
+ this.maxBytes = aStorageRow.getResultByName("maxBytes");
+
+ // Now we have to determine if the download is resumable, but don't want to
+ // access the underlying download object unnecessarily. The only case where
+ // the property is relevant is when we are currently downloading data, and
+ // in this case the download object is already loaded in memory or will be
+ // loaded very soon in any case. In all the other cases, including a paused
+ // download, we assume that the download is resumable. The property will be
+ // updated as soon as the underlying download state changes.
+
+ // We'll start by assuming we're resumable, and then if we're downloading,
+ // update resumable property in case we were wrong.
+ this.resumable = true;
+
+ if (this.state == nsIDM.DOWNLOAD_DOWNLOADING) {
+ this.getDownload(function(aDownload) {
+ this.resumable = aDownload.resumable;
+ }.bind(this));
+ }
+
+ // Compute the other properties without accessing the download object.
+ this.speed = 0;
+ this.percentComplete = this.maxBytes <= 0
+ ? -1
+ : Math.round(this.currBytes / this.maxBytes * 100);
+ },
+
+ /**
+ * Asynchronous getter for the download object corresponding to this data item.
+ *
+ * @param aCallback
+ * A callback function which will be called when the download object is
+ * available. It should accept one argument which will be the download
+ * object.
+ */
+ getDownload: function DDI_getDownload(aCallback) {
+ if (this._download) {
+ // Return the download object asynchronously to the caller
+ let download = this._download;
+ Services.tm.mainThread.dispatch(function () aCallback(download),
+ Ci.nsIThread.DISPATCH_NORMAL);
+ } else {
+ Services.downloads.getDownloadByGUID(this.downloadGuid,
+ function(aStatus, aResult) {
+ if (!Components.isSuccessCode(aStatus)) {
+ Cu.reportError(
+ new Components.Exception("Cannot retrieve download for GUID: " +
+ this.downloadGuid));
+ } else {
+ this._download = aResult;
+ aCallback(aResult);
+ }
+ }.bind(this));
+ }
+ },
+
+ /**
+ * Indicates whether the download is proceeding normally, and not finished
+ * yet. This includes paused downloads. When this property is true, the
+ * "progress" property represents the current progress of the download.
+ */
+ get inProgress()
+ {
+ return [
+ nsIDM.DOWNLOAD_NOTSTARTED,
+ nsIDM.DOWNLOAD_QUEUED,
+ nsIDM.DOWNLOAD_DOWNLOADING,
+ nsIDM.DOWNLOAD_PAUSED,
+ nsIDM.DOWNLOAD_SCANNING,
+ ].indexOf(this.state) != -1;
+ },
+
+ /**
+ * This is true during the initial phases of a download, before the actual
+ * download of data bytes starts.
+ */
+ get starting()
+ {
+ return this.state == nsIDM.DOWNLOAD_NOTSTARTED ||
+ this.state == nsIDM.DOWNLOAD_QUEUED;
+ },
+
+ /**
+ * Indicates whether the download is paused.
+ */
+ get paused()
+ {
+ return this.state == nsIDM.DOWNLOAD_PAUSED;
+ },
+
+ /**
+ * Indicates whether the download is in a final state, either because it
+ * completed successfully or because it was blocked.
+ */
+ get done()
+ {
+ return [
+ nsIDM.DOWNLOAD_FINISHED,
+ nsIDM.DOWNLOAD_BLOCKED_PARENTAL,
+ nsIDM.DOWNLOAD_BLOCKED_POLICY,
+ nsIDM.DOWNLOAD_DIRTY,
+ ].indexOf(this.state) != -1;
+ },
+
+ /**
+ * Indicates whether the download is finished and can be opened.
+ */
+ get openable()
+ {
+ return this.state == nsIDM.DOWNLOAD_FINISHED;
+ },
+
+ /**
+ * Indicates whether the download stopped because of an error, and can be
+ * resumed manually.
+ */
+ get canRetry()
+ {
+ return this.state == nsIDM.DOWNLOAD_CANCELED ||
+ this.state == nsIDM.DOWNLOAD_FAILED;
+ },
+
+ /**
+ * Returns the nsILocalFile for the download target.
+ *
+ * @throws if the native path is not valid. This can happen if the same
+ * profile is used on different platforms, for example if a native
+ * Windows path is stored and then the item is accessed on a Mac.
+ */
+ get localFile()
+ {
+ return this._getFile(this.file);
+ },
+
+ /**
+ * Returns the nsILocalFile for the partially downloaded target.
+ *
+ * @throws if the native path is not valid. This can happen if the same
+ * profile is used on different platforms, for example if a native
+ * Windows path is stored and then the item is accessed on a Mac.
+ */
+ get partFile()
+ {
+ return this._getFile(this.file + kPartialDownloadSuffix);
+ },
+
+ /**
+ * Returns an nsILocalFile for aFilename. aFilename might be a file URL or
+ * a native path.
+ *
+ * @param aFilename the filename of the file to retrieve.
+ * @return an nsILocalFile for the file.
+ * @throws if the native path is not valid. This can happen if the same
+ * profile is used on different platforms, for example if a native
+ * Windows path is stored and then the item is accessed on a Mac.
+ * @note This function makes no guarantees about the file's existence -
+ * callers should check that the returned file exists.
+ */
+ _getFile: function DDI__getFile(aFilename)
+ {
+ // The download database may contain targets stored as file URLs or native
+ // paths. This can still be true for previously stored items, even if new
+ // items are stored using their file URL. See also bug 239948 comment 12.
+ if (aFilename.startsWith("file:")) {
+ // Assume the file URL we obtained from the downloads database or from the
+ // "spec" property of the target has the UTF-8 charset.
+ let fileUrl = NetUtil.newURI(aFilename).QueryInterface(Ci.nsIFileURL);
+ return fileUrl.file.clone().QueryInterface(Ci.nsILocalFile);
+ } else {
+ // The downloads database contains a native path. Try to create a local
+ // file, though this may throw an exception if the path is invalid.
+ return new DownloadsLocalFileCtor(aFilename);
+ }
+ },
+
+ /**
+ * Open the target file for this download.
+ *
+ * @param aOwnerWindow
+ * The window with which the required action is associated.
+ * @throws if the file cannot be opened.
+ */
+ openLocalFile: function DDI_openLocalFile(aOwnerWindow) {
+ this._download.launch().then(null, Cu.reportError);
+ return;
+ },
+
+ /**
+ * Show the downloaded file in the system file manager.
+ */
+ showLocalFile: function DDI_showLocalFile() {
+ DownloadsCommon.showDownloadedFile(this.localFile);
+ },
+
+ /**
+ * Resumes the download if paused, pauses it if active.
+ * @throws if the download is not resumable or if has already done.
+ */
+ togglePauseResume: function DDI_togglePauseResume() {
+ if (this._download.stopped) {
+ this._download.start();
+ } else {
+ this._download.cancel();
+ }
+ return;
+ },
+
+ /**
+ * Attempts to retry the download.
+ * @throws if we cannot.
+ */
+ retry: function DDI_retry() {
+ this._download.start();
+ return;
+ },
+
+ /**
+ * Support function that deletes the local file for a download. This is
+ * used in cases where the Download Manager service doesn't delete the file
+ * from disk when cancelling. See bug 732924.
+ */
+ _ensureLocalFileRemoved: function DDI__ensureLocalFileRemoved()
+ {
+ try {
+ let localFile = this.localFile;
+ if (localFile.exists()) {
+ localFile.remove(false);
+ }
+ } catch (ex) { }
+ },
+
+ /**
+ * Cancels the download.
+ * @throws if the download is already done.
+ */
+ cancel: function() {
+ this._download.cancel();
+ this._download.removePartialData().then(null, Cu.reportError);
+ return;
+ },
+
+ /**
+ * Remove the download.
+ */
+ remove: function DDI_remove() {
+ let promiseList = this._download.source.isPrivate
+ ? Downloads.getList(Downloads.PUBLIC)
+ : Downloads.getList(Downloads.PRIVATE);
+ promiseList.then(list => list.remove(this._download))
+ .then(() => this._download.finalize(true))
+ .then(null, Cu.reportError);
+ return;
+ }
+};
+
+////////////////////////////////////////////////////////////////////////////////
+//// DownloadsViewPrototype
+
+/**
+ * A prototype for an object that registers itself with DownloadsData as soon
+ * as a view is registered with it.
+ */
+const DownloadsViewPrototype = {
+ //////////////////////////////////////////////////////////////////////////////
+ //// 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: function DVP_addView(aView)
+ {
+ // Start receiving events when the first of our views is registered.
+ if (this._views.length == 0) {
+ if (this._isPrivate) {
+ PrivateDownloadsData.addView(this);
+ } else {
+ 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: function DVP_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: function DVP_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 == 0) {
+ if (this._isPrivate) {
+ PrivateDownloadsData.removeView(this);
+ } else {
+ DownloadsData.removeView(this);
+ }
+ }
+ },
+
+ //////////////////////////////////////////////////////////////////////////////
+ //// Callback functions from DownloadsData
+
+ /**
+ * Indicates whether we are still loading downloads data asynchronously.
+ */
+ _loading: false,
+
+ /**
+ * Called before multiple downloads are about to be loaded.
+ */
+ onDataLoadStarting: function DVP_onDataLoadStarting()
+ {
+ this._loading = true;
+ },
+
+ /**
+ * Called after data loading finished.
+ */
+ onDataLoadCompleted: function DVP_onDataLoadCompleted()
+ {
+ this._loading = false;
+ },
+
+ /**
+ * Called when the downloads database becomes unavailable (for example, we
+ * entered Private Browsing Mode and the database backend changed).
+ * References to existing data should be discarded.
+ *
+ * @note Subclasses should override this.
+ */
+ onDataInvalidated: function DVP_onDataInvalidated()
+ {
+ throw Components.results.NS_ERROR_NOT_IMPLEMENTED;
+ },
+
+ /**
+ * Called when a new download data item is available, either during the
+ * asynchronous data load or when a new download is started.
+ *
+ * @param aDataItem
+ * DownloadsDataItem object that was just added.
+ * @param aNewest
+ * When true, indicates that this item is the most recent and should be
+ * added in the topmost position. This happens when a new download is
+ * started. When false, indicates that the item is the least recent
+ * with regard to the items that have been already added. The latter
+ * generally happens during the asynchronous data load.
+ *
+ * @note Subclasses should override this.
+ */
+ onDataItemAdded: function DVP_onDataItemAdded(aDataItem, aNewest)
+ {
+ throw Components.results.NS_ERROR_NOT_IMPLEMENTED;
+ },
+
+ /**
+ * Called when a data item is removed, ensures that the widget associated with
+ * the view item is removed from the user interface.
+ *
+ * @param aDataItem
+ * DownloadsDataItem object that is being removed.
+ *
+ * @note Subclasses should override this.
+ */
+ onDataItemRemoved: function DVP_onDataItemRemoved(aDataItem)
+ {
+ throw Components.results.NS_ERROR_NOT_IMPLEMENTED;
+ },
+
+ /**
+ * Returns the view item associated with the provided data item for this view.
+ *
+ * @param aDataItem
+ * DownloadsDataItem object for which the view item is requested.
+ *
+ * @return Object that can be used to notify item status events.
+ *
+ * @note Subclasses should override this.
+ */
+ getViewItem: function DID_getViewItem(aDataItem)
+ {
+ throw Components.results.NS_ERROR_NOT_IMPLEMENTED;
+ },
+
+ /**
+ * Private function used to refresh the internal properties being sent to
+ * each registered view.
+ *
+ * @note Subclasses should override this.
+ */
+ _refreshProperties: function DID_refreshProperties()
+ {
+ throw Components.results.NS_ERROR_NOT_IMPLEMENTED;
+ },
+
+ /**
+ * Private function used to refresh an individual view.
+ *
+ * @note Subclasses should override this.
+ */
+ _updateView: function DID_updateView()
+ {
+ throw Components.results.NS_ERROR_NOT_IMPLEMENTED;
+ }
+};
+
+////////////////////////////////////////////////////////////////////////////////
+//// 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._isPrivate = aPrivate;
+ this._views = [];
+}
+DownloadsIndicatorDataCtor.prototype = {
+ __proto__: DownloadsViewPrototype,
+
+ /**
+ * Removes an object previously added using addView.
+ *
+ * @param aView
+ * DownloadsIndicatorView object to be removed.
+ */
+ removeView: function DID_removeView(aView)
+ {
+ DownloadsViewPrototype.removeView.call(this, aView);
+
+ if (this._views.length == 0) {
+ this._itemCount = 0;
+ }
+ },
+
+ //////////////////////////////////////////////////////////////////////////////
+ //// Callback functions from DownloadsData
+
+ /**
+ * Called after data loading finished.
+ */
+ onDataLoadCompleted: function DID_onDataLoadCompleted()
+ {
+ DownloadsViewPrototype.onDataLoadCompleted.call(this);
+ this._updateViews();
+ },
+
+ /**
+ * Called when the downloads database becomes unavailable (for example, we
+ * entered Private Browsing Mode and the database backend changed).
+ * References to existing data should be discarded.
+ */
+ onDataInvalidated: function DID_onDataInvalidated()
+ {
+ this._itemCount = 0;
+ },
+
+ /**
+ * Called when a new download data item is available, either during the
+ * asynchronous data load or when a new download is started.
+ *
+ * @param aDataItem
+ * DownloadsDataItem object that was just added.
+ * @param aNewest
+ * When true, indicates that this item is the most recent and should be
+ * added in the topmost position. This happens when a new download is
+ * started. When false, indicates that the item is the least recent
+ * with regard to the items that have been already added. The latter
+ * generally happens during the asynchronous data load.
+ */
+ onDataItemAdded: function DID_onDataItemAdded(aDataItem, aNewest)
+ {
+ this._itemCount++;
+ this._updateViews();
+ },
+
+ /**
+ * Called when a data item is removed, ensures that the widget associated with
+ * the view item is removed from the user interface.
+ *
+ * @param aDataItem
+ * DownloadsDataItem object that is being removed.
+ */
+ onDataItemRemoved: function DID_onDataItemRemoved(aDataItem)
+ {
+ this._itemCount--;
+ this._updateViews();
+ },
+
+ /**
+ * Returns the view item associated with the provided data item for this view.
+ *
+ * @param aDataItem
+ * DownloadsDataItem object for which the view item is requested.
+ *
+ * @return Object that can be used to notify item status events.
+ */
+ getViewItem: function DID_getViewItem(aDataItem)
+ {
+ let data = this._isPrivate ? PrivateDownloadsIndicatorData
+ : DownloadsIndicatorData;
+ return Object.freeze({
+ onStateChange: function DIVI_onStateChange(aOldState)
+ {
+ if (aDataItem.state == nsIDM.DOWNLOAD_FINISHED ||
+ aDataItem.state == nsIDM.DOWNLOAD_FAILED) {
+ data.attention = true;
+ }
+
+ // Since the state of a download changed, reset the estimated time left.
+ data._lastRawTimeLeft = -1;
+ data._lastTimeLeft = -1;
+
+ data._updateViews();
+ },
+ onProgressChange: function DIVI_onProgressChange()
+ {
+ data._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,
+ _counter: "",
+ _percentComplete: -1,
+ _paused: false,
+
+ /**
+ * Indicates whether the download indicators should be highlighted.
+ */
+ set attention(aValue)
+ {
+ this._attention = aValue;
+ this._updateViews();
+ return aValue;
+ },
+ _attention: false,
+
+ /**
+ * Indicates whether the user is interacting with downloads, thus the
+ * attention indication should not be shown even if requested.
+ */
+ set attentionSuppressed(aValue)
+ {
+ this._attentionSuppressed = aValue;
+ this._attention = false;
+ this._updateViews();
+ return aValue;
+ },
+ _attentionSuppressed: false,
+
+ /**
+ * Computes aggregate values and propagates the changes to our views.
+ */
+ _updateViews: function DID_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);
+ },
+
+ /**
+ * Updates the specified view with the current aggregate values.
+ *
+ * @param aView
+ * DownloadsIndicatorView object to be updated.
+ */
+ _updateView: function DID_updateView(aView)
+ {
+ aView.hasDownloads = this._hasDownloads;
+ aView.counter = this._counter;
+ aView.percentComplete = this._percentComplete;
+ aView.paused = this._paused;
+ aView.attention = this._attention && !this._attentionSuppressed;
+ },
+
+ //////////////////////////////////////////////////////////////////////////////
+ //// Property updating based on current download status
+
+ /**
+ * Number of download items that are available to be displayed.
+ */
+ _itemCount: 0,
+
+ /**
+ * 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.
+ */
+ _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.
+ */
+ _lastTimeLeft: -1,
+
+ /**
+ * A generator function for the dataItems that this summary is currently
+ * interested in. This generator is passed off to summarizeDownloads in order
+ * to generate statistics about the dataItems we care about - in this case,
+ * it's all dataItems for active downloads.
+ */
+ _activeDataItems: function DID_activeDataItems()
+ {
+ let dataItems = this._isPrivate ? PrivateDownloadsData.dataItems
+ : DownloadsData.dataItems;
+ for each (let dataItem in dataItems) {
+ if (dataItem && dataItem.inProgress) {
+ yield dataItem;
+ }
+ }
+ },
+
+ /**
+ * Computes aggregate values based on the current state of downloads.
+ */
+ _refreshProperties: function DID_refreshProperties()
+ {
+ let summary =
+ DownloadsCommon.summarizeDownloads(this._activeDataItems());
+
+ // Determine if the indicator should be shown or get attention.
+ this._hasDownloads = (this._itemCount > 0);
+
+ // If all downloads are paused, show the progress indicator as paused.
+ this._paused = summary.numActive > 0 &&
+ summary.numActive == summary.numPaused;
+
+ this._percentComplete = summary.percentComplete;
+
+ // 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._counter = "";
+ } 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._counter = DownloadsCommon.formatTimeLeft(this._lastTimeLeft);
+ }
+ }
+};
+
+XPCOMUtils.defineLazyGetter(this, "PrivateDownloadsIndicatorData", function() {
+ return new DownloadsIndicatorDataCtor(true);
+});
+
+XPCOMUtils.defineLazyGetter(this, "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._dataItems = [];
+
+ // 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._isPrivate = aIsPrivate;
+ this._views = [];
+}
+
+DownloadsSummaryData.prototype = {
+ __proto__: DownloadsViewPrototype,
+
+ /**
+ * Removes an object previously added using addView.
+ *
+ * @param aView
+ * DownloadsSummary view to be removed.
+ */
+ removeView: function DSD_removeView(aView)
+ {
+ DownloadsViewPrototype.removeView.call(this, aView);
+
+ if (this._views.length == 0) {
+ // Clear out our collection of DownloadDataItems. If we ever have
+ // another view registered with us, this will get re-populated.
+ this._dataItems = [];
+ }
+ },
+
+ //////////////////////////////////////////////////////////////////////////////
+ //// Callback functions from DownloadsData - see the documentation in
+ //// DownloadsViewPrototype for more information on what these functions
+ //// are used for.
+
+ onDataLoadCompleted: function DSD_onDataLoadCompleted()
+ {
+ DownloadsViewPrototype.onDataLoadCompleted.call(this);
+ this._updateViews();
+ },
+
+ onDataInvalidated: function DSD_onDataInvalidated()
+ {
+ this._dataItems = [];
+ },
+
+ onDataItemAdded: function DSD_onDataItemAdded(aDataItem, aNewest)
+ {
+ if (aNewest) {
+ this._dataItems.unshift(aDataItem);
+ } else {
+ this._dataItems.push(aDataItem);
+ }
+
+ this._updateViews();
+ },
+
+ onDataItemRemoved: function DSD_onDataItemRemoved(aDataItem)
+ {
+ let itemIndex = this._dataItems.indexOf(aDataItem);
+ this._dataItems.splice(itemIndex, 1);
+ this._updateViews();
+ },
+
+ getViewItem: function DSD_getViewItem(aDataItem)
+ {
+ let self = this;
+ return Object.freeze({
+ onStateChange: function DIVI_onStateChange(aOldState)
+ {
+ // Since the state of a download changed, reset the estimated time left.
+ self._lastRawTimeLeft = -1;
+ self._lastTimeLeft = -1;
+ self._updateViews();
+ },
+ onProgressChange: function DIVI_onProgressChange()
+ {
+ self._updateViews();
+ }
+ });
+ },
+
+ //////////////////////////////////////////////////////////////////////////////
+ //// Propagation of properties to our views
+
+ /**
+ * Computes aggregate values and propagates the changes to our views.
+ */
+ _updateViews: function DSD_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);
+ },
+
+ /**
+ * Updates the specified view with the current aggregate values.
+ *
+ * @param aView
+ * DownloadsIndicatorView object to be updated.
+ */
+ _updateView: function DSD_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 dataItems that this summary is currently
+ * interested in. This generator is passed off to summarizeDownloads in order
+ * to generate statistics about the dataItems we care about - in this case,
+ * it's the dataItems in this._dataItems after the first few to exclude,
+ * which was set when constructing this DownloadsSummaryData instance.
+ */
+ _dataItemsForSummary: function DSD_dataItemsForSummary()
+ {
+ if (this._dataItems.length > 0) {
+ for (let i = this._numToExclude; i < this._dataItems.length; ++i) {
+ yield this._dataItems[i];
+ }
+ }
+ },
+
+ /**
+ * Computes aggregate values based on the current state of downloads.
+ */
+ _refreshProperties: function DSD_refreshProperties()
+ {
+ // Pre-load summary with default values.
+ let summary =
+ DownloadsCommon.summarizeDownloads(this._dataItemsForSummary());
+
+ this._description = DownloadsCommon.strings
+ .otherDownloads2(summary.numActive);
+ this._percentComplete = summary.percentComplete;
+
+ // If all downloads are paused, show the progress indicator as paused.
+ this._showingProgress = summary.numDownloading > 0 ||
+ summary.numPaused > 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] = DownloadUtils.getDownloadStatusNoRate(
+ summary.totalTransferred, summary.totalSize, summary.slowestSpeed,
+ this._lastTimeLeft);
+ }
+ }
+}