1/* -*- indent-tabs-mode: nil; js-indent-level: 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/. */
6
7"use strict";
8
9var EXPORTED_SYMBOLS = ["DownloadsCommon"];
10
11/**
12 * Handles the Downloads panel shared methods and data access.
13 *
14 * This file includes the following constructors and global objects:
15 *
16 * DownloadsCommon
17 * This object is exposed directly to the consumers of this JavaScript module,
18 * and provides shared methods for all the instances of the user interface.
19 *
20 * DownloadsData
21 * Retrieves the list of past and completed downloads from the underlying
22 * Downloads API data, and provides asynchronous notifications allowing
23 * to build a consistent view of the available data.
24 *
25 * DownloadsIndicatorData
26 * This object registers itself with DownloadsData as a view, and transforms the
27 * notifications it receives into overall status data, that is then broadcast to
28 * the registered download status indicators.
29 */
30
31// Globals
32
33const { XPCOMUtils } = ChromeUtils.import(
34  "resource://gre/modules/XPCOMUtils.jsm"
35);
36const { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm");
37
38XPCOMUtils.defineLazyModuleGetters(this, {
39  NetUtil: "resource://gre/modules/NetUtil.jsm",
40  PluralForm: "resource://gre/modules/PluralForm.jsm",
41  BrowserWindowTracker: "resource:///modules/BrowserWindowTracker.jsm",
42  DownloadHistory: "resource://gre/modules/DownloadHistory.jsm",
43  Downloads: "resource://gre/modules/Downloads.jsm",
44  DownloadUtils: "resource://gre/modules/DownloadUtils.jsm",
45  PlacesUtils: "resource://gre/modules/PlacesUtils.jsm",
46  PrivateBrowsingUtils: "resource://gre/modules/PrivateBrowsingUtils.jsm",
47});
48
49XPCOMUtils.defineLazyServiceGetters(this, {
50  gClipboardHelper: [
51    "@mozilla.org/widget/clipboardhelper;1",
52    "nsIClipboardHelper",
53  ],
54  gMIMEService: ["@mozilla.org/mime;1", "nsIMIMEService"],
55});
56
57XPCOMUtils.defineLazyGetter(this, "DownloadsLogger", () => {
58  let { ConsoleAPI } = ChromeUtils.import("resource://gre/modules/Console.jsm");
59  let consoleOptions = {
60    maxLogLevelPref: "browser.download.loglevel",
61    prefix: "Downloads",
62  };
63  return new ConsoleAPI(consoleOptions);
64});
65
66const kDownloadsStringBundleUrl =
67  "chrome://browser/locale/downloads/downloads.properties";
68
69const kDownloadsStringsRequiringFormatting = {
70  sizeWithUnits: true,
71  statusSeparator: true,
72  statusSeparatorBeforeNumber: true,
73};
74
75const kDownloadsStringsRequiringPluralForm = {
76  otherDownloads3: true,
77};
78
79const kMaxHistoryResultsForLimitedView = 42;
80
81const kPrefBranch = Services.prefs.getBranch("browser.download.");
82
83const kGenericContentTypes = [
84  "application/octet-stream",
85  "binary/octet-stream",
86  "application/unknown",
87];
88
89var PrefObserver = {
90  QueryInterface: ChromeUtils.generateQI([
91    "nsIObserver",
92    "nsISupportsWeakReference",
93  ]),
94  getPref(name) {
95    try {
96      switch (typeof this.prefs[name]) {
97        case "boolean":
98          return kPrefBranch.getBoolPref(name);
99      }
100    } catch (ex) {}
101    return this.prefs[name];
102  },
103  observe(aSubject, aTopic, aData) {
104    if (this.prefs.hasOwnProperty(aData)) {
105      delete this[aData];
106      this[aData] = this.getPref(aData);
107    }
108  },
109  register(prefs) {
110    this.prefs = prefs;
111    kPrefBranch.addObserver("", this, true);
112    for (let key in prefs) {
113      let name = key;
114      XPCOMUtils.defineLazyGetter(this, name, function() {
115        return PrefObserver.getPref(name);
116      });
117    }
118  },
119};
120
121PrefObserver.register({
122  // prefName: defaultValue
123  animateNotifications: true,
124  openInSystemViewerContextMenuItem: true,
125  alwaysOpenInSystemViewerContextMenuItem: true,
126});
127
128// DownloadsCommon
129
130/**
131 * This object is exposed directly to the consumers of this JavaScript module,
132 * and provides shared methods for all the instances of the user interface.
133 */
134var DownloadsCommon = {
135  // The following legacy constants are still returned by stateOfDownload, but
136  // individual properties of the Download object should normally be used.
137  DOWNLOAD_NOTSTARTED: -1,
138  DOWNLOAD_DOWNLOADING: 0,
139  DOWNLOAD_FINISHED: 1,
140  DOWNLOAD_FAILED: 2,
141  DOWNLOAD_CANCELED: 3,
142  DOWNLOAD_PAUSED: 4,
143  DOWNLOAD_BLOCKED_PARENTAL: 6,
144  DOWNLOAD_DIRTY: 8,
145  DOWNLOAD_BLOCKED_POLICY: 9,
146
147  // The following are the possible values of the "attention" property.
148  ATTENTION_NONE: "",
149  ATTENTION_SUCCESS: "success",
150  ATTENTION_WARNING: "warning",
151  ATTENTION_SEVERE: "severe",
152
153  /**
154   * Returns an object whose keys are the string names from the downloads string
155   * bundle, and whose values are either the translated strings or functions
156   * returning formatted strings.
157   */
158  get strings() {
159    let strings = {};
160    let sb = Services.strings.createBundle(kDownloadsStringBundleUrl);
161    for (let string of sb.getSimpleEnumeration()) {
162      let stringName = string.key;
163      if (stringName in kDownloadsStringsRequiringFormatting) {
164        strings[stringName] = function() {
165          // Convert "arguments" to a real array before calling into XPCOM.
166          return sb.formatStringFromName(stringName, Array.from(arguments));
167        };
168      } else if (stringName in kDownloadsStringsRequiringPluralForm) {
169        strings[stringName] = function(aCount) {
170          // Convert "arguments" to a real array before calling into XPCOM.
171          let formattedString = sb.formatStringFromName(
172            stringName,
173            Array.from(arguments)
174          );
175          return PluralForm.get(aCount, formattedString);
176        };
177      } else {
178        strings[stringName] = string.value;
179      }
180    }
181    delete this.strings;
182    return (this.strings = strings);
183  },
184
185  /**
186   * Indicates whether we should show visual notification on the indicator
187   * when a download event is triggered.
188   */
189  get animateNotifications() {
190    return PrefObserver.animateNotifications;
191  },
192
193  /**
194   * Indicates whether or not to show the 'Open in system viewer' context menu item when appropriate
195   */
196  get openInSystemViewerItemEnabled() {
197    return PrefObserver.openInSystemViewerContextMenuItem;
198  },
199
200  /**
201   * Indicates whether or not to show the 'Always open...' context menu item when appropriate
202   */
203  get alwaysOpenInSystemViewerItemEnabled() {
204    return PrefObserver.alwaysOpenInSystemViewerContextMenuItem;
205  },
206
207  /**
208   * Get access to one of the DownloadsData, PrivateDownloadsData, or
209   * HistoryDownloadsData objects, depending on the privacy status of the
210   * specified window and on whether history downloads should be included.
211   *
212   * @param [optional] window
213   *        The browser window which owns the download button.
214   *        If not given, the privacy status will be assumed as non-private.
215   * @param [optional] history
216   *        True to include history downloads when the window is public.
217   * @param [optional] privateAll
218   *        Whether to force the public downloads data to be returned together
219   *        with the private downloads data for a private window.
220   * @param [optional] limited
221   *        True to limit the amount of downloads returned to
222   *        `kMaxHistoryResultsForLimitedView`.
223   */
224  getData(window, history = false, privateAll = false, limited = false) {
225    let isPrivate =
226      window && PrivateBrowsingUtils.isContentWindowPrivate(window);
227    if (isPrivate && !privateAll) {
228      return PrivateDownloadsData;
229    }
230    if (history) {
231      if (isPrivate && privateAll) {
232        return LimitedPrivateHistoryDownloadData;
233      }
234      return limited ? LimitedHistoryDownloadsData : HistoryDownloadsData;
235    }
236    return DownloadsData;
237  },
238
239  /**
240   * Initializes the Downloads back-end and starts receiving events for both the
241   * private and non-private downloads data objects.
242   */
243  initializeAllDataLinks() {
244    DownloadsData.initializeDataLink();
245    PrivateDownloadsData.initializeDataLink();
246  },
247
248  /**
249   * Get access to one of the DownloadsIndicatorData or
250   * PrivateDownloadsIndicatorData objects, depending on the privacy status of
251   * the window in question.
252   */
253  getIndicatorData(aWindow) {
254    if (PrivateBrowsingUtils.isContentWindowPrivate(aWindow)) {
255      return PrivateDownloadsIndicatorData;
256    }
257    return DownloadsIndicatorData;
258  },
259
260  /**
261   * Returns a reference to the DownloadsSummaryData singleton - creating one
262   * in the process if one hasn't been instantiated yet.
263   *
264   * @param aWindow
265   *        The browser window which owns the download button.
266   * @param aNumToExclude
267   *        The number of items on the top of the downloads list to exclude
268   *        from the summary.
269   */
270  getSummary(aWindow, aNumToExclude) {
271    if (PrivateBrowsingUtils.isContentWindowPrivate(aWindow)) {
272      if (this._privateSummary) {
273        return this._privateSummary;
274      }
275      return (this._privateSummary = new DownloadsSummaryData(
276        true,
277        aNumToExclude
278      ));
279    }
280    if (this._summary) {
281      return this._summary;
282    }
283    return (this._summary = new DownloadsSummaryData(false, aNumToExclude));
284  },
285  _summary: null,
286  _privateSummary: null,
287
288  /**
289   * Returns the legacy state integer value for the provided Download object.
290   */
291  stateOfDownload(download) {
292    // Collapse state using the correct priority.
293    if (!download.stopped) {
294      return DownloadsCommon.DOWNLOAD_DOWNLOADING;
295    }
296    if (download.succeeded) {
297      return DownloadsCommon.DOWNLOAD_FINISHED;
298    }
299    if (download.error) {
300      if (download.error.becauseBlockedByParentalControls) {
301        return DownloadsCommon.DOWNLOAD_BLOCKED_PARENTAL;
302      }
303      if (download.error.becauseBlockedByReputationCheck) {
304        return DownloadsCommon.DOWNLOAD_DIRTY;
305      }
306      return DownloadsCommon.DOWNLOAD_FAILED;
307    }
308    if (download.canceled) {
309      if (download.hasPartialData) {
310        return DownloadsCommon.DOWNLOAD_PAUSED;
311      }
312      return DownloadsCommon.DOWNLOAD_CANCELED;
313    }
314    return DownloadsCommon.DOWNLOAD_NOTSTARTED;
315  },
316
317  /**
318   * Removes a Download object from both session and history downloads.
319   */
320  async deleteDownload(download) {
321    // Remove the associated history element first, if any, so that the views
322    // that combine history and session downloads won't resurrect the history
323    // download into the view just before it is deleted permanently.
324    try {
325      await PlacesUtils.history.remove(download.source.url);
326    } catch (ex) {
327      Cu.reportError(ex);
328    }
329    let list = await Downloads.getList(Downloads.ALL);
330    await list.remove(download);
331    await download.finalize(true);
332  },
333
334  /**
335   * Get a nsIMIMEInfo object for a download
336   */
337  getMimeInfo(download) {
338    if (!download.succeeded) {
339      return null;
340    }
341    let contentType = download.contentType;
342    let url = Cc["@mozilla.org/network/standard-url-mutator;1"]
343      .createInstance(Ci.nsIURIMutator)
344      .setSpec("http://example.com") // construct the URL
345      .setFilePath(download.target.path)
346      .finalize()
347      .QueryInterface(Ci.nsIURL);
348    let fileExtension = url.fileExtension;
349
350    // look at file extension if there's no contentType or it is generic
351    if (!contentType || kGenericContentTypes.includes(contentType)) {
352      try {
353        contentType = gMIMEService.getTypeFromExtension(fileExtension);
354      } catch (ex) {
355        DownloadsCommon.log(
356          "Cant get mimeType from file extension: ",
357          fileExtension
358        );
359      }
360    }
361    if (!(contentType || fileExtension)) {
362      return null;
363    }
364    let mimeInfo = null;
365    try {
366      mimeInfo = gMIMEService.getFromTypeAndExtension(
367        contentType || "",
368        fileExtension || ""
369      );
370    } catch (ex) {
371      DownloadsCommon.log(
372        "Can't get nsIMIMEInfo for contentType: ",
373        contentType,
374        "and fileExtension:",
375        fileExtension
376      );
377    }
378    return mimeInfo;
379  },
380
381  /**
382   * Confirm if the download exists on the filesystem and is a given mime-type
383   */
384  isFileOfType(download, mimeType) {
385    if (!(download.succeeded && download.target?.exists)) {
386      DownloadsCommon.log(
387        `isFileOfType returning false for mimeType: ${mimeType}, succeeded: ${download.succeeded}, exists: ${download.target?.exists}`
388      );
389      return false;
390    }
391    let mimeInfo = DownloadsCommon.getMimeInfo(download);
392    return mimeInfo?.type === mimeType.toLowerCase();
393  },
394
395  /**
396   * Copies the source URI of the given Download object to the clipboard.
397   */
398  copyDownloadLink(download) {
399    gClipboardHelper.copyString(download.source.url);
400  },
401
402  /**
403   * Given an iterable collection of Download objects, generates and returns
404   * statistics about that collection.
405   *
406   * @param downloads An iterable collection of Download objects.
407   *
408   * @return Object whose properties are the generated statistics. Currently,
409   *         we return the following properties:
410   *
411   *         numActive       : The total number of downloads.
412   *         numPaused       : The total number of paused downloads.
413   *         numDownloading  : The total number of downloads being downloaded.
414   *         totalSize       : The total size of all downloads once completed.
415   *         totalTransferred: The total amount of transferred data for these
416   *                           downloads.
417   *         slowestSpeed    : The slowest download rate.
418   *         rawTimeLeft     : The estimated time left for the downloads to
419   *                           complete.
420   *         percentComplete : The percentage of bytes successfully downloaded.
421   */
422  summarizeDownloads(downloads) {
423    let summary = {
424      numActive: 0,
425      numPaused: 0,
426      numDownloading: 0,
427      totalSize: 0,
428      totalTransferred: 0,
429      // slowestSpeed is Infinity so that we can use Math.min to
430      // find the slowest speed. We'll set this to 0 afterwards if
431      // it's still at Infinity by the time we're done iterating all
432      // download.
433      slowestSpeed: Infinity,
434      rawTimeLeft: -1,
435      percentComplete: -1,
436    };
437
438    for (let download of downloads) {
439      summary.numActive++;
440
441      if (!download.stopped) {
442        summary.numDownloading++;
443        if (download.hasProgress && download.speed > 0) {
444          let sizeLeft = download.totalBytes - download.currentBytes;
445          summary.rawTimeLeft = Math.max(
446            summary.rawTimeLeft,
447            sizeLeft / download.speed
448          );
449          summary.slowestSpeed = Math.min(summary.slowestSpeed, download.speed);
450        }
451      } else if (download.canceled && download.hasPartialData) {
452        summary.numPaused++;
453      }
454
455      // Only add to total values if we actually know the download size.
456      if (download.succeeded) {
457        summary.totalSize += download.target.size;
458        summary.totalTransferred += download.target.size;
459      } else if (download.hasProgress) {
460        summary.totalSize += download.totalBytes;
461        summary.totalTransferred += download.currentBytes;
462      }
463    }
464
465    if (summary.totalSize != 0) {
466      summary.percentComplete = Math.floor(
467        (summary.totalTransferred / summary.totalSize) * 100
468      );
469    }
470
471    if (summary.slowestSpeed == Infinity) {
472      summary.slowestSpeed = 0;
473    }
474
475    return summary;
476  },
477
478  /**
479   * If necessary, smooths the estimated number of seconds remaining for one
480   * or more downloads to complete.
481   *
482   * @param aSeconds
483   *        Current raw estimate on number of seconds left for one or more
484   *        downloads. This is a floating point value to help get sub-second
485   *        accuracy for current and future estimates.
486   */
487  smoothSeconds(aSeconds, aLastSeconds) {
488    // We apply an algorithm similar to the DownloadUtils.getTimeLeft function,
489    // though tailored to a single time estimation for all downloads.  We never
490    // apply something if the new value is less than half the previous value.
491    let shouldApplySmoothing = aLastSeconds >= 0 && aSeconds > aLastSeconds / 2;
492    if (shouldApplySmoothing) {
493      // Apply hysteresis to favor downward over upward swings.  Trust only 30%
494      // of the new value if lower, and 10% if higher (exponential smoothing).
495      let diff = aSeconds - aLastSeconds;
496      aSeconds = aLastSeconds + (diff < 0 ? 0.3 : 0.1) * diff;
497
498      // If the new time is similar, reuse something close to the last time
499      // left, but subtract a little to provide forward progress.
500      diff = aSeconds - aLastSeconds;
501      let diffPercent = (diff / aLastSeconds) * 100;
502      if (Math.abs(diff) < 5 || Math.abs(diffPercent) < 5) {
503        aSeconds = aLastSeconds - (diff < 0 ? 0.4 : 0.2);
504      }
505    }
506
507    // In the last few seconds of downloading, we are always subtracting and
508    // never adding to the time left.  Ensure that we never fall below one
509    // second left until all downloads are actually finished.
510    return (aLastSeconds = Math.max(aSeconds, 1));
511  },
512
513  /**
514   * Opens a downloaded file.
515   *
516   * @param downloadProperties
517   *        A Download object or the initial properties of a serialized download
518   * @param options.openWhere
519   *        Optional string indicating how to handle opening a download target file URI.
520   *        One of "window", "tab", "tabshifted".
521   * @param options.useSystemDefault
522   *        Optional value indicating how to handle launching this download,
523   *        this call only. Will override the associated mimeInfo.preferredAction
524   * @return {Promise}
525   * @resolves When the instruction to launch the file has been
526   *           successfully given to the operating system or handled internally
527   * @rejects  JavaScript exception if there was an error trying to launch
528   *           the file.
529   */
530  async openDownload(download, options) {
531    // some download objects got serialized and need reconstituting
532    if (typeof download.launch !== "function") {
533      download = await Downloads.createDownload(download);
534    }
535    return download.launch(options).catch(ex => Cu.reportError(ex));
536  },
537
538  /**
539   * Show a downloaded file in the system file manager.
540   *
541   * @param aFile
542   *        a downloaded file.
543   */
544  showDownloadedFile(aFile) {
545    if (!(aFile instanceof Ci.nsIFile)) {
546      throw new Error("aFile must be a nsIFile object");
547    }
548    try {
549      // Show the directory containing the file and select the file.
550      aFile.reveal();
551    } catch (ex) {
552      // If reveal fails for some reason (e.g., it's not implemented on unix
553      // or the file doesn't exist), try using the parent if we have it.
554      let parent = aFile.parent;
555      if (parent) {
556        this.showDirectory(parent);
557      }
558    }
559  },
560
561  /**
562   * Show the specified folder in the system file manager.
563   *
564   * @param aDirectory
565   *        a directory to be opened with system file manager.
566   */
567  showDirectory(aDirectory) {
568    if (!(aDirectory instanceof Ci.nsIFile)) {
569      throw new Error("aDirectory must be a nsIFile object");
570    }
571    try {
572      aDirectory.launch();
573    } catch (ex) {
574      // If launch fails (probably because it's not implemented), let
575      // the OS handler try to open the directory.
576      Cc["@mozilla.org/uriloader/external-protocol-service;1"]
577        .getService(Ci.nsIExternalProtocolService)
578        .loadURI(
579          NetUtil.newURI(aDirectory),
580          Services.scriptSecurityManager.getSystemPrincipal()
581        );
582    }
583  },
584
585  /**
586   * Displays an alert message box which asks the user if they want to
587   * unblock the downloaded file or not.
588   *
589   * @param options
590   *        An object with the following properties:
591   *        {
592   *          verdict:
593   *            The detailed reason why the download was blocked, according to
594   *            the "Downloads.Error.BLOCK_VERDICT_" constants. If an unknown
595   *            reason is specified, "Downloads.Error.BLOCK_VERDICT_MALWARE" is
596   *            assumed.
597   *          window:
598   *            The window with which this action is associated.
599   *          dialogType:
600   *            String that determines which actions are available:
601   *             - "unblock" to offer just "unblock".
602   *             - "chooseUnblock" to offer "unblock" and "confirmBlock".
603   *             - "chooseOpen" to offer "open" and "confirmBlock".
604   *        }
605   *
606   * @return {Promise}
607   * @resolves String representing the action that should be executed:
608   *            - "open" to allow the download and open the file.
609   *            - "unblock" to allow the download without opening the file.
610   *            - "confirmBlock" to delete the blocked data permanently.
611   *            - "cancel" to do nothing and cancel the operation.
612   */
613  async confirmUnblockDownload({ verdict, window, dialogType }) {
614    let s = DownloadsCommon.strings;
615
616    // All the dialogs have an action button and a cancel button, while only
617    // some of them have an additonal button to remove the file. The cancel
618    // button must always be the one at BUTTON_POS_1 because this is the value
619    // returned by confirmEx when using ESC or closing the dialog (bug 345067).
620    let title = s.unblockHeaderUnblock;
621    let firstButtonText = s.unblockButtonUnblock;
622    let firstButtonAction = "unblock";
623    let buttonFlags =
624      Ci.nsIPrompt.BUTTON_TITLE_IS_STRING * Ci.nsIPrompt.BUTTON_POS_0 +
625      Ci.nsIPrompt.BUTTON_TITLE_CANCEL * Ci.nsIPrompt.BUTTON_POS_1;
626
627    switch (dialogType) {
628      case "unblock":
629        // Use only the unblock action. The default is to cancel.
630        buttonFlags += Ci.nsIPrompt.BUTTON_POS_1_DEFAULT;
631        break;
632      case "chooseUnblock":
633        // Use the unblock and remove file actions. The default is remove file.
634        buttonFlags +=
635          Ci.nsIPrompt.BUTTON_TITLE_IS_STRING * Ci.nsIPrompt.BUTTON_POS_2 +
636          Ci.nsIPrompt.BUTTON_POS_2_DEFAULT;
637        break;
638      case "chooseOpen":
639        // Use the unblock and open file actions. The default is open file.
640        title = s.unblockHeaderOpen;
641        firstButtonText = s.unblockButtonOpen;
642        firstButtonAction = "open";
643        buttonFlags +=
644          Ci.nsIPrompt.BUTTON_TITLE_IS_STRING * Ci.nsIPrompt.BUTTON_POS_2 +
645          Ci.nsIPrompt.BUTTON_POS_0_DEFAULT;
646        break;
647      default:
648        Cu.reportError("Unexpected dialog type: " + dialogType);
649        return "cancel";
650    }
651
652    let message;
653    switch (verdict) {
654      case Downloads.Error.BLOCK_VERDICT_UNCOMMON:
655        message = s.unblockTypeUncommon2;
656        break;
657      case Downloads.Error.BLOCK_VERDICT_POTENTIALLY_UNWANTED:
658        message = s.unblockTypePotentiallyUnwanted2;
659        break;
660      default:
661        // Assume Downloads.Error.BLOCK_VERDICT_MALWARE
662        message = s.unblockTypeMalware;
663        break;
664    }
665    message += "\n\n" + s.unblockTip2;
666
667    Services.ww.registerNotification(function onOpen(subj, topic) {
668      if (topic == "domwindowopened" && subj instanceof Ci.nsIDOMWindow) {
669        // Make sure to listen for "DOMContentLoaded" because it is fired
670        // before the "load" event.
671        subj.addEventListener(
672          "DOMContentLoaded",
673          function() {
674            if (
675              subj.document.documentURI ==
676              "chrome://global/content/commonDialog.xhtml"
677            ) {
678              Services.ww.unregisterNotification(onOpen);
679              let dialog = subj.document.getElementById("commonDialog");
680              if (dialog) {
681                // Change the dialog to use a warning icon.
682                dialog.classList.add("alert-dialog");
683              }
684            }
685          },
686          { once: true }
687        );
688      }
689    });
690
691    let rv = Services.prompt.confirmEx(
692      window,
693      title,
694      message,
695      buttonFlags,
696      firstButtonText,
697      null,
698      s.unblockButtonConfirmBlock,
699      null,
700      {}
701    );
702    return [firstButtonAction, "cancel", "confirmBlock"][rv];
703  },
704};
705
706XPCOMUtils.defineLazyGetter(DownloadsCommon, "log", () => {
707  return DownloadsLogger.log.bind(DownloadsLogger);
708});
709XPCOMUtils.defineLazyGetter(DownloadsCommon, "error", () => {
710  return DownloadsLogger.error.bind(DownloadsLogger);
711});
712
713/**
714 * Returns true if we are executing on Windows Vista or a later version.
715 */
716XPCOMUtils.defineLazyGetter(DownloadsCommon, "isWinVistaOrHigher", function() {
717  let os = Services.appinfo.OS;
718  if (os != "WINNT") {
719    return false;
720  }
721  return parseFloat(Services.sysinfo.getProperty("version")) >= 6;
722});
723
724// DownloadsData
725
726/**
727 * Retrieves the list of past and completed downloads from the underlying
728 * Downloads API data, and provides asynchronous notifications allowing to
729 * build a consistent view of the available data.
730 *
731 * Note that using this object does not automatically initialize the list of
732 * downloads. This is useful to display a neutral progress indicator in
733 * the main browser window until the autostart timeout elapses.
734 *
735 * This powers the DownloadsData, PrivateDownloadsData, and HistoryDownloadsData
736 * singleton objects.
737 */
738function DownloadsDataCtor({ isPrivate, isHistory, maxHistoryResults } = {}) {
739  this._isPrivate = !!isPrivate;
740
741  // Contains all the available Download objects and their integer state.
742  this.oldDownloadStates = new Map();
743
744  // For the history downloads list we don't need to register this as a view,
745  // but we have to ensure that the DownloadsData object is initialized before
746  // we register more views. This ensures that the view methods of DownloadsData
747  // are invoked before those of views registered on HistoryDownloadsData,
748  // allowing the endTime property to be set correctly.
749  if (isHistory) {
750    if (isPrivate) {
751      PrivateDownloadsData.initializeDataLink();
752    }
753    DownloadsData.initializeDataLink();
754    this._promiseList = DownloadsData._promiseList.then(() => {
755      // For history downloads in Private Browsing mode, we'll fetch the combined
756      // list of public and private downloads.
757      return DownloadHistory.getList({
758        type: isPrivate ? Downloads.ALL : Downloads.PUBLIC,
759        maxHistoryResults,
760      });
761    });
762    return;
763  }
764
765  // This defines "initializeDataLink" and "_promiseList" synchronously, then
766  // continues execution only when "initializeDataLink" is called, allowing the
767  // underlying data to be loaded only when actually needed.
768  this._promiseList = (async () => {
769    await new Promise(resolve => (this.initializeDataLink = resolve));
770    let list = await Downloads.getList(
771      isPrivate ? Downloads.PRIVATE : Downloads.PUBLIC
772    );
773    await list.addView(this);
774    return list;
775  })();
776}
777
778DownloadsDataCtor.prototype = {
779  /**
780   * Starts receiving events for current downloads.
781   */
782  initializeDataLink() {},
783
784  /**
785   * Promise resolved with the underlying DownloadList object once we started
786   * receiving events for current downloads.
787   */
788  _promiseList: null,
789
790  /**
791   * Iterator for all the available Download objects. This is empty until the
792   * data has been loaded using the JavaScript API for downloads.
793   */
794  get downloads() {
795    return this.oldDownloadStates.keys();
796  },
797
798  /**
799   * True if there are finished downloads that can be removed from the list.
800   */
801  get canRemoveFinished() {
802    for (let download of this.downloads) {
803      // Stopped, paused, and failed downloads with partial data are removed.
804      if (download.stopped && !(download.canceled && download.hasPartialData)) {
805        return true;
806      }
807    }
808    return false;
809  },
810
811  /**
812   * Asks the back-end to remove finished downloads from the list. This method
813   * is only called after the data link has been initialized.
814   */
815  removeFinished() {
816    Downloads.getList(this._isPrivate ? Downloads.PRIVATE : Downloads.PUBLIC)
817      .then(list => list.removeFinished())
818      .catch(Cu.reportError);
819    let indicatorData = this._isPrivate
820      ? PrivateDownloadsIndicatorData
821      : DownloadsIndicatorData;
822    indicatorData.attention = DownloadsCommon.ATTENTION_NONE;
823  },
824
825  // Integration with the asynchronous Downloads back-end
826
827  onDownloadAdded(download) {
828    // Download objects do not store the end time of downloads, as the Downloads
829    // API does not need to persist this information for all platforms. Once a
830    // download terminates on a Desktop browser, it becomes a history download,
831    // for which the end time is stored differently, as a Places annotation.
832    download.endTime = Date.now();
833
834    this.oldDownloadStates.set(
835      download,
836      DownloadsCommon.stateOfDownload(download)
837    );
838    if (download.error?.becauseBlockedByReputationCheck) {
839      this._notifyDownloadEvent("error");
840    }
841  },
842
843  onDownloadChanged(download) {
844    let oldState = this.oldDownloadStates.get(download);
845    let newState = DownloadsCommon.stateOfDownload(download);
846    this.oldDownloadStates.set(download, newState);
847
848    if (oldState != newState) {
849      if (
850        download.succeeded ||
851        (download.canceled && !download.hasPartialData) ||
852        download.error
853      ) {
854        // Store the end time that may be displayed by the views.
855        download.endTime = Date.now();
856
857        // This state transition code should actually be located in a Downloads
858        // API module (bug 941009).
859        DownloadHistory.updateMetaData(download).catch(Cu.reportError);
860      }
861
862      if (
863        download.succeeded ||
864        (download.error && download.error.becauseBlocked)
865      ) {
866        this._notifyDownloadEvent("finish");
867      }
868    }
869
870    if (!download.newDownloadNotified) {
871      download.newDownloadNotified = true;
872      this._notifyDownloadEvent("start");
873    }
874  },
875
876  onDownloadRemoved(download) {
877    this.oldDownloadStates.delete(download);
878  },
879
880  // Registration of views
881
882  /**
883   * Adds an object to be notified when the available download data changes.
884   * The specified object is initialized with the currently available downloads.
885   *
886   * @param aView
887   *        DownloadsView object to be added.  This reference must be passed to
888   *        removeView before termination.
889   */
890  addView(aView) {
891    this._promiseList.then(list => list.addView(aView)).catch(Cu.reportError);
892  },
893
894  /**
895   * Removes an object previously added using addView.
896   *
897   * @param aView
898   *        DownloadsView object to be removed.
899   */
900  removeView(aView) {
901    this._promiseList
902      .then(list => list.removeView(aView))
903      .catch(Cu.reportError);
904  },
905
906  // Notifications sent to the most recent browser window only
907
908  /**
909   * Set to true after the first download causes the downloads panel to be
910   * displayed.
911   */
912  get panelHasShownBefore() {
913    try {
914      return Services.prefs.getBoolPref("browser.download.panel.shown");
915    } catch (ex) {}
916    return false;
917  },
918
919  set panelHasShownBefore(aValue) {
920    Services.prefs.setBoolPref("browser.download.panel.shown", aValue);
921  },
922
923  /**
924   * Displays a new or finished download notification in the most recent browser
925   * window, if one is currently available with the required privacy type.
926   *
927   * @param aType
928   *        Set to "start" for new downloads, "finish" for completed downloads,
929   *        "error" for downloads that failed and need attention
930   */
931  _notifyDownloadEvent(aType) {
932    DownloadsCommon.log(
933      "Attempting to notify that a new download has started or finished."
934    );
935
936    // Show the panel in the most recent browser window, if present.
937    let browserWin = BrowserWindowTracker.getTopWindow({
938      private: this._isPrivate,
939    });
940    if (!browserWin) {
941      return;
942    }
943
944    let shouldOpenDownloadsPanel =
945      aType == "start" &&
946      Services.prefs.getBoolPref(
947        "browser.download.improvements_to_download_panel"
948      ) &&
949      DownloadsCommon.summarizeDownloads(this.downloads).numDownloading <= 1;
950
951    if (
952      this.panelHasShownBefore &&
953      aType != "error" &&
954      !shouldOpenDownloadsPanel
955    ) {
956      // For new downloads after the first one, don't show the panel
957      // automatically, but provide a visible notification in the topmost
958      // browser window, if the status indicator is already visible.
959      DownloadsCommon.log("Showing new download notification.");
960      browserWin.DownloadsIndicatorView.showEventNotification(aType);
961      return;
962    }
963    this.panelHasShownBefore = true;
964    browserWin.DownloadsPanel.showPanel();
965  },
966};
967
968XPCOMUtils.defineLazyGetter(this, "HistoryDownloadsData", function() {
969  return new DownloadsDataCtor({ isHistory: true });
970});
971
972XPCOMUtils.defineLazyGetter(this, "LimitedHistoryDownloadsData", function() {
973  return new DownloadsDataCtor({
974    isHistory: true,
975    maxHistoryResults: kMaxHistoryResultsForLimitedView,
976  });
977});
978
979XPCOMUtils.defineLazyGetter(
980  this,
981  "LimitedPrivateHistoryDownloadData",
982  function() {
983    return new DownloadsDataCtor({
984      isPrivate: true,
985      isHistory: true,
986      maxHistoryResults: kMaxHistoryResultsForLimitedView,
987    });
988  }
989);
990
991XPCOMUtils.defineLazyGetter(this, "PrivateDownloadsData", function() {
992  return new DownloadsDataCtor({ isPrivate: true });
993});
994
995XPCOMUtils.defineLazyGetter(this, "DownloadsData", function() {
996  return new DownloadsDataCtor();
997});
998
999// DownloadsViewPrototype
1000
1001/**
1002 * A prototype for an object that registers itself with DownloadsData as soon
1003 * as a view is registered with it.
1004 */
1005const DownloadsViewPrototype = {
1006  /**
1007   * Contains all the available Download objects and their current state value.
1008   *
1009   * SUBCLASSES MUST OVERRIDE THIS PROPERTY.
1010   */
1011  _oldDownloadStates: null,
1012
1013  // Registration of views
1014
1015  /**
1016   * Array of view objects that should be notified when the available status
1017   * data changes.
1018   *
1019   * SUBCLASSES MUST OVERRIDE THIS PROPERTY.
1020   */
1021  _views: null,
1022
1023  /**
1024   * Determines whether this view object is over the private or non-private
1025   * downloads.
1026   *
1027   * SUBCLASSES MUST OVERRIDE THIS PROPERTY.
1028   */
1029  _isPrivate: false,
1030
1031  /**
1032   * Adds an object to be notified when the available status data changes.
1033   * The specified object is initialized with the currently available status.
1034   *
1035   * @param aView
1036   *        View object to be added.  This reference must be
1037   *        passed to removeView before termination.
1038   */
1039  addView(aView) {
1040    // Start receiving events when the first of our views is registered.
1041    if (!this._views.length) {
1042      if (this._isPrivate) {
1043        PrivateDownloadsData.addView(this);
1044      } else {
1045        DownloadsData.addView(this);
1046      }
1047    }
1048
1049    this._views.push(aView);
1050    this.refreshView(aView);
1051  },
1052
1053  /**
1054   * Updates the properties of an object previously added using addView.
1055   *
1056   * @param aView
1057   *        View object to be updated.
1058   */
1059  refreshView(aView) {
1060    // Update immediately even if we are still loading data asynchronously.
1061    // Subclasses must provide these two functions!
1062    this._refreshProperties();
1063    this._updateView(aView);
1064  },
1065
1066  /**
1067   * Removes an object previously added using addView.
1068   *
1069   * @param aView
1070   *        View object to be removed.
1071   */
1072  removeView(aView) {
1073    let index = this._views.indexOf(aView);
1074    if (index != -1) {
1075      this._views.splice(index, 1);
1076    }
1077
1078    // Stop receiving events when the last of our views is unregistered.
1079    if (!this._views.length) {
1080      if (this._isPrivate) {
1081        PrivateDownloadsData.removeView(this);
1082      } else {
1083        DownloadsData.removeView(this);
1084      }
1085    }
1086  },
1087
1088  // Callback functions from DownloadList
1089
1090  /**
1091   * Indicates whether we are still loading downloads data asynchronously.
1092   */
1093  _loading: false,
1094
1095  /**
1096   * Called before multiple downloads are about to be loaded.
1097   */
1098  onDownloadBatchStarting() {
1099    this._loading = true;
1100  },
1101
1102  /**
1103   * Called after data loading finished.
1104   */
1105  onDownloadBatchEnded() {
1106    this._loading = false;
1107    this._updateViews();
1108  },
1109
1110  /**
1111   * Called when a new download data item is available, either during the
1112   * asynchronous data load or when a new download is started.
1113   *
1114   * @param download
1115   *        Download object that was just added.
1116   *
1117   * @note Subclasses should override this and still call the base method.
1118   */
1119  onDownloadAdded(download) {
1120    this._oldDownloadStates.set(
1121      download,
1122      DownloadsCommon.stateOfDownload(download)
1123    );
1124  },
1125
1126  /**
1127   * Called when the overall state of a Download has changed. In particular,
1128   * this is called only once when the download succeeds or is blocked
1129   * permanently, and is never called if only the current progress changed.
1130   *
1131   * The onDownloadChanged notification will always be sent afterwards.
1132   *
1133   * @note Subclasses should override this.
1134   */
1135  onDownloadStateChanged(download) {
1136    throw Components.Exception("", Cr.NS_ERROR_NOT_IMPLEMENTED);
1137  },
1138
1139  /**
1140   * Called every time any state property of a Download may have changed,
1141   * including progress properties.
1142   *
1143   * Note that progress notification changes are throttled at the Downloads.jsm
1144   * API level, and there is no throttling mechanism in the front-end.
1145   *
1146   * @note Subclasses should override this and still call the base method.
1147   */
1148  onDownloadChanged(download) {
1149    let oldState = this._oldDownloadStates.get(download);
1150    let newState = DownloadsCommon.stateOfDownload(download);
1151    this._oldDownloadStates.set(download, newState);
1152
1153    if (oldState != newState) {
1154      this.onDownloadStateChanged(download);
1155    }
1156  },
1157
1158  /**
1159   * Called when a data item is removed, ensures that the widget associated with
1160   * the view item is removed from the user interface.
1161   *
1162   * @param download
1163   *        Download object that is being removed.
1164   *
1165   * @note Subclasses should override this.
1166   */
1167  onDownloadRemoved(download) {
1168    throw Components.Exception("", Cr.NS_ERROR_NOT_IMPLEMENTED);
1169  },
1170
1171  /**
1172   * Private function used to refresh the internal properties being sent to
1173   * each registered view.
1174   *
1175   * @note Subclasses should override this.
1176   */
1177  _refreshProperties() {
1178    throw Components.Exception("", Cr.NS_ERROR_NOT_IMPLEMENTED);
1179  },
1180
1181  /**
1182   * Private function used to refresh an individual view.
1183   *
1184   * @note Subclasses should override this.
1185   */
1186  _updateView() {
1187    throw Components.Exception("", Cr.NS_ERROR_NOT_IMPLEMENTED);
1188  },
1189
1190  /**
1191   * Computes aggregate values and propagates the changes to our views.
1192   */
1193  _updateViews() {
1194    // Do not update the status indicators during batch loads of download items.
1195    if (this._loading) {
1196      return;
1197    }
1198
1199    this._refreshProperties();
1200    this._views.forEach(this._updateView, this);
1201  },
1202};
1203
1204// DownloadsIndicatorData
1205
1206/**
1207 * This object registers itself with DownloadsData as a view, and transforms the
1208 * notifications it receives into overall status data, that is then broadcast to
1209 * the registered download status indicators.
1210 *
1211 * Note that using this object does not automatically start the Download Manager
1212 * service.  Consumers will see an empty list of downloads until the service is
1213 * actually started.  This is useful to display a neutral progress indicator in
1214 * the main browser window until the autostart timeout elapses.
1215 */
1216function DownloadsIndicatorDataCtor(aPrivate) {
1217  this._oldDownloadStates = new WeakMap();
1218  this._isPrivate = aPrivate;
1219  this._views = [];
1220}
1221DownloadsIndicatorDataCtor.prototype = {
1222  __proto__: DownloadsViewPrototype,
1223
1224  /**
1225   * Removes an object previously added using addView.
1226   *
1227   * @param aView
1228   *        DownloadsIndicatorView object to be removed.
1229   */
1230  removeView(aView) {
1231    DownloadsViewPrototype.removeView.call(this, aView);
1232
1233    if (!this._views.length) {
1234      this._itemCount = 0;
1235    }
1236  },
1237
1238  onDownloadAdded(download) {
1239    DownloadsViewPrototype.onDownloadAdded.call(this, download);
1240    this._itemCount++;
1241    this._updateViews();
1242  },
1243
1244  onDownloadStateChanged(download) {
1245    if (
1246      !download.succeeded &&
1247      download.error &&
1248      download.error.reputationCheckVerdict
1249    ) {
1250      switch (download.error.reputationCheckVerdict) {
1251        case Downloads.Error.BLOCK_VERDICT_UNCOMMON: // fall-through
1252        case Downloads.Error.BLOCK_VERDICT_POTENTIALLY_UNWANTED:
1253        case Downloads.Error.BLOCK_VERDICT_INSECURE:
1254          // Existing higher level attention indication trumps ATTENTION_WARNING.
1255          if (this._attention != DownloadsCommon.ATTENTION_SEVERE) {
1256            this.attention = DownloadsCommon.ATTENTION_WARNING;
1257          }
1258          break;
1259        case Downloads.Error.BLOCK_VERDICT_MALWARE:
1260          this.attention = DownloadsCommon.ATTENTION_SEVERE;
1261          break;
1262        default:
1263          this.attention = DownloadsCommon.ATTENTION_SEVERE;
1264          Cu.reportError(
1265            "Unknown reputation verdict: " +
1266              download.error.reputationCheckVerdict
1267          );
1268      }
1269    } else if (download.succeeded) {
1270      // Existing higher level attention indication trumps ATTENTION_SUCCESS.
1271      if (
1272        this._attention != DownloadsCommon.ATTENTION_SEVERE &&
1273        this._attention != DownloadsCommon.ATTENTION_WARNING
1274      ) {
1275        this.attention = DownloadsCommon.ATTENTION_SUCCESS;
1276      }
1277    } else if (download.error) {
1278      // Existing higher level attention indication trumps ATTENTION_WARNING.
1279      if (this._attention != DownloadsCommon.ATTENTION_SEVERE) {
1280        this.attention = DownloadsCommon.ATTENTION_WARNING;
1281      }
1282    }
1283  },
1284
1285  onDownloadChanged(download) {
1286    DownloadsViewPrototype.onDownloadChanged.call(this, download);
1287    this._updateViews();
1288  },
1289
1290  onDownloadRemoved(download) {
1291    this._itemCount--;
1292    this._updateViews();
1293  },
1294
1295  // Propagation of properties to our views
1296
1297  // The following properties are updated by _refreshProperties and are then
1298  // propagated to the views.  See _refreshProperties for details.
1299  _hasDownloads: false,
1300  _percentComplete: -1,
1301
1302  /**
1303   * Indicates whether the download indicators should be highlighted.
1304   */
1305  set attention(aValue) {
1306    this._attention = aValue;
1307    this._updateViews();
1308  },
1309  _attention: DownloadsCommon.ATTENTION_NONE,
1310
1311  /**
1312   * Indicates whether the user is interacting with downloads, thus the
1313   * attention indication should not be shown even if requested.
1314   */
1315  set attentionSuppressed(aValue) {
1316    this._attentionSuppressed = aValue;
1317    this._attention = DownloadsCommon.ATTENTION_NONE;
1318    this._updateViews();
1319  },
1320  _attentionSuppressed: false,
1321
1322  /**
1323   * Updates the specified view with the current aggregate values.
1324   *
1325   * @param aView
1326   *        DownloadsIndicatorView object to be updated.
1327   */
1328  _updateView(aView) {
1329    aView.hasDownloads = this._hasDownloads;
1330    aView.percentComplete = this._percentComplete;
1331    aView.attention = this._attentionSuppressed
1332      ? DownloadsCommon.ATTENTION_NONE
1333      : this._attention;
1334  },
1335
1336  // Property updating based on current download status
1337
1338  /**
1339   * Number of download items that are available to be displayed.
1340   */
1341  _itemCount: 0,
1342
1343  /**
1344   * A generator function for the Download objects this summary is currently
1345   * interested in. This generator is passed off to summarizeDownloads in order
1346   * to generate statistics about the downloads we care about - in this case,
1347   * it's all active downloads.
1348   */
1349  *_activeDownloads() {
1350    let downloads = this._isPrivate
1351      ? PrivateDownloadsData.downloads
1352      : DownloadsData.downloads;
1353    for (let download of downloads) {
1354      if (!download.stopped || (download.canceled && download.hasPartialData)) {
1355        yield download;
1356      }
1357    }
1358  },
1359
1360  /**
1361   * Computes aggregate values based on the current state of downloads.
1362   */
1363  _refreshProperties() {
1364    let summary = DownloadsCommon.summarizeDownloads(this._activeDownloads());
1365
1366    // Determine if the indicator should be shown or get attention.
1367    this._hasDownloads = this._itemCount > 0;
1368
1369    // Always show a progress bar if there are downloads in progress.
1370    if (summary.percentComplete >= 0) {
1371      this._percentComplete = summary.percentComplete;
1372    } else if (summary.numDownloading > 0) {
1373      this._percentComplete = 0;
1374    } else {
1375      this._percentComplete = -1;
1376    }
1377  },
1378};
1379
1380XPCOMUtils.defineLazyGetter(this, "PrivateDownloadsIndicatorData", function() {
1381  return new DownloadsIndicatorDataCtor(true);
1382});
1383
1384XPCOMUtils.defineLazyGetter(this, "DownloadsIndicatorData", function() {
1385  return new DownloadsIndicatorDataCtor(false);
1386});
1387
1388// DownloadsSummaryData
1389
1390/**
1391 * DownloadsSummaryData is a view for DownloadsData that produces a summary
1392 * of all downloads after a certain exclusion point aNumToExclude. For example,
1393 * if there were 5 downloads in progress, and a DownloadsSummaryData was
1394 * constructed with aNumToExclude equal to 3, then that DownloadsSummaryData
1395 * would produce a summary of the last 2 downloads.
1396 *
1397 * @param aIsPrivate
1398 *        True if the browser window which owns the download button is a private
1399 *        window.
1400 * @param aNumToExclude
1401 *        The number of items to exclude from the summary, starting from the
1402 *        top of the list.
1403 */
1404function DownloadsSummaryData(aIsPrivate, aNumToExclude) {
1405  this._numToExclude = aNumToExclude;
1406  // Since we can have multiple instances of DownloadsSummaryData, we
1407  // override these values from the prototype so that each instance can be
1408  // completely separated from one another.
1409  this._loading = false;
1410
1411  this._downloads = [];
1412
1413  // Floating point value indicating the last number of seconds estimated until
1414  // the longest download will finish.  We need to store this value so that we
1415  // don't continuously apply smoothing if the actual download state has not
1416  // changed.  This is set to -1 if the previous value is unknown.
1417  this._lastRawTimeLeft = -1;
1418
1419  // Last number of seconds estimated until all in-progress downloads with a
1420  // known size and speed will finish.  This value is stored to allow smoothing
1421  // in case of small variations.  This is set to -1 if the previous value is
1422  // unknown.
1423  this._lastTimeLeft = -1;
1424
1425  // The following properties are updated by _refreshProperties and are then
1426  // propagated to the views.
1427  this._showingProgress = false;
1428  this._details = "";
1429  this._description = "";
1430  this._numActive = 0;
1431  this._percentComplete = -1;
1432
1433  this._oldDownloadStates = new WeakMap();
1434  this._isPrivate = aIsPrivate;
1435  this._views = [];
1436}
1437
1438DownloadsSummaryData.prototype = {
1439  __proto__: DownloadsViewPrototype,
1440
1441  /**
1442   * Removes an object previously added using addView.
1443   *
1444   * @param aView
1445   *        DownloadsSummary view to be removed.
1446   */
1447  removeView(aView) {
1448    DownloadsViewPrototype.removeView.call(this, aView);
1449
1450    if (!this._views.length) {
1451      // Clear out our collection of Download objects. If we ever have
1452      // another view registered with us, this will get re-populated.
1453      this._downloads = [];
1454    }
1455  },
1456
1457  onDownloadAdded(download) {
1458    DownloadsViewPrototype.onDownloadAdded.call(this, download);
1459    this._downloads.unshift(download);
1460    this._updateViews();
1461  },
1462
1463  onDownloadStateChanged() {
1464    // Since the state of a download changed, reset the estimated time left.
1465    this._lastRawTimeLeft = -1;
1466    this._lastTimeLeft = -1;
1467  },
1468
1469  onDownloadChanged(download) {
1470    DownloadsViewPrototype.onDownloadChanged.call(this, download);
1471    this._updateViews();
1472  },
1473
1474  onDownloadRemoved(download) {
1475    let itemIndex = this._downloads.indexOf(download);
1476    this._downloads.splice(itemIndex, 1);
1477    this._updateViews();
1478  },
1479
1480  // Propagation of properties to our views
1481
1482  /**
1483   * Updates the specified view with the current aggregate values.
1484   *
1485   * @param aView
1486   *        DownloadsIndicatorView object to be updated.
1487   */
1488  _updateView(aView) {
1489    aView.showingProgress = this._showingProgress;
1490    aView.percentComplete = this._percentComplete;
1491    aView.description = this._description;
1492    aView.details = this._details;
1493  },
1494
1495  // Property updating based on current download status
1496
1497  /**
1498   * A generator function for the Download objects this summary is currently
1499   * interested in. This generator is passed off to summarizeDownloads in order
1500   * to generate statistics about the downloads we care about - in this case,
1501   * it's the downloads in this._downloads after the first few to exclude,
1502   * which was set when constructing this DownloadsSummaryData instance.
1503   */
1504  *_downloadsForSummary() {
1505    if (this._downloads.length) {
1506      for (let i = this._numToExclude; i < this._downloads.length; ++i) {
1507        yield this._downloads[i];
1508      }
1509    }
1510  },
1511
1512  /**
1513   * Computes aggregate values based on the current state of downloads.
1514   */
1515  _refreshProperties() {
1516    // Pre-load summary with default values.
1517    let summary = DownloadsCommon.summarizeDownloads(
1518      this._downloadsForSummary()
1519    );
1520
1521    this._description = DownloadsCommon.strings.otherDownloads3(
1522      summary.numDownloading
1523    );
1524    this._percentComplete = summary.percentComplete;
1525
1526    // Only show the downloading items.
1527    this._showingProgress = summary.numDownloading > 0;
1528
1529    // Display the estimated time left, if present.
1530    if (summary.rawTimeLeft == -1) {
1531      // There are no downloads with a known time left.
1532      this._lastRawTimeLeft = -1;
1533      this._lastTimeLeft = -1;
1534      this._details = "";
1535    } else {
1536      // Compute the new time left only if state actually changed.
1537      if (this._lastRawTimeLeft != summary.rawTimeLeft) {
1538        this._lastRawTimeLeft = summary.rawTimeLeft;
1539        this._lastTimeLeft = DownloadsCommon.smoothSeconds(
1540          summary.rawTimeLeft,
1541          this._lastTimeLeft
1542        );
1543      }
1544      [this._details] = DownloadUtils.getDownloadStatusNoRate(
1545        summary.totalTransferred,
1546        summary.totalSize,
1547        summary.slowestSpeed,
1548        this._lastTimeLeft
1549      );
1550    }
1551  },
1552};
1553