From f9cab004186edb425a9b88ad649726605080a17c Mon Sep 17 00:00:00 2001 From: Thomas Groman Date: Mon, 20 Apr 2020 20:49:37 -0700 Subject: move browser to webbrowser/ --- .../components/downloads/DownloadsCommon.jsm | 1920 ++++++++++++++++++++ 1 file changed, 1920 insertions(+) create mode 100644 webbrowser/components/downloads/DownloadsCommon.jsm (limited to 'webbrowser/components/downloads/DownloadsCommon.jsm') diff --git a/webbrowser/components/downloads/DownloadsCommon.jsm b/webbrowser/components/downloads/DownloadsCommon.jsm new file mode 100644 index 0000000..efe31ce --- /dev/null +++ b/webbrowser/components/downloads/DownloadsCommon.jsm @@ -0,0 +1,1920 @@ +/* -*- 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 + * 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 + +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, "FileUtils", + "resource://gre/modules/FileUtils.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 kPrefConfirmOpenExe = "browser.download.confirmOpenExecutable"; + +const kDownloadsStringsRequiringFormatting = { + sizeWithUnits: true, + shortTimeLeftSeconds: true, + shortTimeLeftMinutes: true, + shortTimeLeftHours: true, + shortTimeLeftDays: true, + statusSeparator: true, + statusSeparatorBeforeNumber: true, + fileExecutableSecurityWarning: true +}; + +const kDownloadsStringsRequiringPluralForm = { + otherDownloads2: true +}; + +const kPartialDownloadSuffix = ".part"; + +const kPrefBranch = Services.prefs.getBranch("browser.download."); + +var 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; + }, + + /** + * Helper function required because the Downloads Panel and the Downloads View + * don't share the controller yet. + */ + removeAndFinalizeDownload(download) { + Downloads.getList(Downloads.ALL) + .then(list => list.remove(download)) + .then(() => download.finalize(true)) + .catch(Cu.reportError); + }, + + /** + * 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 = (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. + * + * @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"); + +#ifdef XP_WIN + // On Windows, the system will provide a native confirmation prompt + // for .exe files. Exclude this from our prompt, but prompt on other + // executable types. + let isWindowsExe = aFile.leafName.toLowerCase().endsWith(".exe"); +#else + let isWindowsExe = false; +#endif + + // Confirm opening executable files if required. + if (aFile.isExecutable() && !isWindowsExe) { + let showAlert = true; + try { + showAlert = Services.prefs.getBoolPref(kPrefConfirmOpenExe); + } catch (ex) { + // If the preference does not exist, continue with the prompt. + } + + if (showAlert) { + let name = aFile.leafName; + let message = + DownloadsCommon.strings.fileExecutableSecurityWarning(name, name); + let title = + DownloadsCommon.strings.fileExecutableSecurityWarningTitle; + + let open = Services.prompt.confirm(aOwnerWindow, title, message); + if (!open) { + return; + } + } + } + + // 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. + * + * @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; + + // Contains all the available Download objects and their integer state. + this.oldDownloadStates = new Map(); + + // Array of view objects that should be notified when the available download + // data changes. + this._views = []; +} + +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; + }, + + /** + * Iterator for all the available Download objects. This is empty until the + * data has been loaded using the JavaScript API for downloads. + */ + get downloads() this.oldDownloadStates.keys(), + + /** + * 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. + */ + 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(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)); + + for (let view of this._views) { + view.onDownloadAdded(download, true); + } + }, + + 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). Moreover, the fact that state is stored as + // annotations should be ideally hidden behind methods of + // nsIDownloadHistory (bug 830415). + if (!this._isPrivate) { + try { + let downloadMetaData = { + state: DownloadsCommon.stateOfDownload(download), + endTime: download.endTime, + }; + if (download.succeeded) { + downloadMetaData.fileSize = download.target.size; + } + + PlacesUtils.annotations.setPageAnnotation( + NetUtil.newURI(download.source.url), + "downloads/metaData", + JSON.stringify(downloadMetaData), 0, + PlacesUtils.annotations.EXPIRE_WITH_HISTORY); + } catch (ex) { + Cu.reportError(ex); + } + } + } + + for (let view of this._views) { + try { + view.onDownloadStateChanged(download); + } catch (ex) { + Cu.reportError(ex); + } + } + + if (download.succeeded || + (download.error && download.error.becauseBlocked)) { + this._notifyDownloadEvent("finish"); + } + } + + if (!download.newDownloadNotified) { + download.newDownloadNotified = true; + this._notifyDownloadEvent("start"); + } + + for (let view of this._views) { + view.onDownloadChanged(download); + } + }, + + onDownloadRemoved(download) { + this.oldDownloadStates.delete(download); + + for (let view of this._views) { + view.onDownloadRemoved(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: 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. + // Tycho: + //let loadedItemsArray = [dataItem + // for each (dataItem in this.dataItems) + // if (dataItem)]; + let downloadsArray = [...this.downloads]; + downloadsArray.sort((a, b) => b.startTime - a.startTime); + downloadsArray.forEach(download => aView.onDownloadAdded(download, 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); +}); + +//////////////////////////////////////////////////////////////////////////////// +//// 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 download + * Download object that was just added. + * @param newest + * 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. + */ + onDownloadAdded(download, newest) { + throw Components.results.NS_ERROR_NOT_IMPLEMENTED; + }, + + /** + * 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.results.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.jsm + * API level, and there is no throttling mechanism in the front-end. + * + * @note Subclasses should override this. + */ + onDownloadChanged(download) { + 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 download + * Download object that is being removed. + * + * @note Subclasses should override this. + */ + onDownloadRemoved(download) { + 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 + + 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; + }, + + onDownloadAdded(download, newest) { + this._itemCount++; + this._updateViews(); + }, + + onDownloadStateChanged(download) { + if (download.succeeded || download.error) { + this.attention = true; + } + + // Since the state of a download changed, reset the estimated time left. + this._lastRawTimeLeft = -1; + this._lastTimeLeft = -1; + }, + + onDownloadChanged(download) { + this._updateViews(); + }, + + onDownloadRemoved(download) { + this._itemCount--; + 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, + _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 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 ? PrivateDownloadsData.downloads + : 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: function DID_refreshProperties() + { + let summary = + DownloadsCommon.summarizeDownloads(this._activeDownloads()); + + // 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._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._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 Download objects. If we ever have + // another view registered with us, this will get re-populated. + this._downloads = []; + } + }, + + ////////////////////////////////////////////////////////////////////////////// + //// 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 = []; + }, + + onDownloadAdded(download, newest) { + if (newest) { + this._downloads.unshift(download); + } else { + this._downloads.push(download); + } + + this._updateViews(); + }, + + onDownloadStateChanged() { + // Since the state of a download changed, reset the estimated time left. + this._lastRawTimeLeft = -1; + this._lastTimeLeft = -1; + }, + + onDownloadChanged() { + this._updateViews(); + }, + + onDownloadRemoved(download) { + let itemIndex = this._downloads.indexOf(download); + this._downloads.splice(itemIndex, 1); + this._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 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 > 0) { + 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: function DSD_refreshProperties() + { + // Pre-load summary with default values. + let summary = + DownloadsCommon.summarizeDownloads(this._downloadsForSummary()); + + 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); + } + } +} -- cgit v1.2.3