/* -*- 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", "DownloadsDataItem", ]; /** * Handles the Downloads panel shared methods and data access. * * This file includes the following constructors and global objects: * * DownloadsCommon * This object is exposed directly to the consumers of this JavaScript module, * and provides shared methods for all the instances of the user interface. * * DownloadsData * Retrieves the list of past and completed downloads from the underlying * Download Manager data, and provides asynchronous notifications allowing * to build a consistent view of the available data. * * DownloadsDataItem * Represents a single item in the list of downloads. This object wraps the * Download object from the JavaScript API for downloads. A specialized version * of this object is implemented in the Places front-end view. * * 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; }, /** * Given an iterable collection of DownloadDataItems, generates and returns * statistics about that collection. * * @param aDataItems An iterable collection of DownloadDataItems. * * @return Object whose properties are the generated statistics. Currently, * we return the following properties: * * numActive : The total number of downloads. * numPaused : The total number of paused downloads. * numScanning : The total number of downloads being scanned. * numDownloading : The total number of downloads being downloaded. * totalSize : The total size of all downloads once completed. * totalTransferred: The total amount of transferred data for these * downloads. * slowestSpeed : The slowest download rate. * rawTimeLeft : The estimated time left for the downloads to * complete. * percentComplete : The percentage of bytes successfully downloaded. */ summarizeDownloads: function DC_summarizeDownloads(aDataItems) { let summary = { numActive: 0, numPaused: 0, numScanning: 0, numDownloading: 0, totalSize: 0, totalTransferred: 0, // slowestSpeed is Infinity so that we can use Math.min to // find the slowest speed. We'll set this to 0 afterwards if // it's still at Infinity by the time we're done iterating all // dataItems. slowestSpeed: Infinity, rawTimeLeft: -1, percentComplete: -1 } for (let dataItem of aDataItems) { summary.numActive++; switch (dataItem.state) { case nsIDM.DOWNLOAD_PAUSED: summary.numPaused++; break; case nsIDM.DOWNLOAD_SCANNING: summary.numScanning++; break; case nsIDM.DOWNLOAD_DOWNLOADING: summary.numDownloading++; if (dataItem.maxBytes > 0 && dataItem.download.speed > 0) { let sizeLeft = dataItem.maxBytes - dataItem.download.currentBytes; summary.rawTimeLeft = Math.max(summary.rawTimeLeft, sizeLeft / dataItem.download.speed); summary.slowestSpeed = Math.min(summary.slowestSpeed, dataItem.download.speed); } break; } // Only add to total values if we actually know the download size. if (dataItem.maxBytes > 0 && dataItem.state != nsIDM.DOWNLOAD_CANCELED && dataItem.state != nsIDM.DOWNLOAD_FAILED) { summary.totalSize += dataItem.maxBytes; summary.totalTransferred += dataItem.download.currentBytes; } } if (summary.numActive != 0 && summary.totalSize != 0 && summary.numActive != summary.numScanning) { summary.percentComplete = (summary.totalTransferred / summary.totalSize) * 100; } if (summary.slowestSpeed == Infinity) { summary.slowestSpeed = 0; } return summary; }, /** * If necessary, smooths the estimated number of seconds remaining for one * or more downloads to complete. * * @param aSeconds * Current raw estimate on number of seconds left for one or more * downloads. This is a floating point value to help get sub-second * accuracy for current and future estimates. */ smoothSeconds: function DC_smoothSeconds(aSeconds, aLastSeconds) { // We apply an algorithm similar to the DownloadUtils.getTimeLeft function, // though tailored to a single time estimation for all downloads. We never // apply something if the new value is less than half the previous value. let shouldApplySmoothing = aLastSeconds >= 0 && aSeconds > aLastSeconds / 2; if (shouldApplySmoothing) { // Apply hysteresis to favor downward over upward swings. Trust only 30% // of the new value if lower, and 10% if higher (exponential smoothing). let diff = aSeconds - aLastSeconds; aSeconds = aLastSeconds + (diff < 0 ? .3 : .1) * diff; // If the new time is similar, reuse something close to the last time // left, but subtract a little to provide forward progress. diff = aSeconds - aLastSeconds; let diffPercent = diff / aLastSeconds * 100; if (Math.abs(diff) < 5 || Math.abs(diffPercent) < 5) { aSeconds = aLastSeconds - (diff < 0 ? .4 : .2); } } // In the last few seconds of downloading, we are always subtracting and // never adding to the time left. Ensure that we never fall below one // second left until all downloads are actually finished. return aLastSeconds = Math.max(aSeconds, 1); }, /** * Opens a downloaded file. * If you've a dataItem, you should call dataItem.openLocalFile. * @param aFile * the downloaded file to be opened. * @param aMimeInfo * the mime type info object. May be null. * @param aOwnerWindow * the window with which this action is associated. */ openDownloadedFile: function DC_openDownloadedFile(aFile, aMimeInfo, aOwnerWindow) { if (!(aFile instanceof Ci.nsIFile)) throw new Error("aFile must be a nsIFile object"); if (aMimeInfo && !(aMimeInfo instanceof Ci.nsIMIMEInfo)) throw new Error("Invalid value passed for aMimeInfo"); if (!(aOwnerWindow instanceof Ci.nsIDOMWindow)) throw new Error("aOwnerWindow must be a dom-window object"); #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; // This Object contains all the available DownloadsDataItem objects, indexed by // their globally unique identifier. The identifiers of downloads that have // been removed from the Download Manager data are still present, however the // associated objects are replaced with the value "null". This is required to // prevent race conditions when populating the list asynchronously. this.dataItems = {}; // Array of view objects that should be notified when the available download // data changes. this._views = []; // Maps Download objects to DownloadDataItem objects. this._downloadToDataItemMap = new Map(); } DownloadsDataCtor.prototype = { /** * Starts receiving events for current downloads. */ initializeDataLink() { if (!this._dataLinkInitialized) { let promiseList = Downloads.getList(this._isPrivate ? Downloads.PRIVATE : Downloads.PUBLIC); promiseList.then(list => list.addView(this)).then(null, Cu.reportError); this._dataLinkInitialized = true; } }, _dataLinkInitialized: false, /** * Stops receiving events for current downloads and cancels any pending read. */ terminateDataLink: function DD_terminateDataLink() { Cu.reportError("terminateDataLink not applicable with JS Transfers"); return; }, /** * True if there are finished downloads that can be removed from the list. */ get canRemoveFinished() { for (let [, dataItem] of Iterator(this.dataItems)) { if (dataItem && !dataItem.inProgress) { return true; } } return false; }, /** * Asks the back-end to remove finished downloads from the list. */ removeFinished: function DD_removeFinished() { let promiseList = Downloads.getList(this._isPrivate ? Downloads.PRIVATE : Downloads.PUBLIC); promiseList.then(list => list.removeFinished()) .then(null, Cu.reportError); }, ////////////////////////////////////////////////////////////////////////////// //// Integration with the asynchronous Downloads back-end onDownloadAdded: function (aDownload) { let dataItem = new DownloadsDataItem(aDownload); this._downloadToDataItemMap.set(aDownload, dataItem); this.dataItems[dataItem.downloadGuid] = dataItem; for (let view of this._views) { view.onDataItemAdded(dataItem, true); } this._updateDataItemState(dataItem); }, onDownloadChanged: function (aDownload) { let dataItem = this._downloadToDataItemMap.get(aDownload); if (!dataItem) { Cu.reportError("Download doesn't exist."); return; } this._updateDataItemState(dataItem); }, onDownloadRemoved: function (aDownload) { let dataItem = this._downloadToDataItemMap.get(aDownload); if (!dataItem) { Cu.reportError("Download doesn't exist."); return; } this._downloadToDataItemMap.delete(aDownload); this.dataItems[dataItem.downloadGuid] = null; for (let view of this._views) { view.onDataItemRemoved(dataItem); } }, /** * Updates the given data item and sends related notifications. */ _updateDataItemState: function (aDataItem) { let oldState = aDataItem.state; let wasInProgress = aDataItem.inProgress; let wasDone = aDataItem.done; aDataItem.updateFromJSDownload(); if (wasInProgress && !aDataItem.inProgress) { aDataItem.endTime = Date.now(); } if (oldState != aDataItem.state) { for (let view of this._views) { try { view.onDataItemStateChanged(aDataItem, oldState); } catch (ex) { Cu.reportError(ex); } } // This state transition code should actually be located in a Downloads // API module (bug 941009). Moreover, the fact that state is stored as // annotations should be ideally hidden behind methods of // nsIDownloadHistory (bug 830415). if (!this._isPrivate && !aDataItem.inProgress) { try { let downloadMetaData = { state: aDataItem.state, endTime: aDataItem.endTime }; if (aDataItem.done) { downloadMetaData.fileSize = aDataItem.maxBytes; } // RRR: Annotation service throws here. commented out for now. /*PlacesUtils.annotations.setPageAnnotation( NetUtil.newURI(aDataItem.download.source.url), "downloads/metaData", JSON.stringify(downloadMetaData), 0, PlacesUtils.annotations.EXPIRE_WITH_HISTORY);*/ } catch (ex) { Cu.reportError(ex); } } } if (!aDataItem.newDownloadNotified) { aDataItem.newDownloadNotified = true; this._notifyDownloadEvent("start"); } if (!wasDone && aDataItem.done) { this._notifyDownloadEvent("finish"); } for (let view of this._views) { view.onDataItemChanged(aDataItem); } }, ////////////////////////////////////////////////////////////////////////////// //// 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 loadedItemsArray = []; for each (let dataItem in this.dataItems) { if (dataItem) { loadedItemsArray.push(dataItem); } } loadedItemsArray.sort(function(a, b) b.download.startTime - a.download.startTime); loadedItemsArray.forEach( function (dataItem) aView.onDataItemAdded(dataItem, false) ); // Notify the view that all data is available unless loading is in progress. if (!this._pendingStatement) { aView.onDataLoadCompleted(); } }, ////////////////////////////////////////////////////////////////////////////// //// In-memory downloads data store /** * Clears the loaded data. */ clear: function DD_clear() { this._terminateDataAccess(); this.dataItems = {}; }, /** * Returns the data item associated with the provided source object. The * source can be a download object that we received from the Download Manager * because of a real-time notification, or a row from the downloads database, * during the asynchronous data load. * * In case we receive download status notifications while we are still * populating the list of downloads from the database, we want the real-time * status to take precedence over the state that is read from the database, * which might be older. This is achieved by creating the download item if * it's not already in the list, but never updating the returned object using * the data from the database, if the object already exists. * * @param aSource * Object containing the data with which the item should be initialized * if it doesn't already exist in the list. This should implement * either nsIDownload or mozIStorageRow. If the item exists, this * argument is only used to retrieve the download identifier. * @param aMayReuseGUID * If false, indicates that the download should not be added if a * download with the same identifier was removed in the meantime. This * ensures that, while loading the list asynchronously, downloads that * have been removed in the meantime do no reappear inadvertently. * * @return New or existing data item, or null if the item was deleted from the * list of available downloads. */ _getOrAddDataItem: function DD_getOrAddDataItem(aSource, aMayReuseGUID) { let downloadGuid = (aSource instanceof Ci.nsIDownload) ? aSource.guid : aSource.getResultByName("guid"); if (downloadGuid in this.dataItems) { let existingItem = this.dataItems[downloadGuid]; if (existingItem || !aMayReuseGUID) { // Returns null if the download was removed and we can't reuse the item. return existingItem; } } DownloadsCommon.log("Creating a new DownloadsDataItem with downloadGuid =", downloadGuid); let dataItem = new DownloadsDataItem(aSource); this.dataItems[downloadGuid] = dataItem; // Create the view items before returning. let addToStartOfList = aSource instanceof Ci.nsIDownload; this._views.forEach( function (view) view.onDataItemAdded(dataItem, addToStartOfList) ); return dataItem; }, /** * Removes the data item with the specified identifier. * * This method can be called at most once per download identifier. */ _removeDataItem: function DD_removeDataItem(aDownloadId) { if (aDownloadId in this.dataItems) { let dataItem = this.dataItems[aDownloadId]; this.dataItems[aDownloadId] = null; this._views.forEach( function (view) view.onDataItemRemoved(dataItem) ); } }, ////////////////////////////////////////////////////////////////////////////// //// Persistent data loading /** * Represents an executing statement, allowing its cancellation. */ _pendingStatement: null, /** * Indicates which kind of items from the persistent downloads database have * been fully loaded in memory and are available to the views. This can * assume the value of one of the kLoad constants. */ _loadState: 0, /** No downloads have been fully loaded yet. */ get kLoadNone() 0, /** All the active downloads in the database are loaded in memory. */ get kLoadActive() 1, /** All the downloads in the database are loaded in memory. */ get kLoadAll() 2, /** * Reloads the specified kind of downloads from the persistent database. This * method must only be called when Private Browsing Mode is disabled. * * @param aActiveOnly * True to load only active downloads from the database. */ ensurePersistentDataLoaded: function DD_ensurePersistentDataLoaded(aActiveOnly) { if (this == PrivateDownloadsData) { Cu.reportError("ensurePersistentDataLoaded should not be called on PrivateDownloadsData"); return; } if (this._pendingStatement) { // We are already in the process of reloading all downloads. return; } if (aActiveOnly) { if (this._loadState == this.kLoadNone) { DownloadsCommon.log("Loading only active downloads from the persistence database"); // Indicate to the views that a batch loading operation is in progress. this._views.forEach( function (view) view.onDataLoadStarting() ); // Reload the list using the Download Manager service. The list is // returned in no particular order. let downloads = Services.downloads.activeDownloads; while (downloads.hasMoreElements()) { let download = downloads.getNext().QueryInterface(Ci.nsIDownload); this._getOrAddDataItem(download, true); } this._loadState = this.kLoadActive; // Indicate to the views that the batch loading operation is complete. this._views.forEach( function (view) view.onDataLoadCompleted() ); DownloadsCommon.log("Active downloads done loading."); } } else { if (this._loadState != this.kLoadAll) { // Load only the relevant columns from the downloads database. The // columns are read in the _initFromDataRow method of DownloadsDataItem. // Order by descending download identifier so that the most recent // downloads are notified first to the listening views. DownloadsCommon.log("Loading all downloads from the persistence database."); let dbConnection = Services.downloads.DBConnection; let statement = dbConnection.createAsyncStatement( "SELECT guid, target, name, source, referrer, state, " + "startTime, endTime, currBytes, maxBytes " + "FROM moz_downloads " + "ORDER BY startTime DESC" ); try { this._pendingStatement = statement.executeAsync(this); } finally { statement.finalize(); } } } }, /** * Cancels any pending data access and ensures views are notified. */ _terminateDataAccess: function DD_terminateDataAccess() { if (this._pendingStatement) { this._pendingStatement.cancel(); this._pendingStatement = null; } // Close all the views on the current data. Create a copy of the array // because some views might unregister while processing this event. Array.slice(this._views, 0).forEach( function (view) view.onDataInvalidated() ); }, ////////////////////////////////////////////////////////////////////////////// //// mozIStorageStatementCallback handleResult: function DD_handleResult(aResultSet) { for (let row = aResultSet.getNextRow(); row; row = aResultSet.getNextRow()) { // Add the download to the list and initialize it with the data we read, // unless we already received a notification providing more reliable // information for this download. this._getOrAddDataItem(row, false); } }, handleError: function DD_handleError(aError) { DownloadsCommon.error("Database statement execution error (", aError.result, "): ", aError.message); }, handleCompletion: function DD_handleCompletion(aReason) { DownloadsCommon.log("Loading all downloads from database completed with reason:", aReason); this._pendingStatement = null; // To ensure that we don't inadvertently delete more downloads from the // database than needed on shutdown, we should update the load state only if // the operation completed successfully. if (aReason == Ci.mozIStorageStatementCallback.REASON_FINISHED) { this._loadState = this.kLoadAll; } // Indicate to the views that the batch loading operation is complete, even // if the lookup failed or was canceled. The only possible glitch happens // in case the database backend changes while loading data, when the views // would open and immediately close. This case is rare enough not to need a // special treatment. this._views.forEach( function (view) view.onDataLoadCompleted() ); }, ////////////////////////////////////////////////////////////////////////////// //// nsIObserver observe: function DD_observe(aSubject, aTopic, aData) { switch (aTopic) { case "download-manager-remove-download-guid": // If a single download was removed, remove the corresponding data item. if (aSubject) { let downloadGuid = aSubject.QueryInterface(Ci.nsISupportsCString).data; DownloadsCommon.log("A single download with id", downloadGuid, "was removed."); this._removeDataItem(downloadGuid); break; } // Multiple downloads have been removed. Iterate over known downloads // and remove those that don't exist anymore. DownloadsCommon.log("Multiple downloads were removed."); for each (let dataItem in this.dataItems) { if (dataItem) { // Bug 449811 - We have to bind to the dataItem because Javascript // doesn't do fresh let-bindings per loop iteration. let dataItemBinding = dataItem; Services.downloads.getDownloadByGUID(dataItemBinding.downloadGuid, function(aStatus, aResult) { if (aStatus == Components.results.NS_ERROR_NOT_AVAILABLE) { DownloadsCommon.log("Removing download with id", dataItemBinding.downloadGuid); this._removeDataItem(dataItemBinding.downloadGuid); } }.bind(this)); } } break; } }, ////////////////////////////////////////////////////////////////////////////// //// nsIDownloadProgressListener onDownloadStateChange: function DD_onDownloadStateChange(aOldState, aDownload) { if (aDownload.isPrivate != this._isPrivate) { // Ignore the downloads with a privacy status other than what we are // tracking. return; } // When a new download is added, it may have the same identifier of a // download that we previously deleted during this session, and we also // want to provide a visible indication that the download started. let isNew = aOldState == nsIDM.DOWNLOAD_NOTSTARTED || aOldState == nsIDM.DOWNLOAD_QUEUED; let dataItem = this._getOrAddDataItem(aDownload, isNew); if (!dataItem) { return; } let wasInProgress = dataItem.inProgress; DownloadsCommon.log("A download changed its state to:", aDownload.state); dataItem.state = aDownload.state; dataItem.referrer = aDownload.referrer && aDownload.referrer.spec; dataItem.resumable = aDownload.resumable; dataItem.startTime = Math.round(aDownload.startTime / 1000); dataItem.currBytes = aDownload.amountTransferred; dataItem.maxBytes = aDownload.size; if (wasInProgress && !dataItem.inProgress) { dataItem.endTime = Date.now(); } // When a download is retried, we create a different download object from // the database with the same ID as before. This means that the nsIDownload // that the dataItem holds might now need updating. // // We only overwrite this in the event that _download exists, because if it // doesn't, that means that no caller ever tried to get the nsIDownload, // which means it was never retrieved and doesn't need to be overwritten. if (dataItem._download) { dataItem._download = aDownload; } for (let view of this._views) { try { view.getViewItem(dataItem).onStateChange(aOldState); } catch (ex) { Cu.reportError(ex); } } if (isNew && !dataItem.newDownloadNotified) { dataItem.newDownloadNotified = true; this._notifyDownloadEvent("start"); } // This is a final state of which we are only notified once. if (dataItem.done) { this._notifyDownloadEvent("finish"); } // TODO Bug 830415: this isn't the right place to set these annotation. // It should be set it in places' nsIDownloadHistory implementation. if (!this._isPrivate && !dataItem.inProgress) { let downloadMetaData = { state: dataItem.state, endTime: dataItem.endTime }; if (dataItem.done) downloadMetaData.fileSize = dataItem.maxBytes; try { PlacesUtils.annotations.setPageAnnotation( NetUtil.newURI(dataItem.uri), "downloads/metaData", JSON.stringify(downloadMetaData), 0, PlacesUtils.annotations.EXPIRE_WITH_HISTORY); } catch(ex) { Cu.reportError(ex); } } }, onProgressChange: function DD_onProgressChange(aWebProgress, aRequest, aCurSelfProgress, aMaxSelfProgress, aCurTotalProgress, aMaxTotalProgress, aDownload) { if (aDownload.isPrivate != this._isPrivate) { // Ignore the downloads with a privacy status other than what we are // tracking. return; } let dataItem = this._getOrAddDataItem(aDownload, false); if (!dataItem) { return; } dataItem.currBytes = aDownload.amountTransferred; dataItem.maxBytes = aDownload.size; dataItem.speed = aDownload.speed; dataItem.percentComplete = aDownload.percentComplete; this._views.forEach( function (view) view.getViewItem(dataItem).onProgressChange() ); }, onStateChange: function () { }, onSecurityChange: function () { }, ////////////////////////////////////////////////////////////////////////////// //// Notifications sent to the most recent browser window only /** * Set to true after the first download causes the downloads panel to be * displayed. */ get panelHasShownBefore() { try { return Services.prefs.getBoolPref("browser.download.panel.shown"); } catch (ex) { } return false; }, set panelHasShownBefore(aValue) { Services.prefs.setBoolPref("browser.download.panel.shown", aValue); return aValue; }, /** * Displays a new or finished download notification in the most recent browser * window, if one is currently available with the required privacy type. * * @param aType * Set to "start" for new downloads, "finish" for completed downloads. */ _notifyDownloadEvent: function DD_notifyDownloadEvent(aType) { DownloadsCommon.log("Attempting to notify that a new download has started or finished."); if (DownloadsCommon.useToolkitUI) { DownloadsCommon.log("Cancelling notification - we're using the toolkit downloads manager."); return; } // Show the panel in the most recent browser window, if present. let browserWin = RecentWindow.getMostRecentBrowserWindow({ private: this._isPrivate }); if (!browserWin) { return; } if (this.panelHasShownBefore) { // For new downloads after the first one, don't show the panel // automatically, but provide a visible notification in the topmost // browser window, if the status indicator is already visible. DownloadsCommon.log("Showing new download notification."); browserWin.DownloadsIndicatorView.showEventNotification(aType); return; } this.panelHasShownBefore = true; browserWin.DownloadsPanel.showPanel(); } }; XPCOMUtils.defineLazyGetter(this, "PrivateDownloadsData", function() { return new DownloadsDataCtor(true); }); XPCOMUtils.defineLazyGetter(this, "DownloadsData", function() { return new DownloadsDataCtor(false); }); //////////////////////////////////////////////////////////////////////////////// //// DownloadsDataItem /** * Represents a single item in the list of downloads. This object either wraps * an existing nsIDownload from the Download Manager, or provides the same * information read directly from the downloads database, with the possibility * of querying the nsIDownload lazily, for performance reasons. * * @param aSource * Object containing the data with which the item should be initialized. * This should implement either nsIDownload or mozIStorageRow. If the * JavaScript API for downloads is enabled, this is a Download object. */ function DownloadsDataItem(aSource) { this._initFromJSDownload(aSource); } DownloadsDataItem.prototype = { /** * The JavaScript API does not need identifiers for Download objects, so they * are generated sequentially for the corresponding DownloadDataItem. */ get _autoIncrementId() ++DownloadsDataItem.prototype.__lastId, __lastId: 0, /** * Initializes this object from the JavaScript API for downloads. * * The endTime property is initialized to the current date and time. * * @param aDownload * The Download object with the current state. */ _initFromJSDownload: function (aDownload) { this.download = aDownload; this.downloadGuid = "id:" + this._autoIncrementId; this.endTime = Date.now(); this.updateFromJSDownload(); }, /** * Updates this object from the JavaScript API for downloads. */ updateFromJSDownload: function () { // Collapse state using the correct priority. if (this.download.succeeded) { this.state = nsIDM.DOWNLOAD_FINISHED; } else if (this.download.error && this.download.error.becauseBlockedByParentalControls) { this.state = nsIDM.DOWNLOAD_BLOCKED_PARENTAL; } else if (this.download.error) { this.state = nsIDM.DOWNLOAD_FAILED; } else if (this.download.canceled && this.download.hasPartialData) { this.state = nsIDM.DOWNLOAD_PAUSED; } else if (this.download.canceled) { this.state = nsIDM.DOWNLOAD_CANCELED; } else if (this.download.stopped) { this.state = nsIDM.DOWNLOAD_NOTSTARTED; } else { this.state = nsIDM.DOWNLOAD_DOWNLOADING; } if (this.download.succeeded) { // If the download succeeded, show the final size if available, otherwise // use the last known number of bytes transferred. The final size on disk // will be available when bug 941063 is resolved. this.maxBytes = this.download.hasProgress ? this.download.totalBytes : this.download.currentBytes; this.percentComplete = 100; } else if (this.download.hasProgress) { // If the final size and progress are known, use them. this.maxBytes = this.download.totalBytes; this.percentComplete = this.download.progress; } else { // The download final size and progress percentage is unknown. this.maxBytes = -1; this.percentComplete = -1; } }, /** * Initializes this object from a download object of the Download Manager. * * The endTime property is initialized to the current date and time. * * @param aDownload * The nsIDownload with the current state. */ _initFromDownload: function DDI_initFromDownload(aDownload) { this._download = aDownload; // Fetch all the download properties eagerly. this.downloadGuid = aDownload.guid; this.file = aDownload.target.spec; this.target = aDownload.displayName; this.uri = aDownload.source.spec; this.referrer = aDownload.referrer && aDownload.referrer.spec; this.state = aDownload.state; this.startTime = Math.round(aDownload.startTime / 1000); this.endTime = Date.now(); this.currBytes = aDownload.amountTransferred; this.maxBytes = aDownload.size; this.resumable = aDownload.resumable; this.speed = aDownload.speed; this.percentComplete = aDownload.percentComplete; }, /** * Initializes this object from a data row in the downloads database, without * querying the associated nsIDownload object, to improve performance when * loading the list of downloads asynchronously. * * When this object is initialized in this way, accessing the "download" * property loads the underlying nsIDownload object synchronously, and should * be avoided unless the object is really required. * * @param aStorageRow * The mozIStorageRow from the downloads database. */ _initFromDataRow: function DDI_initFromDataRow(aStorageRow) { // Get the download properties from the data row. this._download = null; this.downloadGuid = aStorageRow.getResultByName("guid"); this.file = aStorageRow.getResultByName("target"); this.target = aStorageRow.getResultByName("name"); this.uri = aStorageRow.getResultByName("source"); this.referrer = aStorageRow.getResultByName("referrer"); this.state = aStorageRow.getResultByName("state"); this.startTime = Math.round(aStorageRow.getResultByName("startTime") / 1000); this.endTime = Math.round(aStorageRow.getResultByName("endTime") / 1000); this.currBytes = aStorageRow.getResultByName("currBytes"); this.maxBytes = aStorageRow.getResultByName("maxBytes"); // Now we have to determine if the download is resumable, but don't want to // access the underlying download object unnecessarily. The only case where // the property is relevant is when we are currently downloading data, and // in this case the download object is already loaded in memory or will be // loaded very soon in any case. In all the other cases, including a paused // download, we assume that the download is resumable. The property will be // updated as soon as the underlying download state changes. // We'll start by assuming we're resumable, and then if we're downloading, // update resumable property in case we were wrong. this.resumable = true; if (this.state == nsIDM.DOWNLOAD_DOWNLOADING) { this.getDownload(function(aDownload) { this.resumable = aDownload.resumable; }.bind(this)); } // Compute the other properties without accessing the download object. this.speed = 0; this.percentComplete = this.maxBytes <= 0 ? -1 : Math.round(this.currBytes / this.maxBytes * 100); }, /** * Asynchronous getter for the download object corresponding to this data item. * * @param aCallback * A callback function which will be called when the download object is * available. It should accept one argument which will be the download * object. */ getDownload: function DDI_getDownload(aCallback) { if (this._download) { // Return the download object asynchronously to the caller let download = this._download; Services.tm.mainThread.dispatch(function () aCallback(download), Ci.nsIThread.DISPATCH_NORMAL); } else { Services.downloads.getDownloadByGUID(this.downloadGuid, function(aStatus, aResult) { if (!Components.isSuccessCode(aStatus)) { Cu.reportError( new Components.Exception("Cannot retrieve download for GUID: " + this.downloadGuid)); } else { this._download = aResult; aCallback(aResult); } }.bind(this)); } }, /** * Indicates whether the download is proceeding normally, and not finished * yet. This includes paused downloads. When this property is true, the * "progress" property represents the current progress of the download. */ get inProgress() { return [ nsIDM.DOWNLOAD_NOTSTARTED, nsIDM.DOWNLOAD_QUEUED, nsIDM.DOWNLOAD_DOWNLOADING, nsIDM.DOWNLOAD_PAUSED, nsIDM.DOWNLOAD_SCANNING, ].indexOf(this.state) != -1; }, /** * This is true during the initial phases of a download, before the actual * download of data bytes starts. */ get starting() { return this.state == nsIDM.DOWNLOAD_NOTSTARTED || this.state == nsIDM.DOWNLOAD_QUEUED; }, /** * Indicates whether the download is paused. */ get paused() { return this.state == nsIDM.DOWNLOAD_PAUSED; }, /** * Indicates whether the download is in a final state, either because it * completed successfully or because it was blocked. */ get done() { return [ nsIDM.DOWNLOAD_FINISHED, nsIDM.DOWNLOAD_BLOCKED_PARENTAL, nsIDM.DOWNLOAD_BLOCKED_POLICY, nsIDM.DOWNLOAD_DIRTY, ].indexOf(this.state) != -1; }, /** * Indicates whether the download stopped because of an error, and can be * resumed manually. */ get canRetry() { return this.state == nsIDM.DOWNLOAD_CANCELED || this.state == nsIDM.DOWNLOAD_FAILED; }, /** * Returns the nsILocalFile for the download target. * * @throws if the native path is not valid. This can happen if the same * profile is used on different platforms, for example if a native * Windows path is stored and then the item is accessed on a Mac. * * @deprecated Callers should use OS.File and "download.target.path". */ get localFile() { // We should remove should use this.download.target.partFilePath and check asyncrhonously. return new FileUtils.File(this.download.target.path); }, /** * Returns the nsILocalFile for the partially downloaded target. * * @throws if the native path is not valid. This can happen if the same * profile is used on different platforms, for example if a native * Windows path is stored and then the item is accessed on a Mac. * * @deprecated Callers should use OS.File and "download.target.partFilePath". */ get partFile() { return new FileUtils.File(this.download.target.path + kPartialDownloadSuffix); }, /** * Support function that deletes the local file for a download. This is * used in cases where the Download Manager service doesn't delete the file * from disk when cancelling. See bug 732924. */ _ensureLocalFileRemoved: function DDI__ensureLocalFileRemoved() { try { let localFile = this.localFile; if (localFile.exists()) { localFile.remove(false); } } catch (ex) { } } }; //////////////////////////////////////////////////////////////////////////////// //// DownloadsViewPrototype /** * A prototype for an object that registers itself with DownloadsData as soon * as a view is registered with it. */ const DownloadsViewPrototype = { ////////////////////////////////////////////////////////////////////////////// //// Registration of views /** * Array of view objects that should be notified when the available status * data changes. * * SUBCLASSES MUST OVERRIDE THIS PROPERTY. */ _views: null, /** * Determines whether this view object is over the private or non-private * downloads. * * SUBCLASSES MUST OVERRIDE THIS PROPERTY. */ _isPrivate: false, /** * Adds an object to be notified when the available status data changes. * The specified object is initialized with the currently available status. * * @param aView * View object to be added. This reference must be * passed to removeView before termination. */ addView: function DVP_addView(aView) { // Start receiving events when the first of our views is registered. if (this._views.length == 0) { if (this._isPrivate) { PrivateDownloadsData.addView(this); } else { DownloadsData.addView(this); } } this._views.push(aView); this.refreshView(aView); }, /** * Updates the properties of an object previously added using addView. * * @param aView * View object to be updated. */ refreshView: function DVP_refreshView(aView) { // Update immediately even if we are still loading data asynchronously. // Subclasses must provide these two functions! this._refreshProperties(); this._updateView(aView); }, /** * Removes an object previously added using addView. * * @param aView * View object to be removed. */ removeView: function DVP_removeView(aView) { let index = this._views.indexOf(aView); if (index != -1) { this._views.splice(index, 1); } // Stop receiving events when the last of our views is unregistered. if (this._views.length == 0) { if (this._isPrivate) { PrivateDownloadsData.removeView(this); } else { DownloadsData.removeView(this); } } }, ////////////////////////////////////////////////////////////////////////////// //// Callback functions from DownloadsData /** * Indicates whether we are still loading downloads data asynchronously. */ _loading: false, /** * Called before multiple downloads are about to be loaded. */ onDataLoadStarting: function DVP_onDataLoadStarting() { this._loading = true; }, /** * Called after data loading finished. */ onDataLoadCompleted: function DVP_onDataLoadCompleted() { this._loading = false; }, /** * Called when the downloads database becomes unavailable (for example, we * entered Private Browsing Mode and the database backend changed). * References to existing data should be discarded. * * @note Subclasses should override this. */ onDataInvalidated: function DVP_onDataInvalidated() { throw Components.results.NS_ERROR_NOT_IMPLEMENTED; }, /** * Called when a new download data item is available, either during the * asynchronous data load or when a new download is started. * * @param aDataItem * DownloadsDataItem object that was just added. * @param aNewest * When true, indicates that this item is the most recent and should be * added in the topmost position. This happens when a new download is * started. When false, indicates that the item is the least recent * with regard to the items that have been already added. The latter * generally happens during the asynchronous data load. * * @note Subclasses should override this. */ onDataItemAdded: function DVP_onDataItemAdded(aDataItem, aNewest) { throw Components.results.NS_ERROR_NOT_IMPLEMENTED; }, /** * Called when a data item is removed, ensures that the widget associated with * the view item is removed from the user interface. * * @param aDataItem * DownloadsDataItem object that is being removed. * * @note Subclasses should override this. */ onDataItemRemoved: function DVP_onDataItemRemoved(aDataItem) { throw Components.results.NS_ERROR_NOT_IMPLEMENTED; }, /** * Called when the "state" property of a DownloadsDataItem has changed. * * The onDataItemChanged notification will be sent afterwards. * * @note Subclasses should override this. */ onDataItemStateChanged(aDataItem) { throw Components.results.NS_ERROR_NOT_IMPLEMENTED; }, /** * Called every time any state property of a DownloadsDataItem may have * changed, including progress properties and the "state" property. * * 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. */ onDataItemChanged(aDataItem) { throw Components.results.NS_ERROR_NOT_IMPLEMENTED; }, /** * Private function used to refresh the internal properties being sent to * each registered view. * * @note Subclasses should override this. */ _refreshProperties: function DID_refreshProperties() { throw Components.results.NS_ERROR_NOT_IMPLEMENTED; }, /** * Private function used to refresh an individual view. * * @note Subclasses should override this. */ _updateView: function DID_updateView() { throw Components.results.NS_ERROR_NOT_IMPLEMENTED; } }; //////////////////////////////////////////////////////////////////////////////// //// DownloadsIndicatorData /** * This object registers itself with DownloadsData as a view, and transforms the * notifications it receives into overall status data, that is then broadcast to * the registered download status indicators. * * Note that using this object does not automatically start the Download Manager * service. Consumers will see an empty list of downloads until the service is * actually started. This is useful to display a neutral progress indicator in * the main browser window until the autostart timeout elapses. */ function DownloadsIndicatorDataCtor(aPrivate) { this._isPrivate = aPrivate; this._views = []; } DownloadsIndicatorDataCtor.prototype = { __proto__: DownloadsViewPrototype, /** * Removes an object previously added using addView. * * @param aView * DownloadsIndicatorView object to be removed. */ removeView: function DID_removeView(aView) { DownloadsViewPrototype.removeView.call(this, aView); if (this._views.length == 0) { this._itemCount = 0; } }, ////////////////////////////////////////////////////////////////////////////// //// Callback functions from DownloadsData /** * Called after data loading finished. */ onDataLoadCompleted: function DID_onDataLoadCompleted() { DownloadsViewPrototype.onDataLoadCompleted.call(this); this._updateViews(); }, /** * Called when the downloads database becomes unavailable (for example, we * entered Private Browsing Mode and the database backend changed). * References to existing data should be discarded. */ onDataInvalidated: function DID_onDataInvalidated() { this._itemCount = 0; }, /** * Called when a new download data item is available, either during the * asynchronous data load or when a new download is started. * * @param aDataItem * DownloadsDataItem object that was just added. * @param aNewest * When true, indicates that this item is the most recent and should be * added in the topmost position. This happens when a new download is * started. When false, indicates that the item is the least recent * with regard to the items that have been already added. The latter * generally happens during the asynchronous data load. */ onDataItemAdded: function DID_onDataItemAdded(aDataItem, aNewest) { this._itemCount++; this._updateViews(); }, /** * Called when a data item is removed, ensures that the widget associated with * the view item is removed from the user interface. * * @param aDataItem * DownloadsDataItem object that is being removed. */ onDataItemRemoved: function DID_onDataItemRemoved(aDataItem) { this._itemCount--; this._updateViews(); }, // DownloadsView onDataItemStateChanged(aDataItem, aOldState) { if (aDataItem.state == nsIDM.DOWNLOAD_FINISHED || aDataItem.state == nsIDM.DOWNLOAD_FAILED) { this.attention = true; } // Since the state of a download changed, reset the estimated time left. this._lastRawTimeLeft = -1; this._lastTimeLeft = -1; }, // DownloadsView onDataItemChanged() { 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 dataItems that this summary is currently * interested in. This generator is passed off to summarizeDownloads in order * to generate statistics about the dataItems we care about - in this case, * it's all dataItems for active downloads. */ _activeDataItems: function DID_activeDataItems() { let dataItems = this._isPrivate ? PrivateDownloadsData.dataItems : DownloadsData.dataItems; for each (let dataItem in dataItems) { if (dataItem && dataItem.inProgress) { yield dataItem; } } }, /** * Computes aggregate values based on the current state of downloads. */ _refreshProperties: function DID_refreshProperties() { let summary = DownloadsCommon.summarizeDownloads(this._activeDataItems()); // Determine if the indicator should be shown or get attention. this._hasDownloads = (this._itemCount > 0); // If all downloads are paused, show the progress indicator as paused. this._paused = summary.numActive > 0 && summary.numActive == summary.numPaused; this._percentComplete = summary.percentComplete; // Display the estimated time left, if present. if (summary.rawTimeLeft == -1) { // There are no downloads with a known time left. this._lastRawTimeLeft = -1; this._lastTimeLeft = -1; this._counter = ""; } else { // Compute the new time left only if state actually changed. if (this._lastRawTimeLeft != summary.rawTimeLeft) { this._lastRawTimeLeft = summary.rawTimeLeft; this._lastTimeLeft = DownloadsCommon.smoothSeconds(summary.rawTimeLeft, this._lastTimeLeft); } this._counter = DownloadsCommon.formatTimeLeft(this._lastTimeLeft); } } }; XPCOMUtils.defineLazyGetter(this, "PrivateDownloadsIndicatorData", function() { return new DownloadsIndicatorDataCtor(true); }); XPCOMUtils.defineLazyGetter(this, "DownloadsIndicatorData", function() { return new DownloadsIndicatorDataCtor(false); }); //////////////////////////////////////////////////////////////////////////////// //// DownloadsSummaryData /** * DownloadsSummaryData is a view for DownloadsData that produces a summary * of all downloads after a certain exclusion point aNumToExclude. For example, * if there were 5 downloads in progress, and a DownloadsSummaryData was * constructed with aNumToExclude equal to 3, then that DownloadsSummaryData * would produce a summary of the last 2 downloads. * * @param aIsPrivate * True if the browser window which owns the download button is a private * window. * @param aNumToExclude * The number of items to exclude from the summary, starting from the * top of the list. */ function DownloadsSummaryData(aIsPrivate, aNumToExclude) { this._numToExclude = aNumToExclude; // Since we can have multiple instances of DownloadsSummaryData, we // override these values from the prototype so that each instance can be // completely separated from one another. this._loading = false; this._dataItems = []; // Floating point value indicating the last number of seconds estimated until // the longest download will finish. We need to store this value so that we // don't continuously apply smoothing if the actual download state has not // changed. This is set to -1 if the previous value is unknown. this._lastRawTimeLeft = -1; // Last number of seconds estimated until all in-progress downloads with a // known size and speed will finish. This value is stored to allow smoothing // in case of small variations. This is set to -1 if the previous value is // unknown. this._lastTimeLeft = -1; // The following properties are updated by _refreshProperties and are then // propagated to the views. this._showingProgress = false; this._details = ""; this._description = ""; this._numActive = 0; this._percentComplete = -1; this._isPrivate = aIsPrivate; this._views = []; } DownloadsSummaryData.prototype = { __proto__: DownloadsViewPrototype, /** * Removes an object previously added using addView. * * @param aView * DownloadsSummary view to be removed. */ removeView: function DSD_removeView(aView) { DownloadsViewPrototype.removeView.call(this, aView); if (this._views.length == 0) { // Clear out our collection of DownloadDataItems. If we ever have // another view registered with us, this will get re-populated. this._dataItems = []; } }, ////////////////////////////////////////////////////////////////////////////// //// Callback functions from DownloadsData - see the documentation in //// DownloadsViewPrototype for more information on what these functions //// are used for. onDataLoadCompleted: function DSD_onDataLoadCompleted() { DownloadsViewPrototype.onDataLoadCompleted.call(this); this._updateViews(); }, onDataInvalidated: function DSD_onDataInvalidated() { this._dataItems = []; }, onDataItemAdded: function DSD_onDataItemAdded(aDataItem, aNewest) { if (aNewest) { this._dataItems.unshift(aDataItem); } else { this._dataItems.push(aDataItem); } this._updateViews(); }, onDataItemRemoved: function DSD_onDataItemRemoved(aDataItem) { let itemIndex = this._dataItems.indexOf(aDataItem); this._dataItems.splice(itemIndex, 1); this._updateViews(); }, // DownloadsView onDataItemStateChanged(aOldState) { // Since the state of a download changed, reset the estimated time left. this._lastRawTimeLeft = -1; this._lastTimeLeft = -1; }, // DownloadsView onDataItemChanged() { 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 dataItems that this summary is currently * interested in. This generator is passed off to summarizeDownloads in order * to generate statistics about the dataItems we care about - in this case, * it's the dataItems in this._dataItems after the first few to exclude, * which was set when constructing this DownloadsSummaryData instance. */ _dataItemsForSummary: function DSD_dataItemsForSummary() { if (this._dataItems.length > 0) { for (let i = this._numToExclude; i < this._dataItems.length; ++i) { yield this._dataItems[i]; } } }, /** * Computes aggregate values based on the current state of downloads. */ _refreshProperties: function DSD_refreshProperties() { // Pre-load summary with default values. let summary = DownloadsCommon.summarizeDownloads(this._dataItemsForSummary()); this._description = DownloadsCommon.strings .otherDownloads2(summary.numActive); this._percentComplete = summary.percentComplete; // If all downloads are paused, show the progress indicator as paused. this._showingProgress = summary.numDownloading > 0 || summary.numPaused > 0; // Display the estimated time left, if present. if (summary.rawTimeLeft == -1) { // There are no downloads with a known time left. this._lastRawTimeLeft = -1; this._lastTimeLeft = -1; this._details = ""; } else { // Compute the new time left only if state actually changed. if (this._lastRawTimeLeft != summary.rawTimeLeft) { this._lastRawTimeLeft = summary.rawTimeLeft; this._lastTimeLeft = DownloadsCommon.smoothSeconds(summary.rawTimeLeft, this._lastTimeLeft); } [this._details] = DownloadUtils.getDownloadStatusNoRate( summary.totalTransferred, summary.totalSize, summary.slowestSpeed, this._lastTimeLeft); } } }