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 michael@0: * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ michael@0: michael@0: /** michael@0: * This file includes the following constructors and global objects: michael@0: * michael@0: * DownloadList michael@0: * Represents a collection of Download objects that can be viewed and managed by michael@0: * the user interface, and persisted across sessions. michael@0: * michael@0: * DownloadCombinedList michael@0: * Provides a unified, unordered list combining public and private downloads. michael@0: * michael@0: * DownloadSummary michael@0: * Provides an aggregated view on the contents of a DownloadList. michael@0: */ michael@0: michael@0: "use strict"; michael@0: michael@0: this.EXPORTED_SYMBOLS = [ michael@0: "DownloadList", michael@0: "DownloadCombinedList", michael@0: "DownloadSummary", 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: michael@0: XPCOMUtils.defineLazyModuleGetter(this, "Promise", michael@0: "resource://gre/modules/Promise.jsm"); michael@0: XPCOMUtils.defineLazyModuleGetter(this, "Task", michael@0: "resource://gre/modules/Task.jsm"); michael@0: michael@0: //////////////////////////////////////////////////////////////////////////////// michael@0: //// DownloadList michael@0: michael@0: /** michael@0: * Represents a collection of Download objects that can be viewed and managed by michael@0: * the user interface, and persisted across sessions. michael@0: */ michael@0: this.DownloadList = function () michael@0: { michael@0: this._downloads = []; michael@0: this._views = new Set(); michael@0: } michael@0: michael@0: this.DownloadList.prototype = { michael@0: /** michael@0: * Array of Download objects currently in the list. michael@0: */ michael@0: _downloads: null, michael@0: michael@0: /** michael@0: * Retrieves a snapshot of the downloads that are currently in the list. The michael@0: * returned array does not change when downloads are added or removed, though michael@0: * the Download objects it contains are still updated in real time. michael@0: * michael@0: * @return {Promise} michael@0: * @resolves An array of Download objects. michael@0: * @rejects JavaScript exception. michael@0: */ michael@0: getAll: function DL_getAll() { michael@0: return Promise.resolve(Array.slice(this._downloads, 0)); michael@0: }, michael@0: michael@0: /** michael@0: * Adds a new download to the end of the items list. michael@0: * michael@0: * @note When a download is added to the list, its "onchange" event is michael@0: * registered by the list, thus it cannot be used to monitor the michael@0: * download. To receive change notifications for downloads that are michael@0: * added to the list, use the addView method to register for michael@0: * onDownloadChanged notifications. michael@0: * michael@0: * @param aDownload michael@0: * The Download object to add. michael@0: * michael@0: * @return {Promise} michael@0: * @resolves When the download has been added. michael@0: * @rejects JavaScript exception. michael@0: */ michael@0: add: function DL_add(aDownload) { michael@0: this._downloads.push(aDownload); michael@0: aDownload.onchange = this._change.bind(this, aDownload); michael@0: this._notifyAllViews("onDownloadAdded", aDownload); michael@0: michael@0: return Promise.resolve(); michael@0: }, michael@0: michael@0: /** michael@0: * Removes a download from the list. If the download was already removed, michael@0: * this method has no effect. michael@0: * michael@0: * This method does not change the state of the download, to allow adding it michael@0: * to another list, or control it directly. If you want to dispose of the michael@0: * download object, you should cancel it afterwards, and remove any partially michael@0: * downloaded data if needed. michael@0: * michael@0: * @param aDownload michael@0: * The Download object to remove. michael@0: * michael@0: * @return {Promise} michael@0: * @resolves When the download has been removed. michael@0: * @rejects JavaScript exception. michael@0: */ michael@0: remove: function DL_remove(aDownload) { michael@0: let index = this._downloads.indexOf(aDownload); michael@0: if (index != -1) { michael@0: this._downloads.splice(index, 1); michael@0: aDownload.onchange = null; michael@0: this._notifyAllViews("onDownloadRemoved", aDownload); michael@0: } michael@0: michael@0: return Promise.resolve(); michael@0: }, michael@0: michael@0: /** michael@0: * This function is called when "onchange" events of downloads occur. michael@0: * michael@0: * @param aDownload michael@0: * The Download object that changed. michael@0: */ michael@0: _change: function DL_change(aDownload) { michael@0: this._notifyAllViews("onDownloadChanged", aDownload); michael@0: }, michael@0: michael@0: /** michael@0: * Set of currently registered views. michael@0: */ michael@0: _views: null, michael@0: michael@0: /** michael@0: * Adds a view that will be notified of changes to downloads. The newly added michael@0: * view will receive onDownloadAdded notifications for all the downloads that michael@0: * are already in the list. michael@0: * michael@0: * @param aView michael@0: * The view object to add. The following methods may be defined: michael@0: * { michael@0: * onDownloadAdded: function (aDownload) { michael@0: * // Called after aDownload is added to the end of the list. michael@0: * }, michael@0: * onDownloadChanged: function (aDownload) { michael@0: * // Called after the properties of aDownload change. michael@0: * }, michael@0: * onDownloadRemoved: function (aDownload) { michael@0: * // Called after aDownload is removed from the list. michael@0: * }, michael@0: * } michael@0: * michael@0: * @return {Promise} michael@0: * @resolves When the view has been registered and all the onDownloadAdded michael@0: * notifications for the existing downloads have been sent. michael@0: * @rejects JavaScript exception. michael@0: */ michael@0: addView: function DL_addView(aView) michael@0: { michael@0: this._views.add(aView); michael@0: michael@0: if ("onDownloadAdded" in aView) { michael@0: for (let download of this._downloads) { michael@0: try { michael@0: aView.onDownloadAdded(download); michael@0: } catch (ex) { michael@0: Cu.reportError(ex); michael@0: } michael@0: } michael@0: } michael@0: michael@0: return Promise.resolve(); michael@0: }, michael@0: michael@0: /** michael@0: * Removes a view that was previously added using addView. michael@0: * michael@0: * @param aView michael@0: * The view object to remove. michael@0: * michael@0: * @return {Promise} michael@0: * @resolves When the view has been removed. At this point, the removed view michael@0: * will not receive any more notifications. michael@0: * @rejects JavaScript exception. michael@0: */ michael@0: removeView: function DL_removeView(aView) michael@0: { michael@0: this._views.delete(aView); michael@0: michael@0: return Promise.resolve(); michael@0: }, michael@0: michael@0: /** michael@0: * Notifies all the views of a download addition, change, or removal. michael@0: * michael@0: * @param aMethodName michael@0: * String containing the name of the method to call on the view. michael@0: * @param aDownload michael@0: * The Download object that changed. michael@0: */ michael@0: _notifyAllViews: function (aMethodName, aDownload) { michael@0: for (let view of this._views) { michael@0: try { michael@0: if (aMethodName in view) { michael@0: view[aMethodName](aDownload); michael@0: } michael@0: } catch (ex) { michael@0: Cu.reportError(ex); michael@0: } michael@0: } michael@0: }, michael@0: michael@0: /** michael@0: * Removes downloads from the list that have finished, have failed, or have michael@0: * been canceled without keeping partial data. A filter function may be michael@0: * specified to remove only a subset of those downloads. michael@0: * michael@0: * This method finalizes each removed download, ensuring that any partially michael@0: * downloaded data associated with it is also removed. michael@0: * michael@0: * @param aFilterFn michael@0: * The filter function is called with each download as its only michael@0: * argument, and should return true to remove the download and false michael@0: * to keep it. This parameter may be null or omitted to have no michael@0: * additional filter. michael@0: */ michael@0: removeFinished: function DL_removeFinished(aFilterFn) { michael@0: Task.spawn(function() { michael@0: let list = yield this.getAll(); michael@0: for (let download of list) { michael@0: // Remove downloads that have been canceled, even if the cancellation michael@0: // operation hasn't completed yet so we don't check "stopped" here. michael@0: // Failed downloads with partial data are also removed. michael@0: if (download.stopped && (!download.hasPartialData || download.error) && michael@0: (!aFilterFn || aFilterFn(download))) { michael@0: // Remove the download first, so that the views don't get the change michael@0: // notifications that may occur during finalization. michael@0: yield this.remove(download); michael@0: // Ensure that the download is stopped and no partial data is kept. michael@0: // This works even if the download state has changed meanwhile. We michael@0: // don't need to wait for the procedure to be complete before michael@0: // processing the other downloads in the list. michael@0: download.finalize(true).then(null, Cu.reportError); michael@0: } michael@0: } michael@0: }.bind(this)).then(null, Cu.reportError); michael@0: }, michael@0: }; michael@0: michael@0: //////////////////////////////////////////////////////////////////////////////// michael@0: //// DownloadCombinedList michael@0: michael@0: /** michael@0: * Provides a unified, unordered list combining public and private downloads. michael@0: * michael@0: * Download objects added to this list are also added to one of the two michael@0: * underlying lists, based on their "source.isPrivate" property. Views on this michael@0: * list will receive notifications for both public and private downloads. michael@0: * michael@0: * @param aPublicList michael@0: * Underlying DownloadList containing public downloads. michael@0: * @param aPrivateList michael@0: * Underlying DownloadList containing private downloads. michael@0: */ michael@0: this.DownloadCombinedList = function (aPublicList, aPrivateList) michael@0: { michael@0: DownloadList.call(this); michael@0: this._publicList = aPublicList; michael@0: this._privateList = aPrivateList; michael@0: aPublicList.addView(this).then(null, Cu.reportError); michael@0: aPrivateList.addView(this).then(null, Cu.reportError); michael@0: } michael@0: michael@0: this.DownloadCombinedList.prototype = { michael@0: __proto__: DownloadList.prototype, michael@0: michael@0: /** michael@0: * Underlying DownloadList containing public downloads. michael@0: */ michael@0: _publicList: null, michael@0: michael@0: /** michael@0: * Underlying DownloadList containing private downloads. michael@0: */ michael@0: _privateList: null, michael@0: michael@0: /** michael@0: * Adds a new download to the end of the items list. michael@0: * michael@0: * @note When a download is added to the list, its "onchange" event is michael@0: * registered by the list, thus it cannot be used to monitor the michael@0: * download. To receive change notifications for downloads that are michael@0: * added to the list, use the addView method to register for michael@0: * onDownloadChanged notifications. michael@0: * michael@0: * @param aDownload michael@0: * The Download object to add. michael@0: * michael@0: * @return {Promise} michael@0: * @resolves When the download has been added. michael@0: * @rejects JavaScript exception. michael@0: */ michael@0: add: function (aDownload) michael@0: { michael@0: if (aDownload.source.isPrivate) { michael@0: return this._privateList.add(aDownload); michael@0: } else { michael@0: return this._publicList.add(aDownload); michael@0: } michael@0: }, michael@0: michael@0: /** michael@0: * Removes a download from the list. If the download was already removed, michael@0: * this method has no effect. michael@0: * michael@0: * This method does not change the state of the download, to allow adding it michael@0: * to another list, or control it directly. If you want to dispose of the michael@0: * download object, you should cancel it afterwards, and remove any partially michael@0: * downloaded data if needed. michael@0: * michael@0: * @param aDownload michael@0: * The Download object to remove. michael@0: * michael@0: * @return {Promise} michael@0: * @resolves When the download has been removed. michael@0: * @rejects JavaScript exception. michael@0: */ michael@0: remove: function (aDownload) michael@0: { michael@0: if (aDownload.source.isPrivate) { michael@0: return this._privateList.remove(aDownload); michael@0: } else { michael@0: return this._publicList.remove(aDownload); michael@0: } michael@0: }, michael@0: michael@0: ////////////////////////////////////////////////////////////////////////////// michael@0: //// DownloadList view michael@0: michael@0: onDownloadAdded: function (aDownload) michael@0: { michael@0: this._downloads.push(aDownload); michael@0: this._notifyAllViews("onDownloadAdded", aDownload); michael@0: }, michael@0: michael@0: onDownloadChanged: function (aDownload) michael@0: { michael@0: this._notifyAllViews("onDownloadChanged", aDownload); michael@0: }, michael@0: michael@0: onDownloadRemoved: function (aDownload) michael@0: { michael@0: let index = this._downloads.indexOf(aDownload); michael@0: if (index != -1) { michael@0: this._downloads.splice(index, 1); michael@0: } michael@0: this._notifyAllViews("onDownloadRemoved", aDownload); michael@0: }, michael@0: }; michael@0: michael@0: //////////////////////////////////////////////////////////////////////////////// michael@0: //// DownloadSummary michael@0: michael@0: /** michael@0: * Provides an aggregated view on the contents of a DownloadList. michael@0: */ michael@0: this.DownloadSummary = function () michael@0: { michael@0: this._downloads = []; michael@0: this._views = new Set(); michael@0: } michael@0: michael@0: this.DownloadSummary.prototype = { michael@0: /** michael@0: * Array of Download objects that are currently part of the summary. michael@0: */ michael@0: _downloads: null, michael@0: michael@0: /** michael@0: * Underlying DownloadList whose contents should be summarized. michael@0: */ michael@0: _list: null, michael@0: michael@0: /** michael@0: * This method may be called once to bind this object to a DownloadList. michael@0: * michael@0: * Views on the summarized data can be registered before this object is bound michael@0: * to an actual list. This allows the summary to be used without requiring michael@0: * the initialization of the DownloadList first. michael@0: * michael@0: * @param aList michael@0: * Underlying DownloadList whose contents should be summarized. michael@0: * michael@0: * @return {Promise} michael@0: * @resolves When the view on the underlying list has been registered. michael@0: * @rejects JavaScript exception. michael@0: */ michael@0: bindToList: function (aList) michael@0: { michael@0: if (this._list) { michael@0: throw new Error("bindToList may be called only once."); michael@0: } michael@0: michael@0: return aList.addView(this).then(() => { michael@0: // Set the list reference only after addView has returned, so that we don't michael@0: // send a notification to our views for each download that is added. michael@0: this._list = aList; michael@0: this._onListChanged(); michael@0: }); michael@0: }, michael@0: michael@0: /** michael@0: * Set of currently registered views. michael@0: */ michael@0: _views: null, michael@0: michael@0: /** michael@0: * Adds a view that will be notified of changes to the summary. The newly michael@0: * added view will receive an initial onSummaryChanged notification. michael@0: * michael@0: * @param aView michael@0: * The view object to add. The following methods may be defined: michael@0: * { michael@0: * onSummaryChanged: function () { michael@0: * // Called after any property of the summary has changed. michael@0: * }, michael@0: * } michael@0: * michael@0: * @return {Promise} michael@0: * @resolves When the view has been registered and the onSummaryChanged michael@0: * notification has been sent. michael@0: * @rejects JavaScript exception. michael@0: */ michael@0: addView: function (aView) michael@0: { michael@0: this._views.add(aView); michael@0: michael@0: if ("onSummaryChanged" in aView) { michael@0: try { michael@0: aView.onSummaryChanged(); michael@0: } catch (ex) { michael@0: Cu.reportError(ex); michael@0: } michael@0: } michael@0: michael@0: return Promise.resolve(); michael@0: }, michael@0: michael@0: /** michael@0: * Removes a view that was previously added using addView. michael@0: * michael@0: * @param aView michael@0: * The view object to remove. michael@0: * michael@0: * @return {Promise} michael@0: * @resolves When the view has been removed. At this point, the removed view michael@0: * will not receive any more notifications. michael@0: * @rejects JavaScript exception. michael@0: */ michael@0: removeView: function (aView) michael@0: { michael@0: this._views.delete(aView); michael@0: michael@0: return Promise.resolve(); michael@0: }, michael@0: michael@0: /** michael@0: * Indicates whether all the downloads are currently stopped. michael@0: */ michael@0: allHaveStopped: true, michael@0: michael@0: /** michael@0: * Indicates the total number of bytes to be transferred before completing all michael@0: * the downloads that are currently in progress. michael@0: * michael@0: * For downloads that do not have a known final size, the number of bytes michael@0: * currently transferred is reported as part of this property. michael@0: * michael@0: * This is zero if no downloads are currently in progress. michael@0: */ michael@0: progressTotalBytes: 0, michael@0: michael@0: /** michael@0: * Number of bytes currently transferred as part of all the downloads that are michael@0: * currently in progress. michael@0: * michael@0: * This is zero if no downloads are currently in progress. michael@0: */ michael@0: progressCurrentBytes: 0, michael@0: michael@0: /** michael@0: * This function is called when any change in the list of downloads occurs, michael@0: * and will recalculate the summary and notify the views in case the michael@0: * aggregated properties are different. michael@0: */ michael@0: _onListChanged: function () { michael@0: let allHaveStopped = true; michael@0: let progressTotalBytes = 0; michael@0: let progressCurrentBytes = 0; michael@0: michael@0: // Recalculate the aggregated state. See the description of the individual michael@0: // properties for an explanation of the summarization logic. michael@0: for (let download of this._downloads) { michael@0: if (!download.stopped) { michael@0: allHaveStopped = false; michael@0: progressTotalBytes += download.hasProgress ? download.totalBytes michael@0: : download.currentBytes; michael@0: progressCurrentBytes += download.currentBytes; michael@0: } michael@0: } michael@0: michael@0: // Exit now if the properties did not change. michael@0: if (this.allHaveStopped == allHaveStopped && michael@0: this.progressTotalBytes == progressTotalBytes && michael@0: this.progressCurrentBytes == progressCurrentBytes) { michael@0: return; michael@0: } michael@0: michael@0: this.allHaveStopped = allHaveStopped; michael@0: this.progressTotalBytes = progressTotalBytes; michael@0: this.progressCurrentBytes = progressCurrentBytes; michael@0: michael@0: // Notify all the views that our properties changed. michael@0: for (let view of this._views) { michael@0: try { michael@0: if ("onSummaryChanged" in view) { michael@0: view.onSummaryChanged(); michael@0: } michael@0: } catch (ex) { michael@0: Cu.reportError(ex); michael@0: } michael@0: } michael@0: }, michael@0: michael@0: ////////////////////////////////////////////////////////////////////////////// michael@0: //// DownloadList view michael@0: michael@0: onDownloadAdded: function (aDownload) michael@0: { michael@0: this._downloads.push(aDownload); michael@0: if (this._list) { michael@0: this._onListChanged(); michael@0: } michael@0: }, michael@0: michael@0: onDownloadChanged: function (aDownload) michael@0: { michael@0: this._onListChanged(); michael@0: }, michael@0: michael@0: onDownloadRemoved: function (aDownload) michael@0: { michael@0: let index = this._downloads.indexOf(aDownload); michael@0: if (index != -1) { michael@0: this._downloads.splice(index, 1); michael@0: } michael@0: this._onListChanged(); michael@0: }, michael@0: };