toolkit/components/jsdownloads/src/DownloadList.jsm

Thu, 22 Jan 2015 13:21:57 +0100

author
Michael Schloh von Bennewitz <michael@schloh.com>
date
Thu, 22 Jan 2015 13:21:57 +0100
branch
TOR_BUG_9701
changeset 15
b8a032363ba2
permissions
-rw-r--r--

Incorporate requested changes from Mozilla in review:
https://bugzilla.mozilla.org/show_bug.cgi?id=1123480#c6

michael@0 1 /* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
michael@0 2 /* vim: set ts=2 et sw=2 tw=80 filetype=javascript: */
michael@0 3 /* This Source Code Form is subject to the terms of the Mozilla Public
michael@0 4 * License, v. 2.0. If a copy of the MPL was not distributed with this
michael@0 5 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
michael@0 6
michael@0 7 /**
michael@0 8 * This file includes the following constructors and global objects:
michael@0 9 *
michael@0 10 * DownloadList
michael@0 11 * Represents a collection of Download objects that can be viewed and managed by
michael@0 12 * the user interface, and persisted across sessions.
michael@0 13 *
michael@0 14 * DownloadCombinedList
michael@0 15 * Provides a unified, unordered list combining public and private downloads.
michael@0 16 *
michael@0 17 * DownloadSummary
michael@0 18 * Provides an aggregated view on the contents of a DownloadList.
michael@0 19 */
michael@0 20
michael@0 21 "use strict";
michael@0 22
michael@0 23 this.EXPORTED_SYMBOLS = [
michael@0 24 "DownloadList",
michael@0 25 "DownloadCombinedList",
michael@0 26 "DownloadSummary",
michael@0 27 ];
michael@0 28
michael@0 29 ////////////////////////////////////////////////////////////////////////////////
michael@0 30 //// Globals
michael@0 31
michael@0 32 const Cc = Components.classes;
michael@0 33 const Ci = Components.interfaces;
michael@0 34 const Cu = Components.utils;
michael@0 35 const Cr = Components.results;
michael@0 36
michael@0 37 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
michael@0 38
michael@0 39 XPCOMUtils.defineLazyModuleGetter(this, "Promise",
michael@0 40 "resource://gre/modules/Promise.jsm");
michael@0 41 XPCOMUtils.defineLazyModuleGetter(this, "Task",
michael@0 42 "resource://gre/modules/Task.jsm");
michael@0 43
michael@0 44 ////////////////////////////////////////////////////////////////////////////////
michael@0 45 //// DownloadList
michael@0 46
michael@0 47 /**
michael@0 48 * Represents a collection of Download objects that can be viewed and managed by
michael@0 49 * the user interface, and persisted across sessions.
michael@0 50 */
michael@0 51 this.DownloadList = function ()
michael@0 52 {
michael@0 53 this._downloads = [];
michael@0 54 this._views = new Set();
michael@0 55 }
michael@0 56
michael@0 57 this.DownloadList.prototype = {
michael@0 58 /**
michael@0 59 * Array of Download objects currently in the list.
michael@0 60 */
michael@0 61 _downloads: null,
michael@0 62
michael@0 63 /**
michael@0 64 * Retrieves a snapshot of the downloads that are currently in the list. The
michael@0 65 * returned array does not change when downloads are added or removed, though
michael@0 66 * the Download objects it contains are still updated in real time.
michael@0 67 *
michael@0 68 * @return {Promise}
michael@0 69 * @resolves An array of Download objects.
michael@0 70 * @rejects JavaScript exception.
michael@0 71 */
michael@0 72 getAll: function DL_getAll() {
michael@0 73 return Promise.resolve(Array.slice(this._downloads, 0));
michael@0 74 },
michael@0 75
michael@0 76 /**
michael@0 77 * Adds a new download to the end of the items list.
michael@0 78 *
michael@0 79 * @note When a download is added to the list, its "onchange" event is
michael@0 80 * registered by the list, thus it cannot be used to monitor the
michael@0 81 * download. To receive change notifications for downloads that are
michael@0 82 * added to the list, use the addView method to register for
michael@0 83 * onDownloadChanged notifications.
michael@0 84 *
michael@0 85 * @param aDownload
michael@0 86 * The Download object to add.
michael@0 87 *
michael@0 88 * @return {Promise}
michael@0 89 * @resolves When the download has been added.
michael@0 90 * @rejects JavaScript exception.
michael@0 91 */
michael@0 92 add: function DL_add(aDownload) {
michael@0 93 this._downloads.push(aDownload);
michael@0 94 aDownload.onchange = this._change.bind(this, aDownload);
michael@0 95 this._notifyAllViews("onDownloadAdded", aDownload);
michael@0 96
michael@0 97 return Promise.resolve();
michael@0 98 },
michael@0 99
michael@0 100 /**
michael@0 101 * Removes a download from the list. If the download was already removed,
michael@0 102 * this method has no effect.
michael@0 103 *
michael@0 104 * This method does not change the state of the download, to allow adding it
michael@0 105 * to another list, or control it directly. If you want to dispose of the
michael@0 106 * download object, you should cancel it afterwards, and remove any partially
michael@0 107 * downloaded data if needed.
michael@0 108 *
michael@0 109 * @param aDownload
michael@0 110 * The Download object to remove.
michael@0 111 *
michael@0 112 * @return {Promise}
michael@0 113 * @resolves When the download has been removed.
michael@0 114 * @rejects JavaScript exception.
michael@0 115 */
michael@0 116 remove: function DL_remove(aDownload) {
michael@0 117 let index = this._downloads.indexOf(aDownload);
michael@0 118 if (index != -1) {
michael@0 119 this._downloads.splice(index, 1);
michael@0 120 aDownload.onchange = null;
michael@0 121 this._notifyAllViews("onDownloadRemoved", aDownload);
michael@0 122 }
michael@0 123
michael@0 124 return Promise.resolve();
michael@0 125 },
michael@0 126
michael@0 127 /**
michael@0 128 * This function is called when "onchange" events of downloads occur.
michael@0 129 *
michael@0 130 * @param aDownload
michael@0 131 * The Download object that changed.
michael@0 132 */
michael@0 133 _change: function DL_change(aDownload) {
michael@0 134 this._notifyAllViews("onDownloadChanged", aDownload);
michael@0 135 },
michael@0 136
michael@0 137 /**
michael@0 138 * Set of currently registered views.
michael@0 139 */
michael@0 140 _views: null,
michael@0 141
michael@0 142 /**
michael@0 143 * Adds a view that will be notified of changes to downloads. The newly added
michael@0 144 * view will receive onDownloadAdded notifications for all the downloads that
michael@0 145 * are already in the list.
michael@0 146 *
michael@0 147 * @param aView
michael@0 148 * The view object to add. The following methods may be defined:
michael@0 149 * {
michael@0 150 * onDownloadAdded: function (aDownload) {
michael@0 151 * // Called after aDownload is added to the end of the list.
michael@0 152 * },
michael@0 153 * onDownloadChanged: function (aDownload) {
michael@0 154 * // Called after the properties of aDownload change.
michael@0 155 * },
michael@0 156 * onDownloadRemoved: function (aDownload) {
michael@0 157 * // Called after aDownload is removed from the list.
michael@0 158 * },
michael@0 159 * }
michael@0 160 *
michael@0 161 * @return {Promise}
michael@0 162 * @resolves When the view has been registered and all the onDownloadAdded
michael@0 163 * notifications for the existing downloads have been sent.
michael@0 164 * @rejects JavaScript exception.
michael@0 165 */
michael@0 166 addView: function DL_addView(aView)
michael@0 167 {
michael@0 168 this._views.add(aView);
michael@0 169
michael@0 170 if ("onDownloadAdded" in aView) {
michael@0 171 for (let download of this._downloads) {
michael@0 172 try {
michael@0 173 aView.onDownloadAdded(download);
michael@0 174 } catch (ex) {
michael@0 175 Cu.reportError(ex);
michael@0 176 }
michael@0 177 }
michael@0 178 }
michael@0 179
michael@0 180 return Promise.resolve();
michael@0 181 },
michael@0 182
michael@0 183 /**
michael@0 184 * Removes a view that was previously added using addView.
michael@0 185 *
michael@0 186 * @param aView
michael@0 187 * The view object to remove.
michael@0 188 *
michael@0 189 * @return {Promise}
michael@0 190 * @resolves When the view has been removed. At this point, the removed view
michael@0 191 * will not receive any more notifications.
michael@0 192 * @rejects JavaScript exception.
michael@0 193 */
michael@0 194 removeView: function DL_removeView(aView)
michael@0 195 {
michael@0 196 this._views.delete(aView);
michael@0 197
michael@0 198 return Promise.resolve();
michael@0 199 },
michael@0 200
michael@0 201 /**
michael@0 202 * Notifies all the views of a download addition, change, or removal.
michael@0 203 *
michael@0 204 * @param aMethodName
michael@0 205 * String containing the name of the method to call on the view.
michael@0 206 * @param aDownload
michael@0 207 * The Download object that changed.
michael@0 208 */
michael@0 209 _notifyAllViews: function (aMethodName, aDownload) {
michael@0 210 for (let view of this._views) {
michael@0 211 try {
michael@0 212 if (aMethodName in view) {
michael@0 213 view[aMethodName](aDownload);
michael@0 214 }
michael@0 215 } catch (ex) {
michael@0 216 Cu.reportError(ex);
michael@0 217 }
michael@0 218 }
michael@0 219 },
michael@0 220
michael@0 221 /**
michael@0 222 * Removes downloads from the list that have finished, have failed, or have
michael@0 223 * been canceled without keeping partial data. A filter function may be
michael@0 224 * specified to remove only a subset of those downloads.
michael@0 225 *
michael@0 226 * This method finalizes each removed download, ensuring that any partially
michael@0 227 * downloaded data associated with it is also removed.
michael@0 228 *
michael@0 229 * @param aFilterFn
michael@0 230 * The filter function is called with each download as its only
michael@0 231 * argument, and should return true to remove the download and false
michael@0 232 * to keep it. This parameter may be null or omitted to have no
michael@0 233 * additional filter.
michael@0 234 */
michael@0 235 removeFinished: function DL_removeFinished(aFilterFn) {
michael@0 236 Task.spawn(function() {
michael@0 237 let list = yield this.getAll();
michael@0 238 for (let download of list) {
michael@0 239 // Remove downloads that have been canceled, even if the cancellation
michael@0 240 // operation hasn't completed yet so we don't check "stopped" here.
michael@0 241 // Failed downloads with partial data are also removed.
michael@0 242 if (download.stopped && (!download.hasPartialData || download.error) &&
michael@0 243 (!aFilterFn || aFilterFn(download))) {
michael@0 244 // Remove the download first, so that the views don't get the change
michael@0 245 // notifications that may occur during finalization.
michael@0 246 yield this.remove(download);
michael@0 247 // Ensure that the download is stopped and no partial data is kept.
michael@0 248 // This works even if the download state has changed meanwhile. We
michael@0 249 // don't need to wait for the procedure to be complete before
michael@0 250 // processing the other downloads in the list.
michael@0 251 download.finalize(true).then(null, Cu.reportError);
michael@0 252 }
michael@0 253 }
michael@0 254 }.bind(this)).then(null, Cu.reportError);
michael@0 255 },
michael@0 256 };
michael@0 257
michael@0 258 ////////////////////////////////////////////////////////////////////////////////
michael@0 259 //// DownloadCombinedList
michael@0 260
michael@0 261 /**
michael@0 262 * Provides a unified, unordered list combining public and private downloads.
michael@0 263 *
michael@0 264 * Download objects added to this list are also added to one of the two
michael@0 265 * underlying lists, based on their "source.isPrivate" property. Views on this
michael@0 266 * list will receive notifications for both public and private downloads.
michael@0 267 *
michael@0 268 * @param aPublicList
michael@0 269 * Underlying DownloadList containing public downloads.
michael@0 270 * @param aPrivateList
michael@0 271 * Underlying DownloadList containing private downloads.
michael@0 272 */
michael@0 273 this.DownloadCombinedList = function (aPublicList, aPrivateList)
michael@0 274 {
michael@0 275 DownloadList.call(this);
michael@0 276 this._publicList = aPublicList;
michael@0 277 this._privateList = aPrivateList;
michael@0 278 aPublicList.addView(this).then(null, Cu.reportError);
michael@0 279 aPrivateList.addView(this).then(null, Cu.reportError);
michael@0 280 }
michael@0 281
michael@0 282 this.DownloadCombinedList.prototype = {
michael@0 283 __proto__: DownloadList.prototype,
michael@0 284
michael@0 285 /**
michael@0 286 * Underlying DownloadList containing public downloads.
michael@0 287 */
michael@0 288 _publicList: null,
michael@0 289
michael@0 290 /**
michael@0 291 * Underlying DownloadList containing private downloads.
michael@0 292 */
michael@0 293 _privateList: null,
michael@0 294
michael@0 295 /**
michael@0 296 * Adds a new download to the end of the items list.
michael@0 297 *
michael@0 298 * @note When a download is added to the list, its "onchange" event is
michael@0 299 * registered by the list, thus it cannot be used to monitor the
michael@0 300 * download. To receive change notifications for downloads that are
michael@0 301 * added to the list, use the addView method to register for
michael@0 302 * onDownloadChanged notifications.
michael@0 303 *
michael@0 304 * @param aDownload
michael@0 305 * The Download object to add.
michael@0 306 *
michael@0 307 * @return {Promise}
michael@0 308 * @resolves When the download has been added.
michael@0 309 * @rejects JavaScript exception.
michael@0 310 */
michael@0 311 add: function (aDownload)
michael@0 312 {
michael@0 313 if (aDownload.source.isPrivate) {
michael@0 314 return this._privateList.add(aDownload);
michael@0 315 } else {
michael@0 316 return this._publicList.add(aDownload);
michael@0 317 }
michael@0 318 },
michael@0 319
michael@0 320 /**
michael@0 321 * Removes a download from the list. If the download was already removed,
michael@0 322 * this method has no effect.
michael@0 323 *
michael@0 324 * This method does not change the state of the download, to allow adding it
michael@0 325 * to another list, or control it directly. If you want to dispose of the
michael@0 326 * download object, you should cancel it afterwards, and remove any partially
michael@0 327 * downloaded data if needed.
michael@0 328 *
michael@0 329 * @param aDownload
michael@0 330 * The Download object to remove.
michael@0 331 *
michael@0 332 * @return {Promise}
michael@0 333 * @resolves When the download has been removed.
michael@0 334 * @rejects JavaScript exception.
michael@0 335 */
michael@0 336 remove: function (aDownload)
michael@0 337 {
michael@0 338 if (aDownload.source.isPrivate) {
michael@0 339 return this._privateList.remove(aDownload);
michael@0 340 } else {
michael@0 341 return this._publicList.remove(aDownload);
michael@0 342 }
michael@0 343 },
michael@0 344
michael@0 345 //////////////////////////////////////////////////////////////////////////////
michael@0 346 //// DownloadList view
michael@0 347
michael@0 348 onDownloadAdded: function (aDownload)
michael@0 349 {
michael@0 350 this._downloads.push(aDownload);
michael@0 351 this._notifyAllViews("onDownloadAdded", aDownload);
michael@0 352 },
michael@0 353
michael@0 354 onDownloadChanged: function (aDownload)
michael@0 355 {
michael@0 356 this._notifyAllViews("onDownloadChanged", aDownload);
michael@0 357 },
michael@0 358
michael@0 359 onDownloadRemoved: function (aDownload)
michael@0 360 {
michael@0 361 let index = this._downloads.indexOf(aDownload);
michael@0 362 if (index != -1) {
michael@0 363 this._downloads.splice(index, 1);
michael@0 364 }
michael@0 365 this._notifyAllViews("onDownloadRemoved", aDownload);
michael@0 366 },
michael@0 367 };
michael@0 368
michael@0 369 ////////////////////////////////////////////////////////////////////////////////
michael@0 370 //// DownloadSummary
michael@0 371
michael@0 372 /**
michael@0 373 * Provides an aggregated view on the contents of a DownloadList.
michael@0 374 */
michael@0 375 this.DownloadSummary = function ()
michael@0 376 {
michael@0 377 this._downloads = [];
michael@0 378 this._views = new Set();
michael@0 379 }
michael@0 380
michael@0 381 this.DownloadSummary.prototype = {
michael@0 382 /**
michael@0 383 * Array of Download objects that are currently part of the summary.
michael@0 384 */
michael@0 385 _downloads: null,
michael@0 386
michael@0 387 /**
michael@0 388 * Underlying DownloadList whose contents should be summarized.
michael@0 389 */
michael@0 390 _list: null,
michael@0 391
michael@0 392 /**
michael@0 393 * This method may be called once to bind this object to a DownloadList.
michael@0 394 *
michael@0 395 * Views on the summarized data can be registered before this object is bound
michael@0 396 * to an actual list. This allows the summary to be used without requiring
michael@0 397 * the initialization of the DownloadList first.
michael@0 398 *
michael@0 399 * @param aList
michael@0 400 * Underlying DownloadList whose contents should be summarized.
michael@0 401 *
michael@0 402 * @return {Promise}
michael@0 403 * @resolves When the view on the underlying list has been registered.
michael@0 404 * @rejects JavaScript exception.
michael@0 405 */
michael@0 406 bindToList: function (aList)
michael@0 407 {
michael@0 408 if (this._list) {
michael@0 409 throw new Error("bindToList may be called only once.");
michael@0 410 }
michael@0 411
michael@0 412 return aList.addView(this).then(() => {
michael@0 413 // Set the list reference only after addView has returned, so that we don't
michael@0 414 // send a notification to our views for each download that is added.
michael@0 415 this._list = aList;
michael@0 416 this._onListChanged();
michael@0 417 });
michael@0 418 },
michael@0 419
michael@0 420 /**
michael@0 421 * Set of currently registered views.
michael@0 422 */
michael@0 423 _views: null,
michael@0 424
michael@0 425 /**
michael@0 426 * Adds a view that will be notified of changes to the summary. The newly
michael@0 427 * added view will receive an initial onSummaryChanged notification.
michael@0 428 *
michael@0 429 * @param aView
michael@0 430 * The view object to add. The following methods may be defined:
michael@0 431 * {
michael@0 432 * onSummaryChanged: function () {
michael@0 433 * // Called after any property of the summary has changed.
michael@0 434 * },
michael@0 435 * }
michael@0 436 *
michael@0 437 * @return {Promise}
michael@0 438 * @resolves When the view has been registered and the onSummaryChanged
michael@0 439 * notification has been sent.
michael@0 440 * @rejects JavaScript exception.
michael@0 441 */
michael@0 442 addView: function (aView)
michael@0 443 {
michael@0 444 this._views.add(aView);
michael@0 445
michael@0 446 if ("onSummaryChanged" in aView) {
michael@0 447 try {
michael@0 448 aView.onSummaryChanged();
michael@0 449 } catch (ex) {
michael@0 450 Cu.reportError(ex);
michael@0 451 }
michael@0 452 }
michael@0 453
michael@0 454 return Promise.resolve();
michael@0 455 },
michael@0 456
michael@0 457 /**
michael@0 458 * Removes a view that was previously added using addView.
michael@0 459 *
michael@0 460 * @param aView
michael@0 461 * The view object to remove.
michael@0 462 *
michael@0 463 * @return {Promise}
michael@0 464 * @resolves When the view has been removed. At this point, the removed view
michael@0 465 * will not receive any more notifications.
michael@0 466 * @rejects JavaScript exception.
michael@0 467 */
michael@0 468 removeView: function (aView)
michael@0 469 {
michael@0 470 this._views.delete(aView);
michael@0 471
michael@0 472 return Promise.resolve();
michael@0 473 },
michael@0 474
michael@0 475 /**
michael@0 476 * Indicates whether all the downloads are currently stopped.
michael@0 477 */
michael@0 478 allHaveStopped: true,
michael@0 479
michael@0 480 /**
michael@0 481 * Indicates the total number of bytes to be transferred before completing all
michael@0 482 * the downloads that are currently in progress.
michael@0 483 *
michael@0 484 * For downloads that do not have a known final size, the number of bytes
michael@0 485 * currently transferred is reported as part of this property.
michael@0 486 *
michael@0 487 * This is zero if no downloads are currently in progress.
michael@0 488 */
michael@0 489 progressTotalBytes: 0,
michael@0 490
michael@0 491 /**
michael@0 492 * Number of bytes currently transferred as part of all the downloads that are
michael@0 493 * currently in progress.
michael@0 494 *
michael@0 495 * This is zero if no downloads are currently in progress.
michael@0 496 */
michael@0 497 progressCurrentBytes: 0,
michael@0 498
michael@0 499 /**
michael@0 500 * This function is called when any change in the list of downloads occurs,
michael@0 501 * and will recalculate the summary and notify the views in case the
michael@0 502 * aggregated properties are different.
michael@0 503 */
michael@0 504 _onListChanged: function () {
michael@0 505 let allHaveStopped = true;
michael@0 506 let progressTotalBytes = 0;
michael@0 507 let progressCurrentBytes = 0;
michael@0 508
michael@0 509 // Recalculate the aggregated state. See the description of the individual
michael@0 510 // properties for an explanation of the summarization logic.
michael@0 511 for (let download of this._downloads) {
michael@0 512 if (!download.stopped) {
michael@0 513 allHaveStopped = false;
michael@0 514 progressTotalBytes += download.hasProgress ? download.totalBytes
michael@0 515 : download.currentBytes;
michael@0 516 progressCurrentBytes += download.currentBytes;
michael@0 517 }
michael@0 518 }
michael@0 519
michael@0 520 // Exit now if the properties did not change.
michael@0 521 if (this.allHaveStopped == allHaveStopped &&
michael@0 522 this.progressTotalBytes == progressTotalBytes &&
michael@0 523 this.progressCurrentBytes == progressCurrentBytes) {
michael@0 524 return;
michael@0 525 }
michael@0 526
michael@0 527 this.allHaveStopped = allHaveStopped;
michael@0 528 this.progressTotalBytes = progressTotalBytes;
michael@0 529 this.progressCurrentBytes = progressCurrentBytes;
michael@0 530
michael@0 531 // Notify all the views that our properties changed.
michael@0 532 for (let view of this._views) {
michael@0 533 try {
michael@0 534 if ("onSummaryChanged" in view) {
michael@0 535 view.onSummaryChanged();
michael@0 536 }
michael@0 537 } catch (ex) {
michael@0 538 Cu.reportError(ex);
michael@0 539 }
michael@0 540 }
michael@0 541 },
michael@0 542
michael@0 543 //////////////////////////////////////////////////////////////////////////////
michael@0 544 //// DownloadList view
michael@0 545
michael@0 546 onDownloadAdded: function (aDownload)
michael@0 547 {
michael@0 548 this._downloads.push(aDownload);
michael@0 549 if (this._list) {
michael@0 550 this._onListChanged();
michael@0 551 }
michael@0 552 },
michael@0 553
michael@0 554 onDownloadChanged: function (aDownload)
michael@0 555 {
michael@0 556 this._onListChanged();
michael@0 557 },
michael@0 558
michael@0 559 onDownloadRemoved: function (aDownload)
michael@0 560 {
michael@0 561 let index = this._downloads.indexOf(aDownload);
michael@0 562 if (index != -1) {
michael@0 563 this._downloads.splice(index, 1);
michael@0 564 }
michael@0 565 this._onListChanged();
michael@0 566 },
michael@0 567 };

mercurial