1.1 --- /dev/null Thu Jan 01 00:00:00 1970 +0000 1.2 +++ b/toolkit/components/jsdownloads/src/DownloadList.jsm Wed Dec 31 06:09:35 2014 +0100 1.3 @@ -0,0 +1,567 @@ 1.4 +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ 1.5 +/* vim: set ts=2 et sw=2 tw=80 filetype=javascript: */ 1.6 +/* This Source Code Form is subject to the terms of the Mozilla Public 1.7 + * License, v. 2.0. If a copy of the MPL was not distributed with this 1.8 + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 1.9 + 1.10 +/** 1.11 + * This file includes the following constructors and global objects: 1.12 + * 1.13 + * DownloadList 1.14 + * Represents a collection of Download objects that can be viewed and managed by 1.15 + * the user interface, and persisted across sessions. 1.16 + * 1.17 + * DownloadCombinedList 1.18 + * Provides a unified, unordered list combining public and private downloads. 1.19 + * 1.20 + * DownloadSummary 1.21 + * Provides an aggregated view on the contents of a DownloadList. 1.22 + */ 1.23 + 1.24 +"use strict"; 1.25 + 1.26 +this.EXPORTED_SYMBOLS = [ 1.27 + "DownloadList", 1.28 + "DownloadCombinedList", 1.29 + "DownloadSummary", 1.30 +]; 1.31 + 1.32 +//////////////////////////////////////////////////////////////////////////////// 1.33 +//// Globals 1.34 + 1.35 +const Cc = Components.classes; 1.36 +const Ci = Components.interfaces; 1.37 +const Cu = Components.utils; 1.38 +const Cr = Components.results; 1.39 + 1.40 +Cu.import("resource://gre/modules/XPCOMUtils.jsm"); 1.41 + 1.42 +XPCOMUtils.defineLazyModuleGetter(this, "Promise", 1.43 + "resource://gre/modules/Promise.jsm"); 1.44 +XPCOMUtils.defineLazyModuleGetter(this, "Task", 1.45 + "resource://gre/modules/Task.jsm"); 1.46 + 1.47 +//////////////////////////////////////////////////////////////////////////////// 1.48 +//// DownloadList 1.49 + 1.50 +/** 1.51 + * Represents a collection of Download objects that can be viewed and managed by 1.52 + * the user interface, and persisted across sessions. 1.53 + */ 1.54 +this.DownloadList = function () 1.55 +{ 1.56 + this._downloads = []; 1.57 + this._views = new Set(); 1.58 +} 1.59 + 1.60 +this.DownloadList.prototype = { 1.61 + /** 1.62 + * Array of Download objects currently in the list. 1.63 + */ 1.64 + _downloads: null, 1.65 + 1.66 + /** 1.67 + * Retrieves a snapshot of the downloads that are currently in the list. The 1.68 + * returned array does not change when downloads are added or removed, though 1.69 + * the Download objects it contains are still updated in real time. 1.70 + * 1.71 + * @return {Promise} 1.72 + * @resolves An array of Download objects. 1.73 + * @rejects JavaScript exception. 1.74 + */ 1.75 + getAll: function DL_getAll() { 1.76 + return Promise.resolve(Array.slice(this._downloads, 0)); 1.77 + }, 1.78 + 1.79 + /** 1.80 + * Adds a new download to the end of the items list. 1.81 + * 1.82 + * @note When a download is added to the list, its "onchange" event is 1.83 + * registered by the list, thus it cannot be used to monitor the 1.84 + * download. To receive change notifications for downloads that are 1.85 + * added to the list, use the addView method to register for 1.86 + * onDownloadChanged notifications. 1.87 + * 1.88 + * @param aDownload 1.89 + * The Download object to add. 1.90 + * 1.91 + * @return {Promise} 1.92 + * @resolves When the download has been added. 1.93 + * @rejects JavaScript exception. 1.94 + */ 1.95 + add: function DL_add(aDownload) { 1.96 + this._downloads.push(aDownload); 1.97 + aDownload.onchange = this._change.bind(this, aDownload); 1.98 + this._notifyAllViews("onDownloadAdded", aDownload); 1.99 + 1.100 + return Promise.resolve(); 1.101 + }, 1.102 + 1.103 + /** 1.104 + * Removes a download from the list. If the download was already removed, 1.105 + * this method has no effect. 1.106 + * 1.107 + * This method does not change the state of the download, to allow adding it 1.108 + * to another list, or control it directly. If you want to dispose of the 1.109 + * download object, you should cancel it afterwards, and remove any partially 1.110 + * downloaded data if needed. 1.111 + * 1.112 + * @param aDownload 1.113 + * The Download object to remove. 1.114 + * 1.115 + * @return {Promise} 1.116 + * @resolves When the download has been removed. 1.117 + * @rejects JavaScript exception. 1.118 + */ 1.119 + remove: function DL_remove(aDownload) { 1.120 + let index = this._downloads.indexOf(aDownload); 1.121 + if (index != -1) { 1.122 + this._downloads.splice(index, 1); 1.123 + aDownload.onchange = null; 1.124 + this._notifyAllViews("onDownloadRemoved", aDownload); 1.125 + } 1.126 + 1.127 + return Promise.resolve(); 1.128 + }, 1.129 + 1.130 + /** 1.131 + * This function is called when "onchange" events of downloads occur. 1.132 + * 1.133 + * @param aDownload 1.134 + * The Download object that changed. 1.135 + */ 1.136 + _change: function DL_change(aDownload) { 1.137 + this._notifyAllViews("onDownloadChanged", aDownload); 1.138 + }, 1.139 + 1.140 + /** 1.141 + * Set of currently registered views. 1.142 + */ 1.143 + _views: null, 1.144 + 1.145 + /** 1.146 + * Adds a view that will be notified of changes to downloads. The newly added 1.147 + * view will receive onDownloadAdded notifications for all the downloads that 1.148 + * are already in the list. 1.149 + * 1.150 + * @param aView 1.151 + * The view object to add. The following methods may be defined: 1.152 + * { 1.153 + * onDownloadAdded: function (aDownload) { 1.154 + * // Called after aDownload is added to the end of the list. 1.155 + * }, 1.156 + * onDownloadChanged: function (aDownload) { 1.157 + * // Called after the properties of aDownload change. 1.158 + * }, 1.159 + * onDownloadRemoved: function (aDownload) { 1.160 + * // Called after aDownload is removed from the list. 1.161 + * }, 1.162 + * } 1.163 + * 1.164 + * @return {Promise} 1.165 + * @resolves When the view has been registered and all the onDownloadAdded 1.166 + * notifications for the existing downloads have been sent. 1.167 + * @rejects JavaScript exception. 1.168 + */ 1.169 + addView: function DL_addView(aView) 1.170 + { 1.171 + this._views.add(aView); 1.172 + 1.173 + if ("onDownloadAdded" in aView) { 1.174 + for (let download of this._downloads) { 1.175 + try { 1.176 + aView.onDownloadAdded(download); 1.177 + } catch (ex) { 1.178 + Cu.reportError(ex); 1.179 + } 1.180 + } 1.181 + } 1.182 + 1.183 + return Promise.resolve(); 1.184 + }, 1.185 + 1.186 + /** 1.187 + * Removes a view that was previously added using addView. 1.188 + * 1.189 + * @param aView 1.190 + * The view object to remove. 1.191 + * 1.192 + * @return {Promise} 1.193 + * @resolves When the view has been removed. At this point, the removed view 1.194 + * will not receive any more notifications. 1.195 + * @rejects JavaScript exception. 1.196 + */ 1.197 + removeView: function DL_removeView(aView) 1.198 + { 1.199 + this._views.delete(aView); 1.200 + 1.201 + return Promise.resolve(); 1.202 + }, 1.203 + 1.204 + /** 1.205 + * Notifies all the views of a download addition, change, or removal. 1.206 + * 1.207 + * @param aMethodName 1.208 + * String containing the name of the method to call on the view. 1.209 + * @param aDownload 1.210 + * The Download object that changed. 1.211 + */ 1.212 + _notifyAllViews: function (aMethodName, aDownload) { 1.213 + for (let view of this._views) { 1.214 + try { 1.215 + if (aMethodName in view) { 1.216 + view[aMethodName](aDownload); 1.217 + } 1.218 + } catch (ex) { 1.219 + Cu.reportError(ex); 1.220 + } 1.221 + } 1.222 + }, 1.223 + 1.224 + /** 1.225 + * Removes downloads from the list that have finished, have failed, or have 1.226 + * been canceled without keeping partial data. A filter function may be 1.227 + * specified to remove only a subset of those downloads. 1.228 + * 1.229 + * This method finalizes each removed download, ensuring that any partially 1.230 + * downloaded data associated with it is also removed. 1.231 + * 1.232 + * @param aFilterFn 1.233 + * The filter function is called with each download as its only 1.234 + * argument, and should return true to remove the download and false 1.235 + * to keep it. This parameter may be null or omitted to have no 1.236 + * additional filter. 1.237 + */ 1.238 + removeFinished: function DL_removeFinished(aFilterFn) { 1.239 + Task.spawn(function() { 1.240 + let list = yield this.getAll(); 1.241 + for (let download of list) { 1.242 + // Remove downloads that have been canceled, even if the cancellation 1.243 + // operation hasn't completed yet so we don't check "stopped" here. 1.244 + // Failed downloads with partial data are also removed. 1.245 + if (download.stopped && (!download.hasPartialData || download.error) && 1.246 + (!aFilterFn || aFilterFn(download))) { 1.247 + // Remove the download first, so that the views don't get the change 1.248 + // notifications that may occur during finalization. 1.249 + yield this.remove(download); 1.250 + // Ensure that the download is stopped and no partial data is kept. 1.251 + // This works even if the download state has changed meanwhile. We 1.252 + // don't need to wait for the procedure to be complete before 1.253 + // processing the other downloads in the list. 1.254 + download.finalize(true).then(null, Cu.reportError); 1.255 + } 1.256 + } 1.257 + }.bind(this)).then(null, Cu.reportError); 1.258 + }, 1.259 +}; 1.260 + 1.261 +//////////////////////////////////////////////////////////////////////////////// 1.262 +//// DownloadCombinedList 1.263 + 1.264 +/** 1.265 + * Provides a unified, unordered list combining public and private downloads. 1.266 + * 1.267 + * Download objects added to this list are also added to one of the two 1.268 + * underlying lists, based on their "source.isPrivate" property. Views on this 1.269 + * list will receive notifications for both public and private downloads. 1.270 + * 1.271 + * @param aPublicList 1.272 + * Underlying DownloadList containing public downloads. 1.273 + * @param aPrivateList 1.274 + * Underlying DownloadList containing private downloads. 1.275 + */ 1.276 +this.DownloadCombinedList = function (aPublicList, aPrivateList) 1.277 +{ 1.278 + DownloadList.call(this); 1.279 + this._publicList = aPublicList; 1.280 + this._privateList = aPrivateList; 1.281 + aPublicList.addView(this).then(null, Cu.reportError); 1.282 + aPrivateList.addView(this).then(null, Cu.reportError); 1.283 +} 1.284 + 1.285 +this.DownloadCombinedList.prototype = { 1.286 + __proto__: DownloadList.prototype, 1.287 + 1.288 + /** 1.289 + * Underlying DownloadList containing public downloads. 1.290 + */ 1.291 + _publicList: null, 1.292 + 1.293 + /** 1.294 + * Underlying DownloadList containing private downloads. 1.295 + */ 1.296 + _privateList: null, 1.297 + 1.298 + /** 1.299 + * Adds a new download to the end of the items list. 1.300 + * 1.301 + * @note When a download is added to the list, its "onchange" event is 1.302 + * registered by the list, thus it cannot be used to monitor the 1.303 + * download. To receive change notifications for downloads that are 1.304 + * added to the list, use the addView method to register for 1.305 + * onDownloadChanged notifications. 1.306 + * 1.307 + * @param aDownload 1.308 + * The Download object to add. 1.309 + * 1.310 + * @return {Promise} 1.311 + * @resolves When the download has been added. 1.312 + * @rejects JavaScript exception. 1.313 + */ 1.314 + add: function (aDownload) 1.315 + { 1.316 + if (aDownload.source.isPrivate) { 1.317 + return this._privateList.add(aDownload); 1.318 + } else { 1.319 + return this._publicList.add(aDownload); 1.320 + } 1.321 + }, 1.322 + 1.323 + /** 1.324 + * Removes a download from the list. If the download was already removed, 1.325 + * this method has no effect. 1.326 + * 1.327 + * This method does not change the state of the download, to allow adding it 1.328 + * to another list, or control it directly. If you want to dispose of the 1.329 + * download object, you should cancel it afterwards, and remove any partially 1.330 + * downloaded data if needed. 1.331 + * 1.332 + * @param aDownload 1.333 + * The Download object to remove. 1.334 + * 1.335 + * @return {Promise} 1.336 + * @resolves When the download has been removed. 1.337 + * @rejects JavaScript exception. 1.338 + */ 1.339 + remove: function (aDownload) 1.340 + { 1.341 + if (aDownload.source.isPrivate) { 1.342 + return this._privateList.remove(aDownload); 1.343 + } else { 1.344 + return this._publicList.remove(aDownload); 1.345 + } 1.346 + }, 1.347 + 1.348 + ////////////////////////////////////////////////////////////////////////////// 1.349 + //// DownloadList view 1.350 + 1.351 + onDownloadAdded: function (aDownload) 1.352 + { 1.353 + this._downloads.push(aDownload); 1.354 + this._notifyAllViews("onDownloadAdded", aDownload); 1.355 + }, 1.356 + 1.357 + onDownloadChanged: function (aDownload) 1.358 + { 1.359 + this._notifyAllViews("onDownloadChanged", aDownload); 1.360 + }, 1.361 + 1.362 + onDownloadRemoved: function (aDownload) 1.363 + { 1.364 + let index = this._downloads.indexOf(aDownload); 1.365 + if (index != -1) { 1.366 + this._downloads.splice(index, 1); 1.367 + } 1.368 + this._notifyAllViews("onDownloadRemoved", aDownload); 1.369 + }, 1.370 +}; 1.371 + 1.372 +//////////////////////////////////////////////////////////////////////////////// 1.373 +//// DownloadSummary 1.374 + 1.375 +/** 1.376 + * Provides an aggregated view on the contents of a DownloadList. 1.377 + */ 1.378 +this.DownloadSummary = function () 1.379 +{ 1.380 + this._downloads = []; 1.381 + this._views = new Set(); 1.382 +} 1.383 + 1.384 +this.DownloadSummary.prototype = { 1.385 + /** 1.386 + * Array of Download objects that are currently part of the summary. 1.387 + */ 1.388 + _downloads: null, 1.389 + 1.390 + /** 1.391 + * Underlying DownloadList whose contents should be summarized. 1.392 + */ 1.393 + _list: null, 1.394 + 1.395 + /** 1.396 + * This method may be called once to bind this object to a DownloadList. 1.397 + * 1.398 + * Views on the summarized data can be registered before this object is bound 1.399 + * to an actual list. This allows the summary to be used without requiring 1.400 + * the initialization of the DownloadList first. 1.401 + * 1.402 + * @param aList 1.403 + * Underlying DownloadList whose contents should be summarized. 1.404 + * 1.405 + * @return {Promise} 1.406 + * @resolves When the view on the underlying list has been registered. 1.407 + * @rejects JavaScript exception. 1.408 + */ 1.409 + bindToList: function (aList) 1.410 + { 1.411 + if (this._list) { 1.412 + throw new Error("bindToList may be called only once."); 1.413 + } 1.414 + 1.415 + return aList.addView(this).then(() => { 1.416 + // Set the list reference only after addView has returned, so that we don't 1.417 + // send a notification to our views for each download that is added. 1.418 + this._list = aList; 1.419 + this._onListChanged(); 1.420 + }); 1.421 + }, 1.422 + 1.423 + /** 1.424 + * Set of currently registered views. 1.425 + */ 1.426 + _views: null, 1.427 + 1.428 + /** 1.429 + * Adds a view that will be notified of changes to the summary. The newly 1.430 + * added view will receive an initial onSummaryChanged notification. 1.431 + * 1.432 + * @param aView 1.433 + * The view object to add. The following methods may be defined: 1.434 + * { 1.435 + * onSummaryChanged: function () { 1.436 + * // Called after any property of the summary has changed. 1.437 + * }, 1.438 + * } 1.439 + * 1.440 + * @return {Promise} 1.441 + * @resolves When the view has been registered and the onSummaryChanged 1.442 + * notification has been sent. 1.443 + * @rejects JavaScript exception. 1.444 + */ 1.445 + addView: function (aView) 1.446 + { 1.447 + this._views.add(aView); 1.448 + 1.449 + if ("onSummaryChanged" in aView) { 1.450 + try { 1.451 + aView.onSummaryChanged(); 1.452 + } catch (ex) { 1.453 + Cu.reportError(ex); 1.454 + } 1.455 + } 1.456 + 1.457 + return Promise.resolve(); 1.458 + }, 1.459 + 1.460 + /** 1.461 + * Removes a view that was previously added using addView. 1.462 + * 1.463 + * @param aView 1.464 + * The view object to remove. 1.465 + * 1.466 + * @return {Promise} 1.467 + * @resolves When the view has been removed. At this point, the removed view 1.468 + * will not receive any more notifications. 1.469 + * @rejects JavaScript exception. 1.470 + */ 1.471 + removeView: function (aView) 1.472 + { 1.473 + this._views.delete(aView); 1.474 + 1.475 + return Promise.resolve(); 1.476 + }, 1.477 + 1.478 + /** 1.479 + * Indicates whether all the downloads are currently stopped. 1.480 + */ 1.481 + allHaveStopped: true, 1.482 + 1.483 + /** 1.484 + * Indicates the total number of bytes to be transferred before completing all 1.485 + * the downloads that are currently in progress. 1.486 + * 1.487 + * For downloads that do not have a known final size, the number of bytes 1.488 + * currently transferred is reported as part of this property. 1.489 + * 1.490 + * This is zero if no downloads are currently in progress. 1.491 + */ 1.492 + progressTotalBytes: 0, 1.493 + 1.494 + /** 1.495 + * Number of bytes currently transferred as part of all the downloads that are 1.496 + * currently in progress. 1.497 + * 1.498 + * This is zero if no downloads are currently in progress. 1.499 + */ 1.500 + progressCurrentBytes: 0, 1.501 + 1.502 + /** 1.503 + * This function is called when any change in the list of downloads occurs, 1.504 + * and will recalculate the summary and notify the views in case the 1.505 + * aggregated properties are different. 1.506 + */ 1.507 + _onListChanged: function () { 1.508 + let allHaveStopped = true; 1.509 + let progressTotalBytes = 0; 1.510 + let progressCurrentBytes = 0; 1.511 + 1.512 + // Recalculate the aggregated state. See the description of the individual 1.513 + // properties for an explanation of the summarization logic. 1.514 + for (let download of this._downloads) { 1.515 + if (!download.stopped) { 1.516 + allHaveStopped = false; 1.517 + progressTotalBytes += download.hasProgress ? download.totalBytes 1.518 + : download.currentBytes; 1.519 + progressCurrentBytes += download.currentBytes; 1.520 + } 1.521 + } 1.522 + 1.523 + // Exit now if the properties did not change. 1.524 + if (this.allHaveStopped == allHaveStopped && 1.525 + this.progressTotalBytes == progressTotalBytes && 1.526 + this.progressCurrentBytes == progressCurrentBytes) { 1.527 + return; 1.528 + } 1.529 + 1.530 + this.allHaveStopped = allHaveStopped; 1.531 + this.progressTotalBytes = progressTotalBytes; 1.532 + this.progressCurrentBytes = progressCurrentBytes; 1.533 + 1.534 + // Notify all the views that our properties changed. 1.535 + for (let view of this._views) { 1.536 + try { 1.537 + if ("onSummaryChanged" in view) { 1.538 + view.onSummaryChanged(); 1.539 + } 1.540 + } catch (ex) { 1.541 + Cu.reportError(ex); 1.542 + } 1.543 + } 1.544 + }, 1.545 + 1.546 + ////////////////////////////////////////////////////////////////////////////// 1.547 + //// DownloadList view 1.548 + 1.549 + onDownloadAdded: function (aDownload) 1.550 + { 1.551 + this._downloads.push(aDownload); 1.552 + if (this._list) { 1.553 + this._onListChanged(); 1.554 + } 1.555 + }, 1.556 + 1.557 + onDownloadChanged: function (aDownload) 1.558 + { 1.559 + this._onListChanged(); 1.560 + }, 1.561 + 1.562 + onDownloadRemoved: function (aDownload) 1.563 + { 1.564 + let index = this._downloads.indexOf(aDownload); 1.565 + if (index != -1) { 1.566 + this._downloads.splice(index, 1); 1.567 + } 1.568 + this._onListChanged(); 1.569 + }, 1.570 +};