browser/components/downloads/src/DownloadsCommon.jsm

changeset 0
6474c204b198
     1.1 --- /dev/null	Thu Jan 01 00:00:00 1970 +0000
     1.2 +++ b/browser/components/downloads/src/DownloadsCommon.jsm	Wed Dec 31 06:09:35 2014 +0100
     1.3 @@ -0,0 +1,1721 @@
     1.4 +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
     1.5 +/* vim: set ts=2 et sw=2 tw=80 filetype=javascript: */
     1.6 +/* This Source Code Form is subject to the terms of the Mozilla Public
     1.7 + * License, v. 2.0. If a copy of the MPL was not distributed with this file,
     1.8 + * You can obtain one at http://mozilla.org/MPL/2.0/. */
     1.9 +
    1.10 +"use strict";
    1.11 +
    1.12 +this.EXPORTED_SYMBOLS = [
    1.13 +  "DownloadsCommon",
    1.14 +];
    1.15 +
    1.16 +/**
    1.17 + * Handles the Downloads panel shared methods and data access.
    1.18 + *
    1.19 + * This file includes the following constructors and global objects:
    1.20 + *
    1.21 + * DownloadsCommon
    1.22 + * This object is exposed directly to the consumers of this JavaScript module,
    1.23 + * and provides shared methods for all the instances of the user interface.
    1.24 + *
    1.25 + * DownloadsData
    1.26 + * Retrieves the list of past and completed downloads from the underlying
    1.27 + * Download Manager data, and provides asynchronous notifications allowing
    1.28 + * to build a consistent view of the available data.
    1.29 + *
    1.30 + * DownloadsDataItem
    1.31 + * Represents a single item in the list of downloads.  This object either wraps
    1.32 + * an existing nsIDownload from the Download Manager, or provides the same
    1.33 + * information read directly from the downloads database, with the possibility
    1.34 + * of querying the nsIDownload lazily, for performance reasons.
    1.35 + *
    1.36 + * DownloadsIndicatorData
    1.37 + * This object registers itself with DownloadsData as a view, and transforms the
    1.38 + * notifications it receives into overall status data, that is then broadcast to
    1.39 + * the registered download status indicators.
    1.40 + */
    1.41 +
    1.42 +////////////////////////////////////////////////////////////////////////////////
    1.43 +//// Globals
    1.44 +
    1.45 +const Cc = Components.classes;
    1.46 +const Ci = Components.interfaces;
    1.47 +const Cu = Components.utils;
    1.48 +const Cr = Components.results;
    1.49 +
    1.50 +Cu.import("resource://gre/modules/XPCOMUtils.jsm");
    1.51 +Cu.import("resource://gre/modules/Services.jsm");
    1.52 +
    1.53 +XPCOMUtils.defineLazyModuleGetter(this, "NetUtil",
    1.54 +                                  "resource://gre/modules/NetUtil.jsm");
    1.55 +XPCOMUtils.defineLazyModuleGetter(this, "PluralForm",
    1.56 +                                  "resource://gre/modules/PluralForm.jsm");
    1.57 +XPCOMUtils.defineLazyModuleGetter(this, "Downloads",
    1.58 +                                  "resource://gre/modules/Downloads.jsm");
    1.59 +XPCOMUtils.defineLazyModuleGetter(this, "DownloadUIHelper",
    1.60 +                                  "resource://gre/modules/DownloadUIHelper.jsm");
    1.61 +XPCOMUtils.defineLazyModuleGetter(this, "DownloadUtils",
    1.62 +                                  "resource://gre/modules/DownloadUtils.jsm");
    1.63 +XPCOMUtils.defineLazyModuleGetter(this, "OS",
    1.64 +                                  "resource://gre/modules/osfile.jsm")
    1.65 +XPCOMUtils.defineLazyModuleGetter(this, "PlacesUtils",
    1.66 +                                  "resource://gre/modules/PlacesUtils.jsm");
    1.67 +XPCOMUtils.defineLazyModuleGetter(this, "PrivateBrowsingUtils",
    1.68 +                                  "resource://gre/modules/PrivateBrowsingUtils.jsm");
    1.69 +XPCOMUtils.defineLazyModuleGetter(this, "RecentWindow",
    1.70 +                                  "resource:///modules/RecentWindow.jsm");
    1.71 +XPCOMUtils.defineLazyModuleGetter(this, "Promise",
    1.72 +                                  "resource://gre/modules/Promise.jsm");
    1.73 +XPCOMUtils.defineLazyModuleGetter(this, "DownloadsLogger",
    1.74 +                                  "resource:///modules/DownloadsLogger.jsm");
    1.75 +
    1.76 +const nsIDM = Ci.nsIDownloadManager;
    1.77 +
    1.78 +const kDownloadsStringBundleUrl =
    1.79 +  "chrome://browser/locale/downloads/downloads.properties";
    1.80 +
    1.81 +const kDownloadsStringsRequiringFormatting = {
    1.82 +  sizeWithUnits: true,
    1.83 +  shortTimeLeftSeconds: true,
    1.84 +  shortTimeLeftMinutes: true,
    1.85 +  shortTimeLeftHours: true,
    1.86 +  shortTimeLeftDays: true,
    1.87 +  statusSeparator: true,
    1.88 +  statusSeparatorBeforeNumber: true,
    1.89 +  fileExecutableSecurityWarning: true
    1.90 +};
    1.91 +
    1.92 +const kDownloadsStringsRequiringPluralForm = {
    1.93 +  otherDownloads2: true
    1.94 +};
    1.95 +
    1.96 +XPCOMUtils.defineLazyGetter(this, "DownloadsLocalFileCtor", function () {
    1.97 +  return Components.Constructor("@mozilla.org/file/local;1",
    1.98 +                                "nsILocalFile", "initWithPath");
    1.99 +});
   1.100 +
   1.101 +const kPartialDownloadSuffix = ".part";
   1.102 +
   1.103 +const kPrefBranch = Services.prefs.getBranch("browser.download.");
   1.104 +
   1.105 +let PrefObserver = {
   1.106 +  QueryInterface: XPCOMUtils.generateQI([Ci.nsIObserver,
   1.107 +                                         Ci.nsISupportsWeakReference]),
   1.108 +  getPref: function PO_getPref(name) {
   1.109 +    try {
   1.110 +      switch (typeof this.prefs[name]) {
   1.111 +        case "boolean":
   1.112 +          return kPrefBranch.getBoolPref(name);
   1.113 +      }
   1.114 +    } catch (ex) { }
   1.115 +    return this.prefs[name];
   1.116 +  },
   1.117 +  observe: function PO_observe(aSubject, aTopic, aData) {
   1.118 +    if (this.prefs.hasOwnProperty(aData)) {
   1.119 +      return this[aData] = this.getPref(aData);
   1.120 +    }
   1.121 +  },
   1.122 +  register: function PO_register(prefs) {
   1.123 +    this.prefs = prefs;
   1.124 +    kPrefBranch.addObserver("", this, true);
   1.125 +    for (let key in prefs) {
   1.126 +      let name = key;
   1.127 +      XPCOMUtils.defineLazyGetter(this, name, function () {
   1.128 +        return PrefObserver.getPref(name);
   1.129 +      });
   1.130 +    }
   1.131 +  },
   1.132 +};
   1.133 +
   1.134 +PrefObserver.register({
   1.135 +  // prefName: defaultValue
   1.136 +  debug: false,
   1.137 +  animateNotifications: true
   1.138 +});
   1.139 +
   1.140 +
   1.141 +////////////////////////////////////////////////////////////////////////////////
   1.142 +//// DownloadsCommon
   1.143 +
   1.144 +/**
   1.145 + * This object is exposed directly to the consumers of this JavaScript module,
   1.146 + * and provides shared methods for all the instances of the user interface.
   1.147 + */
   1.148 +this.DownloadsCommon = {
   1.149 +  log: function DC_log(...aMessageArgs) {
   1.150 +    delete this.log;
   1.151 +    this.log = function DC_log(...aMessageArgs) {
   1.152 +      if (!PrefObserver.debug) {
   1.153 +        return;
   1.154 +      }
   1.155 +      DownloadsLogger.log.apply(DownloadsLogger, aMessageArgs);
   1.156 +    }
   1.157 +    this.log.apply(this, aMessageArgs);
   1.158 +  },
   1.159 +
   1.160 +  error: function DC_error(...aMessageArgs) {
   1.161 +    delete this.error;
   1.162 +    this.error = function DC_error(...aMessageArgs) {
   1.163 +      if (!PrefObserver.debug) {
   1.164 +        return;
   1.165 +      }
   1.166 +      DownloadsLogger.reportError.apply(DownloadsLogger, aMessageArgs);
   1.167 +    }
   1.168 +    this.error.apply(this, aMessageArgs);
   1.169 +  },
   1.170 +  /**
   1.171 +   * Returns an object whose keys are the string names from the downloads string
   1.172 +   * bundle, and whose values are either the translated strings or functions
   1.173 +   * returning formatted strings.
   1.174 +   */
   1.175 +  get strings()
   1.176 +  {
   1.177 +    let strings = {};
   1.178 +    let sb = Services.strings.createBundle(kDownloadsStringBundleUrl);
   1.179 +    let enumerator = sb.getSimpleEnumeration();
   1.180 +    while (enumerator.hasMoreElements()) {
   1.181 +      let string = enumerator.getNext().QueryInterface(Ci.nsIPropertyElement);
   1.182 +      let stringName = string.key;
   1.183 +      if (stringName in kDownloadsStringsRequiringFormatting) {
   1.184 +        strings[stringName] = function () {
   1.185 +          // Convert "arguments" to a real array before calling into XPCOM.
   1.186 +          return sb.formatStringFromName(stringName,
   1.187 +                                         Array.slice(arguments, 0),
   1.188 +                                         arguments.length);
   1.189 +        };
   1.190 +      } else if (stringName in kDownloadsStringsRequiringPluralForm) {
   1.191 +        strings[stringName] = function (aCount) {
   1.192 +          // Convert "arguments" to a real array before calling into XPCOM.
   1.193 +          let formattedString = sb.formatStringFromName(stringName,
   1.194 +                                         Array.slice(arguments, 0),
   1.195 +                                         arguments.length);
   1.196 +          return PluralForm.get(aCount, formattedString);
   1.197 +        };
   1.198 +      } else {
   1.199 +        strings[stringName] = string.value;
   1.200 +      }
   1.201 +    }
   1.202 +    delete this.strings;
   1.203 +    return this.strings = strings;
   1.204 +  },
   1.205 +
   1.206 +  /**
   1.207 +   * Generates a very short string representing the given time left.
   1.208 +   *
   1.209 +   * @param aSeconds
   1.210 +   *        Value to be formatted.  It represents the number of seconds, it must
   1.211 +   *        be positive but does not need to be an integer.
   1.212 +   *
   1.213 +   * @return Formatted string, for example "30s" or "2h".  The returned value is
   1.214 +   *         maximum three characters long, at least in English.
   1.215 +   */
   1.216 +  formatTimeLeft: function DC_formatTimeLeft(aSeconds)
   1.217 +  {
   1.218 +    // Decide what text to show for the time
   1.219 +    let seconds = Math.round(aSeconds);
   1.220 +    if (!seconds) {
   1.221 +      return "";
   1.222 +    } else if (seconds <= 30) {
   1.223 +      return DownloadsCommon.strings["shortTimeLeftSeconds"](seconds);
   1.224 +    }
   1.225 +    let minutes = Math.round(aSeconds / 60);
   1.226 +    if (minutes < 60) {
   1.227 +      return DownloadsCommon.strings["shortTimeLeftMinutes"](minutes);
   1.228 +    }
   1.229 +    let hours = Math.round(minutes / 60);
   1.230 +    if (hours < 48) { // two days
   1.231 +      return DownloadsCommon.strings["shortTimeLeftHours"](hours);
   1.232 +    }
   1.233 +    let days = Math.round(hours / 24);
   1.234 +    return DownloadsCommon.strings["shortTimeLeftDays"](Math.min(days, 99));
   1.235 +  },
   1.236 +
   1.237 +  /**
   1.238 +   * Indicates whether we should show visual notification on the indicator
   1.239 +   * when a download event is triggered.
   1.240 +   */
   1.241 +  get animateNotifications()
   1.242 +  {
   1.243 +    return PrefObserver.animateNotifications;
   1.244 +  },
   1.245 +
   1.246 +  /**
   1.247 +   * Get access to one of the DownloadsData or PrivateDownloadsData objects,
   1.248 +   * depending on the privacy status of the window in question.
   1.249 +   *
   1.250 +   * @param aWindow
   1.251 +   *        The browser window which owns the download button.
   1.252 +   */
   1.253 +  getData: function DC_getData(aWindow) {
   1.254 +    if (PrivateBrowsingUtils.isWindowPrivate(aWindow)) {
   1.255 +      return PrivateDownloadsData;
   1.256 +    } else {
   1.257 +      return DownloadsData;
   1.258 +    }
   1.259 +  },
   1.260 +
   1.261 +  /**
   1.262 +   * Initializes the Downloads back-end and starts receiving events for both the
   1.263 +   * private and non-private downloads data objects.
   1.264 +   */
   1.265 +  initializeAllDataLinks: function () {
   1.266 +    DownloadsData.initializeDataLink();
   1.267 +    PrivateDownloadsData.initializeDataLink();
   1.268 +  },
   1.269 +
   1.270 +  /**
   1.271 +   * Get access to one of the DownloadsIndicatorData or
   1.272 +   * PrivateDownloadsIndicatorData objects, depending on the privacy status of
   1.273 +   * the window in question.
   1.274 +   */
   1.275 +  getIndicatorData: function DC_getIndicatorData(aWindow) {
   1.276 +    if (PrivateBrowsingUtils.isWindowPrivate(aWindow)) {
   1.277 +      return PrivateDownloadsIndicatorData;
   1.278 +    } else {
   1.279 +      return DownloadsIndicatorData;
   1.280 +    }
   1.281 +  },
   1.282 +
   1.283 +  /**
   1.284 +   * Returns a reference to the DownloadsSummaryData singleton - creating one
   1.285 +   * in the process if one hasn't been instantiated yet.
   1.286 +   *
   1.287 +   * @param aWindow
   1.288 +   *        The browser window which owns the download button.
   1.289 +   * @param aNumToExclude
   1.290 +   *        The number of items on the top of the downloads list to exclude
   1.291 +   *        from the summary.
   1.292 +   */
   1.293 +  getSummary: function DC_getSummary(aWindow, aNumToExclude)
   1.294 +  {
   1.295 +    if (PrivateBrowsingUtils.isWindowPrivate(aWindow)) {
   1.296 +      if (this._privateSummary) {
   1.297 +        return this._privateSummary;
   1.298 +      }
   1.299 +      return this._privateSummary = new DownloadsSummaryData(true, aNumToExclude);
   1.300 +    } else {
   1.301 +      if (this._summary) {
   1.302 +        return this._summary;
   1.303 +      }
   1.304 +      return this._summary = new DownloadsSummaryData(false, aNumToExclude);
   1.305 +    }
   1.306 +  },
   1.307 +  _summary: null,
   1.308 +  _privateSummary: null,
   1.309 +
   1.310 +  /**
   1.311 +   * Given an iterable collection of DownloadDataItems, generates and returns
   1.312 +   * statistics about that collection.
   1.313 +   *
   1.314 +   * @param aDataItems An iterable collection of DownloadDataItems.
   1.315 +   *
   1.316 +   * @return Object whose properties are the generated statistics. Currently,
   1.317 +   *         we return the following properties:
   1.318 +   *
   1.319 +   *         numActive       : The total number of downloads.
   1.320 +   *         numPaused       : The total number of paused downloads.
   1.321 +   *         numScanning     : The total number of downloads being scanned.
   1.322 +   *         numDownloading  : The total number of downloads being downloaded.
   1.323 +   *         totalSize       : The total size of all downloads once completed.
   1.324 +   *         totalTransferred: The total amount of transferred data for these
   1.325 +   *                           downloads.
   1.326 +   *         slowestSpeed    : The slowest download rate.
   1.327 +   *         rawTimeLeft     : The estimated time left for the downloads to
   1.328 +   *                           complete.
   1.329 +   *         percentComplete : The percentage of bytes successfully downloaded.
   1.330 +   */
   1.331 +  summarizeDownloads: function DC_summarizeDownloads(aDataItems)
   1.332 +  {
   1.333 +    let summary = {
   1.334 +      numActive: 0,
   1.335 +      numPaused: 0,
   1.336 +      numScanning: 0,
   1.337 +      numDownloading: 0,
   1.338 +      totalSize: 0,
   1.339 +      totalTransferred: 0,
   1.340 +      // slowestSpeed is Infinity so that we can use Math.min to
   1.341 +      // find the slowest speed. We'll set this to 0 afterwards if
   1.342 +      // it's still at Infinity by the time we're done iterating all
   1.343 +      // dataItems.
   1.344 +      slowestSpeed: Infinity,
   1.345 +      rawTimeLeft: -1,
   1.346 +      percentComplete: -1
   1.347 +    }
   1.348 +
   1.349 +    for (let dataItem of aDataItems) {
   1.350 +      summary.numActive++;
   1.351 +      switch (dataItem.state) {
   1.352 +        case nsIDM.DOWNLOAD_PAUSED:
   1.353 +          summary.numPaused++;
   1.354 +          break;
   1.355 +        case nsIDM.DOWNLOAD_SCANNING:
   1.356 +          summary.numScanning++;
   1.357 +          break;
   1.358 +        case nsIDM.DOWNLOAD_DOWNLOADING:
   1.359 +          summary.numDownloading++;
   1.360 +          if (dataItem.maxBytes > 0 && dataItem.speed > 0) {
   1.361 +            let sizeLeft = dataItem.maxBytes - dataItem.currBytes;
   1.362 +            summary.rawTimeLeft = Math.max(summary.rawTimeLeft,
   1.363 +                                           sizeLeft / dataItem.speed);
   1.364 +            summary.slowestSpeed = Math.min(summary.slowestSpeed,
   1.365 +                                            dataItem.speed);
   1.366 +          }
   1.367 +          break;
   1.368 +      }
   1.369 +      // Only add to total values if we actually know the download size.
   1.370 +      if (dataItem.maxBytes > 0 &&
   1.371 +          dataItem.state != nsIDM.DOWNLOAD_CANCELED &&
   1.372 +          dataItem.state != nsIDM.DOWNLOAD_FAILED) {
   1.373 +        summary.totalSize += dataItem.maxBytes;
   1.374 +        summary.totalTransferred += dataItem.currBytes;
   1.375 +      }
   1.376 +    }
   1.377 +
   1.378 +    if (summary.numActive != 0 && summary.totalSize != 0 &&
   1.379 +        summary.numActive != summary.numScanning) {
   1.380 +      summary.percentComplete = (summary.totalTransferred /
   1.381 +                                 summary.totalSize) * 100;
   1.382 +    }
   1.383 +
   1.384 +    if (summary.slowestSpeed == Infinity) {
   1.385 +      summary.slowestSpeed = 0;
   1.386 +    }
   1.387 +
   1.388 +    return summary;
   1.389 +  },
   1.390 +
   1.391 +  /**
   1.392 +   * If necessary, smooths the estimated number of seconds remaining for one
   1.393 +   * or more downloads to complete.
   1.394 +   *
   1.395 +   * @param aSeconds
   1.396 +   *        Current raw estimate on number of seconds left for one or more
   1.397 +   *        downloads. This is a floating point value to help get sub-second
   1.398 +   *        accuracy for current and future estimates.
   1.399 +   */
   1.400 +  smoothSeconds: function DC_smoothSeconds(aSeconds, aLastSeconds)
   1.401 +  {
   1.402 +    // We apply an algorithm similar to the DownloadUtils.getTimeLeft function,
   1.403 +    // though tailored to a single time estimation for all downloads.  We never
   1.404 +    // apply sommothing if the new value is less than half the previous value.
   1.405 +    let shouldApplySmoothing = aLastSeconds >= 0 &&
   1.406 +                               aSeconds > aLastSeconds / 2;
   1.407 +    if (shouldApplySmoothing) {
   1.408 +      // Apply hysteresis to favor downward over upward swings.  Trust only 30%
   1.409 +      // of the new value if lower, and 10% if higher (exponential smoothing).
   1.410 +      let (diff = aSeconds - aLastSeconds) {
   1.411 +        aSeconds = aLastSeconds + (diff < 0 ? .3 : .1) * diff;
   1.412 +      }
   1.413 +
   1.414 +      // If the new time is similar, reuse something close to the last time
   1.415 +      // left, but subtract a little to provide forward progress.
   1.416 +      let diff = aSeconds - aLastSeconds;
   1.417 +      let diffPercent = diff / aLastSeconds * 100;
   1.418 +      if (Math.abs(diff) < 5 || Math.abs(diffPercent) < 5) {
   1.419 +        aSeconds = aLastSeconds - (diff < 0 ? .4 : .2);
   1.420 +      }
   1.421 +    }
   1.422 +
   1.423 +    // In the last few seconds of downloading, we are always subtracting and
   1.424 +    // never adding to the time left.  Ensure that we never fall below one
   1.425 +    // second left until all downloads are actually finished.
   1.426 +    return aLastSeconds = Math.max(aSeconds, 1);
   1.427 +  },
   1.428 +
   1.429 +  /**
   1.430 +   * Opens a downloaded file.
   1.431 +   * If you've a dataItem, you should call dataItem.openLocalFile.
   1.432 +   * @param aFile
   1.433 +   *        the downloaded file to be opened.
   1.434 +   * @param aMimeInfo
   1.435 +   *        the mime type info object.  May be null.
   1.436 +   * @param aOwnerWindow
   1.437 +   *        the window with which this action is associated.
   1.438 +   */
   1.439 +  openDownloadedFile: function DC_openDownloadedFile(aFile, aMimeInfo, aOwnerWindow) {
   1.440 +    if (!(aFile instanceof Ci.nsIFile))
   1.441 +      throw new Error("aFile must be a nsIFile object");
   1.442 +    if (aMimeInfo && !(aMimeInfo instanceof Ci.nsIMIMEInfo))
   1.443 +      throw new Error("Invalid value passed for aMimeInfo");
   1.444 +    if (!(aOwnerWindow instanceof Ci.nsIDOMWindow))
   1.445 +      throw new Error("aOwnerWindow must be a dom-window object");
   1.446 +
   1.447 +    let promiseShouldLaunch;
   1.448 +    if (aFile.isExecutable()) {
   1.449 +      // We get a prompter for the provided window here, even though anchoring
   1.450 +      // to the most recently active window should work as well.
   1.451 +      promiseShouldLaunch =
   1.452 +        DownloadUIHelper.getPrompter(aOwnerWindow)
   1.453 +                        .confirmLaunchExecutable(aFile.path);
   1.454 +    } else {
   1.455 +      promiseShouldLaunch = Promise.resolve(true);
   1.456 +    }
   1.457 +
   1.458 +    promiseShouldLaunch.then(shouldLaunch => {
   1.459 +      if (!shouldLaunch) {
   1.460 +        return;
   1.461 +      }
   1.462 +  
   1.463 +      // Actually open the file.
   1.464 +      try {
   1.465 +        if (aMimeInfo && aMimeInfo.preferredAction == aMimeInfo.useHelperApp) {
   1.466 +          aMimeInfo.launchWithFile(aFile);
   1.467 +          return;
   1.468 +        }
   1.469 +      }
   1.470 +      catch(ex) { }
   1.471 +  
   1.472 +      // If either we don't have the mime info, or the preferred action failed,
   1.473 +      // attempt to launch the file directly.
   1.474 +      try {
   1.475 +        aFile.launch();
   1.476 +      }
   1.477 +      catch(ex) {
   1.478 +        // If launch fails, try sending it through the system's external "file:"
   1.479 +        // URL handler.
   1.480 +        Cc["@mozilla.org/uriloader/external-protocol-service;1"]
   1.481 +          .getService(Ci.nsIExternalProtocolService)
   1.482 +          .loadUrl(NetUtil.newURI(aFile));
   1.483 +      }
   1.484 +    }).then(null, Cu.reportError);
   1.485 +  },
   1.486 +
   1.487 +  /**
   1.488 +   * Show a donwloaded file in the system file manager.
   1.489 +   * If you have a dataItem, use dataItem.showLocalFile.
   1.490 +   *
   1.491 +   * @param aFile
   1.492 +   *        a downloaded file.
   1.493 +   */
   1.494 +  showDownloadedFile: function DC_showDownloadedFile(aFile) {
   1.495 +    if (!(aFile instanceof Ci.nsIFile))
   1.496 +      throw new Error("aFile must be a nsIFile object");
   1.497 +    try {
   1.498 +      // Show the directory containing the file and select the file.
   1.499 +      aFile.reveal();
   1.500 +    } catch (ex) {
   1.501 +      // If reveal fails for some reason (e.g., it's not implemented on unix
   1.502 +      // or the file doesn't exist), try using the parent if we have it.
   1.503 +      let parent = aFile.parent;
   1.504 +      if (parent) {
   1.505 +        try {
   1.506 +          // Open the parent directory to show where the file should be.
   1.507 +          parent.launch();
   1.508 +        } catch (ex) {
   1.509 +          // If launch also fails (probably because it's not implemented), let
   1.510 +          // the OS handler try to open the parent.
   1.511 +          Cc["@mozilla.org/uriloader/external-protocol-service;1"]
   1.512 +            .getService(Ci.nsIExternalProtocolService)
   1.513 +            .loadUrl(NetUtil.newURI(parent));
   1.514 +        }
   1.515 +      }
   1.516 +    }
   1.517 +  }
   1.518 +};
   1.519 +
   1.520 +/**
   1.521 + * Returns true if we are executing on Windows Vista or a later version.
   1.522 + */
   1.523 +XPCOMUtils.defineLazyGetter(DownloadsCommon, "isWinVistaOrHigher", function () {
   1.524 +  let os = Cc["@mozilla.org/xre/app-info;1"].getService(Ci.nsIXULRuntime).OS;
   1.525 +  if (os != "WINNT") {
   1.526 +    return false;
   1.527 +  }
   1.528 +  let sysInfo = Cc["@mozilla.org/system-info;1"].getService(Ci.nsIPropertyBag2);
   1.529 +  return parseFloat(sysInfo.getProperty("version")) >= 6;
   1.530 +});
   1.531 +
   1.532 +////////////////////////////////////////////////////////////////////////////////
   1.533 +//// DownloadsData
   1.534 +
   1.535 +/**
   1.536 + * Retrieves the list of past and completed downloads from the underlying
   1.537 + * Download Manager data, and provides asynchronous notifications allowing to
   1.538 + * build a consistent view of the available data.
   1.539 + *
   1.540 + * This object responds to real-time changes in the underlying Download Manager
   1.541 + * data.  For example, the deletion of one or more downloads is notified through
   1.542 + * the nsIObserver interface, while any state or progress change is notified
   1.543 + * through the nsIDownloadProgressListener interface.
   1.544 + *
   1.545 + * Note that using this object does not automatically start the Download Manager
   1.546 + * service.  Consumers will see an empty list of downloads until the service is
   1.547 + * actually started.  This is useful to display a neutral progress indicator in
   1.548 + * the main browser window until the autostart timeout elapses.
   1.549 + *
   1.550 + * Note that DownloadsData and PrivateDownloadsData are two equivalent singleton
   1.551 + * objects, one accessing non-private downloads, and the other accessing private
   1.552 + * ones.
   1.553 + */
   1.554 +function DownloadsDataCtor(aPrivate) {
   1.555 +  this._isPrivate = aPrivate;
   1.556 +
   1.557 +  // This Object contains all the available DownloadsDataItem objects, indexed by
   1.558 +  // their globally unique identifier.  The identifiers of downloads that have
   1.559 +  // been removed from the Download Manager data are still present, however the
   1.560 +  // associated objects are replaced with the value "null".  This is required to
   1.561 +  // prevent race conditions when populating the list asynchronously.
   1.562 +  this.dataItems = {};
   1.563 +
   1.564 +  // Array of view objects that should be notified when the available download
   1.565 +  // data changes.
   1.566 +  this._views = [];
   1.567 +
   1.568 +  // Maps Download objects to DownloadDataItem objects.
   1.569 +  this._downloadToDataItemMap = new Map();
   1.570 +}
   1.571 +
   1.572 +DownloadsDataCtor.prototype = {
   1.573 +  /**
   1.574 +   * Starts receiving events for current downloads.
   1.575 +   */
   1.576 +  initializeDataLink: function ()
   1.577 +  {
   1.578 +    if (!this._dataLinkInitialized) {
   1.579 +      let promiseList = Downloads.getList(this._isPrivate ? Downloads.PRIVATE
   1.580 +                                                          : Downloads.PUBLIC);
   1.581 +      promiseList.then(list => list.addView(this)).then(null, Cu.reportError);
   1.582 +      this._dataLinkInitialized = true;
   1.583 +    }
   1.584 +  },
   1.585 +  _dataLinkInitialized: false,
   1.586 +
   1.587 +  /**
   1.588 +   * True if there are finished downloads that can be removed from the list.
   1.589 +   */
   1.590 +  get canRemoveFinished()
   1.591 +  {
   1.592 +    for (let [, dataItem] of Iterator(this.dataItems)) {
   1.593 +      if (dataItem && !dataItem.inProgress) {
   1.594 +        return true;
   1.595 +      }
   1.596 +    }
   1.597 +    return false;
   1.598 +  },
   1.599 +
   1.600 +  /**
   1.601 +   * Asks the back-end to remove finished downloads from the list.
   1.602 +   */
   1.603 +  removeFinished: function DD_removeFinished()
   1.604 +  {
   1.605 +    let promiseList = Downloads.getList(this._isPrivate ? Downloads.PRIVATE
   1.606 +                                                        : Downloads.PUBLIC);
   1.607 +    promiseList.then(list => list.removeFinished())
   1.608 +               .then(null, Cu.reportError);
   1.609 +  },
   1.610 +
   1.611 +  //////////////////////////////////////////////////////////////////////////////
   1.612 +  //// Integration with the asynchronous Downloads back-end
   1.613 +
   1.614 +  onDownloadAdded: function (aDownload)
   1.615 +  {
   1.616 +    let dataItem = new DownloadsDataItem(aDownload);
   1.617 +    this._downloadToDataItemMap.set(aDownload, dataItem);
   1.618 +    this.dataItems[dataItem.downloadGuid] = dataItem;
   1.619 +
   1.620 +    for (let view of this._views) {
   1.621 +      view.onDataItemAdded(dataItem, true);
   1.622 +    }
   1.623 +
   1.624 +    this._updateDataItemState(dataItem);
   1.625 +  },
   1.626 +
   1.627 +  onDownloadChanged: function (aDownload)
   1.628 +  {
   1.629 +    let dataItem = this._downloadToDataItemMap.get(aDownload);
   1.630 +    if (!dataItem) {
   1.631 +      Cu.reportError("Download doesn't exist.");
   1.632 +      return;
   1.633 +    }
   1.634 +
   1.635 +    this._updateDataItemState(dataItem);
   1.636 +  },
   1.637 +
   1.638 +  onDownloadRemoved: function (aDownload)
   1.639 +  {
   1.640 +    let dataItem = this._downloadToDataItemMap.get(aDownload);
   1.641 +    if (!dataItem) {
   1.642 +      Cu.reportError("Download doesn't exist.");
   1.643 +      return;
   1.644 +    }
   1.645 +
   1.646 +    this._downloadToDataItemMap.delete(aDownload);
   1.647 +    this.dataItems[dataItem.downloadGuid] = null;
   1.648 +    for (let view of this._views) {
   1.649 +      view.onDataItemRemoved(dataItem);
   1.650 +    }
   1.651 +  },
   1.652 +
   1.653 +  /**
   1.654 +   * Updates the given data item and sends related notifications.
   1.655 +   */
   1.656 +  _updateDataItemState: function (aDataItem)
   1.657 +  {
   1.658 +    let oldState = aDataItem.state;
   1.659 +    let wasInProgress = aDataItem.inProgress;
   1.660 +    let wasDone = aDataItem.done;
   1.661 +
   1.662 +    aDataItem.updateFromDownload();
   1.663 +
   1.664 +    if (wasInProgress && !aDataItem.inProgress) {
   1.665 +      aDataItem.endTime = Date.now();
   1.666 +    }
   1.667 +
   1.668 +    if (oldState != aDataItem.state) {
   1.669 +      for (let view of this._views) {
   1.670 +        try {
   1.671 +          view.getViewItem(aDataItem).onStateChange(oldState);
   1.672 +        } catch (ex) {
   1.673 +          Cu.reportError(ex);
   1.674 +        }
   1.675 +      }
   1.676 +
   1.677 +      // This state transition code should actually be located in a Downloads
   1.678 +      // API module (bug 941009).  Moreover, the fact that state is stored as
   1.679 +      // annotations should be ideally hidden behind methods of
   1.680 +      // nsIDownloadHistory (bug 830415).
   1.681 +      if (!this._isPrivate && !aDataItem.inProgress) {
   1.682 +        try {
   1.683 +          let downloadMetaData = { state: aDataItem.state,
   1.684 +                                   endTime: aDataItem.endTime };
   1.685 +          if (aDataItem.done) {
   1.686 +            downloadMetaData.fileSize = aDataItem.maxBytes;
   1.687 +          }
   1.688 +
   1.689 +          PlacesUtils.annotations.setPageAnnotation(
   1.690 +                        NetUtil.newURI(aDataItem.uri), "downloads/metaData",
   1.691 +                        JSON.stringify(downloadMetaData), 0,
   1.692 +                        PlacesUtils.annotations.EXPIRE_WITH_HISTORY);
   1.693 +        } catch (ex) {
   1.694 +          Cu.reportError(ex);
   1.695 +        }
   1.696 +      }
   1.697 +    }
   1.698 +
   1.699 +    if (!aDataItem.newDownloadNotified) {
   1.700 +      aDataItem.newDownloadNotified = true;
   1.701 +      this._notifyDownloadEvent("start");
   1.702 +    }
   1.703 +
   1.704 +    if (!wasDone && aDataItem.done) {
   1.705 +      this._notifyDownloadEvent("finish");
   1.706 +    }
   1.707 +
   1.708 +    for (let view of this._views) {
   1.709 +      view.getViewItem(aDataItem).onProgressChange();
   1.710 +    }
   1.711 +  },
   1.712 +
   1.713 +  //////////////////////////////////////////////////////////////////////////////
   1.714 +  //// Registration of views
   1.715 +
   1.716 +  /**
   1.717 +   * Adds an object to be notified when the available download data changes.
   1.718 +   * The specified object is initialized with the currently available downloads.
   1.719 +   *
   1.720 +   * @param aView
   1.721 +   *        DownloadsView object to be added.  This reference must be passed to
   1.722 +   *        removeView before termination.
   1.723 +   */
   1.724 +  addView: function DD_addView(aView)
   1.725 +  {
   1.726 +    this._views.push(aView);
   1.727 +    this._updateView(aView);
   1.728 +  },
   1.729 +
   1.730 +  /**
   1.731 +   * Removes an object previously added using addView.
   1.732 +   *
   1.733 +   * @param aView
   1.734 +   *        DownloadsView object to be removed.
   1.735 +   */
   1.736 +  removeView: function DD_removeView(aView)
   1.737 +  {
   1.738 +    let index = this._views.indexOf(aView);
   1.739 +    if (index != -1) {
   1.740 +      this._views.splice(index, 1);
   1.741 +    }
   1.742 +  },
   1.743 +
   1.744 +  /**
   1.745 +   * Ensures that the currently loaded data is added to the specified view.
   1.746 +   *
   1.747 +   * @param aView
   1.748 +   *        DownloadsView object to be initialized.
   1.749 +   */
   1.750 +  _updateView: function DD_updateView(aView)
   1.751 +  {
   1.752 +    // Indicate to the view that a batch loading operation is in progress.
   1.753 +    aView.onDataLoadStarting();
   1.754 +
   1.755 +    // Sort backwards by start time, ensuring that the most recent
   1.756 +    // downloads are added first regardless of their state.
   1.757 +    let loadedItemsArray = [dataItem
   1.758 +                            for each (dataItem in this.dataItems)
   1.759 +                            if (dataItem)];
   1.760 +    loadedItemsArray.sort(function(a, b) b.startTime - a.startTime);
   1.761 +    loadedItemsArray.forEach(
   1.762 +      function (dataItem) aView.onDataItemAdded(dataItem, false)
   1.763 +    );
   1.764 +
   1.765 +    // Notify the view that all data is available.
   1.766 +    aView.onDataLoadCompleted();
   1.767 +  },
   1.768 +
   1.769 +  //////////////////////////////////////////////////////////////////////////////
   1.770 +  //// Notifications sent to the most recent browser window only
   1.771 +
   1.772 +  /**
   1.773 +   * Set to true after the first download causes the downloads panel to be
   1.774 +   * displayed.
   1.775 +   */
   1.776 +  get panelHasShownBefore() {
   1.777 +    try {
   1.778 +      return Services.prefs.getBoolPref("browser.download.panel.shown");
   1.779 +    } catch (ex) { }
   1.780 +    return false;
   1.781 +  },
   1.782 +
   1.783 +  set panelHasShownBefore(aValue) {
   1.784 +    Services.prefs.setBoolPref("browser.download.panel.shown", aValue);
   1.785 +    return aValue;
   1.786 +  },
   1.787 +
   1.788 +  /**
   1.789 +   * Displays a new or finished download notification in the most recent browser
   1.790 +   * window, if one is currently available with the required privacy type.
   1.791 +   *
   1.792 +   * @param aType
   1.793 +   *        Set to "start" for new downloads, "finish" for completed downloads.
   1.794 +   */
   1.795 +  _notifyDownloadEvent: function DD_notifyDownloadEvent(aType)
   1.796 +  {
   1.797 +    DownloadsCommon.log("Attempting to notify that a new download has started or finished.");
   1.798 +
   1.799 +    // Show the panel in the most recent browser window, if present.
   1.800 +    let browserWin = RecentWindow.getMostRecentBrowserWindow({ private: this._isPrivate });
   1.801 +    if (!browserWin) {
   1.802 +      return;
   1.803 +    }
   1.804 +
   1.805 +    if (this.panelHasShownBefore) {
   1.806 +      // For new downloads after the first one, don't show the panel
   1.807 +      // automatically, but provide a visible notification in the topmost
   1.808 +      // browser window, if the status indicator is already visible.
   1.809 +      DownloadsCommon.log("Showing new download notification.");
   1.810 +      browserWin.DownloadsIndicatorView.showEventNotification(aType);
   1.811 +      return;
   1.812 +    }
   1.813 +    this.panelHasShownBefore = true;
   1.814 +    browserWin.DownloadsPanel.showPanel();
   1.815 +  }
   1.816 +};
   1.817 +
   1.818 +XPCOMUtils.defineLazyGetter(this, "PrivateDownloadsData", function() {
   1.819 +  return new DownloadsDataCtor(true);
   1.820 +});
   1.821 +
   1.822 +XPCOMUtils.defineLazyGetter(this, "DownloadsData", function() {
   1.823 +  return new DownloadsDataCtor(false);
   1.824 +});
   1.825 +
   1.826 +////////////////////////////////////////////////////////////////////////////////
   1.827 +//// DownloadsDataItem
   1.828 +
   1.829 +/**
   1.830 + * Represents a single item in the list of downloads.
   1.831 + *
   1.832 + * The endTime property is initialized to the current date and time.
   1.833 + *
   1.834 + * @param aDownload
   1.835 + *        The Download object with the current state.
   1.836 + */
   1.837 +function DownloadsDataItem(aDownload)
   1.838 +{
   1.839 +  this._download = aDownload;
   1.840 +
   1.841 +  this.downloadGuid = "id:" + this._autoIncrementId;
   1.842 +  this.file = aDownload.target.path;
   1.843 +  this.target = OS.Path.basename(aDownload.target.path);
   1.844 +  this.uri = aDownload.source.url;
   1.845 +  this.endTime = Date.now();
   1.846 +
   1.847 +  this.updateFromDownload();
   1.848 +}
   1.849 +
   1.850 +DownloadsDataItem.prototype = {
   1.851 +  /**
   1.852 +   * The JavaScript API does not need identifiers for Download objects, so they
   1.853 +   * are generated sequentially for the corresponding DownloadDataItem.
   1.854 +   */
   1.855 +  get _autoIncrementId() ++DownloadsDataItem.prototype.__lastId,
   1.856 +  __lastId: 0,
   1.857 +
   1.858 +  /**
   1.859 +   * Updates this object from the underlying Download object.
   1.860 +   */
   1.861 +  updateFromDownload: function ()
   1.862 +  {
   1.863 +    // Collapse state using the correct priority.
   1.864 +    if (this._download.succeeded) {
   1.865 +      this.state = nsIDM.DOWNLOAD_FINISHED;
   1.866 +    } else if (this._download.error &&
   1.867 +               this._download.error.becauseBlockedByParentalControls) {
   1.868 +      this.state = nsIDM.DOWNLOAD_BLOCKED_PARENTAL;
   1.869 +    } else if (this._download.error &&
   1.870 +               this._download.error.becauseBlockedByReputationCheck) {
   1.871 +      this.state = nsIDM.DOWNLOAD_DIRTY;
   1.872 +    } else if (this._download.error) {
   1.873 +      this.state = nsIDM.DOWNLOAD_FAILED;
   1.874 +    } else if (this._download.canceled && this._download.hasPartialData) {
   1.875 +      this.state = nsIDM.DOWNLOAD_PAUSED;
   1.876 +    } else if (this._download.canceled) {
   1.877 +      this.state = nsIDM.DOWNLOAD_CANCELED;
   1.878 +    } else if (this._download.stopped) {
   1.879 +      this.state = nsIDM.DOWNLOAD_NOTSTARTED;
   1.880 +    } else {
   1.881 +      this.state = nsIDM.DOWNLOAD_DOWNLOADING;
   1.882 +    }
   1.883 +
   1.884 +    this.referrer = this._download.source.referrer;
   1.885 +    this.startTime = this._download.startTime;
   1.886 +    this.currBytes = this._download.currentBytes;
   1.887 +    this.resumable = this._download.hasPartialData;
   1.888 +    this.speed = this._download.speed;
   1.889 +
   1.890 +    if (this._download.succeeded) {
   1.891 +      // If the download succeeded, show the final size if available, otherwise
   1.892 +      // use the last known number of bytes transferred.  The final size on disk
   1.893 +      // will be available when bug 941063 is resolved.
   1.894 +      this.maxBytes = this._download.hasProgress ?
   1.895 +                             this._download.totalBytes :
   1.896 +                             this._download.currentBytes;
   1.897 +      this.percentComplete = 100;
   1.898 +    } else if (this._download.hasProgress) {
   1.899 +      // If the final size and progress are known, use them.
   1.900 +      this.maxBytes = this._download.totalBytes;
   1.901 +      this.percentComplete = this._download.progress;
   1.902 +    } else {
   1.903 +      // The download final size and progress percentage is unknown.
   1.904 +      this.maxBytes = -1;
   1.905 +      this.percentComplete = -1;
   1.906 +    }
   1.907 +  },
   1.908 +
   1.909 +  /**
   1.910 +   * Indicates whether the download is proceeding normally, and not finished
   1.911 +   * yet.  This includes paused downloads.  When this property is true, the
   1.912 +   * "progress" property represents the current progress of the download.
   1.913 +   */
   1.914 +  get inProgress()
   1.915 +  {
   1.916 +    return [
   1.917 +      nsIDM.DOWNLOAD_NOTSTARTED,
   1.918 +      nsIDM.DOWNLOAD_QUEUED,
   1.919 +      nsIDM.DOWNLOAD_DOWNLOADING,
   1.920 +      nsIDM.DOWNLOAD_PAUSED,
   1.921 +      nsIDM.DOWNLOAD_SCANNING,
   1.922 +    ].indexOf(this.state) != -1;
   1.923 +  },
   1.924 +
   1.925 +  /**
   1.926 +   * This is true during the initial phases of a download, before the actual
   1.927 +   * download of data bytes starts.
   1.928 +   */
   1.929 +  get starting()
   1.930 +  {
   1.931 +    return this.state == nsIDM.DOWNLOAD_NOTSTARTED ||
   1.932 +           this.state == nsIDM.DOWNLOAD_QUEUED;
   1.933 +  },
   1.934 +
   1.935 +  /**
   1.936 +   * Indicates whether the download is paused.
   1.937 +   */
   1.938 +  get paused()
   1.939 +  {
   1.940 +    return this.state == nsIDM.DOWNLOAD_PAUSED;
   1.941 +  },
   1.942 +
   1.943 +  /**
   1.944 +   * Indicates whether the download is in a final state, either because it
   1.945 +   * completed successfully or because it was blocked.
   1.946 +   */
   1.947 +  get done()
   1.948 +  {
   1.949 +    return [
   1.950 +      nsIDM.DOWNLOAD_FINISHED,
   1.951 +      nsIDM.DOWNLOAD_BLOCKED_PARENTAL,
   1.952 +      nsIDM.DOWNLOAD_BLOCKED_POLICY,
   1.953 +      nsIDM.DOWNLOAD_DIRTY,
   1.954 +    ].indexOf(this.state) != -1;
   1.955 +  },
   1.956 +
   1.957 +  /**
   1.958 +   * Indicates whether the download is finished and can be opened.
   1.959 +   */
   1.960 +  get openable()
   1.961 +  {
   1.962 +    return this.state == nsIDM.DOWNLOAD_FINISHED;
   1.963 +  },
   1.964 +
   1.965 +  /**
   1.966 +   * Indicates whether the download stopped because of an error, and can be
   1.967 +   * resumed manually.
   1.968 +   */
   1.969 +  get canRetry()
   1.970 +  {
   1.971 +    return this.state == nsIDM.DOWNLOAD_CANCELED ||
   1.972 +           this.state == nsIDM.DOWNLOAD_FAILED;
   1.973 +  },
   1.974 +
   1.975 +  /**
   1.976 +   * Returns the nsILocalFile for the download target.
   1.977 +   *
   1.978 +   * @throws if the native path is not valid.  This can happen if the same
   1.979 +   *         profile is used on different platforms, for example if a native
   1.980 +   *         Windows path is stored and then the item is accessed on a Mac.
   1.981 +   */
   1.982 +  get localFile()
   1.983 +  {
   1.984 +    return this._getFile(this.file);
   1.985 +  },
   1.986 +
   1.987 +  /**
   1.988 +   * Returns the nsILocalFile for the partially downloaded target.
   1.989 +   *
   1.990 +   * @throws if the native path is not valid.  This can happen if the same
   1.991 +   *         profile is used on different platforms, for example if a native
   1.992 +   *         Windows path is stored and then the item is accessed on a Mac.
   1.993 +   */
   1.994 +  get partFile()
   1.995 +  {
   1.996 +    return this._getFile(this.file + kPartialDownloadSuffix);
   1.997 +  },
   1.998 +
   1.999 +  /**
  1.1000 +   * Returns an nsILocalFile for aFilename. aFilename might be a file URL or
  1.1001 +   * a native path.
  1.1002 +   *
  1.1003 +   * @param aFilename the filename of the file to retrieve.
  1.1004 +   * @return an nsILocalFile for the file.
  1.1005 +   * @throws if the native path is not valid.  This can happen if the same
  1.1006 +   *         profile is used on different platforms, for example if a native
  1.1007 +   *         Windows path is stored and then the item is accessed on a Mac.
  1.1008 +   * @note This function makes no guarantees about the file's existence -
  1.1009 +   *       callers should check that the returned file exists.
  1.1010 +   */
  1.1011 +  _getFile: function DDI__getFile(aFilename)
  1.1012 +  {
  1.1013 +    // The download database may contain targets stored as file URLs or native
  1.1014 +    // paths.  This can still be true for previously stored items, even if new
  1.1015 +    // items are stored using their file URL.  See also bug 239948 comment 12.
  1.1016 +    if (aFilename.startsWith("file:")) {
  1.1017 +      // Assume the file URL we obtained from the downloads database or from the
  1.1018 +      // "spec" property of the target has the UTF-8 charset.
  1.1019 +      let fileUrl = NetUtil.newURI(aFilename).QueryInterface(Ci.nsIFileURL);
  1.1020 +      return fileUrl.file.clone().QueryInterface(Ci.nsILocalFile);
  1.1021 +    } else {
  1.1022 +      // The downloads database contains a native path.  Try to create a local
  1.1023 +      // file, though this may throw an exception if the path is invalid.
  1.1024 +      return new DownloadsLocalFileCtor(aFilename);
  1.1025 +    }
  1.1026 +  },
  1.1027 +
  1.1028 +  /**
  1.1029 +   * Open the target file for this download.
  1.1030 +   */
  1.1031 +  openLocalFile: function () {
  1.1032 +    this._download.launch().then(null, Cu.reportError);
  1.1033 +  },
  1.1034 +
  1.1035 +  /**
  1.1036 +   * Show the downloaded file in the system file manager.
  1.1037 +   */
  1.1038 +  showLocalFile: function DDI_showLocalFile() {
  1.1039 +    DownloadsCommon.showDownloadedFile(this.localFile);
  1.1040 +  },
  1.1041 +
  1.1042 +  /**
  1.1043 +   * Resumes the download if paused, pauses it if active.
  1.1044 +   * @throws if the download is not resumable or if has already done.
  1.1045 +   */
  1.1046 +  togglePauseResume: function DDI_togglePauseResume() {
  1.1047 +    if (this._download.stopped) {
  1.1048 +      this._download.start();
  1.1049 +    } else {
  1.1050 +      this._download.cancel();
  1.1051 +    }
  1.1052 +  },
  1.1053 +
  1.1054 +  /**
  1.1055 +   * Attempts to retry the download.
  1.1056 +   * @throws if we cannot.
  1.1057 +   */
  1.1058 +  retry: function DDI_retry() {
  1.1059 +    this._download.start();
  1.1060 +  },
  1.1061 +
  1.1062 +  /**
  1.1063 +   * Cancels the download.
  1.1064 +   */
  1.1065 +  cancel: function() {
  1.1066 +    this._download.cancel();
  1.1067 +    this._download.removePartialData().then(null, Cu.reportError);
  1.1068 +  },
  1.1069 +
  1.1070 +  /**
  1.1071 +   * Remove the download.
  1.1072 +   */
  1.1073 +  remove: function DDI_remove() {
  1.1074 +    Downloads.getList(Downloads.ALL)
  1.1075 +             .then(list => list.remove(this._download))
  1.1076 +             .then(() => this._download.finalize(true))
  1.1077 +             .then(null, Cu.reportError);
  1.1078 +  }
  1.1079 +};
  1.1080 +
  1.1081 +////////////////////////////////////////////////////////////////////////////////
  1.1082 +//// DownloadsViewPrototype
  1.1083 +
  1.1084 +/**
  1.1085 + * A prototype for an object that registers itself with DownloadsData as soon
  1.1086 + * as a view is registered with it.
  1.1087 + */
  1.1088 +const DownloadsViewPrototype = {
  1.1089 +  //////////////////////////////////////////////////////////////////////////////
  1.1090 +  //// Registration of views
  1.1091 +
  1.1092 +  /**
  1.1093 +   * Array of view objects that should be notified when the available status
  1.1094 +   * data changes.
  1.1095 +   *
  1.1096 +   * SUBCLASSES MUST OVERRIDE THIS PROPERTY.
  1.1097 +   */
  1.1098 +  _views: null,
  1.1099 +
  1.1100 +  /**
  1.1101 +   * Determines whether this view object is over the private or non-private
  1.1102 +   * downloads.
  1.1103 +   *
  1.1104 +   * SUBCLASSES MUST OVERRIDE THIS PROPERTY.
  1.1105 +   */
  1.1106 +  _isPrivate: false,
  1.1107 +
  1.1108 +  /**
  1.1109 +   * Adds an object to be notified when the available status data changes.
  1.1110 +   * The specified object is initialized with the currently available status.
  1.1111 +   *
  1.1112 +   * @param aView
  1.1113 +   *        View object to be added.  This reference must be
  1.1114 +   *        passed to removeView before termination.
  1.1115 +   */
  1.1116 +  addView: function DVP_addView(aView)
  1.1117 +  {
  1.1118 +    // Start receiving events when the first of our views is registered.
  1.1119 +    if (this._views.length == 0) {
  1.1120 +      if (this._isPrivate) {
  1.1121 +        PrivateDownloadsData.addView(this);
  1.1122 +      } else {
  1.1123 +        DownloadsData.addView(this);
  1.1124 +      }
  1.1125 +    }
  1.1126 +
  1.1127 +    this._views.push(aView);
  1.1128 +    this.refreshView(aView);
  1.1129 +  },
  1.1130 +
  1.1131 +  /**
  1.1132 +   * Updates the properties of an object previously added using addView.
  1.1133 +   *
  1.1134 +   * @param aView
  1.1135 +   *        View object to be updated.
  1.1136 +   */
  1.1137 +  refreshView: function DVP_refreshView(aView)
  1.1138 +  {
  1.1139 +    // Update immediately even if we are still loading data asynchronously.
  1.1140 +    // Subclasses must provide these two functions!
  1.1141 +    this._refreshProperties();
  1.1142 +    this._updateView(aView);
  1.1143 +  },
  1.1144 +
  1.1145 +  /**
  1.1146 +   * Removes an object previously added using addView.
  1.1147 +   *
  1.1148 +   * @param aView
  1.1149 +   *        View object to be removed.
  1.1150 +   */
  1.1151 +  removeView: function DVP_removeView(aView)
  1.1152 +  {
  1.1153 +    let index = this._views.indexOf(aView);
  1.1154 +    if (index != -1) {
  1.1155 +      this._views.splice(index, 1);
  1.1156 +    }
  1.1157 +
  1.1158 +    // Stop receiving events when the last of our views is unregistered.
  1.1159 +    if (this._views.length == 0) {
  1.1160 +      if (this._isPrivate) {
  1.1161 +        PrivateDownloadsData.removeView(this);
  1.1162 +      } else {
  1.1163 +        DownloadsData.removeView(this);
  1.1164 +      }
  1.1165 +    }
  1.1166 +  },
  1.1167 +
  1.1168 +  //////////////////////////////////////////////////////////////////////////////
  1.1169 +  //// Callback functions from DownloadsData
  1.1170 +
  1.1171 +  /**
  1.1172 +   * Indicates whether we are still loading downloads data asynchronously.
  1.1173 +   */
  1.1174 +  _loading: false,
  1.1175 +
  1.1176 +  /**
  1.1177 +   * Called before multiple downloads are about to be loaded.
  1.1178 +   */
  1.1179 +  onDataLoadStarting: function DVP_onDataLoadStarting()
  1.1180 +  {
  1.1181 +    this._loading = true;
  1.1182 +  },
  1.1183 +
  1.1184 +  /**
  1.1185 +   * Called after data loading finished.
  1.1186 +   */
  1.1187 +  onDataLoadCompleted: function DVP_onDataLoadCompleted()
  1.1188 +  {
  1.1189 +    this._loading = false;
  1.1190 +  },
  1.1191 +
  1.1192 +  /**
  1.1193 +   * Called when a new download data item is available, either during the
  1.1194 +   * asynchronous data load or when a new download is started.
  1.1195 +   *
  1.1196 +   * @param aDataItem
  1.1197 +   *        DownloadsDataItem object that was just added.
  1.1198 +   * @param aNewest
  1.1199 +   *        When true, indicates that this item is the most recent and should be
  1.1200 +   *        added in the topmost position.  This happens when a new download is
  1.1201 +   *        started.  When false, indicates that the item is the least recent
  1.1202 +   *        with regard to the items that have been already added. The latter
  1.1203 +   *        generally happens during the asynchronous data load.
  1.1204 +   *
  1.1205 +   * @note Subclasses should override this.
  1.1206 +   */
  1.1207 +  onDataItemAdded: function DVP_onDataItemAdded(aDataItem, aNewest)
  1.1208 +  {
  1.1209 +    throw Components.results.NS_ERROR_NOT_IMPLEMENTED;
  1.1210 +  },
  1.1211 +
  1.1212 +  /**
  1.1213 +   * Called when a data item is removed, ensures that the widget associated with
  1.1214 +   * the view item is removed from the user interface.
  1.1215 +   *
  1.1216 +   * @param aDataItem
  1.1217 +   *        DownloadsDataItem object that is being removed.
  1.1218 +   *
  1.1219 +   * @note Subclasses should override this.
  1.1220 +   */
  1.1221 +  onDataItemRemoved: function DVP_onDataItemRemoved(aDataItem)
  1.1222 +  {
  1.1223 +    throw Components.results.NS_ERROR_NOT_IMPLEMENTED;
  1.1224 +  },
  1.1225 +
  1.1226 +  /**
  1.1227 +   * Returns the view item associated with the provided data item for this view.
  1.1228 +   *
  1.1229 +   * @param aDataItem
  1.1230 +   *        DownloadsDataItem object for which the view item is requested.
  1.1231 +   *
  1.1232 +   * @return Object that can be used to notify item status events.
  1.1233 +   *
  1.1234 +   * @note Subclasses should override this.
  1.1235 +   */
  1.1236 +  getViewItem: function DID_getViewItem(aDataItem)
  1.1237 +  {
  1.1238 +    throw Components.results.NS_ERROR_NOT_IMPLEMENTED;
  1.1239 +  },
  1.1240 +
  1.1241 +  /**
  1.1242 +   * Private function used to refresh the internal properties being sent to
  1.1243 +   * each registered view.
  1.1244 +   *
  1.1245 +   * @note Subclasses should override this.
  1.1246 +   */
  1.1247 +  _refreshProperties: function DID_refreshProperties()
  1.1248 +  {
  1.1249 +    throw Components.results.NS_ERROR_NOT_IMPLEMENTED;
  1.1250 +  },
  1.1251 +
  1.1252 +  /**
  1.1253 +   * Private function used to refresh an individual view.
  1.1254 +   *
  1.1255 +   * @note Subclasses should override this.
  1.1256 +   */
  1.1257 +  _updateView: function DID_updateView()
  1.1258 +  {
  1.1259 +    throw Components.results.NS_ERROR_NOT_IMPLEMENTED;
  1.1260 +  }
  1.1261 +};
  1.1262 +
  1.1263 +////////////////////////////////////////////////////////////////////////////////
  1.1264 +//// DownloadsIndicatorData
  1.1265 +
  1.1266 +/**
  1.1267 + * This object registers itself with DownloadsData as a view, and transforms the
  1.1268 + * notifications it receives into overall status data, that is then broadcast to
  1.1269 + * the registered download status indicators.
  1.1270 + *
  1.1271 + * Note that using this object does not automatically start the Download Manager
  1.1272 + * service.  Consumers will see an empty list of downloads until the service is
  1.1273 + * actually started.  This is useful to display a neutral progress indicator in
  1.1274 + * the main browser window until the autostart timeout elapses.
  1.1275 + */
  1.1276 +function DownloadsIndicatorDataCtor(aPrivate) {
  1.1277 +  this._isPrivate = aPrivate;
  1.1278 +  this._views = [];
  1.1279 +}
  1.1280 +DownloadsIndicatorDataCtor.prototype = {
  1.1281 +  __proto__: DownloadsViewPrototype,
  1.1282 +
  1.1283 +  /**
  1.1284 +   * Removes an object previously added using addView.
  1.1285 +   *
  1.1286 +   * @param aView
  1.1287 +   *        DownloadsIndicatorView object to be removed.
  1.1288 +   */
  1.1289 +  removeView: function DID_removeView(aView)
  1.1290 +  {
  1.1291 +    DownloadsViewPrototype.removeView.call(this, aView);
  1.1292 +
  1.1293 +    if (this._views.length == 0) {
  1.1294 +      this._itemCount = 0;
  1.1295 +    }
  1.1296 +  },
  1.1297 +
  1.1298 +  //////////////////////////////////////////////////////////////////////////////
  1.1299 +  //// Callback functions from DownloadsData
  1.1300 +
  1.1301 +  /**
  1.1302 +   * Called after data loading finished.
  1.1303 +   */
  1.1304 +  onDataLoadCompleted: function DID_onDataLoadCompleted()
  1.1305 +  {
  1.1306 +    DownloadsViewPrototype.onDataLoadCompleted.call(this);
  1.1307 +    this._updateViews();
  1.1308 +  },
  1.1309 +
  1.1310 +  /**
  1.1311 +   * Called when a new download data item is available, either during the
  1.1312 +   * asynchronous data load or when a new download is started.
  1.1313 +   *
  1.1314 +   * @param aDataItem
  1.1315 +   *        DownloadsDataItem object that was just added.
  1.1316 +   * @param aNewest
  1.1317 +   *        When true, indicates that this item is the most recent and should be
  1.1318 +   *        added in the topmost position.  This happens when a new download is
  1.1319 +   *        started.  When false, indicates that the item is the least recent
  1.1320 +   *        with regard to the items that have been already added. The latter
  1.1321 +   *        generally happens during the asynchronous data load.
  1.1322 +   */
  1.1323 +  onDataItemAdded: function DID_onDataItemAdded(aDataItem, aNewest)
  1.1324 +  {
  1.1325 +    this._itemCount++;
  1.1326 +    this._updateViews();
  1.1327 +  },
  1.1328 +
  1.1329 +  /**
  1.1330 +   * Called when a data item is removed, ensures that the widget associated with
  1.1331 +   * the view item is removed from the user interface.
  1.1332 +   *
  1.1333 +   * @param aDataItem
  1.1334 +   *        DownloadsDataItem object that is being removed.
  1.1335 +   */
  1.1336 +  onDataItemRemoved: function DID_onDataItemRemoved(aDataItem)
  1.1337 +  {
  1.1338 +    this._itemCount--;
  1.1339 +    this._updateViews();
  1.1340 +  },
  1.1341 +
  1.1342 +  /**
  1.1343 +   * Returns the view item associated with the provided data item for this view.
  1.1344 +   *
  1.1345 +   * @param aDataItem
  1.1346 +   *        DownloadsDataItem object for which the view item is requested.
  1.1347 +   *
  1.1348 +   * @return Object that can be used to notify item status events.
  1.1349 +   */
  1.1350 +  getViewItem: function DID_getViewItem(aDataItem)
  1.1351 +  {
  1.1352 +    let data = this._isPrivate ? PrivateDownloadsIndicatorData
  1.1353 +                               : DownloadsIndicatorData;
  1.1354 +    return Object.freeze({
  1.1355 +      onStateChange: function DIVI_onStateChange(aOldState)
  1.1356 +      {
  1.1357 +        if (aDataItem.state == nsIDM.DOWNLOAD_FINISHED ||
  1.1358 +            aDataItem.state == nsIDM.DOWNLOAD_FAILED) {
  1.1359 +          data.attention = true;
  1.1360 +        }
  1.1361 +
  1.1362 +        // Since the state of a download changed, reset the estimated time left.
  1.1363 +        data._lastRawTimeLeft = -1;
  1.1364 +        data._lastTimeLeft = -1;
  1.1365 +
  1.1366 +        data._updateViews();
  1.1367 +      },
  1.1368 +      onProgressChange: function DIVI_onProgressChange()
  1.1369 +      {
  1.1370 +        data._updateViews();
  1.1371 +      }
  1.1372 +    });
  1.1373 +  },
  1.1374 +
  1.1375 +  //////////////////////////////////////////////////////////////////////////////
  1.1376 +  //// Propagation of properties to our views
  1.1377 +
  1.1378 +  // The following properties are updated by _refreshProperties and are then
  1.1379 +  // propagated to the views.  See _refreshProperties for details.
  1.1380 +  _hasDownloads: false,
  1.1381 +  _counter: "",
  1.1382 +  _percentComplete: -1,
  1.1383 +  _paused: false,
  1.1384 +
  1.1385 +  /**
  1.1386 +   * Indicates whether the download indicators should be highlighted.
  1.1387 +   */
  1.1388 +  set attention(aValue)
  1.1389 +  {
  1.1390 +    this._attention = aValue;
  1.1391 +    this._updateViews();
  1.1392 +    return aValue;
  1.1393 +  },
  1.1394 +  _attention: false,
  1.1395 +
  1.1396 +  /**
  1.1397 +   * Indicates whether the user is interacting with downloads, thus the
  1.1398 +   * attention indication should not be shown even if requested.
  1.1399 +   */
  1.1400 +  set attentionSuppressed(aValue)
  1.1401 +  {
  1.1402 +    this._attentionSuppressed = aValue;
  1.1403 +    this._attention = false;
  1.1404 +    this._updateViews();
  1.1405 +    return aValue;
  1.1406 +  },
  1.1407 +  _attentionSuppressed: false,
  1.1408 +
  1.1409 +  /**
  1.1410 +   * Computes aggregate values and propagates the changes to our views.
  1.1411 +   */
  1.1412 +  _updateViews: function DID_updateViews()
  1.1413 +  {
  1.1414 +    // Do not update the status indicators during batch loads of download items.
  1.1415 +    if (this._loading) {
  1.1416 +      return;
  1.1417 +    }
  1.1418 +
  1.1419 +    this._refreshProperties();
  1.1420 +    this._views.forEach(this._updateView, this);
  1.1421 +  },
  1.1422 +
  1.1423 +  /**
  1.1424 +   * Updates the specified view with the current aggregate values.
  1.1425 +   *
  1.1426 +   * @param aView
  1.1427 +   *        DownloadsIndicatorView object to be updated.
  1.1428 +   */
  1.1429 +  _updateView: function DID_updateView(aView)
  1.1430 +  {
  1.1431 +    aView.hasDownloads = this._hasDownloads;
  1.1432 +    aView.counter = this._counter;
  1.1433 +    aView.percentComplete = this._percentComplete;
  1.1434 +    aView.paused = this._paused;
  1.1435 +    aView.attention = this._attention && !this._attentionSuppressed;
  1.1436 +  },
  1.1437 +
  1.1438 +  //////////////////////////////////////////////////////////////////////////////
  1.1439 +  //// Property updating based on current download status
  1.1440 +
  1.1441 +  /**
  1.1442 +   * Number of download items that are available to be displayed.
  1.1443 +   */
  1.1444 +  _itemCount: 0,
  1.1445 +
  1.1446 +  /**
  1.1447 +   * Floating point value indicating the last number of seconds estimated until
  1.1448 +   * the longest download will finish.  We need to store this value so that we
  1.1449 +   * don't continuously apply smoothing if the actual download state has not
  1.1450 +   * changed.  This is set to -1 if the previous value is unknown.
  1.1451 +   */
  1.1452 +  _lastRawTimeLeft: -1,
  1.1453 +
  1.1454 +  /**
  1.1455 +   * Last number of seconds estimated until all in-progress downloads with a
  1.1456 +   * known size and speed will finish.  This value is stored to allow smoothing
  1.1457 +   * in case of small variations.  This is set to -1 if the previous value is
  1.1458 +   * unknown.
  1.1459 +   */
  1.1460 +  _lastTimeLeft: -1,
  1.1461 +
  1.1462 +  /**
  1.1463 +   * A generator function for the dataItems that this summary is currently
  1.1464 +   * interested in. This generator is passed off to summarizeDownloads in order
  1.1465 +   * to generate statistics about the dataItems we care about - in this case,
  1.1466 +   * it's all dataItems for active downloads.
  1.1467 +   */
  1.1468 +  _activeDataItems: function DID_activeDataItems()
  1.1469 +  {
  1.1470 +    let dataItems = this._isPrivate ? PrivateDownloadsData.dataItems
  1.1471 +                                    : DownloadsData.dataItems;
  1.1472 +    for each (let dataItem in dataItems) {
  1.1473 +      if (dataItem && dataItem.inProgress) {
  1.1474 +        yield dataItem;
  1.1475 +      }
  1.1476 +    }
  1.1477 +  },
  1.1478 +
  1.1479 +  /**
  1.1480 +   * Computes aggregate values based on the current state of downloads.
  1.1481 +   */
  1.1482 +  _refreshProperties: function DID_refreshProperties()
  1.1483 +  {
  1.1484 +    let summary =
  1.1485 +      DownloadsCommon.summarizeDownloads(this._activeDataItems());
  1.1486 +
  1.1487 +    // Determine if the indicator should be shown or get attention.
  1.1488 +    this._hasDownloads = (this._itemCount > 0);
  1.1489 +
  1.1490 +    // If all downloads are paused, show the progress indicator as paused.
  1.1491 +    this._paused = summary.numActive > 0 &&
  1.1492 +                   summary.numActive == summary.numPaused;
  1.1493 +
  1.1494 +    this._percentComplete = summary.percentComplete;
  1.1495 +
  1.1496 +    // Display the estimated time left, if present.
  1.1497 +    if (summary.rawTimeLeft == -1) {
  1.1498 +      // There are no downloads with a known time left.
  1.1499 +      this._lastRawTimeLeft = -1;
  1.1500 +      this._lastTimeLeft = -1;
  1.1501 +      this._counter = "";
  1.1502 +    } else {
  1.1503 +      // Compute the new time left only if state actually changed.
  1.1504 +      if (this._lastRawTimeLeft != summary.rawTimeLeft) {
  1.1505 +        this._lastRawTimeLeft = summary.rawTimeLeft;
  1.1506 +        this._lastTimeLeft = DownloadsCommon.smoothSeconds(summary.rawTimeLeft,
  1.1507 +                                                           this._lastTimeLeft);
  1.1508 +      }
  1.1509 +      this._counter = DownloadsCommon.formatTimeLeft(this._lastTimeLeft);
  1.1510 +    }
  1.1511 +  }
  1.1512 +};
  1.1513 +
  1.1514 +XPCOMUtils.defineLazyGetter(this, "PrivateDownloadsIndicatorData", function() {
  1.1515 +  return new DownloadsIndicatorDataCtor(true);
  1.1516 +});
  1.1517 +
  1.1518 +XPCOMUtils.defineLazyGetter(this, "DownloadsIndicatorData", function() {
  1.1519 +  return new DownloadsIndicatorDataCtor(false);
  1.1520 +});
  1.1521 +
  1.1522 +////////////////////////////////////////////////////////////////////////////////
  1.1523 +//// DownloadsSummaryData
  1.1524 +
  1.1525 +/**
  1.1526 + * DownloadsSummaryData is a view for DownloadsData that produces a summary
  1.1527 + * of all downloads after a certain exclusion point aNumToExclude. For example,
  1.1528 + * if there were 5 downloads in progress, and a DownloadsSummaryData was
  1.1529 + * constructed with aNumToExclude equal to 3, then that DownloadsSummaryData
  1.1530 + * would produce a summary of the last 2 downloads.
  1.1531 + *
  1.1532 + * @param aIsPrivate
  1.1533 + *        True if the browser window which owns the download button is a private
  1.1534 + *        window.
  1.1535 + * @param aNumToExclude
  1.1536 + *        The number of items to exclude from the summary, starting from the
  1.1537 + *        top of the list.
  1.1538 + */
  1.1539 +function DownloadsSummaryData(aIsPrivate, aNumToExclude) {
  1.1540 +  this._numToExclude = aNumToExclude;
  1.1541 +  // Since we can have multiple instances of DownloadsSummaryData, we
  1.1542 +  // override these values from the prototype so that each instance can be
  1.1543 +  // completely separated from one another.
  1.1544 +  this._loading = false;
  1.1545 +
  1.1546 +  this._dataItems = [];
  1.1547 +
  1.1548 +  // Floating point value indicating the last number of seconds estimated until
  1.1549 +  // the longest download will finish.  We need to store this value so that we
  1.1550 +  // don't continuously apply smoothing if the actual download state has not
  1.1551 +  // changed.  This is set to -1 if the previous value is unknown.
  1.1552 +  this._lastRawTimeLeft = -1;
  1.1553 +
  1.1554 +  // Last number of seconds estimated until all in-progress downloads with a
  1.1555 +  // known size and speed will finish.  This value is stored to allow smoothing
  1.1556 +  // in case of small variations.  This is set to -1 if the previous value is
  1.1557 +  // unknown.
  1.1558 +  this._lastTimeLeft = -1;
  1.1559 +
  1.1560 +  // The following properties are updated by _refreshProperties and are then
  1.1561 +  // propagated to the views.
  1.1562 +  this._showingProgress = false;
  1.1563 +  this._details = "";
  1.1564 +  this._description = "";
  1.1565 +  this._numActive = 0;
  1.1566 +  this._percentComplete = -1;
  1.1567 +
  1.1568 +  this._isPrivate = aIsPrivate;
  1.1569 +  this._views = [];
  1.1570 +}
  1.1571 +
  1.1572 +DownloadsSummaryData.prototype = {
  1.1573 +  __proto__: DownloadsViewPrototype,
  1.1574 +
  1.1575 +  /**
  1.1576 +   * Removes an object previously added using addView.
  1.1577 +   *
  1.1578 +   * @param aView
  1.1579 +   *        DownloadsSummary view to be removed.
  1.1580 +   */
  1.1581 +  removeView: function DSD_removeView(aView)
  1.1582 +  {
  1.1583 +    DownloadsViewPrototype.removeView.call(this, aView);
  1.1584 +
  1.1585 +    if (this._views.length == 0) {
  1.1586 +      // Clear out our collection of DownloadDataItems. If we ever have
  1.1587 +      // another view registered with us, this will get re-populated.
  1.1588 +      this._dataItems = [];
  1.1589 +    }
  1.1590 +  },
  1.1591 +
  1.1592 +  //////////////////////////////////////////////////////////////////////////////
  1.1593 +  //// Callback functions from DownloadsData - see the documentation in
  1.1594 +  //// DownloadsViewPrototype for more information on what these functions
  1.1595 +  //// are used for.
  1.1596 +
  1.1597 +  onDataLoadCompleted: function DSD_onDataLoadCompleted()
  1.1598 +  {
  1.1599 +    DownloadsViewPrototype.onDataLoadCompleted.call(this);
  1.1600 +    this._updateViews();
  1.1601 +  },
  1.1602 +
  1.1603 +  onDataItemAdded: function DSD_onDataItemAdded(aDataItem, aNewest)
  1.1604 +  {
  1.1605 +    if (aNewest) {
  1.1606 +      this._dataItems.unshift(aDataItem);
  1.1607 +    } else {
  1.1608 +      this._dataItems.push(aDataItem);
  1.1609 +    }
  1.1610 +
  1.1611 +    this._updateViews();
  1.1612 +  },
  1.1613 +
  1.1614 +  onDataItemRemoved: function DSD_onDataItemRemoved(aDataItem)
  1.1615 +  {
  1.1616 +    let itemIndex = this._dataItems.indexOf(aDataItem);
  1.1617 +    this._dataItems.splice(itemIndex, 1);
  1.1618 +    this._updateViews();
  1.1619 +  },
  1.1620 +
  1.1621 +  getViewItem: function DSD_getViewItem(aDataItem)
  1.1622 +  {
  1.1623 +    let self = this;
  1.1624 +    return Object.freeze({
  1.1625 +      onStateChange: function DIVI_onStateChange(aOldState)
  1.1626 +      {
  1.1627 +        // Since the state of a download changed, reset the estimated time left.
  1.1628 +        self._lastRawTimeLeft = -1;
  1.1629 +        self._lastTimeLeft = -1;
  1.1630 +        self._updateViews();
  1.1631 +      },
  1.1632 +      onProgressChange: function DIVI_onProgressChange()
  1.1633 +      {
  1.1634 +        self._updateViews();
  1.1635 +      }
  1.1636 +    });
  1.1637 +  },
  1.1638 +
  1.1639 +  //////////////////////////////////////////////////////////////////////////////
  1.1640 +  //// Propagation of properties to our views
  1.1641 +
  1.1642 +  /**
  1.1643 +   * Computes aggregate values and propagates the changes to our views.
  1.1644 +   */
  1.1645 +  _updateViews: function DSD_updateViews()
  1.1646 +  {
  1.1647 +    // Do not update the status indicators during batch loads of download items.
  1.1648 +    if (this._loading) {
  1.1649 +      return;
  1.1650 +    }
  1.1651 +
  1.1652 +    this._refreshProperties();
  1.1653 +    this._views.forEach(this._updateView, this);
  1.1654 +  },
  1.1655 +
  1.1656 +  /**
  1.1657 +   * Updates the specified view with the current aggregate values.
  1.1658 +   *
  1.1659 +   * @param aView
  1.1660 +   *        DownloadsIndicatorView object to be updated.
  1.1661 +   */
  1.1662 +  _updateView: function DSD_updateView(aView)
  1.1663 +  {
  1.1664 +    aView.showingProgress = this._showingProgress;
  1.1665 +    aView.percentComplete = this._percentComplete;
  1.1666 +    aView.description = this._description;
  1.1667 +    aView.details = this._details;
  1.1668 +  },
  1.1669 +
  1.1670 +  //////////////////////////////////////////////////////////////////////////////
  1.1671 +  //// Property updating based on current download status
  1.1672 +
  1.1673 +  /**
  1.1674 +   * A generator function for the dataItems that this summary is currently
  1.1675 +   * interested in. This generator is passed off to summarizeDownloads in order
  1.1676 +   * to generate statistics about the dataItems we care about - in this case,
  1.1677 +   * it's the dataItems in this._dataItems after the first few to exclude,
  1.1678 +   * which was set when constructing this DownloadsSummaryData instance.
  1.1679 +   */
  1.1680 +  _dataItemsForSummary: function DSD_dataItemsForSummary()
  1.1681 +  {
  1.1682 +    if (this._dataItems.length > 0) {
  1.1683 +      for (let i = this._numToExclude; i < this._dataItems.length; ++i) {
  1.1684 +        yield this._dataItems[i];
  1.1685 +      }
  1.1686 +    }
  1.1687 +  },
  1.1688 +
  1.1689 +  /**
  1.1690 +   * Computes aggregate values based on the current state of downloads.
  1.1691 +   */
  1.1692 +  _refreshProperties: function DSD_refreshProperties()
  1.1693 +  {
  1.1694 +    // Pre-load summary with default values.
  1.1695 +    let summary =
  1.1696 +      DownloadsCommon.summarizeDownloads(this._dataItemsForSummary());
  1.1697 +
  1.1698 +    this._description = DownloadsCommon.strings
  1.1699 +                                       .otherDownloads2(summary.numActive);
  1.1700 +    this._percentComplete = summary.percentComplete;
  1.1701 +
  1.1702 +    // If all downloads are paused, show the progress indicator as paused.
  1.1703 +    this._showingProgress = summary.numDownloading > 0 ||
  1.1704 +                            summary.numPaused > 0;
  1.1705 +
  1.1706 +    // Display the estimated time left, if present.
  1.1707 +    if (summary.rawTimeLeft == -1) {
  1.1708 +      // There are no downloads with a known time left.
  1.1709 +      this._lastRawTimeLeft = -1;
  1.1710 +      this._lastTimeLeft = -1;
  1.1711 +      this._details = "";
  1.1712 +    } else {
  1.1713 +      // Compute the new time left only if state actually changed.
  1.1714 +      if (this._lastRawTimeLeft != summary.rawTimeLeft) {
  1.1715 +        this._lastRawTimeLeft = summary.rawTimeLeft;
  1.1716 +        this._lastTimeLeft = DownloadsCommon.smoothSeconds(summary.rawTimeLeft,
  1.1717 +                                                           this._lastTimeLeft);
  1.1718 +      }
  1.1719 +      [this._details] = DownloadUtils.getDownloadStatusNoRate(
  1.1720 +        summary.totalTransferred, summary.totalSize, summary.slowestSpeed,
  1.1721 +        this._lastTimeLeft);
  1.1722 +    }
  1.1723 +  }
  1.1724 +}

mercurial