michael@0: /* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ michael@0: /* vim: set ts=2 et sw=2 tw=80 filetype=javascript: */ michael@0: /* This Source Code Form is subject to the terms of the Mozilla Public michael@0: * License, v. 2.0. If a copy of the MPL was not distributed with this file, michael@0: * You can obtain one at http://mozilla.org/MPL/2.0/. */ michael@0: michael@0: "use strict"; michael@0: michael@0: this.EXPORTED_SYMBOLS = [ michael@0: "DownloadsCommon", michael@0: ]; michael@0: michael@0: /** michael@0: * Handles the Downloads panel shared methods and data access. michael@0: * michael@0: * This file includes the following constructors and global objects: michael@0: * michael@0: * DownloadsCommon michael@0: * This object is exposed directly to the consumers of this JavaScript module, michael@0: * and provides shared methods for all the instances of the user interface. michael@0: * michael@0: * DownloadsData michael@0: * Retrieves the list of past and completed downloads from the underlying michael@0: * Download Manager data, and provides asynchronous notifications allowing michael@0: * to build a consistent view of the available data. michael@0: * michael@0: * DownloadsDataItem michael@0: * Represents a single item in the list of downloads. This object either wraps michael@0: * an existing nsIDownload from the Download Manager, or provides the same michael@0: * information read directly from the downloads database, with the possibility michael@0: * of querying the nsIDownload lazily, for performance reasons. michael@0: * michael@0: * DownloadsIndicatorData michael@0: * This object registers itself with DownloadsData as a view, and transforms the michael@0: * notifications it receives into overall status data, that is then broadcast to michael@0: * the registered download status indicators. michael@0: */ michael@0: michael@0: //////////////////////////////////////////////////////////////////////////////// michael@0: //// Globals michael@0: michael@0: const Cc = Components.classes; michael@0: const Ci = Components.interfaces; michael@0: const Cu = Components.utils; michael@0: const Cr = Components.results; michael@0: michael@0: Cu.import("resource://gre/modules/XPCOMUtils.jsm"); michael@0: Cu.import("resource://gre/modules/Services.jsm"); michael@0: michael@0: XPCOMUtils.defineLazyModuleGetter(this, "NetUtil", michael@0: "resource://gre/modules/NetUtil.jsm"); michael@0: XPCOMUtils.defineLazyModuleGetter(this, "PluralForm", michael@0: "resource://gre/modules/PluralForm.jsm"); michael@0: XPCOMUtils.defineLazyModuleGetter(this, "Downloads", michael@0: "resource://gre/modules/Downloads.jsm"); michael@0: XPCOMUtils.defineLazyModuleGetter(this, "DownloadUIHelper", michael@0: "resource://gre/modules/DownloadUIHelper.jsm"); michael@0: XPCOMUtils.defineLazyModuleGetter(this, "DownloadUtils", michael@0: "resource://gre/modules/DownloadUtils.jsm"); michael@0: XPCOMUtils.defineLazyModuleGetter(this, "OS", michael@0: "resource://gre/modules/osfile.jsm") michael@0: XPCOMUtils.defineLazyModuleGetter(this, "PlacesUtils", michael@0: "resource://gre/modules/PlacesUtils.jsm"); michael@0: XPCOMUtils.defineLazyModuleGetter(this, "PrivateBrowsingUtils", michael@0: "resource://gre/modules/PrivateBrowsingUtils.jsm"); michael@0: XPCOMUtils.defineLazyModuleGetter(this, "RecentWindow", michael@0: "resource:///modules/RecentWindow.jsm"); michael@0: XPCOMUtils.defineLazyModuleGetter(this, "Promise", michael@0: "resource://gre/modules/Promise.jsm"); michael@0: XPCOMUtils.defineLazyModuleGetter(this, "DownloadsLogger", michael@0: "resource:///modules/DownloadsLogger.jsm"); michael@0: michael@0: const nsIDM = Ci.nsIDownloadManager; michael@0: michael@0: const kDownloadsStringBundleUrl = michael@0: "chrome://browser/locale/downloads/downloads.properties"; michael@0: michael@0: const kDownloadsStringsRequiringFormatting = { michael@0: sizeWithUnits: true, michael@0: shortTimeLeftSeconds: true, michael@0: shortTimeLeftMinutes: true, michael@0: shortTimeLeftHours: true, michael@0: shortTimeLeftDays: true, michael@0: statusSeparator: true, michael@0: statusSeparatorBeforeNumber: true, michael@0: fileExecutableSecurityWarning: true michael@0: }; michael@0: michael@0: const kDownloadsStringsRequiringPluralForm = { michael@0: otherDownloads2: true michael@0: }; michael@0: michael@0: XPCOMUtils.defineLazyGetter(this, "DownloadsLocalFileCtor", function () { michael@0: return Components.Constructor("@mozilla.org/file/local;1", michael@0: "nsILocalFile", "initWithPath"); michael@0: }); michael@0: michael@0: const kPartialDownloadSuffix = ".part"; michael@0: michael@0: const kPrefBranch = Services.prefs.getBranch("browser.download."); michael@0: michael@0: let PrefObserver = { michael@0: QueryInterface: XPCOMUtils.generateQI([Ci.nsIObserver, michael@0: Ci.nsISupportsWeakReference]), michael@0: getPref: function PO_getPref(name) { michael@0: try { michael@0: switch (typeof this.prefs[name]) { michael@0: case "boolean": michael@0: return kPrefBranch.getBoolPref(name); michael@0: } michael@0: } catch (ex) { } michael@0: return this.prefs[name]; michael@0: }, michael@0: observe: function PO_observe(aSubject, aTopic, aData) { michael@0: if (this.prefs.hasOwnProperty(aData)) { michael@0: return this[aData] = this.getPref(aData); michael@0: } michael@0: }, michael@0: register: function PO_register(prefs) { michael@0: this.prefs = prefs; michael@0: kPrefBranch.addObserver("", this, true); michael@0: for (let key in prefs) { michael@0: let name = key; michael@0: XPCOMUtils.defineLazyGetter(this, name, function () { michael@0: return PrefObserver.getPref(name); michael@0: }); michael@0: } michael@0: }, michael@0: }; michael@0: michael@0: PrefObserver.register({ michael@0: // prefName: defaultValue michael@0: debug: false, michael@0: animateNotifications: true michael@0: }); michael@0: michael@0: michael@0: //////////////////////////////////////////////////////////////////////////////// michael@0: //// DownloadsCommon michael@0: michael@0: /** michael@0: * This object is exposed directly to the consumers of this JavaScript module, michael@0: * and provides shared methods for all the instances of the user interface. michael@0: */ michael@0: this.DownloadsCommon = { michael@0: log: function DC_log(...aMessageArgs) { michael@0: delete this.log; michael@0: this.log = function DC_log(...aMessageArgs) { michael@0: if (!PrefObserver.debug) { michael@0: return; michael@0: } michael@0: DownloadsLogger.log.apply(DownloadsLogger, aMessageArgs); michael@0: } michael@0: this.log.apply(this, aMessageArgs); michael@0: }, michael@0: michael@0: error: function DC_error(...aMessageArgs) { michael@0: delete this.error; michael@0: this.error = function DC_error(...aMessageArgs) { michael@0: if (!PrefObserver.debug) { michael@0: return; michael@0: } michael@0: DownloadsLogger.reportError.apply(DownloadsLogger, aMessageArgs); michael@0: } michael@0: this.error.apply(this, aMessageArgs); michael@0: }, michael@0: /** michael@0: * Returns an object whose keys are the string names from the downloads string michael@0: * bundle, and whose values are either the translated strings or functions michael@0: * returning formatted strings. michael@0: */ michael@0: get strings() michael@0: { michael@0: let strings = {}; michael@0: let sb = Services.strings.createBundle(kDownloadsStringBundleUrl); michael@0: let enumerator = sb.getSimpleEnumeration(); michael@0: while (enumerator.hasMoreElements()) { michael@0: let string = enumerator.getNext().QueryInterface(Ci.nsIPropertyElement); michael@0: let stringName = string.key; michael@0: if (stringName in kDownloadsStringsRequiringFormatting) { michael@0: strings[stringName] = function () { michael@0: // Convert "arguments" to a real array before calling into XPCOM. michael@0: return sb.formatStringFromName(stringName, michael@0: Array.slice(arguments, 0), michael@0: arguments.length); michael@0: }; michael@0: } else if (stringName in kDownloadsStringsRequiringPluralForm) { michael@0: strings[stringName] = function (aCount) { michael@0: // Convert "arguments" to a real array before calling into XPCOM. michael@0: let formattedString = sb.formatStringFromName(stringName, michael@0: Array.slice(arguments, 0), michael@0: arguments.length); michael@0: return PluralForm.get(aCount, formattedString); michael@0: }; michael@0: } else { michael@0: strings[stringName] = string.value; michael@0: } michael@0: } michael@0: delete this.strings; michael@0: return this.strings = strings; michael@0: }, michael@0: michael@0: /** michael@0: * Generates a very short string representing the given time left. michael@0: * michael@0: * @param aSeconds michael@0: * Value to be formatted. It represents the number of seconds, it must michael@0: * be positive but does not need to be an integer. michael@0: * michael@0: * @return Formatted string, for example "30s" or "2h". The returned value is michael@0: * maximum three characters long, at least in English. michael@0: */ michael@0: formatTimeLeft: function DC_formatTimeLeft(aSeconds) michael@0: { michael@0: // Decide what text to show for the time michael@0: let seconds = Math.round(aSeconds); michael@0: if (!seconds) { michael@0: return ""; michael@0: } else if (seconds <= 30) { michael@0: return DownloadsCommon.strings["shortTimeLeftSeconds"](seconds); michael@0: } michael@0: let minutes = Math.round(aSeconds / 60); michael@0: if (minutes < 60) { michael@0: return DownloadsCommon.strings["shortTimeLeftMinutes"](minutes); michael@0: } michael@0: let hours = Math.round(minutes / 60); michael@0: if (hours < 48) { // two days michael@0: return DownloadsCommon.strings["shortTimeLeftHours"](hours); michael@0: } michael@0: let days = Math.round(hours / 24); michael@0: return DownloadsCommon.strings["shortTimeLeftDays"](Math.min(days, 99)); michael@0: }, michael@0: michael@0: /** michael@0: * Indicates whether we should show visual notification on the indicator michael@0: * when a download event is triggered. michael@0: */ michael@0: get animateNotifications() michael@0: { michael@0: return PrefObserver.animateNotifications; michael@0: }, michael@0: michael@0: /** michael@0: * Get access to one of the DownloadsData or PrivateDownloadsData objects, michael@0: * depending on the privacy status of the window in question. michael@0: * michael@0: * @param aWindow michael@0: * The browser window which owns the download button. michael@0: */ michael@0: getData: function DC_getData(aWindow) { michael@0: if (PrivateBrowsingUtils.isWindowPrivate(aWindow)) { michael@0: return PrivateDownloadsData; michael@0: } else { michael@0: return DownloadsData; michael@0: } michael@0: }, michael@0: michael@0: /** michael@0: * Initializes the Downloads back-end and starts receiving events for both the michael@0: * private and non-private downloads data objects. michael@0: */ michael@0: initializeAllDataLinks: function () { michael@0: DownloadsData.initializeDataLink(); michael@0: PrivateDownloadsData.initializeDataLink(); michael@0: }, michael@0: michael@0: /** michael@0: * Get access to one of the DownloadsIndicatorData or michael@0: * PrivateDownloadsIndicatorData objects, depending on the privacy status of michael@0: * the window in question. michael@0: */ michael@0: getIndicatorData: function DC_getIndicatorData(aWindow) { michael@0: if (PrivateBrowsingUtils.isWindowPrivate(aWindow)) { michael@0: return PrivateDownloadsIndicatorData; michael@0: } else { michael@0: return DownloadsIndicatorData; michael@0: } michael@0: }, michael@0: michael@0: /** michael@0: * Returns a reference to the DownloadsSummaryData singleton - creating one michael@0: * in the process if one hasn't been instantiated yet. michael@0: * michael@0: * @param aWindow michael@0: * The browser window which owns the download button. michael@0: * @param aNumToExclude michael@0: * The number of items on the top of the downloads list to exclude michael@0: * from the summary. michael@0: */ michael@0: getSummary: function DC_getSummary(aWindow, aNumToExclude) michael@0: { michael@0: if (PrivateBrowsingUtils.isWindowPrivate(aWindow)) { michael@0: if (this._privateSummary) { michael@0: return this._privateSummary; michael@0: } michael@0: return this._privateSummary = new DownloadsSummaryData(true, aNumToExclude); michael@0: } else { michael@0: if (this._summary) { michael@0: return this._summary; michael@0: } michael@0: return this._summary = new DownloadsSummaryData(false, aNumToExclude); michael@0: } michael@0: }, michael@0: _summary: null, michael@0: _privateSummary: null, michael@0: michael@0: /** michael@0: * Given an iterable collection of DownloadDataItems, generates and returns michael@0: * statistics about that collection. michael@0: * michael@0: * @param aDataItems An iterable collection of DownloadDataItems. michael@0: * michael@0: * @return Object whose properties are the generated statistics. Currently, michael@0: * we return the following properties: michael@0: * michael@0: * numActive : The total number of downloads. michael@0: * numPaused : The total number of paused downloads. michael@0: * numScanning : The total number of downloads being scanned. michael@0: * numDownloading : The total number of downloads being downloaded. michael@0: * totalSize : The total size of all downloads once completed. michael@0: * totalTransferred: The total amount of transferred data for these michael@0: * downloads. michael@0: * slowestSpeed : The slowest download rate. michael@0: * rawTimeLeft : The estimated time left for the downloads to michael@0: * complete. michael@0: * percentComplete : The percentage of bytes successfully downloaded. michael@0: */ michael@0: summarizeDownloads: function DC_summarizeDownloads(aDataItems) michael@0: { michael@0: let summary = { michael@0: numActive: 0, michael@0: numPaused: 0, michael@0: numScanning: 0, michael@0: numDownloading: 0, michael@0: totalSize: 0, michael@0: totalTransferred: 0, michael@0: // slowestSpeed is Infinity so that we can use Math.min to michael@0: // find the slowest speed. We'll set this to 0 afterwards if michael@0: // it's still at Infinity by the time we're done iterating all michael@0: // dataItems. michael@0: slowestSpeed: Infinity, michael@0: rawTimeLeft: -1, michael@0: percentComplete: -1 michael@0: } michael@0: michael@0: for (let dataItem of aDataItems) { michael@0: summary.numActive++; michael@0: switch (dataItem.state) { michael@0: case nsIDM.DOWNLOAD_PAUSED: michael@0: summary.numPaused++; michael@0: break; michael@0: case nsIDM.DOWNLOAD_SCANNING: michael@0: summary.numScanning++; michael@0: break; michael@0: case nsIDM.DOWNLOAD_DOWNLOADING: michael@0: summary.numDownloading++; michael@0: if (dataItem.maxBytes > 0 && dataItem.speed > 0) { michael@0: let sizeLeft = dataItem.maxBytes - dataItem.currBytes; michael@0: summary.rawTimeLeft = Math.max(summary.rawTimeLeft, michael@0: sizeLeft / dataItem.speed); michael@0: summary.slowestSpeed = Math.min(summary.slowestSpeed, michael@0: dataItem.speed); michael@0: } michael@0: break; michael@0: } michael@0: // Only add to total values if we actually know the download size. michael@0: if (dataItem.maxBytes > 0 && michael@0: dataItem.state != nsIDM.DOWNLOAD_CANCELED && michael@0: dataItem.state != nsIDM.DOWNLOAD_FAILED) { michael@0: summary.totalSize += dataItem.maxBytes; michael@0: summary.totalTransferred += dataItem.currBytes; michael@0: } michael@0: } michael@0: michael@0: if (summary.numActive != 0 && summary.totalSize != 0 && michael@0: summary.numActive != summary.numScanning) { michael@0: summary.percentComplete = (summary.totalTransferred / michael@0: summary.totalSize) * 100; michael@0: } michael@0: michael@0: if (summary.slowestSpeed == Infinity) { michael@0: summary.slowestSpeed = 0; michael@0: } michael@0: michael@0: return summary; michael@0: }, michael@0: michael@0: /** michael@0: * If necessary, smooths the estimated number of seconds remaining for one michael@0: * or more downloads to complete. michael@0: * michael@0: * @param aSeconds michael@0: * Current raw estimate on number of seconds left for one or more michael@0: * downloads. This is a floating point value to help get sub-second michael@0: * accuracy for current and future estimates. michael@0: */ michael@0: smoothSeconds: function DC_smoothSeconds(aSeconds, aLastSeconds) michael@0: { michael@0: // We apply an algorithm similar to the DownloadUtils.getTimeLeft function, michael@0: // though tailored to a single time estimation for all downloads. We never michael@0: // apply sommothing if the new value is less than half the previous value. michael@0: let shouldApplySmoothing = aLastSeconds >= 0 && michael@0: aSeconds > aLastSeconds / 2; michael@0: if (shouldApplySmoothing) { michael@0: // Apply hysteresis to favor downward over upward swings. Trust only 30% michael@0: // of the new value if lower, and 10% if higher (exponential smoothing). michael@0: let (diff = aSeconds - aLastSeconds) { michael@0: aSeconds = aLastSeconds + (diff < 0 ? .3 : .1) * diff; michael@0: } michael@0: michael@0: // If the new time is similar, reuse something close to the last time michael@0: // left, but subtract a little to provide forward progress. michael@0: let diff = aSeconds - aLastSeconds; michael@0: let diffPercent = diff / aLastSeconds * 100; michael@0: if (Math.abs(diff) < 5 || Math.abs(diffPercent) < 5) { michael@0: aSeconds = aLastSeconds - (diff < 0 ? .4 : .2); michael@0: } michael@0: } michael@0: michael@0: // In the last few seconds of downloading, we are always subtracting and michael@0: // never adding to the time left. Ensure that we never fall below one michael@0: // second left until all downloads are actually finished. michael@0: return aLastSeconds = Math.max(aSeconds, 1); michael@0: }, michael@0: michael@0: /** michael@0: * Opens a downloaded file. michael@0: * If you've a dataItem, you should call dataItem.openLocalFile. michael@0: * @param aFile michael@0: * the downloaded file to be opened. michael@0: * @param aMimeInfo michael@0: * the mime type info object. May be null. michael@0: * @param aOwnerWindow michael@0: * the window with which this action is associated. michael@0: */ michael@0: openDownloadedFile: function DC_openDownloadedFile(aFile, aMimeInfo, aOwnerWindow) { michael@0: if (!(aFile instanceof Ci.nsIFile)) michael@0: throw new Error("aFile must be a nsIFile object"); michael@0: if (aMimeInfo && !(aMimeInfo instanceof Ci.nsIMIMEInfo)) michael@0: throw new Error("Invalid value passed for aMimeInfo"); michael@0: if (!(aOwnerWindow instanceof Ci.nsIDOMWindow)) michael@0: throw new Error("aOwnerWindow must be a dom-window object"); michael@0: michael@0: let promiseShouldLaunch; michael@0: if (aFile.isExecutable()) { michael@0: // We get a prompter for the provided window here, even though anchoring michael@0: // to the most recently active window should work as well. michael@0: promiseShouldLaunch = michael@0: DownloadUIHelper.getPrompter(aOwnerWindow) michael@0: .confirmLaunchExecutable(aFile.path); michael@0: } else { michael@0: promiseShouldLaunch = Promise.resolve(true); michael@0: } michael@0: michael@0: promiseShouldLaunch.then(shouldLaunch => { michael@0: if (!shouldLaunch) { michael@0: return; michael@0: } michael@0: michael@0: // Actually open the file. michael@0: try { michael@0: if (aMimeInfo && aMimeInfo.preferredAction == aMimeInfo.useHelperApp) { michael@0: aMimeInfo.launchWithFile(aFile); michael@0: return; michael@0: } michael@0: } michael@0: catch(ex) { } michael@0: michael@0: // If either we don't have the mime info, or the preferred action failed, michael@0: // attempt to launch the file directly. michael@0: try { michael@0: aFile.launch(); michael@0: } michael@0: catch(ex) { michael@0: // If launch fails, try sending it through the system's external "file:" michael@0: // URL handler. michael@0: Cc["@mozilla.org/uriloader/external-protocol-service;1"] michael@0: .getService(Ci.nsIExternalProtocolService) michael@0: .loadUrl(NetUtil.newURI(aFile)); michael@0: } michael@0: }).then(null, Cu.reportError); michael@0: }, michael@0: michael@0: /** michael@0: * Show a donwloaded file in the system file manager. michael@0: * If you have a dataItem, use dataItem.showLocalFile. michael@0: * michael@0: * @param aFile michael@0: * a downloaded file. michael@0: */ michael@0: showDownloadedFile: function DC_showDownloadedFile(aFile) { michael@0: if (!(aFile instanceof Ci.nsIFile)) michael@0: throw new Error("aFile must be a nsIFile object"); michael@0: try { michael@0: // Show the directory containing the file and select the file. michael@0: aFile.reveal(); michael@0: } catch (ex) { michael@0: // If reveal fails for some reason (e.g., it's not implemented on unix michael@0: // or the file doesn't exist), try using the parent if we have it. michael@0: let parent = aFile.parent; michael@0: if (parent) { michael@0: try { michael@0: // Open the parent directory to show where the file should be. michael@0: parent.launch(); michael@0: } catch (ex) { michael@0: // If launch also fails (probably because it's not implemented), let michael@0: // the OS handler try to open the parent. michael@0: Cc["@mozilla.org/uriloader/external-protocol-service;1"] michael@0: .getService(Ci.nsIExternalProtocolService) michael@0: .loadUrl(NetUtil.newURI(parent)); michael@0: } michael@0: } michael@0: } michael@0: } michael@0: }; michael@0: michael@0: /** michael@0: * Returns true if we are executing on Windows Vista or a later version. michael@0: */ michael@0: XPCOMUtils.defineLazyGetter(DownloadsCommon, "isWinVistaOrHigher", function () { michael@0: let os = Cc["@mozilla.org/xre/app-info;1"].getService(Ci.nsIXULRuntime).OS; michael@0: if (os != "WINNT") { michael@0: return false; michael@0: } michael@0: let sysInfo = Cc["@mozilla.org/system-info;1"].getService(Ci.nsIPropertyBag2); michael@0: return parseFloat(sysInfo.getProperty("version")) >= 6; michael@0: }); michael@0: michael@0: //////////////////////////////////////////////////////////////////////////////// michael@0: //// DownloadsData michael@0: michael@0: /** michael@0: * Retrieves the list of past and completed downloads from the underlying michael@0: * Download Manager data, and provides asynchronous notifications allowing to michael@0: * build a consistent view of the available data. michael@0: * michael@0: * This object responds to real-time changes in the underlying Download Manager michael@0: * data. For example, the deletion of one or more downloads is notified through michael@0: * the nsIObserver interface, while any state or progress change is notified michael@0: * through the nsIDownloadProgressListener interface. michael@0: * michael@0: * Note that using this object does not automatically start the Download Manager michael@0: * service. Consumers will see an empty list of downloads until the service is michael@0: * actually started. This is useful to display a neutral progress indicator in michael@0: * the main browser window until the autostart timeout elapses. michael@0: * michael@0: * Note that DownloadsData and PrivateDownloadsData are two equivalent singleton michael@0: * objects, one accessing non-private downloads, and the other accessing private michael@0: * ones. michael@0: */ michael@0: function DownloadsDataCtor(aPrivate) { michael@0: this._isPrivate = aPrivate; michael@0: michael@0: // This Object contains all the available DownloadsDataItem objects, indexed by michael@0: // their globally unique identifier. The identifiers of downloads that have michael@0: // been removed from the Download Manager data are still present, however the michael@0: // associated objects are replaced with the value "null". This is required to michael@0: // prevent race conditions when populating the list asynchronously. michael@0: this.dataItems = {}; michael@0: michael@0: // Array of view objects that should be notified when the available download michael@0: // data changes. michael@0: this._views = []; michael@0: michael@0: // Maps Download objects to DownloadDataItem objects. michael@0: this._downloadToDataItemMap = new Map(); michael@0: } michael@0: michael@0: DownloadsDataCtor.prototype = { michael@0: /** michael@0: * Starts receiving events for current downloads. michael@0: */ michael@0: initializeDataLink: function () michael@0: { michael@0: if (!this._dataLinkInitialized) { michael@0: let promiseList = Downloads.getList(this._isPrivate ? Downloads.PRIVATE michael@0: : Downloads.PUBLIC); michael@0: promiseList.then(list => list.addView(this)).then(null, Cu.reportError); michael@0: this._dataLinkInitialized = true; michael@0: } michael@0: }, michael@0: _dataLinkInitialized: false, michael@0: michael@0: /** michael@0: * True if there are finished downloads that can be removed from the list. michael@0: */ michael@0: get canRemoveFinished() michael@0: { michael@0: for (let [, dataItem] of Iterator(this.dataItems)) { michael@0: if (dataItem && !dataItem.inProgress) { michael@0: return true; michael@0: } michael@0: } michael@0: return false; michael@0: }, michael@0: michael@0: /** michael@0: * Asks the back-end to remove finished downloads from the list. michael@0: */ michael@0: removeFinished: function DD_removeFinished() michael@0: { michael@0: let promiseList = Downloads.getList(this._isPrivate ? Downloads.PRIVATE michael@0: : Downloads.PUBLIC); michael@0: promiseList.then(list => list.removeFinished()) michael@0: .then(null, Cu.reportError); michael@0: }, michael@0: michael@0: ////////////////////////////////////////////////////////////////////////////// michael@0: //// Integration with the asynchronous Downloads back-end michael@0: michael@0: onDownloadAdded: function (aDownload) michael@0: { michael@0: let dataItem = new DownloadsDataItem(aDownload); michael@0: this._downloadToDataItemMap.set(aDownload, dataItem); michael@0: this.dataItems[dataItem.downloadGuid] = dataItem; michael@0: michael@0: for (let view of this._views) { michael@0: view.onDataItemAdded(dataItem, true); michael@0: } michael@0: michael@0: this._updateDataItemState(dataItem); michael@0: }, michael@0: michael@0: onDownloadChanged: function (aDownload) michael@0: { michael@0: let dataItem = this._downloadToDataItemMap.get(aDownload); michael@0: if (!dataItem) { michael@0: Cu.reportError("Download doesn't exist."); michael@0: return; michael@0: } michael@0: michael@0: this._updateDataItemState(dataItem); michael@0: }, michael@0: michael@0: onDownloadRemoved: function (aDownload) michael@0: { michael@0: let dataItem = this._downloadToDataItemMap.get(aDownload); michael@0: if (!dataItem) { michael@0: Cu.reportError("Download doesn't exist."); michael@0: return; michael@0: } michael@0: michael@0: this._downloadToDataItemMap.delete(aDownload); michael@0: this.dataItems[dataItem.downloadGuid] = null; michael@0: for (let view of this._views) { michael@0: view.onDataItemRemoved(dataItem); michael@0: } michael@0: }, michael@0: michael@0: /** michael@0: * Updates the given data item and sends related notifications. michael@0: */ michael@0: _updateDataItemState: function (aDataItem) michael@0: { michael@0: let oldState = aDataItem.state; michael@0: let wasInProgress = aDataItem.inProgress; michael@0: let wasDone = aDataItem.done; michael@0: michael@0: aDataItem.updateFromDownload(); michael@0: michael@0: if (wasInProgress && !aDataItem.inProgress) { michael@0: aDataItem.endTime = Date.now(); michael@0: } michael@0: michael@0: if (oldState != aDataItem.state) { michael@0: for (let view of this._views) { michael@0: try { michael@0: view.getViewItem(aDataItem).onStateChange(oldState); michael@0: } catch (ex) { michael@0: Cu.reportError(ex); michael@0: } michael@0: } michael@0: michael@0: // This state transition code should actually be located in a Downloads michael@0: // API module (bug 941009). Moreover, the fact that state is stored as michael@0: // annotations should be ideally hidden behind methods of michael@0: // nsIDownloadHistory (bug 830415). michael@0: if (!this._isPrivate && !aDataItem.inProgress) { michael@0: try { michael@0: let downloadMetaData = { state: aDataItem.state, michael@0: endTime: aDataItem.endTime }; michael@0: if (aDataItem.done) { michael@0: downloadMetaData.fileSize = aDataItem.maxBytes; michael@0: } michael@0: michael@0: PlacesUtils.annotations.setPageAnnotation( michael@0: NetUtil.newURI(aDataItem.uri), "downloads/metaData", michael@0: JSON.stringify(downloadMetaData), 0, michael@0: PlacesUtils.annotations.EXPIRE_WITH_HISTORY); michael@0: } catch (ex) { michael@0: Cu.reportError(ex); michael@0: } michael@0: } michael@0: } michael@0: michael@0: if (!aDataItem.newDownloadNotified) { michael@0: aDataItem.newDownloadNotified = true; michael@0: this._notifyDownloadEvent("start"); michael@0: } michael@0: michael@0: if (!wasDone && aDataItem.done) { michael@0: this._notifyDownloadEvent("finish"); michael@0: } michael@0: michael@0: for (let view of this._views) { michael@0: view.getViewItem(aDataItem).onProgressChange(); michael@0: } michael@0: }, michael@0: michael@0: ////////////////////////////////////////////////////////////////////////////// michael@0: //// Registration of views michael@0: michael@0: /** michael@0: * Adds an object to be notified when the available download data changes. michael@0: * The specified object is initialized with the currently available downloads. michael@0: * michael@0: * @param aView michael@0: * DownloadsView object to be added. This reference must be passed to michael@0: * removeView before termination. michael@0: */ michael@0: addView: function DD_addView(aView) michael@0: { michael@0: this._views.push(aView); michael@0: this._updateView(aView); michael@0: }, michael@0: michael@0: /** michael@0: * Removes an object previously added using addView. michael@0: * michael@0: * @param aView michael@0: * DownloadsView object to be removed. michael@0: */ michael@0: removeView: function DD_removeView(aView) michael@0: { michael@0: let index = this._views.indexOf(aView); michael@0: if (index != -1) { michael@0: this._views.splice(index, 1); michael@0: } michael@0: }, michael@0: michael@0: /** michael@0: * Ensures that the currently loaded data is added to the specified view. michael@0: * michael@0: * @param aView michael@0: * DownloadsView object to be initialized. michael@0: */ michael@0: _updateView: function DD_updateView(aView) michael@0: { michael@0: // Indicate to the view that a batch loading operation is in progress. michael@0: aView.onDataLoadStarting(); michael@0: michael@0: // Sort backwards by start time, ensuring that the most recent michael@0: // downloads are added first regardless of their state. michael@0: let loadedItemsArray = [dataItem michael@0: for each (dataItem in this.dataItems) michael@0: if (dataItem)]; michael@0: loadedItemsArray.sort(function(a, b) b.startTime - a.startTime); michael@0: loadedItemsArray.forEach( michael@0: function (dataItem) aView.onDataItemAdded(dataItem, false) michael@0: ); michael@0: michael@0: // Notify the view that all data is available. michael@0: aView.onDataLoadCompleted(); michael@0: }, michael@0: michael@0: ////////////////////////////////////////////////////////////////////////////// michael@0: //// Notifications sent to the most recent browser window only michael@0: michael@0: /** michael@0: * Set to true after the first download causes the downloads panel to be michael@0: * displayed. michael@0: */ michael@0: get panelHasShownBefore() { michael@0: try { michael@0: return Services.prefs.getBoolPref("browser.download.panel.shown"); michael@0: } catch (ex) { } michael@0: return false; michael@0: }, michael@0: michael@0: set panelHasShownBefore(aValue) { michael@0: Services.prefs.setBoolPref("browser.download.panel.shown", aValue); michael@0: return aValue; michael@0: }, michael@0: michael@0: /** michael@0: * Displays a new or finished download notification in the most recent browser michael@0: * window, if one is currently available with the required privacy type. michael@0: * michael@0: * @param aType michael@0: * Set to "start" for new downloads, "finish" for completed downloads. michael@0: */ michael@0: _notifyDownloadEvent: function DD_notifyDownloadEvent(aType) michael@0: { michael@0: DownloadsCommon.log("Attempting to notify that a new download has started or finished."); michael@0: michael@0: // Show the panel in the most recent browser window, if present. michael@0: let browserWin = RecentWindow.getMostRecentBrowserWindow({ private: this._isPrivate }); michael@0: if (!browserWin) { michael@0: return; michael@0: } michael@0: michael@0: if (this.panelHasShownBefore) { michael@0: // For new downloads after the first one, don't show the panel michael@0: // automatically, but provide a visible notification in the topmost michael@0: // browser window, if the status indicator is already visible. michael@0: DownloadsCommon.log("Showing new download notification."); michael@0: browserWin.DownloadsIndicatorView.showEventNotification(aType); michael@0: return; michael@0: } michael@0: this.panelHasShownBefore = true; michael@0: browserWin.DownloadsPanel.showPanel(); michael@0: } michael@0: }; michael@0: michael@0: XPCOMUtils.defineLazyGetter(this, "PrivateDownloadsData", function() { michael@0: return new DownloadsDataCtor(true); michael@0: }); michael@0: michael@0: XPCOMUtils.defineLazyGetter(this, "DownloadsData", function() { michael@0: return new DownloadsDataCtor(false); michael@0: }); michael@0: michael@0: //////////////////////////////////////////////////////////////////////////////// michael@0: //// DownloadsDataItem michael@0: michael@0: /** michael@0: * Represents a single item in the list of downloads. michael@0: * michael@0: * The endTime property is initialized to the current date and time. michael@0: * michael@0: * @param aDownload michael@0: * The Download object with the current state. michael@0: */ michael@0: function DownloadsDataItem(aDownload) michael@0: { michael@0: this._download = aDownload; michael@0: michael@0: this.downloadGuid = "id:" + this._autoIncrementId; michael@0: this.file = aDownload.target.path; michael@0: this.target = OS.Path.basename(aDownload.target.path); michael@0: this.uri = aDownload.source.url; michael@0: this.endTime = Date.now(); michael@0: michael@0: this.updateFromDownload(); michael@0: } michael@0: michael@0: DownloadsDataItem.prototype = { michael@0: /** michael@0: * The JavaScript API does not need identifiers for Download objects, so they michael@0: * are generated sequentially for the corresponding DownloadDataItem. michael@0: */ michael@0: get _autoIncrementId() ++DownloadsDataItem.prototype.__lastId, michael@0: __lastId: 0, michael@0: michael@0: /** michael@0: * Updates this object from the underlying Download object. michael@0: */ michael@0: updateFromDownload: function () michael@0: { michael@0: // Collapse state using the correct priority. michael@0: if (this._download.succeeded) { michael@0: this.state = nsIDM.DOWNLOAD_FINISHED; michael@0: } else if (this._download.error && michael@0: this._download.error.becauseBlockedByParentalControls) { michael@0: this.state = nsIDM.DOWNLOAD_BLOCKED_PARENTAL; michael@0: } else if (this._download.error && michael@0: this._download.error.becauseBlockedByReputationCheck) { michael@0: this.state = nsIDM.DOWNLOAD_DIRTY; michael@0: } else if (this._download.error) { michael@0: this.state = nsIDM.DOWNLOAD_FAILED; michael@0: } else if (this._download.canceled && this._download.hasPartialData) { michael@0: this.state = nsIDM.DOWNLOAD_PAUSED; michael@0: } else if (this._download.canceled) { michael@0: this.state = nsIDM.DOWNLOAD_CANCELED; michael@0: } else if (this._download.stopped) { michael@0: this.state = nsIDM.DOWNLOAD_NOTSTARTED; michael@0: } else { michael@0: this.state = nsIDM.DOWNLOAD_DOWNLOADING; michael@0: } michael@0: michael@0: this.referrer = this._download.source.referrer; michael@0: this.startTime = this._download.startTime; michael@0: this.currBytes = this._download.currentBytes; michael@0: this.resumable = this._download.hasPartialData; michael@0: this.speed = this._download.speed; michael@0: michael@0: if (this._download.succeeded) { michael@0: // If the download succeeded, show the final size if available, otherwise michael@0: // use the last known number of bytes transferred. The final size on disk michael@0: // will be available when bug 941063 is resolved. michael@0: this.maxBytes = this._download.hasProgress ? michael@0: this._download.totalBytes : michael@0: this._download.currentBytes; michael@0: this.percentComplete = 100; michael@0: } else if (this._download.hasProgress) { michael@0: // If the final size and progress are known, use them. michael@0: this.maxBytes = this._download.totalBytes; michael@0: this.percentComplete = this._download.progress; michael@0: } else { michael@0: // The download final size and progress percentage is unknown. michael@0: this.maxBytes = -1; michael@0: this.percentComplete = -1; michael@0: } michael@0: }, michael@0: michael@0: /** michael@0: * Indicates whether the download is proceeding normally, and not finished michael@0: * yet. This includes paused downloads. When this property is true, the michael@0: * "progress" property represents the current progress of the download. michael@0: */ michael@0: get inProgress() michael@0: { michael@0: return [ michael@0: nsIDM.DOWNLOAD_NOTSTARTED, michael@0: nsIDM.DOWNLOAD_QUEUED, michael@0: nsIDM.DOWNLOAD_DOWNLOADING, michael@0: nsIDM.DOWNLOAD_PAUSED, michael@0: nsIDM.DOWNLOAD_SCANNING, michael@0: ].indexOf(this.state) != -1; michael@0: }, michael@0: michael@0: /** michael@0: * This is true during the initial phases of a download, before the actual michael@0: * download of data bytes starts. michael@0: */ michael@0: get starting() michael@0: { michael@0: return this.state == nsIDM.DOWNLOAD_NOTSTARTED || michael@0: this.state == nsIDM.DOWNLOAD_QUEUED; michael@0: }, michael@0: michael@0: /** michael@0: * Indicates whether the download is paused. michael@0: */ michael@0: get paused() michael@0: { michael@0: return this.state == nsIDM.DOWNLOAD_PAUSED; michael@0: }, michael@0: michael@0: /** michael@0: * Indicates whether the download is in a final state, either because it michael@0: * completed successfully or because it was blocked. michael@0: */ michael@0: get done() michael@0: { michael@0: return [ michael@0: nsIDM.DOWNLOAD_FINISHED, michael@0: nsIDM.DOWNLOAD_BLOCKED_PARENTAL, michael@0: nsIDM.DOWNLOAD_BLOCKED_POLICY, michael@0: nsIDM.DOWNLOAD_DIRTY, michael@0: ].indexOf(this.state) != -1; michael@0: }, michael@0: michael@0: /** michael@0: * Indicates whether the download is finished and can be opened. michael@0: */ michael@0: get openable() michael@0: { michael@0: return this.state == nsIDM.DOWNLOAD_FINISHED; michael@0: }, michael@0: michael@0: /** michael@0: * Indicates whether the download stopped because of an error, and can be michael@0: * resumed manually. michael@0: */ michael@0: get canRetry() michael@0: { michael@0: return this.state == nsIDM.DOWNLOAD_CANCELED || michael@0: this.state == nsIDM.DOWNLOAD_FAILED; michael@0: }, michael@0: michael@0: /** michael@0: * Returns the nsILocalFile for the download target. michael@0: * michael@0: * @throws if the native path is not valid. This can happen if the same michael@0: * profile is used on different platforms, for example if a native michael@0: * Windows path is stored and then the item is accessed on a Mac. michael@0: */ michael@0: get localFile() michael@0: { michael@0: return this._getFile(this.file); michael@0: }, michael@0: michael@0: /** michael@0: * Returns the nsILocalFile for the partially downloaded target. michael@0: * michael@0: * @throws if the native path is not valid. This can happen if the same michael@0: * profile is used on different platforms, for example if a native michael@0: * Windows path is stored and then the item is accessed on a Mac. michael@0: */ michael@0: get partFile() michael@0: { michael@0: return this._getFile(this.file + kPartialDownloadSuffix); michael@0: }, michael@0: michael@0: /** michael@0: * Returns an nsILocalFile for aFilename. aFilename might be a file URL or michael@0: * a native path. michael@0: * michael@0: * @param aFilename the filename of the file to retrieve. michael@0: * @return an nsILocalFile for the file. michael@0: * @throws if the native path is not valid. This can happen if the same michael@0: * profile is used on different platforms, for example if a native michael@0: * Windows path is stored and then the item is accessed on a Mac. michael@0: * @note This function makes no guarantees about the file's existence - michael@0: * callers should check that the returned file exists. michael@0: */ michael@0: _getFile: function DDI__getFile(aFilename) michael@0: { michael@0: // The download database may contain targets stored as file URLs or native michael@0: // paths. This can still be true for previously stored items, even if new michael@0: // items are stored using their file URL. See also bug 239948 comment 12. michael@0: if (aFilename.startsWith("file:")) { michael@0: // Assume the file URL we obtained from the downloads database or from the michael@0: // "spec" property of the target has the UTF-8 charset. michael@0: let fileUrl = NetUtil.newURI(aFilename).QueryInterface(Ci.nsIFileURL); michael@0: return fileUrl.file.clone().QueryInterface(Ci.nsILocalFile); michael@0: } else { michael@0: // The downloads database contains a native path. Try to create a local michael@0: // file, though this may throw an exception if the path is invalid. michael@0: return new DownloadsLocalFileCtor(aFilename); michael@0: } michael@0: }, michael@0: michael@0: /** michael@0: * Open the target file for this download. michael@0: */ michael@0: openLocalFile: function () { michael@0: this._download.launch().then(null, Cu.reportError); michael@0: }, michael@0: michael@0: /** michael@0: * Show the downloaded file in the system file manager. michael@0: */ michael@0: showLocalFile: function DDI_showLocalFile() { michael@0: DownloadsCommon.showDownloadedFile(this.localFile); michael@0: }, michael@0: michael@0: /** michael@0: * Resumes the download if paused, pauses it if active. michael@0: * @throws if the download is not resumable or if has already done. michael@0: */ michael@0: togglePauseResume: function DDI_togglePauseResume() { michael@0: if (this._download.stopped) { michael@0: this._download.start(); michael@0: } else { michael@0: this._download.cancel(); michael@0: } michael@0: }, michael@0: michael@0: /** michael@0: * Attempts to retry the download. michael@0: * @throws if we cannot. michael@0: */ michael@0: retry: function DDI_retry() { michael@0: this._download.start(); michael@0: }, michael@0: michael@0: /** michael@0: * Cancels the download. michael@0: */ michael@0: cancel: function() { michael@0: this._download.cancel(); michael@0: this._download.removePartialData().then(null, Cu.reportError); michael@0: }, michael@0: michael@0: /** michael@0: * Remove the download. michael@0: */ michael@0: remove: function DDI_remove() { michael@0: Downloads.getList(Downloads.ALL) michael@0: .then(list => list.remove(this._download)) michael@0: .then(() => this._download.finalize(true)) michael@0: .then(null, Cu.reportError); michael@0: } michael@0: }; michael@0: michael@0: //////////////////////////////////////////////////////////////////////////////// michael@0: //// DownloadsViewPrototype michael@0: michael@0: /** michael@0: * A prototype for an object that registers itself with DownloadsData as soon michael@0: * as a view is registered with it. michael@0: */ michael@0: const DownloadsViewPrototype = { michael@0: ////////////////////////////////////////////////////////////////////////////// michael@0: //// Registration of views michael@0: michael@0: /** michael@0: * Array of view objects that should be notified when the available status michael@0: * data changes. michael@0: * michael@0: * SUBCLASSES MUST OVERRIDE THIS PROPERTY. michael@0: */ michael@0: _views: null, michael@0: michael@0: /** michael@0: * Determines whether this view object is over the private or non-private michael@0: * downloads. michael@0: * michael@0: * SUBCLASSES MUST OVERRIDE THIS PROPERTY. michael@0: */ michael@0: _isPrivate: false, michael@0: michael@0: /** michael@0: * Adds an object to be notified when the available status data changes. michael@0: * The specified object is initialized with the currently available status. michael@0: * michael@0: * @param aView michael@0: * View object to be added. This reference must be michael@0: * passed to removeView before termination. michael@0: */ michael@0: addView: function DVP_addView(aView) michael@0: { michael@0: // Start receiving events when the first of our views is registered. michael@0: if (this._views.length == 0) { michael@0: if (this._isPrivate) { michael@0: PrivateDownloadsData.addView(this); michael@0: } else { michael@0: DownloadsData.addView(this); michael@0: } michael@0: } michael@0: michael@0: this._views.push(aView); michael@0: this.refreshView(aView); michael@0: }, michael@0: michael@0: /** michael@0: * Updates the properties of an object previously added using addView. michael@0: * michael@0: * @param aView michael@0: * View object to be updated. michael@0: */ michael@0: refreshView: function DVP_refreshView(aView) michael@0: { michael@0: // Update immediately even if we are still loading data asynchronously. michael@0: // Subclasses must provide these two functions! michael@0: this._refreshProperties(); michael@0: this._updateView(aView); michael@0: }, michael@0: michael@0: /** michael@0: * Removes an object previously added using addView. michael@0: * michael@0: * @param aView michael@0: * View object to be removed. michael@0: */ michael@0: removeView: function DVP_removeView(aView) michael@0: { michael@0: let index = this._views.indexOf(aView); michael@0: if (index != -1) { michael@0: this._views.splice(index, 1); michael@0: } michael@0: michael@0: // Stop receiving events when the last of our views is unregistered. michael@0: if (this._views.length == 0) { michael@0: if (this._isPrivate) { michael@0: PrivateDownloadsData.removeView(this); michael@0: } else { michael@0: DownloadsData.removeView(this); michael@0: } michael@0: } michael@0: }, michael@0: michael@0: ////////////////////////////////////////////////////////////////////////////// michael@0: //// Callback functions from DownloadsData michael@0: michael@0: /** michael@0: * Indicates whether we are still loading downloads data asynchronously. michael@0: */ michael@0: _loading: false, michael@0: michael@0: /** michael@0: * Called before multiple downloads are about to be loaded. michael@0: */ michael@0: onDataLoadStarting: function DVP_onDataLoadStarting() michael@0: { michael@0: this._loading = true; michael@0: }, michael@0: michael@0: /** michael@0: * Called after data loading finished. michael@0: */ michael@0: onDataLoadCompleted: function DVP_onDataLoadCompleted() michael@0: { michael@0: this._loading = false; michael@0: }, michael@0: michael@0: /** michael@0: * Called when a new download data item is available, either during the michael@0: * asynchronous data load or when a new download is started. michael@0: * michael@0: * @param aDataItem michael@0: * DownloadsDataItem object that was just added. michael@0: * @param aNewest michael@0: * When true, indicates that this item is the most recent and should be michael@0: * added in the topmost position. This happens when a new download is michael@0: * started. When false, indicates that the item is the least recent michael@0: * with regard to the items that have been already added. The latter michael@0: * generally happens during the asynchronous data load. michael@0: * michael@0: * @note Subclasses should override this. michael@0: */ michael@0: onDataItemAdded: function DVP_onDataItemAdded(aDataItem, aNewest) michael@0: { michael@0: throw Components.results.NS_ERROR_NOT_IMPLEMENTED; michael@0: }, michael@0: michael@0: /** michael@0: * Called when a data item is removed, ensures that the widget associated with michael@0: * the view item is removed from the user interface. michael@0: * michael@0: * @param aDataItem michael@0: * DownloadsDataItem object that is being removed. michael@0: * michael@0: * @note Subclasses should override this. michael@0: */ michael@0: onDataItemRemoved: function DVP_onDataItemRemoved(aDataItem) michael@0: { michael@0: throw Components.results.NS_ERROR_NOT_IMPLEMENTED; michael@0: }, michael@0: michael@0: /** michael@0: * Returns the view item associated with the provided data item for this view. michael@0: * michael@0: * @param aDataItem michael@0: * DownloadsDataItem object for which the view item is requested. michael@0: * michael@0: * @return Object that can be used to notify item status events. michael@0: * michael@0: * @note Subclasses should override this. michael@0: */ michael@0: getViewItem: function DID_getViewItem(aDataItem) michael@0: { michael@0: throw Components.results.NS_ERROR_NOT_IMPLEMENTED; michael@0: }, michael@0: michael@0: /** michael@0: * Private function used to refresh the internal properties being sent to michael@0: * each registered view. michael@0: * michael@0: * @note Subclasses should override this. michael@0: */ michael@0: _refreshProperties: function DID_refreshProperties() michael@0: { michael@0: throw Components.results.NS_ERROR_NOT_IMPLEMENTED; michael@0: }, michael@0: michael@0: /** michael@0: * Private function used to refresh an individual view. michael@0: * michael@0: * @note Subclasses should override this. michael@0: */ michael@0: _updateView: function DID_updateView() michael@0: { michael@0: throw Components.results.NS_ERROR_NOT_IMPLEMENTED; michael@0: } michael@0: }; michael@0: michael@0: //////////////////////////////////////////////////////////////////////////////// michael@0: //// DownloadsIndicatorData michael@0: michael@0: /** michael@0: * This object registers itself with DownloadsData as a view, and transforms the michael@0: * notifications it receives into overall status data, that is then broadcast to michael@0: * the registered download status indicators. michael@0: * michael@0: * Note that using this object does not automatically start the Download Manager michael@0: * service. Consumers will see an empty list of downloads until the service is michael@0: * actually started. This is useful to display a neutral progress indicator in michael@0: * the main browser window until the autostart timeout elapses. michael@0: */ michael@0: function DownloadsIndicatorDataCtor(aPrivate) { michael@0: this._isPrivate = aPrivate; michael@0: this._views = []; michael@0: } michael@0: DownloadsIndicatorDataCtor.prototype = { michael@0: __proto__: DownloadsViewPrototype, michael@0: michael@0: /** michael@0: * Removes an object previously added using addView. michael@0: * michael@0: * @param aView michael@0: * DownloadsIndicatorView object to be removed. michael@0: */ michael@0: removeView: function DID_removeView(aView) michael@0: { michael@0: DownloadsViewPrototype.removeView.call(this, aView); michael@0: michael@0: if (this._views.length == 0) { michael@0: this._itemCount = 0; michael@0: } michael@0: }, michael@0: michael@0: ////////////////////////////////////////////////////////////////////////////// michael@0: //// Callback functions from DownloadsData michael@0: michael@0: /** michael@0: * Called after data loading finished. michael@0: */ michael@0: onDataLoadCompleted: function DID_onDataLoadCompleted() michael@0: { michael@0: DownloadsViewPrototype.onDataLoadCompleted.call(this); michael@0: this._updateViews(); michael@0: }, michael@0: michael@0: /** michael@0: * Called when a new download data item is available, either during the michael@0: * asynchronous data load or when a new download is started. michael@0: * michael@0: * @param aDataItem michael@0: * DownloadsDataItem object that was just added. michael@0: * @param aNewest michael@0: * When true, indicates that this item is the most recent and should be michael@0: * added in the topmost position. This happens when a new download is michael@0: * started. When false, indicates that the item is the least recent michael@0: * with regard to the items that have been already added. The latter michael@0: * generally happens during the asynchronous data load. michael@0: */ michael@0: onDataItemAdded: function DID_onDataItemAdded(aDataItem, aNewest) michael@0: { michael@0: this._itemCount++; michael@0: this._updateViews(); michael@0: }, michael@0: michael@0: /** michael@0: * Called when a data item is removed, ensures that the widget associated with michael@0: * the view item is removed from the user interface. michael@0: * michael@0: * @param aDataItem michael@0: * DownloadsDataItem object that is being removed. michael@0: */ michael@0: onDataItemRemoved: function DID_onDataItemRemoved(aDataItem) michael@0: { michael@0: this._itemCount--; michael@0: this._updateViews(); michael@0: }, michael@0: michael@0: /** michael@0: * Returns the view item associated with the provided data item for this view. michael@0: * michael@0: * @param aDataItem michael@0: * DownloadsDataItem object for which the view item is requested. michael@0: * michael@0: * @return Object that can be used to notify item status events. michael@0: */ michael@0: getViewItem: function DID_getViewItem(aDataItem) michael@0: { michael@0: let data = this._isPrivate ? PrivateDownloadsIndicatorData michael@0: : DownloadsIndicatorData; michael@0: return Object.freeze({ michael@0: onStateChange: function DIVI_onStateChange(aOldState) michael@0: { michael@0: if (aDataItem.state == nsIDM.DOWNLOAD_FINISHED || michael@0: aDataItem.state == nsIDM.DOWNLOAD_FAILED) { michael@0: data.attention = true; michael@0: } michael@0: michael@0: // Since the state of a download changed, reset the estimated time left. michael@0: data._lastRawTimeLeft = -1; michael@0: data._lastTimeLeft = -1; michael@0: michael@0: data._updateViews(); michael@0: }, michael@0: onProgressChange: function DIVI_onProgressChange() michael@0: { michael@0: data._updateViews(); michael@0: } michael@0: }); michael@0: }, michael@0: michael@0: ////////////////////////////////////////////////////////////////////////////// michael@0: //// Propagation of properties to our views michael@0: michael@0: // The following properties are updated by _refreshProperties and are then michael@0: // propagated to the views. See _refreshProperties for details. michael@0: _hasDownloads: false, michael@0: _counter: "", michael@0: _percentComplete: -1, michael@0: _paused: false, michael@0: michael@0: /** michael@0: * Indicates whether the download indicators should be highlighted. michael@0: */ michael@0: set attention(aValue) michael@0: { michael@0: this._attention = aValue; michael@0: this._updateViews(); michael@0: return aValue; michael@0: }, michael@0: _attention: false, michael@0: michael@0: /** michael@0: * Indicates whether the user is interacting with downloads, thus the michael@0: * attention indication should not be shown even if requested. michael@0: */ michael@0: set attentionSuppressed(aValue) michael@0: { michael@0: this._attentionSuppressed = aValue; michael@0: this._attention = false; michael@0: this._updateViews(); michael@0: return aValue; michael@0: }, michael@0: _attentionSuppressed: false, michael@0: michael@0: /** michael@0: * Computes aggregate values and propagates the changes to our views. michael@0: */ michael@0: _updateViews: function DID_updateViews() michael@0: { michael@0: // Do not update the status indicators during batch loads of download items. michael@0: if (this._loading) { michael@0: return; michael@0: } michael@0: michael@0: this._refreshProperties(); michael@0: this._views.forEach(this._updateView, this); michael@0: }, michael@0: michael@0: /** michael@0: * Updates the specified view with the current aggregate values. michael@0: * michael@0: * @param aView michael@0: * DownloadsIndicatorView object to be updated. michael@0: */ michael@0: _updateView: function DID_updateView(aView) michael@0: { michael@0: aView.hasDownloads = this._hasDownloads; michael@0: aView.counter = this._counter; michael@0: aView.percentComplete = this._percentComplete; michael@0: aView.paused = this._paused; michael@0: aView.attention = this._attention && !this._attentionSuppressed; michael@0: }, michael@0: michael@0: ////////////////////////////////////////////////////////////////////////////// michael@0: //// Property updating based on current download status michael@0: michael@0: /** michael@0: * Number of download items that are available to be displayed. michael@0: */ michael@0: _itemCount: 0, michael@0: michael@0: /** michael@0: * Floating point value indicating the last number of seconds estimated until michael@0: * the longest download will finish. We need to store this value so that we michael@0: * don't continuously apply smoothing if the actual download state has not michael@0: * changed. This is set to -1 if the previous value is unknown. michael@0: */ michael@0: _lastRawTimeLeft: -1, michael@0: michael@0: /** michael@0: * Last number of seconds estimated until all in-progress downloads with a michael@0: * known size and speed will finish. This value is stored to allow smoothing michael@0: * in case of small variations. This is set to -1 if the previous value is michael@0: * unknown. michael@0: */ michael@0: _lastTimeLeft: -1, michael@0: michael@0: /** michael@0: * A generator function for the dataItems that this summary is currently michael@0: * interested in. This generator is passed off to summarizeDownloads in order michael@0: * to generate statistics about the dataItems we care about - in this case, michael@0: * it's all dataItems for active downloads. michael@0: */ michael@0: _activeDataItems: function DID_activeDataItems() michael@0: { michael@0: let dataItems = this._isPrivate ? PrivateDownloadsData.dataItems michael@0: : DownloadsData.dataItems; michael@0: for each (let dataItem in dataItems) { michael@0: if (dataItem && dataItem.inProgress) { michael@0: yield dataItem; michael@0: } michael@0: } michael@0: }, michael@0: michael@0: /** michael@0: * Computes aggregate values based on the current state of downloads. michael@0: */ michael@0: _refreshProperties: function DID_refreshProperties() michael@0: { michael@0: let summary = michael@0: DownloadsCommon.summarizeDownloads(this._activeDataItems()); michael@0: michael@0: // Determine if the indicator should be shown or get attention. michael@0: this._hasDownloads = (this._itemCount > 0); michael@0: michael@0: // If all downloads are paused, show the progress indicator as paused. michael@0: this._paused = summary.numActive > 0 && michael@0: summary.numActive == summary.numPaused; michael@0: michael@0: this._percentComplete = summary.percentComplete; michael@0: michael@0: // Display the estimated time left, if present. michael@0: if (summary.rawTimeLeft == -1) { michael@0: // There are no downloads with a known time left. michael@0: this._lastRawTimeLeft = -1; michael@0: this._lastTimeLeft = -1; michael@0: this._counter = ""; michael@0: } else { michael@0: // Compute the new time left only if state actually changed. michael@0: if (this._lastRawTimeLeft != summary.rawTimeLeft) { michael@0: this._lastRawTimeLeft = summary.rawTimeLeft; michael@0: this._lastTimeLeft = DownloadsCommon.smoothSeconds(summary.rawTimeLeft, michael@0: this._lastTimeLeft); michael@0: } michael@0: this._counter = DownloadsCommon.formatTimeLeft(this._lastTimeLeft); michael@0: } michael@0: } michael@0: }; michael@0: michael@0: XPCOMUtils.defineLazyGetter(this, "PrivateDownloadsIndicatorData", function() { michael@0: return new DownloadsIndicatorDataCtor(true); michael@0: }); michael@0: michael@0: XPCOMUtils.defineLazyGetter(this, "DownloadsIndicatorData", function() { michael@0: return new DownloadsIndicatorDataCtor(false); michael@0: }); michael@0: michael@0: //////////////////////////////////////////////////////////////////////////////// michael@0: //// DownloadsSummaryData michael@0: michael@0: /** michael@0: * DownloadsSummaryData is a view for DownloadsData that produces a summary michael@0: * of all downloads after a certain exclusion point aNumToExclude. For example, michael@0: * if there were 5 downloads in progress, and a DownloadsSummaryData was michael@0: * constructed with aNumToExclude equal to 3, then that DownloadsSummaryData michael@0: * would produce a summary of the last 2 downloads. michael@0: * michael@0: * @param aIsPrivate michael@0: * True if the browser window which owns the download button is a private michael@0: * window. michael@0: * @param aNumToExclude michael@0: * The number of items to exclude from the summary, starting from the michael@0: * top of the list. michael@0: */ michael@0: function DownloadsSummaryData(aIsPrivate, aNumToExclude) { michael@0: this._numToExclude = aNumToExclude; michael@0: // Since we can have multiple instances of DownloadsSummaryData, we michael@0: // override these values from the prototype so that each instance can be michael@0: // completely separated from one another. michael@0: this._loading = false; michael@0: michael@0: this._dataItems = []; michael@0: michael@0: // Floating point value indicating the last number of seconds estimated until michael@0: // the longest download will finish. We need to store this value so that we michael@0: // don't continuously apply smoothing if the actual download state has not michael@0: // changed. This is set to -1 if the previous value is unknown. michael@0: this._lastRawTimeLeft = -1; michael@0: michael@0: // Last number of seconds estimated until all in-progress downloads with a michael@0: // known size and speed will finish. This value is stored to allow smoothing michael@0: // in case of small variations. This is set to -1 if the previous value is michael@0: // unknown. michael@0: this._lastTimeLeft = -1; michael@0: michael@0: // The following properties are updated by _refreshProperties and are then michael@0: // propagated to the views. michael@0: this._showingProgress = false; michael@0: this._details = ""; michael@0: this._description = ""; michael@0: this._numActive = 0; michael@0: this._percentComplete = -1; michael@0: michael@0: this._isPrivate = aIsPrivate; michael@0: this._views = []; michael@0: } michael@0: michael@0: DownloadsSummaryData.prototype = { michael@0: __proto__: DownloadsViewPrototype, michael@0: michael@0: /** michael@0: * Removes an object previously added using addView. michael@0: * michael@0: * @param aView michael@0: * DownloadsSummary view to be removed. michael@0: */ michael@0: removeView: function DSD_removeView(aView) michael@0: { michael@0: DownloadsViewPrototype.removeView.call(this, aView); michael@0: michael@0: if (this._views.length == 0) { michael@0: // Clear out our collection of DownloadDataItems. If we ever have michael@0: // another view registered with us, this will get re-populated. michael@0: this._dataItems = []; michael@0: } michael@0: }, michael@0: michael@0: ////////////////////////////////////////////////////////////////////////////// michael@0: //// Callback functions from DownloadsData - see the documentation in michael@0: //// DownloadsViewPrototype for more information on what these functions michael@0: //// are used for. michael@0: michael@0: onDataLoadCompleted: function DSD_onDataLoadCompleted() michael@0: { michael@0: DownloadsViewPrototype.onDataLoadCompleted.call(this); michael@0: this._updateViews(); michael@0: }, michael@0: michael@0: onDataItemAdded: function DSD_onDataItemAdded(aDataItem, aNewest) michael@0: { michael@0: if (aNewest) { michael@0: this._dataItems.unshift(aDataItem); michael@0: } else { michael@0: this._dataItems.push(aDataItem); michael@0: } michael@0: michael@0: this._updateViews(); michael@0: }, michael@0: michael@0: onDataItemRemoved: function DSD_onDataItemRemoved(aDataItem) michael@0: { michael@0: let itemIndex = this._dataItems.indexOf(aDataItem); michael@0: this._dataItems.splice(itemIndex, 1); michael@0: this._updateViews(); michael@0: }, michael@0: michael@0: getViewItem: function DSD_getViewItem(aDataItem) michael@0: { michael@0: let self = this; michael@0: return Object.freeze({ michael@0: onStateChange: function DIVI_onStateChange(aOldState) michael@0: { michael@0: // Since the state of a download changed, reset the estimated time left. michael@0: self._lastRawTimeLeft = -1; michael@0: self._lastTimeLeft = -1; michael@0: self._updateViews(); michael@0: }, michael@0: onProgressChange: function DIVI_onProgressChange() michael@0: { michael@0: self._updateViews(); michael@0: } michael@0: }); michael@0: }, michael@0: michael@0: ////////////////////////////////////////////////////////////////////////////// michael@0: //// Propagation of properties to our views michael@0: michael@0: /** michael@0: * Computes aggregate values and propagates the changes to our views. michael@0: */ michael@0: _updateViews: function DSD_updateViews() michael@0: { michael@0: // Do not update the status indicators during batch loads of download items. michael@0: if (this._loading) { michael@0: return; michael@0: } michael@0: michael@0: this._refreshProperties(); michael@0: this._views.forEach(this._updateView, this); michael@0: }, michael@0: michael@0: /** michael@0: * Updates the specified view with the current aggregate values. michael@0: * michael@0: * @param aView michael@0: * DownloadsIndicatorView object to be updated. michael@0: */ michael@0: _updateView: function DSD_updateView(aView) michael@0: { michael@0: aView.showingProgress = this._showingProgress; michael@0: aView.percentComplete = this._percentComplete; michael@0: aView.description = this._description; michael@0: aView.details = this._details; michael@0: }, michael@0: michael@0: ////////////////////////////////////////////////////////////////////////////// michael@0: //// Property updating based on current download status michael@0: michael@0: /** michael@0: * A generator function for the dataItems that this summary is currently michael@0: * interested in. This generator is passed off to summarizeDownloads in order michael@0: * to generate statistics about the dataItems we care about - in this case, michael@0: * it's the dataItems in this._dataItems after the first few to exclude, michael@0: * which was set when constructing this DownloadsSummaryData instance. michael@0: */ michael@0: _dataItemsForSummary: function DSD_dataItemsForSummary() michael@0: { michael@0: if (this._dataItems.length > 0) { michael@0: for (let i = this._numToExclude; i < this._dataItems.length; ++i) { michael@0: yield this._dataItems[i]; michael@0: } michael@0: } michael@0: }, michael@0: michael@0: /** michael@0: * Computes aggregate values based on the current state of downloads. michael@0: */ michael@0: _refreshProperties: function DSD_refreshProperties() michael@0: { michael@0: // Pre-load summary with default values. michael@0: let summary = michael@0: DownloadsCommon.summarizeDownloads(this._dataItemsForSummary()); michael@0: michael@0: this._description = DownloadsCommon.strings michael@0: .otherDownloads2(summary.numActive); michael@0: this._percentComplete = summary.percentComplete; michael@0: michael@0: // If all downloads are paused, show the progress indicator as paused. michael@0: this._showingProgress = summary.numDownloading > 0 || michael@0: summary.numPaused > 0; michael@0: michael@0: // Display the estimated time left, if present. michael@0: if (summary.rawTimeLeft == -1) { michael@0: // There are no downloads with a known time left. michael@0: this._lastRawTimeLeft = -1; michael@0: this._lastTimeLeft = -1; michael@0: this._details = ""; michael@0: } else { michael@0: // Compute the new time left only if state actually changed. michael@0: if (this._lastRawTimeLeft != summary.rawTimeLeft) { michael@0: this._lastRawTimeLeft = summary.rawTimeLeft; michael@0: this._lastTimeLeft = DownloadsCommon.smoothSeconds(summary.rawTimeLeft, michael@0: this._lastTimeLeft); michael@0: } michael@0: [this._details] = DownloadUtils.getDownloadStatusNoRate( michael@0: summary.totalTransferred, summary.totalSize, summary.slowestSpeed, michael@0: this._lastTimeLeft); michael@0: } michael@0: } michael@0: }