browser/components/downloads/src/DownloadsCommon.jsm

Wed, 31 Dec 2014 06:09:35 +0100

author
Michael Schloh von Bennewitz <michael@schloh.com>
date
Wed, 31 Dec 2014 06:09:35 +0100
changeset 0
6474c204b198
permissions
-rw-r--r--

Cloned upstream origin tor-browser at tor-browser-31.3.0esr-4.5-1-build1
revision ID fc1c9ff7c1b2defdbc039f12214767608f46423f for hacking purpose.

     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 file,
     5  * You can obtain one at http://mozilla.org/MPL/2.0/. */
     7 "use strict";
     9 this.EXPORTED_SYMBOLS = [
    10   "DownloadsCommon",
    11 ];
    13 /**
    14  * Handles the Downloads panel shared methods and data access.
    15  *
    16  * This file includes the following constructors and global objects:
    17  *
    18  * DownloadsCommon
    19  * This object is exposed directly to the consumers of this JavaScript module,
    20  * and provides shared methods for all the instances of the user interface.
    21  *
    22  * DownloadsData
    23  * Retrieves the list of past and completed downloads from the underlying
    24  * Download Manager data, and provides asynchronous notifications allowing
    25  * to build a consistent view of the available data.
    26  *
    27  * DownloadsDataItem
    28  * Represents a single item in the list of downloads.  This object either wraps
    29  * an existing nsIDownload from the Download Manager, or provides the same
    30  * information read directly from the downloads database, with the possibility
    31  * of querying the nsIDownload lazily, for performance reasons.
    32  *
    33  * DownloadsIndicatorData
    34  * This object registers itself with DownloadsData as a view, and transforms the
    35  * notifications it receives into overall status data, that is then broadcast to
    36  * the registered download status indicators.
    37  */
    39 ////////////////////////////////////////////////////////////////////////////////
    40 //// Globals
    42 const Cc = Components.classes;
    43 const Ci = Components.interfaces;
    44 const Cu = Components.utils;
    45 const Cr = Components.results;
    47 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
    48 Cu.import("resource://gre/modules/Services.jsm");
    50 XPCOMUtils.defineLazyModuleGetter(this, "NetUtil",
    51                                   "resource://gre/modules/NetUtil.jsm");
    52 XPCOMUtils.defineLazyModuleGetter(this, "PluralForm",
    53                                   "resource://gre/modules/PluralForm.jsm");
    54 XPCOMUtils.defineLazyModuleGetter(this, "Downloads",
    55                                   "resource://gre/modules/Downloads.jsm");
    56 XPCOMUtils.defineLazyModuleGetter(this, "DownloadUIHelper",
    57                                   "resource://gre/modules/DownloadUIHelper.jsm");
    58 XPCOMUtils.defineLazyModuleGetter(this, "DownloadUtils",
    59                                   "resource://gre/modules/DownloadUtils.jsm");
    60 XPCOMUtils.defineLazyModuleGetter(this, "OS",
    61                                   "resource://gre/modules/osfile.jsm")
    62 XPCOMUtils.defineLazyModuleGetter(this, "PlacesUtils",
    63                                   "resource://gre/modules/PlacesUtils.jsm");
    64 XPCOMUtils.defineLazyModuleGetter(this, "PrivateBrowsingUtils",
    65                                   "resource://gre/modules/PrivateBrowsingUtils.jsm");
    66 XPCOMUtils.defineLazyModuleGetter(this, "RecentWindow",
    67                                   "resource:///modules/RecentWindow.jsm");
    68 XPCOMUtils.defineLazyModuleGetter(this, "Promise",
    69                                   "resource://gre/modules/Promise.jsm");
    70 XPCOMUtils.defineLazyModuleGetter(this, "DownloadsLogger",
    71                                   "resource:///modules/DownloadsLogger.jsm");
    73 const nsIDM = Ci.nsIDownloadManager;
    75 const kDownloadsStringBundleUrl =
    76   "chrome://browser/locale/downloads/downloads.properties";
    78 const kDownloadsStringsRequiringFormatting = {
    79   sizeWithUnits: true,
    80   shortTimeLeftSeconds: true,
    81   shortTimeLeftMinutes: true,
    82   shortTimeLeftHours: true,
    83   shortTimeLeftDays: true,
    84   statusSeparator: true,
    85   statusSeparatorBeforeNumber: true,
    86   fileExecutableSecurityWarning: true
    87 };
    89 const kDownloadsStringsRequiringPluralForm = {
    90   otherDownloads2: true
    91 };
    93 XPCOMUtils.defineLazyGetter(this, "DownloadsLocalFileCtor", function () {
    94   return Components.Constructor("@mozilla.org/file/local;1",
    95                                 "nsILocalFile", "initWithPath");
    96 });
    98 const kPartialDownloadSuffix = ".part";
   100 const kPrefBranch = Services.prefs.getBranch("browser.download.");
   102 let PrefObserver = {
   103   QueryInterface: XPCOMUtils.generateQI([Ci.nsIObserver,
   104                                          Ci.nsISupportsWeakReference]),
   105   getPref: function PO_getPref(name) {
   106     try {
   107       switch (typeof this.prefs[name]) {
   108         case "boolean":
   109           return kPrefBranch.getBoolPref(name);
   110       }
   111     } catch (ex) { }
   112     return this.prefs[name];
   113   },
   114   observe: function PO_observe(aSubject, aTopic, aData) {
   115     if (this.prefs.hasOwnProperty(aData)) {
   116       return this[aData] = this.getPref(aData);
   117     }
   118   },
   119   register: function PO_register(prefs) {
   120     this.prefs = prefs;
   121     kPrefBranch.addObserver("", this, true);
   122     for (let key in prefs) {
   123       let name = key;
   124       XPCOMUtils.defineLazyGetter(this, name, function () {
   125         return PrefObserver.getPref(name);
   126       });
   127     }
   128   },
   129 };
   131 PrefObserver.register({
   132   // prefName: defaultValue
   133   debug: false,
   134   animateNotifications: true
   135 });
   138 ////////////////////////////////////////////////////////////////////////////////
   139 //// DownloadsCommon
   141 /**
   142  * This object is exposed directly to the consumers of this JavaScript module,
   143  * and provides shared methods for all the instances of the user interface.
   144  */
   145 this.DownloadsCommon = {
   146   log: function DC_log(...aMessageArgs) {
   147     delete this.log;
   148     this.log = function DC_log(...aMessageArgs) {
   149       if (!PrefObserver.debug) {
   150         return;
   151       }
   152       DownloadsLogger.log.apply(DownloadsLogger, aMessageArgs);
   153     }
   154     this.log.apply(this, aMessageArgs);
   155   },
   157   error: function DC_error(...aMessageArgs) {
   158     delete this.error;
   159     this.error = function DC_error(...aMessageArgs) {
   160       if (!PrefObserver.debug) {
   161         return;
   162       }
   163       DownloadsLogger.reportError.apply(DownloadsLogger, aMessageArgs);
   164     }
   165     this.error.apply(this, aMessageArgs);
   166   },
   167   /**
   168    * Returns an object whose keys are the string names from the downloads string
   169    * bundle, and whose values are either the translated strings or functions
   170    * returning formatted strings.
   171    */
   172   get strings()
   173   {
   174     let strings = {};
   175     let sb = Services.strings.createBundle(kDownloadsStringBundleUrl);
   176     let enumerator = sb.getSimpleEnumeration();
   177     while (enumerator.hasMoreElements()) {
   178       let string = enumerator.getNext().QueryInterface(Ci.nsIPropertyElement);
   179       let stringName = string.key;
   180       if (stringName in kDownloadsStringsRequiringFormatting) {
   181         strings[stringName] = function () {
   182           // Convert "arguments" to a real array before calling into XPCOM.
   183           return sb.formatStringFromName(stringName,
   184                                          Array.slice(arguments, 0),
   185                                          arguments.length);
   186         };
   187       } else if (stringName in kDownloadsStringsRequiringPluralForm) {
   188         strings[stringName] = function (aCount) {
   189           // Convert "arguments" to a real array before calling into XPCOM.
   190           let formattedString = sb.formatStringFromName(stringName,
   191                                          Array.slice(arguments, 0),
   192                                          arguments.length);
   193           return PluralForm.get(aCount, formattedString);
   194         };
   195       } else {
   196         strings[stringName] = string.value;
   197       }
   198     }
   199     delete this.strings;
   200     return this.strings = strings;
   201   },
   203   /**
   204    * Generates a very short string representing the given time left.
   205    *
   206    * @param aSeconds
   207    *        Value to be formatted.  It represents the number of seconds, it must
   208    *        be positive but does not need to be an integer.
   209    *
   210    * @return Formatted string, for example "30s" or "2h".  The returned value is
   211    *         maximum three characters long, at least in English.
   212    */
   213   formatTimeLeft: function DC_formatTimeLeft(aSeconds)
   214   {
   215     // Decide what text to show for the time
   216     let seconds = Math.round(aSeconds);
   217     if (!seconds) {
   218       return "";
   219     } else if (seconds <= 30) {
   220       return DownloadsCommon.strings["shortTimeLeftSeconds"](seconds);
   221     }
   222     let minutes = Math.round(aSeconds / 60);
   223     if (minutes < 60) {
   224       return DownloadsCommon.strings["shortTimeLeftMinutes"](minutes);
   225     }
   226     let hours = Math.round(minutes / 60);
   227     if (hours < 48) { // two days
   228       return DownloadsCommon.strings["shortTimeLeftHours"](hours);
   229     }
   230     let days = Math.round(hours / 24);
   231     return DownloadsCommon.strings["shortTimeLeftDays"](Math.min(days, 99));
   232   },
   234   /**
   235    * Indicates whether we should show visual notification on the indicator
   236    * when a download event is triggered.
   237    */
   238   get animateNotifications()
   239   {
   240     return PrefObserver.animateNotifications;
   241   },
   243   /**
   244    * Get access to one of the DownloadsData or PrivateDownloadsData objects,
   245    * depending on the privacy status of the window in question.
   246    *
   247    * @param aWindow
   248    *        The browser window which owns the download button.
   249    */
   250   getData: function DC_getData(aWindow) {
   251     if (PrivateBrowsingUtils.isWindowPrivate(aWindow)) {
   252       return PrivateDownloadsData;
   253     } else {
   254       return DownloadsData;
   255     }
   256   },
   258   /**
   259    * Initializes the Downloads back-end and starts receiving events for both the
   260    * private and non-private downloads data objects.
   261    */
   262   initializeAllDataLinks: function () {
   263     DownloadsData.initializeDataLink();
   264     PrivateDownloadsData.initializeDataLink();
   265   },
   267   /**
   268    * Get access to one of the DownloadsIndicatorData or
   269    * PrivateDownloadsIndicatorData objects, depending on the privacy status of
   270    * the window in question.
   271    */
   272   getIndicatorData: function DC_getIndicatorData(aWindow) {
   273     if (PrivateBrowsingUtils.isWindowPrivate(aWindow)) {
   274       return PrivateDownloadsIndicatorData;
   275     } else {
   276       return DownloadsIndicatorData;
   277     }
   278   },
   280   /**
   281    * Returns a reference to the DownloadsSummaryData singleton - creating one
   282    * in the process if one hasn't been instantiated yet.
   283    *
   284    * @param aWindow
   285    *        The browser window which owns the download button.
   286    * @param aNumToExclude
   287    *        The number of items on the top of the downloads list to exclude
   288    *        from the summary.
   289    */
   290   getSummary: function DC_getSummary(aWindow, aNumToExclude)
   291   {
   292     if (PrivateBrowsingUtils.isWindowPrivate(aWindow)) {
   293       if (this._privateSummary) {
   294         return this._privateSummary;
   295       }
   296       return this._privateSummary = new DownloadsSummaryData(true, aNumToExclude);
   297     } else {
   298       if (this._summary) {
   299         return this._summary;
   300       }
   301       return this._summary = new DownloadsSummaryData(false, aNumToExclude);
   302     }
   303   },
   304   _summary: null,
   305   _privateSummary: null,
   307   /**
   308    * Given an iterable collection of DownloadDataItems, generates and returns
   309    * statistics about that collection.
   310    *
   311    * @param aDataItems An iterable collection of DownloadDataItems.
   312    *
   313    * @return Object whose properties are the generated statistics. Currently,
   314    *         we return the following properties:
   315    *
   316    *         numActive       : The total number of downloads.
   317    *         numPaused       : The total number of paused downloads.
   318    *         numScanning     : The total number of downloads being scanned.
   319    *         numDownloading  : The total number of downloads being downloaded.
   320    *         totalSize       : The total size of all downloads once completed.
   321    *         totalTransferred: The total amount of transferred data for these
   322    *                           downloads.
   323    *         slowestSpeed    : The slowest download rate.
   324    *         rawTimeLeft     : The estimated time left for the downloads to
   325    *                           complete.
   326    *         percentComplete : The percentage of bytes successfully downloaded.
   327    */
   328   summarizeDownloads: function DC_summarizeDownloads(aDataItems)
   329   {
   330     let summary = {
   331       numActive: 0,
   332       numPaused: 0,
   333       numScanning: 0,
   334       numDownloading: 0,
   335       totalSize: 0,
   336       totalTransferred: 0,
   337       // slowestSpeed is Infinity so that we can use Math.min to
   338       // find the slowest speed. We'll set this to 0 afterwards if
   339       // it's still at Infinity by the time we're done iterating all
   340       // dataItems.
   341       slowestSpeed: Infinity,
   342       rawTimeLeft: -1,
   343       percentComplete: -1
   344     }
   346     for (let dataItem of aDataItems) {
   347       summary.numActive++;
   348       switch (dataItem.state) {
   349         case nsIDM.DOWNLOAD_PAUSED:
   350           summary.numPaused++;
   351           break;
   352         case nsIDM.DOWNLOAD_SCANNING:
   353           summary.numScanning++;
   354           break;
   355         case nsIDM.DOWNLOAD_DOWNLOADING:
   356           summary.numDownloading++;
   357           if (dataItem.maxBytes > 0 && dataItem.speed > 0) {
   358             let sizeLeft = dataItem.maxBytes - dataItem.currBytes;
   359             summary.rawTimeLeft = Math.max(summary.rawTimeLeft,
   360                                            sizeLeft / dataItem.speed);
   361             summary.slowestSpeed = Math.min(summary.slowestSpeed,
   362                                             dataItem.speed);
   363           }
   364           break;
   365       }
   366       // Only add to total values if we actually know the download size.
   367       if (dataItem.maxBytes > 0 &&
   368           dataItem.state != nsIDM.DOWNLOAD_CANCELED &&
   369           dataItem.state != nsIDM.DOWNLOAD_FAILED) {
   370         summary.totalSize += dataItem.maxBytes;
   371         summary.totalTransferred += dataItem.currBytes;
   372       }
   373     }
   375     if (summary.numActive != 0 && summary.totalSize != 0 &&
   376         summary.numActive != summary.numScanning) {
   377       summary.percentComplete = (summary.totalTransferred /
   378                                  summary.totalSize) * 100;
   379     }
   381     if (summary.slowestSpeed == Infinity) {
   382       summary.slowestSpeed = 0;
   383     }
   385     return summary;
   386   },
   388   /**
   389    * If necessary, smooths the estimated number of seconds remaining for one
   390    * or more downloads to complete.
   391    *
   392    * @param aSeconds
   393    *        Current raw estimate on number of seconds left for one or more
   394    *        downloads. This is a floating point value to help get sub-second
   395    *        accuracy for current and future estimates.
   396    */
   397   smoothSeconds: function DC_smoothSeconds(aSeconds, aLastSeconds)
   398   {
   399     // We apply an algorithm similar to the DownloadUtils.getTimeLeft function,
   400     // though tailored to a single time estimation for all downloads.  We never
   401     // apply sommothing if the new value is less than half the previous value.
   402     let shouldApplySmoothing = aLastSeconds >= 0 &&
   403                                aSeconds > aLastSeconds / 2;
   404     if (shouldApplySmoothing) {
   405       // Apply hysteresis to favor downward over upward swings.  Trust only 30%
   406       // of the new value if lower, and 10% if higher (exponential smoothing).
   407       let (diff = aSeconds - aLastSeconds) {
   408         aSeconds = aLastSeconds + (diff < 0 ? .3 : .1) * diff;
   409       }
   411       // If the new time is similar, reuse something close to the last time
   412       // left, but subtract a little to provide forward progress.
   413       let diff = aSeconds - aLastSeconds;
   414       let diffPercent = diff / aLastSeconds * 100;
   415       if (Math.abs(diff) < 5 || Math.abs(diffPercent) < 5) {
   416         aSeconds = aLastSeconds - (diff < 0 ? .4 : .2);
   417       }
   418     }
   420     // In the last few seconds of downloading, we are always subtracting and
   421     // never adding to the time left.  Ensure that we never fall below one
   422     // second left until all downloads are actually finished.
   423     return aLastSeconds = Math.max(aSeconds, 1);
   424   },
   426   /**
   427    * Opens a downloaded file.
   428    * If you've a dataItem, you should call dataItem.openLocalFile.
   429    * @param aFile
   430    *        the downloaded file to be opened.
   431    * @param aMimeInfo
   432    *        the mime type info object.  May be null.
   433    * @param aOwnerWindow
   434    *        the window with which this action is associated.
   435    */
   436   openDownloadedFile: function DC_openDownloadedFile(aFile, aMimeInfo, aOwnerWindow) {
   437     if (!(aFile instanceof Ci.nsIFile))
   438       throw new Error("aFile must be a nsIFile object");
   439     if (aMimeInfo && !(aMimeInfo instanceof Ci.nsIMIMEInfo))
   440       throw new Error("Invalid value passed for aMimeInfo");
   441     if (!(aOwnerWindow instanceof Ci.nsIDOMWindow))
   442       throw new Error("aOwnerWindow must be a dom-window object");
   444     let promiseShouldLaunch;
   445     if (aFile.isExecutable()) {
   446       // We get a prompter for the provided window here, even though anchoring
   447       // to the most recently active window should work as well.
   448       promiseShouldLaunch =
   449         DownloadUIHelper.getPrompter(aOwnerWindow)
   450                         .confirmLaunchExecutable(aFile.path);
   451     } else {
   452       promiseShouldLaunch = Promise.resolve(true);
   453     }
   455     promiseShouldLaunch.then(shouldLaunch => {
   456       if (!shouldLaunch) {
   457         return;
   458       }
   460       // Actually open the file.
   461       try {
   462         if (aMimeInfo && aMimeInfo.preferredAction == aMimeInfo.useHelperApp) {
   463           aMimeInfo.launchWithFile(aFile);
   464           return;
   465         }
   466       }
   467       catch(ex) { }
   469       // If either we don't have the mime info, or the preferred action failed,
   470       // attempt to launch the file directly.
   471       try {
   472         aFile.launch();
   473       }
   474       catch(ex) {
   475         // If launch fails, try sending it through the system's external "file:"
   476         // URL handler.
   477         Cc["@mozilla.org/uriloader/external-protocol-service;1"]
   478           .getService(Ci.nsIExternalProtocolService)
   479           .loadUrl(NetUtil.newURI(aFile));
   480       }
   481     }).then(null, Cu.reportError);
   482   },
   484   /**
   485    * Show a donwloaded file in the system file manager.
   486    * If you have a dataItem, use dataItem.showLocalFile.
   487    *
   488    * @param aFile
   489    *        a downloaded file.
   490    */
   491   showDownloadedFile: function DC_showDownloadedFile(aFile) {
   492     if (!(aFile instanceof Ci.nsIFile))
   493       throw new Error("aFile must be a nsIFile object");
   494     try {
   495       // Show the directory containing the file and select the file.
   496       aFile.reveal();
   497     } catch (ex) {
   498       // If reveal fails for some reason (e.g., it's not implemented on unix
   499       // or the file doesn't exist), try using the parent if we have it.
   500       let parent = aFile.parent;
   501       if (parent) {
   502         try {
   503           // Open the parent directory to show where the file should be.
   504           parent.launch();
   505         } catch (ex) {
   506           // If launch also fails (probably because it's not implemented), let
   507           // the OS handler try to open the parent.
   508           Cc["@mozilla.org/uriloader/external-protocol-service;1"]
   509             .getService(Ci.nsIExternalProtocolService)
   510             .loadUrl(NetUtil.newURI(parent));
   511         }
   512       }
   513     }
   514   }
   515 };
   517 /**
   518  * Returns true if we are executing on Windows Vista or a later version.
   519  */
   520 XPCOMUtils.defineLazyGetter(DownloadsCommon, "isWinVistaOrHigher", function () {
   521   let os = Cc["@mozilla.org/xre/app-info;1"].getService(Ci.nsIXULRuntime).OS;
   522   if (os != "WINNT") {
   523     return false;
   524   }
   525   let sysInfo = Cc["@mozilla.org/system-info;1"].getService(Ci.nsIPropertyBag2);
   526   return parseFloat(sysInfo.getProperty("version")) >= 6;
   527 });
   529 ////////////////////////////////////////////////////////////////////////////////
   530 //// DownloadsData
   532 /**
   533  * Retrieves the list of past and completed downloads from the underlying
   534  * Download Manager data, and provides asynchronous notifications allowing to
   535  * build a consistent view of the available data.
   536  *
   537  * This object responds to real-time changes in the underlying Download Manager
   538  * data.  For example, the deletion of one or more downloads is notified through
   539  * the nsIObserver interface, while any state or progress change is notified
   540  * through the nsIDownloadProgressListener interface.
   541  *
   542  * Note that using this object does not automatically start the Download Manager
   543  * service.  Consumers will see an empty list of downloads until the service is
   544  * actually started.  This is useful to display a neutral progress indicator in
   545  * the main browser window until the autostart timeout elapses.
   546  *
   547  * Note that DownloadsData and PrivateDownloadsData are two equivalent singleton
   548  * objects, one accessing non-private downloads, and the other accessing private
   549  * ones.
   550  */
   551 function DownloadsDataCtor(aPrivate) {
   552   this._isPrivate = aPrivate;
   554   // This Object contains all the available DownloadsDataItem objects, indexed by
   555   // their globally unique identifier.  The identifiers of downloads that have
   556   // been removed from the Download Manager data are still present, however the
   557   // associated objects are replaced with the value "null".  This is required to
   558   // prevent race conditions when populating the list asynchronously.
   559   this.dataItems = {};
   561   // Array of view objects that should be notified when the available download
   562   // data changes.
   563   this._views = [];
   565   // Maps Download objects to DownloadDataItem objects.
   566   this._downloadToDataItemMap = new Map();
   567 }
   569 DownloadsDataCtor.prototype = {
   570   /**
   571    * Starts receiving events for current downloads.
   572    */
   573   initializeDataLink: function ()
   574   {
   575     if (!this._dataLinkInitialized) {
   576       let promiseList = Downloads.getList(this._isPrivate ? Downloads.PRIVATE
   577                                                           : Downloads.PUBLIC);
   578       promiseList.then(list => list.addView(this)).then(null, Cu.reportError);
   579       this._dataLinkInitialized = true;
   580     }
   581   },
   582   _dataLinkInitialized: false,
   584   /**
   585    * True if there are finished downloads that can be removed from the list.
   586    */
   587   get canRemoveFinished()
   588   {
   589     for (let [, dataItem] of Iterator(this.dataItems)) {
   590       if (dataItem && !dataItem.inProgress) {
   591         return true;
   592       }
   593     }
   594     return false;
   595   },
   597   /**
   598    * Asks the back-end to remove finished downloads from the list.
   599    */
   600   removeFinished: function DD_removeFinished()
   601   {
   602     let promiseList = Downloads.getList(this._isPrivate ? Downloads.PRIVATE
   603                                                         : Downloads.PUBLIC);
   604     promiseList.then(list => list.removeFinished())
   605                .then(null, Cu.reportError);
   606   },
   608   //////////////////////////////////////////////////////////////////////////////
   609   //// Integration with the asynchronous Downloads back-end
   611   onDownloadAdded: function (aDownload)
   612   {
   613     let dataItem = new DownloadsDataItem(aDownload);
   614     this._downloadToDataItemMap.set(aDownload, dataItem);
   615     this.dataItems[dataItem.downloadGuid] = dataItem;
   617     for (let view of this._views) {
   618       view.onDataItemAdded(dataItem, true);
   619     }
   621     this._updateDataItemState(dataItem);
   622   },
   624   onDownloadChanged: function (aDownload)
   625   {
   626     let dataItem = this._downloadToDataItemMap.get(aDownload);
   627     if (!dataItem) {
   628       Cu.reportError("Download doesn't exist.");
   629       return;
   630     }
   632     this._updateDataItemState(dataItem);
   633   },
   635   onDownloadRemoved: function (aDownload)
   636   {
   637     let dataItem = this._downloadToDataItemMap.get(aDownload);
   638     if (!dataItem) {
   639       Cu.reportError("Download doesn't exist.");
   640       return;
   641     }
   643     this._downloadToDataItemMap.delete(aDownload);
   644     this.dataItems[dataItem.downloadGuid] = null;
   645     for (let view of this._views) {
   646       view.onDataItemRemoved(dataItem);
   647     }
   648   },
   650   /**
   651    * Updates the given data item and sends related notifications.
   652    */
   653   _updateDataItemState: function (aDataItem)
   654   {
   655     let oldState = aDataItem.state;
   656     let wasInProgress = aDataItem.inProgress;
   657     let wasDone = aDataItem.done;
   659     aDataItem.updateFromDownload();
   661     if (wasInProgress && !aDataItem.inProgress) {
   662       aDataItem.endTime = Date.now();
   663     }
   665     if (oldState != aDataItem.state) {
   666       for (let view of this._views) {
   667         try {
   668           view.getViewItem(aDataItem).onStateChange(oldState);
   669         } catch (ex) {
   670           Cu.reportError(ex);
   671         }
   672       }
   674       // This state transition code should actually be located in a Downloads
   675       // API module (bug 941009).  Moreover, the fact that state is stored as
   676       // annotations should be ideally hidden behind methods of
   677       // nsIDownloadHistory (bug 830415).
   678       if (!this._isPrivate && !aDataItem.inProgress) {
   679         try {
   680           let downloadMetaData = { state: aDataItem.state,
   681                                    endTime: aDataItem.endTime };
   682           if (aDataItem.done) {
   683             downloadMetaData.fileSize = aDataItem.maxBytes;
   684           }
   686           PlacesUtils.annotations.setPageAnnotation(
   687                         NetUtil.newURI(aDataItem.uri), "downloads/metaData",
   688                         JSON.stringify(downloadMetaData), 0,
   689                         PlacesUtils.annotations.EXPIRE_WITH_HISTORY);
   690         } catch (ex) {
   691           Cu.reportError(ex);
   692         }
   693       }
   694     }
   696     if (!aDataItem.newDownloadNotified) {
   697       aDataItem.newDownloadNotified = true;
   698       this._notifyDownloadEvent("start");
   699     }
   701     if (!wasDone && aDataItem.done) {
   702       this._notifyDownloadEvent("finish");
   703     }
   705     for (let view of this._views) {
   706       view.getViewItem(aDataItem).onProgressChange();
   707     }
   708   },
   710   //////////////////////////////////////////////////////////////////////////////
   711   //// Registration of views
   713   /**
   714    * Adds an object to be notified when the available download data changes.
   715    * The specified object is initialized with the currently available downloads.
   716    *
   717    * @param aView
   718    *        DownloadsView object to be added.  This reference must be passed to
   719    *        removeView before termination.
   720    */
   721   addView: function DD_addView(aView)
   722   {
   723     this._views.push(aView);
   724     this._updateView(aView);
   725   },
   727   /**
   728    * Removes an object previously added using addView.
   729    *
   730    * @param aView
   731    *        DownloadsView object to be removed.
   732    */
   733   removeView: function DD_removeView(aView)
   734   {
   735     let index = this._views.indexOf(aView);
   736     if (index != -1) {
   737       this._views.splice(index, 1);
   738     }
   739   },
   741   /**
   742    * Ensures that the currently loaded data is added to the specified view.
   743    *
   744    * @param aView
   745    *        DownloadsView object to be initialized.
   746    */
   747   _updateView: function DD_updateView(aView)
   748   {
   749     // Indicate to the view that a batch loading operation is in progress.
   750     aView.onDataLoadStarting();
   752     // Sort backwards by start time, ensuring that the most recent
   753     // downloads are added first regardless of their state.
   754     let loadedItemsArray = [dataItem
   755                             for each (dataItem in this.dataItems)
   756                             if (dataItem)];
   757     loadedItemsArray.sort(function(a, b) b.startTime - a.startTime);
   758     loadedItemsArray.forEach(
   759       function (dataItem) aView.onDataItemAdded(dataItem, false)
   760     );
   762     // Notify the view that all data is available.
   763     aView.onDataLoadCompleted();
   764   },
   766   //////////////////////////////////////////////////////////////////////////////
   767   //// Notifications sent to the most recent browser window only
   769   /**
   770    * Set to true after the first download causes the downloads panel to be
   771    * displayed.
   772    */
   773   get panelHasShownBefore() {
   774     try {
   775       return Services.prefs.getBoolPref("browser.download.panel.shown");
   776     } catch (ex) { }
   777     return false;
   778   },
   780   set panelHasShownBefore(aValue) {
   781     Services.prefs.setBoolPref("browser.download.panel.shown", aValue);
   782     return aValue;
   783   },
   785   /**
   786    * Displays a new or finished download notification in the most recent browser
   787    * window, if one is currently available with the required privacy type.
   788    *
   789    * @param aType
   790    *        Set to "start" for new downloads, "finish" for completed downloads.
   791    */
   792   _notifyDownloadEvent: function DD_notifyDownloadEvent(aType)
   793   {
   794     DownloadsCommon.log("Attempting to notify that a new download has started or finished.");
   796     // Show the panel in the most recent browser window, if present.
   797     let browserWin = RecentWindow.getMostRecentBrowserWindow({ private: this._isPrivate });
   798     if (!browserWin) {
   799       return;
   800     }
   802     if (this.panelHasShownBefore) {
   803       // For new downloads after the first one, don't show the panel
   804       // automatically, but provide a visible notification in the topmost
   805       // browser window, if the status indicator is already visible.
   806       DownloadsCommon.log("Showing new download notification.");
   807       browserWin.DownloadsIndicatorView.showEventNotification(aType);
   808       return;
   809     }
   810     this.panelHasShownBefore = true;
   811     browserWin.DownloadsPanel.showPanel();
   812   }
   813 };
   815 XPCOMUtils.defineLazyGetter(this, "PrivateDownloadsData", function() {
   816   return new DownloadsDataCtor(true);
   817 });
   819 XPCOMUtils.defineLazyGetter(this, "DownloadsData", function() {
   820   return new DownloadsDataCtor(false);
   821 });
   823 ////////////////////////////////////////////////////////////////////////////////
   824 //// DownloadsDataItem
   826 /**
   827  * Represents a single item in the list of downloads.
   828  *
   829  * The endTime property is initialized to the current date and time.
   830  *
   831  * @param aDownload
   832  *        The Download object with the current state.
   833  */
   834 function DownloadsDataItem(aDownload)
   835 {
   836   this._download = aDownload;
   838   this.downloadGuid = "id:" + this._autoIncrementId;
   839   this.file = aDownload.target.path;
   840   this.target = OS.Path.basename(aDownload.target.path);
   841   this.uri = aDownload.source.url;
   842   this.endTime = Date.now();
   844   this.updateFromDownload();
   845 }
   847 DownloadsDataItem.prototype = {
   848   /**
   849    * The JavaScript API does not need identifiers for Download objects, so they
   850    * are generated sequentially for the corresponding DownloadDataItem.
   851    */
   852   get _autoIncrementId() ++DownloadsDataItem.prototype.__lastId,
   853   __lastId: 0,
   855   /**
   856    * Updates this object from the underlying Download object.
   857    */
   858   updateFromDownload: function ()
   859   {
   860     // Collapse state using the correct priority.
   861     if (this._download.succeeded) {
   862       this.state = nsIDM.DOWNLOAD_FINISHED;
   863     } else if (this._download.error &&
   864                this._download.error.becauseBlockedByParentalControls) {
   865       this.state = nsIDM.DOWNLOAD_BLOCKED_PARENTAL;
   866     } else if (this._download.error &&
   867                this._download.error.becauseBlockedByReputationCheck) {
   868       this.state = nsIDM.DOWNLOAD_DIRTY;
   869     } else if (this._download.error) {
   870       this.state = nsIDM.DOWNLOAD_FAILED;
   871     } else if (this._download.canceled && this._download.hasPartialData) {
   872       this.state = nsIDM.DOWNLOAD_PAUSED;
   873     } else if (this._download.canceled) {
   874       this.state = nsIDM.DOWNLOAD_CANCELED;
   875     } else if (this._download.stopped) {
   876       this.state = nsIDM.DOWNLOAD_NOTSTARTED;
   877     } else {
   878       this.state = nsIDM.DOWNLOAD_DOWNLOADING;
   879     }
   881     this.referrer = this._download.source.referrer;
   882     this.startTime = this._download.startTime;
   883     this.currBytes = this._download.currentBytes;
   884     this.resumable = this._download.hasPartialData;
   885     this.speed = this._download.speed;
   887     if (this._download.succeeded) {
   888       // If the download succeeded, show the final size if available, otherwise
   889       // use the last known number of bytes transferred.  The final size on disk
   890       // will be available when bug 941063 is resolved.
   891       this.maxBytes = this._download.hasProgress ?
   892                              this._download.totalBytes :
   893                              this._download.currentBytes;
   894       this.percentComplete = 100;
   895     } else if (this._download.hasProgress) {
   896       // If the final size and progress are known, use them.
   897       this.maxBytes = this._download.totalBytes;
   898       this.percentComplete = this._download.progress;
   899     } else {
   900       // The download final size and progress percentage is unknown.
   901       this.maxBytes = -1;
   902       this.percentComplete = -1;
   903     }
   904   },
   906   /**
   907    * Indicates whether the download is proceeding normally, and not finished
   908    * yet.  This includes paused downloads.  When this property is true, the
   909    * "progress" property represents the current progress of the download.
   910    */
   911   get inProgress()
   912   {
   913     return [
   914       nsIDM.DOWNLOAD_NOTSTARTED,
   915       nsIDM.DOWNLOAD_QUEUED,
   916       nsIDM.DOWNLOAD_DOWNLOADING,
   917       nsIDM.DOWNLOAD_PAUSED,
   918       nsIDM.DOWNLOAD_SCANNING,
   919     ].indexOf(this.state) != -1;
   920   },
   922   /**
   923    * This is true during the initial phases of a download, before the actual
   924    * download of data bytes starts.
   925    */
   926   get starting()
   927   {
   928     return this.state == nsIDM.DOWNLOAD_NOTSTARTED ||
   929            this.state == nsIDM.DOWNLOAD_QUEUED;
   930   },
   932   /**
   933    * Indicates whether the download is paused.
   934    */
   935   get paused()
   936   {
   937     return this.state == nsIDM.DOWNLOAD_PAUSED;
   938   },
   940   /**
   941    * Indicates whether the download is in a final state, either because it
   942    * completed successfully or because it was blocked.
   943    */
   944   get done()
   945   {
   946     return [
   947       nsIDM.DOWNLOAD_FINISHED,
   948       nsIDM.DOWNLOAD_BLOCKED_PARENTAL,
   949       nsIDM.DOWNLOAD_BLOCKED_POLICY,
   950       nsIDM.DOWNLOAD_DIRTY,
   951     ].indexOf(this.state) != -1;
   952   },
   954   /**
   955    * Indicates whether the download is finished and can be opened.
   956    */
   957   get openable()
   958   {
   959     return this.state == nsIDM.DOWNLOAD_FINISHED;
   960   },
   962   /**
   963    * Indicates whether the download stopped because of an error, and can be
   964    * resumed manually.
   965    */
   966   get canRetry()
   967   {
   968     return this.state == nsIDM.DOWNLOAD_CANCELED ||
   969            this.state == nsIDM.DOWNLOAD_FAILED;
   970   },
   972   /**
   973    * Returns the nsILocalFile for the download target.
   974    *
   975    * @throws if the native path is not valid.  This can happen if the same
   976    *         profile is used on different platforms, for example if a native
   977    *         Windows path is stored and then the item is accessed on a Mac.
   978    */
   979   get localFile()
   980   {
   981     return this._getFile(this.file);
   982   },
   984   /**
   985    * Returns the nsILocalFile for the partially downloaded target.
   986    *
   987    * @throws if the native path is not valid.  This can happen if the same
   988    *         profile is used on different platforms, for example if a native
   989    *         Windows path is stored and then the item is accessed on a Mac.
   990    */
   991   get partFile()
   992   {
   993     return this._getFile(this.file + kPartialDownloadSuffix);
   994   },
   996   /**
   997    * Returns an nsILocalFile for aFilename. aFilename might be a file URL or
   998    * a native path.
   999    *
  1000    * @param aFilename the filename of the file to retrieve.
  1001    * @return an nsILocalFile for the file.
  1002    * @throws if the native path is not valid.  This can happen if the same
  1003    *         profile is used on different platforms, for example if a native
  1004    *         Windows path is stored and then the item is accessed on a Mac.
  1005    * @note This function makes no guarantees about the file's existence -
  1006    *       callers should check that the returned file exists.
  1007    */
  1008   _getFile: function DDI__getFile(aFilename)
  1010     // The download database may contain targets stored as file URLs or native
  1011     // paths.  This can still be true for previously stored items, even if new
  1012     // items are stored using their file URL.  See also bug 239948 comment 12.
  1013     if (aFilename.startsWith("file:")) {
  1014       // Assume the file URL we obtained from the downloads database or from the
  1015       // "spec" property of the target has the UTF-8 charset.
  1016       let fileUrl = NetUtil.newURI(aFilename).QueryInterface(Ci.nsIFileURL);
  1017       return fileUrl.file.clone().QueryInterface(Ci.nsILocalFile);
  1018     } else {
  1019       // The downloads database contains a native path.  Try to create a local
  1020       // file, though this may throw an exception if the path is invalid.
  1021       return new DownloadsLocalFileCtor(aFilename);
  1023   },
  1025   /**
  1026    * Open the target file for this download.
  1027    */
  1028   openLocalFile: function () {
  1029     this._download.launch().then(null, Cu.reportError);
  1030   },
  1032   /**
  1033    * Show the downloaded file in the system file manager.
  1034    */
  1035   showLocalFile: function DDI_showLocalFile() {
  1036     DownloadsCommon.showDownloadedFile(this.localFile);
  1037   },
  1039   /**
  1040    * Resumes the download if paused, pauses it if active.
  1041    * @throws if the download is not resumable or if has already done.
  1042    */
  1043   togglePauseResume: function DDI_togglePauseResume() {
  1044     if (this._download.stopped) {
  1045       this._download.start();
  1046     } else {
  1047       this._download.cancel();
  1049   },
  1051   /**
  1052    * Attempts to retry the download.
  1053    * @throws if we cannot.
  1054    */
  1055   retry: function DDI_retry() {
  1056     this._download.start();
  1057   },
  1059   /**
  1060    * Cancels the download.
  1061    */
  1062   cancel: function() {
  1063     this._download.cancel();
  1064     this._download.removePartialData().then(null, Cu.reportError);
  1065   },
  1067   /**
  1068    * Remove the download.
  1069    */
  1070   remove: function DDI_remove() {
  1071     Downloads.getList(Downloads.ALL)
  1072              .then(list => list.remove(this._download))
  1073              .then(() => this._download.finalize(true))
  1074              .then(null, Cu.reportError);
  1076 };
  1078 ////////////////////////////////////////////////////////////////////////////////
  1079 //// DownloadsViewPrototype
  1081 /**
  1082  * A prototype for an object that registers itself with DownloadsData as soon
  1083  * as a view is registered with it.
  1084  */
  1085 const DownloadsViewPrototype = {
  1086   //////////////////////////////////////////////////////////////////////////////
  1087   //// Registration of views
  1089   /**
  1090    * Array of view objects that should be notified when the available status
  1091    * data changes.
  1093    * SUBCLASSES MUST OVERRIDE THIS PROPERTY.
  1094    */
  1095   _views: null,
  1097   /**
  1098    * Determines whether this view object is over the private or non-private
  1099    * downloads.
  1101    * SUBCLASSES MUST OVERRIDE THIS PROPERTY.
  1102    */
  1103   _isPrivate: false,
  1105   /**
  1106    * Adds an object to be notified when the available status data changes.
  1107    * The specified object is initialized with the currently available status.
  1109    * @param aView
  1110    *        View object to be added.  This reference must be
  1111    *        passed to removeView before termination.
  1112    */
  1113   addView: function DVP_addView(aView)
  1115     // Start receiving events when the first of our views is registered.
  1116     if (this._views.length == 0) {
  1117       if (this._isPrivate) {
  1118         PrivateDownloadsData.addView(this);
  1119       } else {
  1120         DownloadsData.addView(this);
  1124     this._views.push(aView);
  1125     this.refreshView(aView);
  1126   },
  1128   /**
  1129    * Updates the properties of an object previously added using addView.
  1131    * @param aView
  1132    *        View object to be updated.
  1133    */
  1134   refreshView: function DVP_refreshView(aView)
  1136     // Update immediately even if we are still loading data asynchronously.
  1137     // Subclasses must provide these two functions!
  1138     this._refreshProperties();
  1139     this._updateView(aView);
  1140   },
  1142   /**
  1143    * Removes an object previously added using addView.
  1145    * @param aView
  1146    *        View object to be removed.
  1147    */
  1148   removeView: function DVP_removeView(aView)
  1150     let index = this._views.indexOf(aView);
  1151     if (index != -1) {
  1152       this._views.splice(index, 1);
  1155     // Stop receiving events when the last of our views is unregistered.
  1156     if (this._views.length == 0) {
  1157       if (this._isPrivate) {
  1158         PrivateDownloadsData.removeView(this);
  1159       } else {
  1160         DownloadsData.removeView(this);
  1163   },
  1165   //////////////////////////////////////////////////////////////////////////////
  1166   //// Callback functions from DownloadsData
  1168   /**
  1169    * Indicates whether we are still loading downloads data asynchronously.
  1170    */
  1171   _loading: false,
  1173   /**
  1174    * Called before multiple downloads are about to be loaded.
  1175    */
  1176   onDataLoadStarting: function DVP_onDataLoadStarting()
  1178     this._loading = true;
  1179   },
  1181   /**
  1182    * Called after data loading finished.
  1183    */
  1184   onDataLoadCompleted: function DVP_onDataLoadCompleted()
  1186     this._loading = false;
  1187   },
  1189   /**
  1190    * Called when a new download data item is available, either during the
  1191    * asynchronous data load or when a new download is started.
  1193    * @param aDataItem
  1194    *        DownloadsDataItem object that was just added.
  1195    * @param aNewest
  1196    *        When true, indicates that this item is the most recent and should be
  1197    *        added in the topmost position.  This happens when a new download is
  1198    *        started.  When false, indicates that the item is the least recent
  1199    *        with regard to the items that have been already added. The latter
  1200    *        generally happens during the asynchronous data load.
  1202    * @note Subclasses should override this.
  1203    */
  1204   onDataItemAdded: function DVP_onDataItemAdded(aDataItem, aNewest)
  1206     throw Components.results.NS_ERROR_NOT_IMPLEMENTED;
  1207   },
  1209   /**
  1210    * Called when a data item is removed, ensures that the widget associated with
  1211    * the view item is removed from the user interface.
  1213    * @param aDataItem
  1214    *        DownloadsDataItem object that is being removed.
  1216    * @note Subclasses should override this.
  1217    */
  1218   onDataItemRemoved: function DVP_onDataItemRemoved(aDataItem)
  1220     throw Components.results.NS_ERROR_NOT_IMPLEMENTED;
  1221   },
  1223   /**
  1224    * Returns the view item associated with the provided data item for this view.
  1226    * @param aDataItem
  1227    *        DownloadsDataItem object for which the view item is requested.
  1229    * @return Object that can be used to notify item status events.
  1231    * @note Subclasses should override this.
  1232    */
  1233   getViewItem: function DID_getViewItem(aDataItem)
  1235     throw Components.results.NS_ERROR_NOT_IMPLEMENTED;
  1236   },
  1238   /**
  1239    * Private function used to refresh the internal properties being sent to
  1240    * each registered view.
  1242    * @note Subclasses should override this.
  1243    */
  1244   _refreshProperties: function DID_refreshProperties()
  1246     throw Components.results.NS_ERROR_NOT_IMPLEMENTED;
  1247   },
  1249   /**
  1250    * Private function used to refresh an individual view.
  1252    * @note Subclasses should override this.
  1253    */
  1254   _updateView: function DID_updateView()
  1256     throw Components.results.NS_ERROR_NOT_IMPLEMENTED;
  1258 };
  1260 ////////////////////////////////////////////////////////////////////////////////
  1261 //// DownloadsIndicatorData
  1263 /**
  1264  * This object registers itself with DownloadsData as a view, and transforms the
  1265  * notifications it receives into overall status data, that is then broadcast to
  1266  * the registered download status indicators.
  1268  * Note that using this object does not automatically start the Download Manager
  1269  * service.  Consumers will see an empty list of downloads until the service is
  1270  * actually started.  This is useful to display a neutral progress indicator in
  1271  * the main browser window until the autostart timeout elapses.
  1272  */
  1273 function DownloadsIndicatorDataCtor(aPrivate) {
  1274   this._isPrivate = aPrivate;
  1275   this._views = [];
  1277 DownloadsIndicatorDataCtor.prototype = {
  1278   __proto__: DownloadsViewPrototype,
  1280   /**
  1281    * Removes an object previously added using addView.
  1283    * @param aView
  1284    *        DownloadsIndicatorView object to be removed.
  1285    */
  1286   removeView: function DID_removeView(aView)
  1288     DownloadsViewPrototype.removeView.call(this, aView);
  1290     if (this._views.length == 0) {
  1291       this._itemCount = 0;
  1293   },
  1295   //////////////////////////////////////////////////////////////////////////////
  1296   //// Callback functions from DownloadsData
  1298   /**
  1299    * Called after data loading finished.
  1300    */
  1301   onDataLoadCompleted: function DID_onDataLoadCompleted()
  1303     DownloadsViewPrototype.onDataLoadCompleted.call(this);
  1304     this._updateViews();
  1305   },
  1307   /**
  1308    * Called when a new download data item is available, either during the
  1309    * asynchronous data load or when a new download is started.
  1311    * @param aDataItem
  1312    *        DownloadsDataItem object that was just added.
  1313    * @param aNewest
  1314    *        When true, indicates that this item is the most recent and should be
  1315    *        added in the topmost position.  This happens when a new download is
  1316    *        started.  When false, indicates that the item is the least recent
  1317    *        with regard to the items that have been already added. The latter
  1318    *        generally happens during the asynchronous data load.
  1319    */
  1320   onDataItemAdded: function DID_onDataItemAdded(aDataItem, aNewest)
  1322     this._itemCount++;
  1323     this._updateViews();
  1324   },
  1326   /**
  1327    * Called when a data item is removed, ensures that the widget associated with
  1328    * the view item is removed from the user interface.
  1330    * @param aDataItem
  1331    *        DownloadsDataItem object that is being removed.
  1332    */
  1333   onDataItemRemoved: function DID_onDataItemRemoved(aDataItem)
  1335     this._itemCount--;
  1336     this._updateViews();
  1337   },
  1339   /**
  1340    * Returns the view item associated with the provided data item for this view.
  1342    * @param aDataItem
  1343    *        DownloadsDataItem object for which the view item is requested.
  1345    * @return Object that can be used to notify item status events.
  1346    */
  1347   getViewItem: function DID_getViewItem(aDataItem)
  1349     let data = this._isPrivate ? PrivateDownloadsIndicatorData
  1350                                : DownloadsIndicatorData;
  1351     return Object.freeze({
  1352       onStateChange: function DIVI_onStateChange(aOldState)
  1354         if (aDataItem.state == nsIDM.DOWNLOAD_FINISHED ||
  1355             aDataItem.state == nsIDM.DOWNLOAD_FAILED) {
  1356           data.attention = true;
  1359         // Since the state of a download changed, reset the estimated time left.
  1360         data._lastRawTimeLeft = -1;
  1361         data._lastTimeLeft = -1;
  1363         data._updateViews();
  1364       },
  1365       onProgressChange: function DIVI_onProgressChange()
  1367         data._updateViews();
  1369     });
  1370   },
  1372   //////////////////////////////////////////////////////////////////////////////
  1373   //// Propagation of properties to our views
  1375   // The following properties are updated by _refreshProperties and are then
  1376   // propagated to the views.  See _refreshProperties for details.
  1377   _hasDownloads: false,
  1378   _counter: "",
  1379   _percentComplete: -1,
  1380   _paused: false,
  1382   /**
  1383    * Indicates whether the download indicators should be highlighted.
  1384    */
  1385   set attention(aValue)
  1387     this._attention = aValue;
  1388     this._updateViews();
  1389     return aValue;
  1390   },
  1391   _attention: false,
  1393   /**
  1394    * Indicates whether the user is interacting with downloads, thus the
  1395    * attention indication should not be shown even if requested.
  1396    */
  1397   set attentionSuppressed(aValue)
  1399     this._attentionSuppressed = aValue;
  1400     this._attention = false;
  1401     this._updateViews();
  1402     return aValue;
  1403   },
  1404   _attentionSuppressed: false,
  1406   /**
  1407    * Computes aggregate values and propagates the changes to our views.
  1408    */
  1409   _updateViews: function DID_updateViews()
  1411     // Do not update the status indicators during batch loads of download items.
  1412     if (this._loading) {
  1413       return;
  1416     this._refreshProperties();
  1417     this._views.forEach(this._updateView, this);
  1418   },
  1420   /**
  1421    * Updates the specified view with the current aggregate values.
  1423    * @param aView
  1424    *        DownloadsIndicatorView object to be updated.
  1425    */
  1426   _updateView: function DID_updateView(aView)
  1428     aView.hasDownloads = this._hasDownloads;
  1429     aView.counter = this._counter;
  1430     aView.percentComplete = this._percentComplete;
  1431     aView.paused = this._paused;
  1432     aView.attention = this._attention && !this._attentionSuppressed;
  1433   },
  1435   //////////////////////////////////////////////////////////////////////////////
  1436   //// Property updating based on current download status
  1438   /**
  1439    * Number of download items that are available to be displayed.
  1440    */
  1441   _itemCount: 0,
  1443   /**
  1444    * Floating point value indicating the last number of seconds estimated until
  1445    * the longest download will finish.  We need to store this value so that we
  1446    * don't continuously apply smoothing if the actual download state has not
  1447    * changed.  This is set to -1 if the previous value is unknown.
  1448    */
  1449   _lastRawTimeLeft: -1,
  1451   /**
  1452    * Last number of seconds estimated until all in-progress downloads with a
  1453    * known size and speed will finish.  This value is stored to allow smoothing
  1454    * in case of small variations.  This is set to -1 if the previous value is
  1455    * unknown.
  1456    */
  1457   _lastTimeLeft: -1,
  1459   /**
  1460    * A generator function for the dataItems that this summary is currently
  1461    * interested in. This generator is passed off to summarizeDownloads in order
  1462    * to generate statistics about the dataItems we care about - in this case,
  1463    * it's all dataItems for active downloads.
  1464    */
  1465   _activeDataItems: function DID_activeDataItems()
  1467     let dataItems = this._isPrivate ? PrivateDownloadsData.dataItems
  1468                                     : DownloadsData.dataItems;
  1469     for each (let dataItem in dataItems) {
  1470       if (dataItem && dataItem.inProgress) {
  1471         yield dataItem;
  1474   },
  1476   /**
  1477    * Computes aggregate values based on the current state of downloads.
  1478    */
  1479   _refreshProperties: function DID_refreshProperties()
  1481     let summary =
  1482       DownloadsCommon.summarizeDownloads(this._activeDataItems());
  1484     // Determine if the indicator should be shown or get attention.
  1485     this._hasDownloads = (this._itemCount > 0);
  1487     // If all downloads are paused, show the progress indicator as paused.
  1488     this._paused = summary.numActive > 0 &&
  1489                    summary.numActive == summary.numPaused;
  1491     this._percentComplete = summary.percentComplete;
  1493     // Display the estimated time left, if present.
  1494     if (summary.rawTimeLeft == -1) {
  1495       // There are no downloads with a known time left.
  1496       this._lastRawTimeLeft = -1;
  1497       this._lastTimeLeft = -1;
  1498       this._counter = "";
  1499     } else {
  1500       // Compute the new time left only if state actually changed.
  1501       if (this._lastRawTimeLeft != summary.rawTimeLeft) {
  1502         this._lastRawTimeLeft = summary.rawTimeLeft;
  1503         this._lastTimeLeft = DownloadsCommon.smoothSeconds(summary.rawTimeLeft,
  1504                                                            this._lastTimeLeft);
  1506       this._counter = DownloadsCommon.formatTimeLeft(this._lastTimeLeft);
  1509 };
  1511 XPCOMUtils.defineLazyGetter(this, "PrivateDownloadsIndicatorData", function() {
  1512   return new DownloadsIndicatorDataCtor(true);
  1513 });
  1515 XPCOMUtils.defineLazyGetter(this, "DownloadsIndicatorData", function() {
  1516   return new DownloadsIndicatorDataCtor(false);
  1517 });
  1519 ////////////////////////////////////////////////////////////////////////////////
  1520 //// DownloadsSummaryData
  1522 /**
  1523  * DownloadsSummaryData is a view for DownloadsData that produces a summary
  1524  * of all downloads after a certain exclusion point aNumToExclude. For example,
  1525  * if there were 5 downloads in progress, and a DownloadsSummaryData was
  1526  * constructed with aNumToExclude equal to 3, then that DownloadsSummaryData
  1527  * would produce a summary of the last 2 downloads.
  1529  * @param aIsPrivate
  1530  *        True if the browser window which owns the download button is a private
  1531  *        window.
  1532  * @param aNumToExclude
  1533  *        The number of items to exclude from the summary, starting from the
  1534  *        top of the list.
  1535  */
  1536 function DownloadsSummaryData(aIsPrivate, aNumToExclude) {
  1537   this._numToExclude = aNumToExclude;
  1538   // Since we can have multiple instances of DownloadsSummaryData, we
  1539   // override these values from the prototype so that each instance can be
  1540   // completely separated from one another.
  1541   this._loading = false;
  1543   this._dataItems = [];
  1545   // Floating point value indicating the last number of seconds estimated until
  1546   // the longest download will finish.  We need to store this value so that we
  1547   // don't continuously apply smoothing if the actual download state has not
  1548   // changed.  This is set to -1 if the previous value is unknown.
  1549   this._lastRawTimeLeft = -1;
  1551   // Last number of seconds estimated until all in-progress downloads with a
  1552   // known size and speed will finish.  This value is stored to allow smoothing
  1553   // in case of small variations.  This is set to -1 if the previous value is
  1554   // unknown.
  1555   this._lastTimeLeft = -1;
  1557   // The following properties are updated by _refreshProperties and are then
  1558   // propagated to the views.
  1559   this._showingProgress = false;
  1560   this._details = "";
  1561   this._description = "";
  1562   this._numActive = 0;
  1563   this._percentComplete = -1;
  1565   this._isPrivate = aIsPrivate;
  1566   this._views = [];
  1569 DownloadsSummaryData.prototype = {
  1570   __proto__: DownloadsViewPrototype,
  1572   /**
  1573    * Removes an object previously added using addView.
  1575    * @param aView
  1576    *        DownloadsSummary view to be removed.
  1577    */
  1578   removeView: function DSD_removeView(aView)
  1580     DownloadsViewPrototype.removeView.call(this, aView);
  1582     if (this._views.length == 0) {
  1583       // Clear out our collection of DownloadDataItems. If we ever have
  1584       // another view registered with us, this will get re-populated.
  1585       this._dataItems = [];
  1587   },
  1589   //////////////////////////////////////////////////////////////////////////////
  1590   //// Callback functions from DownloadsData - see the documentation in
  1591   //// DownloadsViewPrototype for more information on what these functions
  1592   //// are used for.
  1594   onDataLoadCompleted: function DSD_onDataLoadCompleted()
  1596     DownloadsViewPrototype.onDataLoadCompleted.call(this);
  1597     this._updateViews();
  1598   },
  1600   onDataItemAdded: function DSD_onDataItemAdded(aDataItem, aNewest)
  1602     if (aNewest) {
  1603       this._dataItems.unshift(aDataItem);
  1604     } else {
  1605       this._dataItems.push(aDataItem);
  1608     this._updateViews();
  1609   },
  1611   onDataItemRemoved: function DSD_onDataItemRemoved(aDataItem)
  1613     let itemIndex = this._dataItems.indexOf(aDataItem);
  1614     this._dataItems.splice(itemIndex, 1);
  1615     this._updateViews();
  1616   },
  1618   getViewItem: function DSD_getViewItem(aDataItem)
  1620     let self = this;
  1621     return Object.freeze({
  1622       onStateChange: function DIVI_onStateChange(aOldState)
  1624         // Since the state of a download changed, reset the estimated time left.
  1625         self._lastRawTimeLeft = -1;
  1626         self._lastTimeLeft = -1;
  1627         self._updateViews();
  1628       },
  1629       onProgressChange: function DIVI_onProgressChange()
  1631         self._updateViews();
  1633     });
  1634   },
  1636   //////////////////////////////////////////////////////////////////////////////
  1637   //// Propagation of properties to our views
  1639   /**
  1640    * Computes aggregate values and propagates the changes to our views.
  1641    */
  1642   _updateViews: function DSD_updateViews()
  1644     // Do not update the status indicators during batch loads of download items.
  1645     if (this._loading) {
  1646       return;
  1649     this._refreshProperties();
  1650     this._views.forEach(this._updateView, this);
  1651   },
  1653   /**
  1654    * Updates the specified view with the current aggregate values.
  1656    * @param aView
  1657    *        DownloadsIndicatorView object to be updated.
  1658    */
  1659   _updateView: function DSD_updateView(aView)
  1661     aView.showingProgress = this._showingProgress;
  1662     aView.percentComplete = this._percentComplete;
  1663     aView.description = this._description;
  1664     aView.details = this._details;
  1665   },
  1667   //////////////////////////////////////////////////////////////////////////////
  1668   //// Property updating based on current download status
  1670   /**
  1671    * A generator function for the dataItems that this summary is currently
  1672    * interested in. This generator is passed off to summarizeDownloads in order
  1673    * to generate statistics about the dataItems we care about - in this case,
  1674    * it's the dataItems in this._dataItems after the first few to exclude,
  1675    * which was set when constructing this DownloadsSummaryData instance.
  1676    */
  1677   _dataItemsForSummary: function DSD_dataItemsForSummary()
  1679     if (this._dataItems.length > 0) {
  1680       for (let i = this._numToExclude; i < this._dataItems.length; ++i) {
  1681         yield this._dataItems[i];
  1684   },
  1686   /**
  1687    * Computes aggregate values based on the current state of downloads.
  1688    */
  1689   _refreshProperties: function DSD_refreshProperties()
  1691     // Pre-load summary with default values.
  1692     let summary =
  1693       DownloadsCommon.summarizeDownloads(this._dataItemsForSummary());
  1695     this._description = DownloadsCommon.strings
  1696                                        .otherDownloads2(summary.numActive);
  1697     this._percentComplete = summary.percentComplete;
  1699     // If all downloads are paused, show the progress indicator as paused.
  1700     this._showingProgress = summary.numDownloading > 0 ||
  1701                             summary.numPaused > 0;
  1703     // Display the estimated time left, if present.
  1704     if (summary.rawTimeLeft == -1) {
  1705       // There are no downloads with a known time left.
  1706       this._lastRawTimeLeft = -1;
  1707       this._lastTimeLeft = -1;
  1708       this._details = "";
  1709     } else {
  1710       // Compute the new time left only if state actually changed.
  1711       if (this._lastRawTimeLeft != summary.rawTimeLeft) {
  1712         this._lastRawTimeLeft = summary.rawTimeLeft;
  1713         this._lastTimeLeft = DownloadsCommon.smoothSeconds(summary.rawTimeLeft,
  1714                                                            this._lastTimeLeft);
  1716       [this._details] = DownloadUtils.getDownloadStatusNoRate(
  1717         summary.totalTransferred, summary.totalSize, summary.slowestSpeed,
  1718         this._lastTimeLeft);

mercurial