Bug 888915 - Part 3. Fix the downloads taskbar progress. r=IanN
authorFrank-Rainer Grahl <frgrahl@gmx.net>
Tue, 05 Jun 2018 12:53:23 +0200
changeset 61599 7a0e82f04b5817984b32d9dca4ae29956dd3115f
parent 61598 72c25a32b2105a84d7c02e4784dbb055a4742235
child 61600 1e99d17fb28f219b9e801f4a1b5d5b5cda171d17
push id6346
push usermozilla@jorgk.com
push dateWed, 06 Jun 2018 09:48:02 +0000
treeherdertry-comm-central@17c53edc38b4 [default view] [failures only]
reviewersIanN
bugs888915
Bug 888915 - Part 3. Fix the downloads taskbar progress. r=IanN
suite/browser/navigator.js
suite/common/src/nsSuiteGlue.js
suite/components/downloads/DownloadsCommon.jsm
suite/components/downloads/DownloadsTaskbar.jsm
suite/components/downloads/moz.build
--- a/suite/browser/navigator.js
+++ b/suite/browser/navigator.js
@@ -1,16 +1,15 @@
 /* -*- Mode: Java; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 ChromeUtils.import("resource://gre/modules/XPCOMUtils.jsm");
 ChromeUtils.import("resource://gre/modules/Services.jsm");
-ChromeUtils.import("resource://gre/modules/DownloadsTaskbar.jsm");
 ChromeUtils.import("resource:///modules/WindowsPreviewPerTab.jsm");
 
 XPCOMUtils.defineLazyModuleGetters(this, {
   PluralForm: "resource://gre/modules/PluralForm.jsm",
   PrivateBrowsingUtils: "resource://gre/modules/PrivateBrowsingUtils.jsm",
   PromiseUtils: "resource://gre/modules/PromiseUtils.jsm",
   SafeBrowsing: "resource://gre/modules/SafeBrowsing.jsm",
 });
@@ -743,18 +742,16 @@ function Startup()
 
   gBrowser.mPanelContainer.addEventListener("InstallBrowserTheme", LightWeightThemeWebInstaller, false, true);
   gBrowser.mPanelContainer.addEventListener("PreviewBrowserTheme", LightWeightThemeWebInstaller, false, true);
   gBrowser.mPanelContainer.addEventListener("ResetBrowserThemePreview", LightWeightThemeWebInstaller, false, true);
 
   AeroPeek.onOpenWindow(window);
 
   if (!gPrivate) {
-    DownloadTaskbarProgress.onBrowserWindowLoad(window);
-
     // initialize the sync UI
     // gSyncUI.init();
 
     // initialize the session-restore service
     setTimeout(InitSessionStoreCallback, 0);
   }
 
   window.addEventListener("MozAfterPaint", DelayedStartup);
--- a/suite/common/src/nsSuiteGlue.js
+++ b/suite/common/src/nsSuiteGlue.js
@@ -35,16 +35,19 @@ ChromeUtils.defineModuleGetter(this, "Bo
                                "resource://gre/modules/BookmarkHTMLUtils.jsm");
 
 ChromeUtils.defineModuleGetter(this, "BookmarkJSONUtils",
                                "resource://gre/modules/BookmarkJSONUtils.jsm");
 
 ChromeUtils.defineModuleGetter(this, "RecentWindow",
                                "resource:///modules/RecentWindow.jsm");
 
+ChromeUtils.defineModuleGetter(this, "DownloadsCommon",
+                               "resource:///modules/DownloadsCommon.jsm");
+
 XPCOMUtils.defineLazyGetter(this, "DebuggerServer", () => {
   var tmp = {};
   ChromeUtils.import("resource://devtools/shared/Loader.jsm", tmp);
   return tmp.require("devtools/server/main").DebuggerServer;
 });
 
 const global = this;
 
@@ -91,50 +94,20 @@ const BOOKMARKS_BACKUP_MIN_INTERVAL_DAYS
 const DEBUGGER_REMOTE_ENABLED = "devtools.debugger.remote-enabled";
 const DEBUGGER_REMOTE_PORT = "devtools.debugger.remote-port";
 const DEBUGGER_FORCE_LOCAL = "devtools.debugger.force-local";
 const DEBUGGER_WIFI_VISIBLE = "devtools.remote.wifi.visible";
 const DOWNLOAD_MANAGER_URL = "chrome://communicator/content/downloads/downloadmanager.xul";
 const PROGRESS_DIALOG_URL = "chrome://communicator/content/downloads/progressDialog.xul";
 const PREF_FOCUS_WHEN_STARTING = "browser.download.manager.focusWhenStarting";
 const PREF_FLASH_COUNT = "browser.download.manager.flashCount";
-const PREF_DM_BEHAVIOR = "browser.download.manager.behavior";
 
 var gDownloadManager;
-var gDownloadsLoaded;
-var gTaskbarProgress;
-var gWinTaskbar;
-var gDownloadsSummary;
-
-function onSummaryChanged()
-{
-  if (!gTaskbarProgress)
-    return;
-
-  const nsITaskbarProgress = Ci.nsITaskbarProgress;
-  var currentBytes = gDownloadsSummary.progressCurrentBytes;
-  var totalBytes = gDownloadsSummary.progressTotalBytes;
-  var state = gDownloadsSummary.allHaveStopped ?
-                currentBytes ? nsITaskbarProgress.STATE_PAUSED :
-                               nsITaskbarProgress.STATE_NO_PROGRESS :
-                currentBytes < totalBytes ? nsITaskbarProgress.STATE_NORMAL :
-                             nsITaskbarProgress.STATE_INDETERMINATE;
-  switch (state) {
-    case nsITaskbarProgress.STATE_NO_PROGRESS:
-    case nsITaskbarProgress.STATE_INDETERMINATE:
-      gTaskbarProgress.setProgressState(state, 0, 0);
-      break;
-    default:
-      gTaskbarProgress.setProgressState(state, currentBytes, totalBytes);
-      break;
-  }
-}
 
 // Constructor
-
 function SuiteGlue() {
   XPCOMUtils.defineLazyServiceGetter(this, "_idleService",
                                      "@mozilla.org/widget/idleservice;1",
                                      "nsIIdleService");
 
   this._init();
   extensionDefaults(); // extensionSupport.jsm
 }
@@ -234,43 +207,21 @@ SuiteGlue.prototype = {
         this._onProfileAfterChange();
         break;
       case "final-ui-startup":
         this._onProfileStartup();
         this._promptForMasterPassword();
         this._checkForNewAddons();
         Services.search.init();
         listeners.init();
+
         Cc["@mozilla.org/globalmessagemanager;1"]
           .getService(Ci.nsIMessageListenerManager)
           .loadFrameScript("chrome://navigator/content/content.js", true);
         ChromeUtils.import("resource://gre/modules/NotificationDB.jsm");
-        ChromeUtils.import("resource://gre/modules/Downloads.jsm");
-        ChromeUtils.import("resource://gre/modules/DownloadIntegration.jsm");
-        DownloadIntegration.shouldPersistDownload = function() { return true; }
-        Downloads.getList(Downloads.ALL).then(list => list.addView(this))
-                                        .then(() => gDownloadsLoaded = true);
-
-        if ("@mozilla.org/widget/macdocksupport;1" in Cc)
-          gTaskbarProgress = Cc["@mozilla.org/widget/macdocksupport;1"]
-                               .getService(Ci.nsITaskbarProgress);
-        else if ("@mozilla.org/windows-taskbar;1" in Cc) {
-          gWinTaskbar = Cc["@mozilla.org/windows-taskbar;1"]
-                          .getService(Ci.nsIWinTaskbar);
-          if (!gWinTaskbar.available) {
-            gWinTaskbar = null;
-            break;
-          }
-        } else {
-          break;
-        }
-        Downloads.getSummary(Downloads.PUBLIC).then(list => {
-          gDownloadsSummary = list;
-          list.addView(this);
-        });
         break;
       case "sessionstore-windows-restored":
         this._onBrowserStartup(subject);
         break;
       case "browser:purge-session-history":
         // reset the console service's error buffer
         Services.console.logStringMessage(null); // clear the console (in case it's open)
         Services.console.reset();
@@ -653,16 +604,33 @@ SuiteGlue.prototype = {
       notifyBox.showPlacesLockedWarning();
     }
 
     // Detect if updates are off and warn for outdated builds.
     if (this._shouldShowUpdateWarning())
       notifyBox.showUpdateWarning();
 
     this._checkForDefaultClient(aWindow);
+
+    // Initialize the download manager after the app starts so that
+    // auto-resume downloads begin (such as after crashing or quitting with
+    // active downloads) and speeds up the first-load of the download manager.
+    // If the user manually opens the download manager before the init is
+    // done, the downloads will start right away, and initializing again
+    // won't hurt.
+    // Afterwards init the taskbar and eventuall show the download progress if
+    // on a supported platform.
+    (async () => {
+      DownloadsCommon.init();
+    })().catch(ex => {
+      Cu.reportError(ex);
+    }).then(() => {
+      Cu.import("resource:///modules/DownloadsTaskbar.jsm", {})
+        .DownloadsTaskbar.registerIndicator(aWindow);
+    });
   },
 
   /**
    * Application shutdown handler.
    */
   _onQuitApplicationGranted: function()
   {
     if (this._saveSession) {
@@ -1273,83 +1241,33 @@ SuiteGlue.prototype = {
   },
 
   dbgRestart: function()
   {
     this.dbgStop();
     this.dbgStart();
   },
 
-  // Download view
-  onDownloadAdded: function(aDownload, aNewest)
-  {
-    aDownload.displayName =
-                 aDownload.target.path ? OS.Path.basename(aDownload.target.path)
-                                       : aDownload.source.url;
-    this.onDownloadChanged(aDownload);
-    if (!gDownloadsLoaded)
-      return;
-
-    var behavior = aDownload.source.isPrivate ? 1 :
-                     Services.prefs.getIntPref(PREF_DM_BEHAVIOR);
-    switch (behavior) {
-      case 0:
-        this.showDownloadManager(aDownload);
-        break;
-      case 1:
-        Services.ww.openWindow(null, PROGRESS_DIALOG_URL, null,
-                               "chrome,titlebar,centerscreen,minimizable=yes,dialog=no",
-                               { wrappedJSObject: aDownload });
-        break;
-    }
-
-    return; // No UI for behavior >= 2
-  },
-
-  onDownloadChanged: function(aDownload) {
-    const nsIDownloadManager = Ci.nsIDownloadManager;
-    aDownload.state =
-                !aDownload.stopped ? nsIDownloadManager.DOWNLOAD_DOWNLOADING :
-                aDownload.succeeded ? nsIDownloadManager.DOWNLOAD_FINISHED :
-                aDownload.error ?  aDownload.error.becauseBlocked ?
-                  nsIDownloadManager.DOWNLOAD_BLOCKED_POLICY :
-                  nsIDownloadManager.DOWNLOAD_FAILED :
-                !aDownload.canceled ? nsIDownloadManager.DOWNLOAD_NOTSTARTED :
-                aDownload.hasPartialData ? nsIDownloadManager.DOWNLOAD_PAUSED :
-                                           nsIDownloadManager.DOWNLOAD_CANCELED;
-    if (gDownloadsLoaded && (aDownload.succeeded || !aDownload.stopped))
-      aDownload.endTime = Date.now();
-  },
-
-  // Download summary
-  onSummaryChanged: onSummaryChanged,
-
   // ------------------------------
   // public nsISuiteGlue members
   // ------------------------------
 
   showDownloadManager: function(aDownload)
   {
     if (!gDownloadManager) {
       gDownloadManager = Services.ww.openWindow(null, DOWNLOAD_MANAGER_URL,
                                                 null, "all,dialog=no",
                                                 { wrappedJSObject: aDownload });
       gDownloadManager.addEventListener("load", function() {
         gDownloadManager.addEventListener("unload", function() {
           gDownloadManager = null;
-          if (gWinTaskbar)
-            gTaskbarProgress = null;
         });
-        if (gWinTaskbar) {
-          var docShell = gDownloadManager.QueryInterface(Ci.nsIInterfaceRequestor)
-                                         .getInterface(Ci.nsIWebNavigation)
-                                         .QueryInterface(Ci.nsIDocShell);
-          gTaskbarProgress = gWinTaskbar.getTaskbarProgress(docShell);
-          onSummaryChanged();
-        }
+        // Attach the taskbar progress meter to the download manager window.
+        Components.utils.import("resource:///modules/DownloadsTaskbar.jsm", {})
+                  .DownloadsTaskbar.attachIndicator(gDownloadManager);
       });
     } else if (!aDownload ||
                Services.prefs.getBoolPref(PREF_FOCUS_WHEN_STARTING)) {
         gDownloadManager.focus();
     } else {
       // This preference may not be set, so defaulting to two.
       var flashCount = 2;
       try {
new file mode 100644
--- /dev/null
+++ b/suite/components/downloads/DownloadsCommon.jsm
@@ -0,0 +1,572 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set ts=2 et sw=2 tw=80 filetype=javascript: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+this.EXPORTED_SYMBOLS = [
+  "DownloadsCommon",
+];
+
+/**
+ * Handles the Downloads panel shared methods and data access.
+ *
+ * This file includes the following constructors and global objects:
+ *
+ * DownloadsCommon
+ * This object is exposed directly to the consumers of this JavaScript module,
+ * and provides shared methods for all the instances of the user interface.
+ *
+ * DownloadsData
+ * Retrieves the list of past and completed downloads from the underlying
+ * Downloads API data, and provides asynchronous notifications allowing
+ * to build a consistent view of the available data.
+ *
+ */
+
+// Globals
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+Cu.import("resource://gre/modules/Services.jsm");
+
+XPCOMUtils.defineLazyModuleGetter(this, "NetUtil",
+                                  "resource://gre/modules/NetUtil.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "AppConstants",
+                                  "resource://gre/modules/AppConstants.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "DownloadHistory",
+                                  "resource://gre/modules/DownloadHistory.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "Downloads",
+                                  "resource://gre/modules/Downloads.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "DownloadUIHelper",
+                                  "resource://gre/modules/DownloadUIHelper.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "DownloadUtils",
+                                  "resource://gre/modules/DownloadUtils.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "FileUtils",
+                                  "resource://gre/modules/FileUtils.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "OS",
+                                  "resource://gre/modules/osfile.jsm");
+
+XPCOMUtils.defineLazyGetter(this, "DownloadsLogger", () => {
+  let { ConsoleAPI } = Cu.import("resource://gre/modules/Console.jsm", {});
+  let consoleOptions = {
+    maxLogLevelPref: "browser.download.loglevel",
+    prefix: "Downloads"
+  };
+  return new ConsoleAPI(consoleOptions);
+});
+
+const kPartialDownloadSuffix = ".part";
+
+const kPrefBranch = Services.prefs.getBranch("browser.download.");
+
+const PREF_DM_BEHAVIOR = "browser.download.manager.behavior";
+
+var PrefObserver = {
+  QueryInterface: XPCOMUtils.generateQI([Ci.nsIObserver,
+                                         Ci.nsISupportsWeakReference]),
+  getPref(name) {
+    try {
+      switch (typeof this.prefs[name]) {
+        case "boolean":
+          return kPrefBranch.getBoolPref(name);
+      }
+    } catch (ex) { }
+    return this.prefs[name];
+  },
+  observe(aSubject, aTopic, aData) {
+    if (this.prefs.hasOwnProperty(aData)) {
+      delete this[aData];
+      this[aData] = this.getPref(aData);
+    }
+  },
+  register(prefs) {
+    this.prefs = prefs;
+    kPrefBranch.addObserver("", this, true);
+    for (let key in prefs) {
+      let name = key;
+      XPCOMUtils.defineLazyGetter(this, name, function() {
+        return PrefObserver.getPref(name);
+      });
+    }
+  },
+};
+
+// PrefObserver.register({
+  // prefName: defaultValue
+// });
+
+
+// DownloadsCommon
+
+/**
+ * This object is exposed directly to the consumers of this JavaScript module,
+ * and provides shared methods for all the instances of the user interface.
+ */
+this.DownloadsCommon = {
+  // The following legacy constants are still returned by stateOfDownload, but
+  // individual properties of the Download object should normally be used.
+  DOWNLOAD_NOTSTARTED: -1,
+  DOWNLOAD_DOWNLOADING: 0,
+  DOWNLOAD_FINISHED: 1,
+  DOWNLOAD_FAILED: 2,
+  DOWNLOAD_CANCELED: 3,
+  DOWNLOAD_PAUSED: 4,
+  DOWNLOAD_BLOCKED_PARENTAL: 6,
+  DOWNLOAD_DIRTY: 8,
+  DOWNLOAD_BLOCKED_POLICY: 9,
+
+  // The following are the possible values of the "attention" property.
+  ATTENTION_NONE: "",
+  ATTENTION_SUCCESS: "success",
+  ATTENTION_WARNING: "warning",
+  ATTENTION_SEVERE: "severe",
+
+  /**
+   * Initializes the Downloads Manager common code.
+   */
+  init() {
+    Cu.import("resource://gre/modules/Downloads.jsm");
+    Cu.import("resource://gre/modules/DownloadIntegration.jsm");
+    DownloadIntegration.shouldPersistDownload = function() { return true; }
+    DownloadsData.initializeDataLink();
+  },
+
+  /**
+   * Returns the legacy state integer value for the provided Download object.
+   */
+  stateOfDownload(download) {
+    // Collapse state using the correct priority.
+    if (!download.stopped) {
+      return DownloadsCommon.DOWNLOAD_DOWNLOADING;
+    }
+    if (download.succeeded) {
+      return DownloadsCommon.DOWNLOAD_FINISHED;
+    }
+    if (download.error) {
+      if (download.error.becauseBlockedByParentalControls) {
+        return DownloadsCommon.DOWNLOAD_BLOCKED_PARENTAL;
+      }
+      if (download.error.becauseBlockedByReputationCheck) {
+        return DownloadsCommon.DOWNLOAD_DIRTY;
+      }
+      return DownloadsCommon.DOWNLOAD_FAILED;
+    }
+    if (download.canceled) {
+      if (download.hasPartialData) {
+        return DownloadsCommon.DOWNLOAD_PAUSED;
+      }
+      return DownloadsCommon.DOWNLOAD_CANCELED;
+    }
+    return DownloadsCommon.DOWNLOAD_NOTSTARTED;
+  },
+};
+
+XPCOMUtils.defineLazyGetter(this.DownloadsCommon, "log", () => {
+  return DownloadsLogger.log.bind(DownloadsLogger);
+});
+XPCOMUtils.defineLazyGetter(this.DownloadsCommon, "error", () => {
+  return DownloadsLogger.error.bind(DownloadsLogger);
+});
+
+// DownloadsData
+
+/**
+ * Retrieves the list of past and completed downloads from the underlying
+ * Downloads API data, and provides asynchronous notifications allowing to
+ * build a consistent view of the available data.
+ *
+ * Note that using this object does not automatically initialize the list of
+ * downloads. This is useful to display a neutral progress indicator in
+ * the main browser window until the autostart timeout elapses.
+ *
+ * This powers the DownloadsData and HistoryDownloadsData singleton objects.
+ */
+ function DownloadsDataCtor({ isHistory } = {}) {
+
+  // Contains all the available Download objects and their integer state.
+  this.oldDownloadStates = new Map();
+
+  // For the history downloads list we don't need to register this as a view,
+  // but we have to ensure that the DownloadsData object is initialized before
+  // we register more views. This ensures that the view methods of DownloadsData
+  // are invoked before those of views registered on HistoryDownloadsData,
+  // allowing the endTime property to be set correctly.
+  if (isHistory) {
+    DownloadsData.initializeDataLink();
+    this._promiseList = DownloadsData._promiseList
+                                     .then(() => DownloadHistory.getList());
+    return;
+  }
+
+  // This defines "initializeDataLink" and "_promiseList" synchronously, then
+  // continues execution only when "initializeDataLink" is called, allowing the
+  // underlying data to be loaded only when actually needed.
+  this._promiseList = (async () => {
+    await new Promise(resolve => this.initializeDataLink = resolve);
+    let list = await Downloads.getList(Downloads.ALL);
+
+    await list.addView(this);
+    this._downloadsLoaded = true;
+
+    return list;
+  })();
+}
+
+DownloadsDataCtor.prototype = {
+  /**
+   * Starts receiving events for current downloads.
+   */
+  initializeDataLink() {},
+
+  /**
+   * Promise resolved with the underlying DownloadList object once we started
+   * receiving events for current downloads.
+   */
+  _promiseList: null,
+
+  _downloadsLoaded: null,
+
+  /**
+   * Iterator for all the available Download objects. This is empty until the
+   * data has been loaded using the JavaScript API for downloads.
+   */
+  get downloads() {
+    return this.oldDownloadStates.keys();
+  },
+
+  /**
+   * True if there are finished downloads that can be removed from the list.
+   */
+  get canRemoveFinished() {
+    for (let download of this.downloads) {
+      // Stopped, paused, and failed downloads with partial data are removed.
+      if (download.stopped && !(download.canceled && download.hasPartialData)) {
+        return true;
+      }
+    }
+    return false;
+  },
+
+  /**
+   * Asks the back-end to remove finished downloads from the list. This method
+   * is only called after the data link has been initialized.
+   */
+  removeFinished() {
+    Downloads.getList(Downloads.ALL)
+             .then(list => list.removeFinished())
+             .catch(Cu.reportError);
+  },
+
+  // Integration with the asynchronous Downloads back-end
+
+  // Download view
+  onDownloadAdded: function(download)
+  {
+    // Download objects do not store the end time of downloads, as the Downloads
+    // API does not need to persist this information for all platforms. Once a
+    // download terminates on a Desktop browser, it becomes a history download,
+    // for which the end time is stored differently, as a Places annotation.
+    download.endTime = Date.now();
+    this.oldDownloadStates.set(download,
+                               DownloadsCommon.stateOfDownload(download));
+
+    download.displayName =
+                 download.target.path ? OS.Path.basename(download.target.path)
+                                       : download.source.url;
+    this.onDownloadChanged(download);
+    if (!this._downloadsLoaded)
+      return;
+
+    var behavior = download.source.isPrivate ? 1 :
+                     Services.prefs.getIntPref(PREF_DM_BEHAVIOR);
+    switch (behavior) {
+      case 0:
+        // TODO Better move this out of nsSuiteGlue.
+        Cc["@mozilla.org/suite/suiteglue;1"]
+          .getService(Ci.nsISuiteGlue)
+          .showDownloadManager(download);
+        break;
+      case 1:
+        Services.ww.openWindow(null, PROGRESS_DIALOG_URL, null,
+                               "chrome,titlebar,centerscreen,minimizable=yes,dialog=no",
+                               { wrappedJSObject: download });
+        break;
+    }
+
+    return; // No UI for behavior >= 2
+  },
+
+  onDownloadChanged(download) {
+    let oldState = this.oldDownloadStates.get(download);
+    let newState = DownloadsCommon.stateOfDownload(download);
+    this.oldDownloadStates.set(download, newState);
+
+    if (oldState != newState) {
+      if (download.succeeded ||
+          (download.canceled && !download.hasPartialData) ||
+          download.error) {
+        // Store the end time that may be displayed by the views.
+        download.endTime = Date.now();
+
+        // This state transition code should actually be located in a Downloads
+        // API module (bug 941009).
+        DownloadHistory.updateMetaData(download);
+      }
+
+      if (download.succeeded ||
+          (download.error && download.error.becauseBlocked)) {
+        this._notifyDownloadEvent("finish");
+      }
+    }
+
+    if (!download.newDownloadNotified) {
+      download.newDownloadNotified = true;
+      this._notifyDownloadEvent("start");
+    }
+  },
+
+  onDownloadRemoved(download) {
+    this.oldDownloadStates.delete(download);
+  },
+
+  onDownloadChanged: function(aDownload) {
+    // This mighe be effective but the staement s*cks.
+    aDownload.state = DownloadsCommon.stateOfDownload(aDownload);
+    if (this._downloadsLoaded && (aDownload.succeeded || !aDownload.stopped))
+      aDownload.endTime = Date.now();
+  },
+
+  // Download summary
+  onSummaryChanged:  function() {
+
+  if (!gTaskbarProgress)
+    return;
+
+  const nsITaskbarProgress = Ci.nsITaskbarProgress;
+  var currentBytes = gDownloadsSummary.progressCurrentBytes;
+  var totalBytes = gDownloadsSummary.progressTotalBytes;
+  var state = gDownloadsSummary.allHaveStopped ?
+                currentBytes ? nsITaskbarProgress.STATE_PAUSED :
+                               nsITaskbarProgress.STATE_NO_PROGRESS :
+                currentBytes < totalBytes ? nsITaskbarProgress.STATE_NORMAL :
+                             nsITaskbarProgress.STATE_INDETERMINATE;
+  switch (state) {
+    case nsITaskbarProgress.STATE_NO_PROGRESS:
+    case nsITaskbarProgress.STATE_INDETERMINATE:
+      gTaskbarProgress.setProgressState(state, 0, 0);
+      break;
+    default:
+      gTaskbarProgress.setProgressState(state, currentBytes, totalBytes);
+      break;
+  }
+},
+
+  // Registration of views
+
+  /**
+   * Adds an object to be notified when the available download data changes.
+   * The specified object is initialized with the currently available downloads.
+   *
+   * @param aView
+   *        DownloadsView object to be added.  This reference must be passed to
+   *        removeView before termination.
+   */
+  addView(aView) {
+    this._promiseList.then(list => list.addView(aView))
+                     .catch(Cu.reportError);
+  },
+
+  /**
+   * Removes an object previously added using addView.
+   *
+   * @param aView
+   *        DownloadsView object to be removed.
+   */
+  removeView(aView) {
+    this._promiseList.then(list => list.removeView(aView))
+                     .catch(Cu.reportError);
+  },
+
+};
+
+XPCOMUtils.defineLazyGetter(this, "HistoryDownloadsData", function() {
+  return new DownloadsDataCtor({ isHistory: true });
+});
+
+XPCOMUtils.defineLazyGetter(this, "DownloadsData", function() {
+  return new DownloadsDataCtor();
+});
+
+// DownloadsViewPrototype
+
+/**
+ * A prototype for an object that registers itself with DownloadsData as soon
+ * as a view is registered with it.
+ */
+const DownloadsViewPrototype = {
+  /**
+   * Contains all the available Download objects and their current state value.
+   *
+   * SUBCLASSES MUST OVERRIDE THIS PROPERTY.
+   */
+  _oldDownloadStates: null,
+
+  // Registration of views
+
+  /**
+   * Array of view objects that should be notified when the available status
+   * data changes.
+   *
+   * SUBCLASSES MUST OVERRIDE THIS PROPERTY.
+   */
+  _views: null,
+
+  /**
+   * Adds an object to be notified when the available status data changes.
+   * The specified object is initialized with the currently available status.
+   *
+   * @param aView
+   *        View object to be added.  This reference must be
+   *        passed to removeView before termination.
+   */
+  addView(aView) {
+    // Start receiving events when the first of our views is registered.
+    if (this._views.length == 0) {
+      DownloadsData.addView(this);
+    }
+
+    this._views.push(aView);
+    this.refreshView(aView);
+  },
+
+  /**
+   * Updates the properties of an object previously added using addView.
+   *
+   * @param aView
+   *        View object to be updated.
+   */
+  refreshView(aView) {
+    // Update immediately even if we are still loading data asynchronously.
+    // Subclasses must provide these two functions!
+    this._refreshProperties();
+    this._updateView(aView);
+  },
+
+  /**
+   * Removes an object previously added using addView.
+   *
+   * @param aView
+   *        View object to be removed.
+   */
+  removeView(aView) {
+    let index = this._views.indexOf(aView);
+    if (index != -1) {
+      this._views.splice(index, 1);
+    }
+
+    // Stop receiving events when the last of our views is unregistered.
+    if (this._views.length == 0) {
+      DownloadsData.removeView(this);
+    }
+  },
+
+  // Callback functions from DownloadList
+
+  /**
+   * Indicates whether we are still loading downloads data asynchronously.
+   */
+  _loading: false,
+
+  /**
+   * Called before multiple downloads are about to be loaded.
+   */
+  onDownloadBatchStarting() {
+    this._loading = true;
+  },
+
+  /**
+   * Called after data loading finished.
+   */
+  onDownloadBatchEnded() {
+    this._loading = false;
+  },
+
+  /**
+   * Called when a new download data item is available, either during the
+   * asynchronous data load or when a new download is started.
+   *
+   * @param download
+   *        Download object that was just added.
+   *
+   * @note Subclasses should override this and still call the base method.
+   */
+  onDownloadAdded(download) {
+    this._oldDownloadStates.set(download,
+                                DownloadsCommon.stateOfDownload(download));
+  },
+
+  /**
+   * Called when the overall state of a Download has changed. In particular,
+   * this is called only once when the download succeeds or is blocked
+   * permanently, and is never called if only the current progress changed.
+   *
+   * The onDownloadChanged notification will always be sent afterwards.
+   *
+   * @note Subclasses should override this.
+   */
+  onDownloadStateChanged(download) {
+    throw Cr.NS_ERROR_NOT_IMPLEMENTED;
+  },
+
+  /**
+   * Called every time any state property of a Download may have changed,
+   * including progress properties.
+   *
+   * Note that progress notification changes are throttled at the Downloads.jsm
+   * API level, and there is no throttling mechanism in the front-end.
+   *
+   * @note Subclasses should override this and still call the base method.
+   */
+  onDownloadChanged(download) {
+    let oldState = this._oldDownloadStates.get(download);
+    let newState = DownloadsCommon.stateOfDownload(download);
+    this._oldDownloadStates.set(download, newState);
+
+    if (oldState != newState) {
+      this.onDownloadStateChanged(download);
+    }
+  },
+
+  /**
+   * Called when a data item is removed, ensures that the widget associated with
+   * the view item is removed from the user interface.
+   *
+   * @param download
+   *        Download object that is being removed.
+   *
+   * @note Subclasses should override this.
+   */
+  onDownloadRemoved(download) {
+    throw Cr.NS_ERROR_NOT_IMPLEMENTED;
+  },
+
+  /**
+   * Private function used to refresh the internal properties being sent to
+   * each registered view.
+   *
+   * @note Subclasses should override this.
+   */
+  _refreshProperties() {
+    throw Cr.NS_ERROR_NOT_IMPLEMENTED;
+  },
+
+  /**
+   * Private function used to refresh an individual view.
+   *
+   * @note Subclasses should override this.
+   */
+  _updateView() {
+    throw Cr.NS_ERROR_NOT_IMPLEMENTED;
+  },
+};
--- a/suite/components/downloads/DownloadsTaskbar.jsm
+++ b/suite/components/downloads/DownloadsTaskbar.jsm
@@ -1,385 +1,166 @@
 /* -*- indent-tabs-mode: nil; js-indent-level: 2 -*-
  * vim: sw=2 ts=2 sts=2 et filetype=javascript
  * This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 this.EXPORTED_SYMBOLS = [
-  "DownloadTaskbarProgress",
+  "DownloadsTaskbar",
 ];
 
+ChromeUtils.import("resource://gre/modules/XPCOMUtils.jsm");
 
-ChromeUtils.import("resource://gre/modules/AppConstants.jsm");
+ChromeUtils.defineModuleGetter(this, "Downloads",
+                               "resource://gre/modules/Downloads.jsm");
 ChromeUtils.defineModuleGetter(this, "Services",
                                "resource://gre/modules/Services.jsm");
 
-// Constants
+XPCOMUtils.defineLazyGetter(this, "gWinTaskbar", function() {
+  if (!("@mozilla.org/windows-taskbar;1" in Cc)) {
+    return null;
+  }
+  let winTaskbar = Cc["@mozilla.org/windows-taskbar;1"]
+                     .getService(Ci.nsIWinTaskbar);
+  return winTaskbar.available && winTaskbar;
+});
 
-const kTaskbarIDWin = "@mozilla.org/windows-taskbar;1";
-const kTaskbarIDMac = "@mozilla.org/widget/macdocksupport;1";
+XPCOMUtils.defineLazyGetter(this, "gMacTaskbarProgress", function() {
+  return ("@mozilla.org/widget/macdocksupport;1" in Cc) &&
+         Cc["@mozilla.org/widget/macdocksupport;1"]
+           .getService(Ci.nsITaskbarProgress);
+});
 
-// DownloadTaskbarProgress Object
+// DownloadsTaskbar
 
-this.DownloadTaskbarProgress =
-{
-  init: function DTP_init()
-  {
-    if (DownloadTaskbarProgressUpdater) {
-      DownloadTaskbarProgressUpdater._init();
-    }
-  },
+/**
+ * Handles the download progress indicator in the taskbar.
+ */
+var DownloadsTaskbar = {
+  /**
+   * Underlying DownloadSummary providing the aggregate download information, or
+   * null if the indicator has never been initialized.
+   */
+  _summary: null,
+
+  /**
+   * nsITaskbarProgress object to which download information is dispatched.
+   * This can be null if the indicator has never been initialized or if the
+   * indicator is currently hidden on Windows.
+   */
+  _taskbarProgress: null,
 
   /**
-   * Called when a browser window appears. This has an effect only when we
-   * don't already have an active window.
+   * This method is called after a new browser window is opened, and ensures
+   * that the download progress indicator is displayed in the taskbar.
    *
-   * @param aWindow
-   *        The browser window that we'll potentially use to display the
-   *        progress.
+   * On Windows, the indicator is attached to the first browser window that
+   * calls this method.  When the window is closed, the indicator is moved to
+   * another browser window, if available, in no particular order.  When there
+   * are no browser windows visible, the indicator is hidden.
+   *
+   * On Mac OS X, the indicator is initialized globally when this method is
+   * called for the first time.  Subsequent calls have no effect.
+   *
+   * @param aBrowserWindow
+   *        nsIDOMWindow object of the newly opened browser window to which the
+   *        indicator may be attached.
    */
-  onBrowserWindowLoad: function DTP_onBrowserWindowLoad(aWindow)
-  {
-    this.init();
-    if (!DownloadTaskbarProgressUpdater) {
-      return;
+  registerIndicator(aWindow) {
+    if (!this._taskbarProgress) {
+      if (gMacTaskbarProgress) {
+        // On Mac OS X, we have to register the global indicator only once.
+        this._taskbarProgress = gMacTaskbarProgress;
+        // Free the XPCOM reference on shutdown, to prevent detecting a leak.
+        Services.obs.addObserver(() => {
+          this._taskbarProgress = null;
+          gMacTaskbarProgress = null;
+        }, "quit-application-granted");
+      } else if (gWinTaskbar) {
+        // On Windows, the indicator is currently hidden because we have no
+        // previous window, thus we should attach the indicator now.
+        this.attachIndicator(aWindow);
+      } else {
+        // The taskbar indicator is not available on this platform.
+        return;
+      }
     }
-    if (!DownloadTaskbarProgressUpdater._activeTaskbarProgress) {
-      DownloadTaskbarProgressUpdater._setActiveWindow(aWindow, false);
+
+    // Ensure that the DownloadSummary object will be created asynchronously.
+    if (!this._summary) {
+      Downloads.getSummary(Downloads.ALL).then(summary => {
+        // In case the method is re-entered, we simply ignore redundant
+        // invocations of the callback, instead of keeping separate state.
+        if (this._summary) {
+          return undefined;
+        }
+        this._summary = summary;
+        return this._summary.addView(this);
+      }).catch(Cu.reportError);
     }
   },
 
   /**
-   * Called when the download window appears. The download window will take
-   * over as the active window.
-   */
-  onDownloadWindowLoad: function DTP_onDownloadWindowLoad(aWindow)
-  {
-    if (!DownloadTaskbarProgressUpdater) {
-      return;
-    }
-    DownloadTaskbarProgressUpdater._setActiveWindow(aWindow, true);
-  },
-
-  /**
-   * Getters for internal DownloadTaskbarProgressUpdater values
+   * On Windows, attaches the taskbar indicator to the specified window.
    */
-
-  get activeTaskbarProgress() {
-    if (!DownloadTaskbarProgressUpdater) {
-      return null;
-    }
-    return DownloadTaskbarProgressUpdater._activeTaskbarProgress;
-  },
-
-  get activeWindowIsDownloadWindow() {
-    if (!DownloadTaskbarProgressUpdater) {
-      return null;
-    }
-    return DownloadTaskbarProgressUpdater._activeWindowIsDownloadWindow;
-  },
-
-  get taskbarState() {
-    if (!DownloadTaskbarProgressUpdater) {
-      return null;
-    }
-    return DownloadTaskbarProgressUpdater._taskbarState;
-  },
-
-};
-
-// DownloadTaskbarProgressUpdater Object
-
-var DownloadTaskbarProgressUpdater =
-{
-  // / Whether the taskbar is initialized.
-  _initialized: false,
-
-  // / Reference to the taskbar.
-  _taskbar: null,
-
-  /**
-   * Initialize and register ourselves as a download progress listener.
-   */
-  _init: function DTPU_init()
-  {
-    if (this._initialized) {
-      return; // Already initialized
-    }
-    this._initialized = true;
-
-    if (kTaskbarIDWin in Cc) {
-      this._taskbar = Cc[kTaskbarIDWin].getService(Ci.nsIWinTaskbar);
-      if (!this._taskbar.available) {
-        // The Windows version is probably too old
-        DownloadTaskbarProgressUpdater = null;
-        return;
-      }
-    } else if (kTaskbarIDMac in Cc) {
-      this._activeTaskbarProgress = Cc[kTaskbarIDMac].
-                                      getService(Ci.nsITaskbarProgress);
-    } else {
-      DownloadTaskbarProgressUpdater = null;
-      return;
+  attachIndicator(aWindow) {
+    // If there is already a taskbarProgress this usually means the download
+    //  manager became active. So clear the taskbar state first.
+    if (this._taskbarProgress) {
+      this._taskbarProgress.setProgressState(Ci.nsITaskbarProgress.STATE_NO_PROGRESS);
     }
 
-    this._taskbarState = Ci.nsITaskbarProgress.STATE_NO_PROGRESS;
-
-    this._os = Cc["@mozilla.org/observer-service;1"].
-               getService(Ci.nsIObserverService);
-    this._os.addObserver(this, "quit-application-granted");
-
-    this._updateStatus();
-    // onBrowserWindowLoad/onDownloadWindowLoad are going to set the active
-    // window, so don't do it here.
-  },
-
-  /**
-   * Unregisters ourselves as a download progress listener.
-   */
-  _uninit: function DTPU_uninit() {
-    this._os.removeObserver(this, "quit-application-granted");
-    this._activeTaskbarProgress = null;
-    this._initialized = false;
-  },
-
-  /**
-   * This holds a reference to the taskbar progress for the window we're
-   * working with. This window would preferably be download window, but can be
-   * another window if it isn't open.
-   */
-  _activeTaskbarProgress: null,
-
-  // / Whether the active window is the download window
-  _activeWindowIsDownloadWindow: false,
-
-  /**
-   * Sets the active window, and whether it's the download window. This takes
-   * care of clearing out the previous active window's taskbar item, updating
-   * the taskbar, and setting an onunload listener.
-   *
-   * @param aWindow
-   *        The window to set as active.
-   * @param aIsDownloadWindow
-   *        Whether this window is a download window.
-   */
-  _setActiveWindow: function DTPU_setActiveWindow(aWindow, aIsDownloadWindow)
-  {
-    if (AppConstants.platform == "win") {
-      // Clear out the taskbar for the old active window. (If there was no active
-      // window, this is a no-op.)
-      this._clearTaskbar();
+    // Activate the indicator on the specified window.
+    let docShell = aWindow.QueryInterface(Ci.nsIInterfaceRequestor)
+                          .getInterface(Ci.nsIWebNavigation)
+                          .QueryInterface(Ci.nsIDocShellTreeItem).treeOwner
+                          .QueryInterface(Ci.nsIInterfaceRequestor)
+                          .getInterface(Ci.nsIXULWindow).docShell;
+    this._taskbarProgress = gWinTaskbar.getTaskbarProgress(docShell);
 
-      this._activeWindowIsDownloadWindow = aIsDownloadWindow;
-      if (aWindow) {
-        // Get the taskbar progress for this window
-        let docShell = aWindow.QueryInterface(Ci.nsIInterfaceRequestor).
-                         getInterface(Ci.nsIWebNavigation).
-                         QueryInterface(Ci.nsIDocShellTreeItem).treeOwner.
-                         QueryInterface(Ci.nsIInterfaceRequestor).
-                         getInterface(Ci.nsIXULWindow).docShell;
-        let taskbarProgress = this._taskbar.getTaskbarProgress(docShell);
-        this._activeTaskbarProgress = taskbarProgress;
-
-        this._updateTaskbar();
-        // _onActiveWindowUnload is idempotent, so we don't need to check whether
-        // we've already set this before or not.
-        aWindow.addEventListener("unload", function() {
-          DownloadTaskbarProgressUpdater._onActiveWindowUnload(taskbarProgress);
-        });
-      }
-      else {
-        this._activeTaskbarProgress = null;
-      }
-    }
-  },
-
-  // / Current state displayed on the active window's taskbar item
-  _taskbarState: null,
-  _totalSize: 0,
-  _totalTransferred: 0,
-
-  _shouldSetState: function DTPU_shouldSetState()
-  {
-    if (AppConstants.platform == "win") {
-      // If the active window is not the download manager window, set the state
-      // only if it is normal or indeterminate.
-      return this._activeWindowIsDownloadWindow ||
-             (this._taskbarState == Ci.nsITaskbarProgress.STATE_NORMAL ||
-              this._taskbarState == Ci.nsITaskbarProgress.STATE_INDETERMINATE);
-    }
-    return true;
-  },
-
-  /**
-   * Update the active window's taskbar indicator with the current state. There
-   * are two cases here:
-   * 1. If the active window is the download window, then we always update
-   *    the taskbar indicator.
-   * 2. If the active window isn't the download window, then we update only if
-   *    the status is normal or indeterminate. i.e. one or more downloads are
-   *    currently progressing or in scan mode. If we aren't, then we clear the
-   *    indicator.
-   */
-  _updateTaskbar: function DTPU_updateTaskbar()
-  {
-    if (!this._activeTaskbarProgress) {
-      return;
+    // If the DownloadSummary object has already been created, we should update
+    // the state of the new indicator, otherwise it will be updated as soon as
+    // the DownloadSummary view is registered.
+    if (this._summary) {
+      this.onSummaryChanged();
     }
 
-    if (this._shouldSetState()) {
-      this._activeTaskbarProgress.setProgressState(this._taskbarState,
-                                                   this._totalTransferred,
-                                                   this._totalSize);
-    }
-    // Clear any state otherwise
-    else {
-      this._clearTaskbar();
-    }
-  },
-
-  /**
-   * Clear taskbar state. This is needed:
-   * - to transfer the indicator off a window before transferring it onto
-   *   another one
-   * - whenever we don't want to show it for a non-download window.
-   */
-  _clearTaskbar: function DTPU_clearTaskbar()
-  {
-    if (this._activeTaskbarProgress) {
-      this._activeTaskbarProgress.setProgressState(
-        Ci.nsITaskbarProgress.STATE_NO_PROGRESS
-      );
-    }
-  },
-
-  /**
-   * Update this._taskbarState, this._totalSize and this._totalTransferred.
-   * This is called when the download manager is initialized or when the
-   * progress or state of a download changes.
-   * We compute the number of active and paused downloads, and the total size
-   * and total amount already transferred across whichever downloads we have
-   * the data for.
-   * - If there are no active downloads, then we don't want to show any
-   *   progress.
-   * - If the number of active downloads is equal to the number of paused
-   *   downloads, then we show a paused indicator if we know the size of at
-   *   least one download, and no indicator if we don't.
-   * - If the number of active downloads is more than the number of paused
-   *   downloads, then we show a "normal" indicator if we know the size of at
-   *   least one download, and an indeterminate indicator if we don't.
-   */
-  _updateStatus: function DTPU_updateStatus()
-  {
-    let numActive = 0;
-    // this._dm.activeDownloadCount + this._dm.activePrivateDownloadCount;
-    let totalSize = 0, totalTransferred = 0;
-
-    if (numActive == 0) {
-      this._taskbarState = Ci.nsITaskbarProgress.STATE_NO_PROGRESS;
-    }
-    else {
-      let numPaused = 0, numScanning = 0;
-
-      // Enumerate all active downloads
-      /* [this._dm.activeDownloads, this._dm.activePrivateDownloads].forEach(function(downloads) {
-        while (downloads.hasMoreElements()) {
-          let download = downloads.getNext().QueryInterface(Ci.nsIDownload);
-          // Only set values if we actually know the download size
-          if (download.percentComplete != -1) {
-            totalSize += download.size;
-            totalTransferred += download.amountTransferred;
-          }
-          // We might need to display a paused state, so track this
-          if (download.state == this._dm.DOWNLOAD_PAUSED) {
-            numPaused++;
-          } else if (download.state == this._dm.DOWNLOAD_SCANNING) {
-            numScanning++;
-          }
-        } 
-      }.bind(this)); */
-
-      // If all downloads are paused, show the progress as paused, unless we
-      // don't have any information about sizes, in which case we don't
-      // display anything
-      if (numActive == numPaused) {
-        if (totalSize == 0) {
-          this._taskbarState = Ci.nsITaskbarProgress.STATE_NO_PROGRESS;
-          totalTransferred = 0;
-        }
-        else {
-          this._taskbarState = Ci.nsITaskbarProgress.STATE_PAUSED;
-        }
-      }
-      // If at least one download is not paused, and we don't have any
-      // information about download sizes, display an indeterminate indicator
-      else if (totalSize == 0 || numActive == numScanning) {
-        this._taskbarState = Ci.nsITaskbarProgress.STATE_INDETERMINATE;
-        totalSize = 0;
-        totalTransferred = 0;
-      }
-      // Otherwise display a normal progress bar
-      else {
-        this._taskbarState = Ci.nsITaskbarProgress.STATE_NORMAL;
-      }
-    }
-
-    this._totalSize = totalSize;
-    this._totalTransferred = totalTransferred;
-  },
-
-  /**
-   * Called when a window that at one point has been an active window is
-   * closed. If this window is currently the active window, we need to look for
-   * another window and make that our active window.
-   *
-   * This function is idempotent, so multiple calls for the same window are not
-   * a problem.
-   *
-   * @param aTaskbarProgress
-   *        The taskbar progress for the window that is being unloaded.
-   */
-  _onActiveWindowUnload: function DTPU_onActiveWindowUnload(aTaskbarProgress)
-  {
-    if (this._activeTaskbarProgress == aTaskbarProgress) {
+    aWindow.addEventListener("unload", () => {
       let windows = Services.wm.getEnumerator(null);
       let newActiveWindow = null;
       if (windows.hasMoreElements()) {
         newActiveWindow = windows.getNext().QueryInterface(Ci.nsIDOMWindow);
       }
-
-      // We aren't ever going to reach this point while the download manager is
-      // open, so it's safe to assume false for the second operand
-      this._setActiveWindow(newActiveWindow, false);
-    }
-  },
-
-  // nsIDownloadProgressListener
-
-  /**
-   * Update status if a download's progress has changed.
-   */
-  onProgressChange: function DTPU_onProgressChange()
-  {
-    this._updateStatus();
-    this._updateTaskbar();
+      if (newActiveWindow) {
+        // Move the progress indicator to the other browser window.
+        this.attachIndicator(newActiveWindow, false);
+      } else {
+        // The last window has been closed. We remove the reference to
+        // the taskbar progress object.
+        this._taskbarProgress = null;
+      }
+    });
   },
 
-  /**
-   * Update status if a download's state has changed.
-   */
-  onDownloadStateChange: function DTPU_onDownloadStateChange()
-  {
-    this._updateStatus();
-    this._updateTaskbar();
-  },
+  // DownloadSummary view
+  onSummaryChanged() {
+    // If the last browser window has been closed, we have no indicator any more.
+    if (!this._taskbarProgress) {
+      return;
+    }
 
-  onSecurityChange: function() { },
-
-  onStateChange: function() { },
-
-  observe: function DTPU_observe(aSubject, aTopic, aData) {
-    if (aTopic == "quit-application-granted") {
-      this._uninit();
+    if (this._summary.allHaveStopped || this._summary.progressTotalBytes == 0) {
+      this._taskbarProgress.setProgressState(
+                               Ci.nsITaskbarProgress.STATE_NO_PROGRESS, 0, 0);
+    } else {
+      // For a brief moment before completion, some download components may
+      // report more transferred bytes than the total number of bytes.  Thus,
+      // ensure that we never break the expectations of the progress indicator.
+      let progressCurrentBytes = Math.min(this._summary.progressTotalBytes,
+                                          this._summary.progressCurrentBytes);
+      this._taskbarProgress.setProgressState(
+                               Ci.nsITaskbarProgress.STATE_NORMAL,
+                               progressCurrentBytes,
+                               this._summary.progressTotalBytes);
     }
-  }
+  },
 };
--- a/suite/components/downloads/moz.build
+++ b/suite/components/downloads/moz.build
@@ -4,13 +4,14 @@
 # License, v. 2.0. If a copy of the MPL was not distributed with this
 # file, You can obtain one at http://mozilla.org/MPL/2.0/.
 
 MOCHITEST_CHROME_MANIFESTS += ['tests/chrome/chrome.ini']
 
 JAR_MANIFESTS += ['jar.mn']
 
 EXTRA_JS_MODULES += [
+    'DownloadsCommon.jsm',
     'DownloadsTaskbar.jsm',
 ]
 
 with Files('**'):
     BUG_COMPONENT = ('SeaMonkey', 'Downloads')