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

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

mercurial