Bug 1117141 - Part 1 of 2 - Bypass all the DownloadsDataItem properties. r=mak
authorPaolo Amadini <paolo.mozmail@amadzone.org>
Mon, 16 Feb 2015 18:49:49 +0000
changeset 229502 2b8d8208e2c59c1da8bc885b5f3b3453d8336769
parent 229501 37616c2fcc8b65d0a92028be89bab6be96ab711d
child 229503 448f00fe77e1474b1576eb06141a1ffaaa9aeb8a
push id55709
push userryanvm@gmail.com
push dateTue, 17 Feb 2015 19:27:34 +0000
treeherdermozilla-inbound@ebd50d4250b2 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersmak
bugs1117141
milestone38.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 1117141 - Part 1 of 2 - Bypass all the DownloadsDataItem properties. r=mak
browser/components/downloads/DownloadsCommon.jsm
browser/components/downloads/content/allDownloadsViewOverlay.js
browser/components/downloads/content/downloads.js
browser/components/downloads/content/downloadsViewCommon.js
browser/components/downloads/test/browser/browser_basic_functionality.js
--- a/browser/components/downloads/DownloadsCommon.jsm
+++ b/browser/components/downloads/DownloadsCommon.jsm
@@ -296,16 +296,65 @@ this.DownloadsCommon = {
       }
       return this._summary = new DownloadsSummaryData(false, aNumToExclude);
     }
   },
   _summary: null,
   _privateSummary: null,
 
   /**
+   * Returns the legacy state integer value for the provided Download object.
+   */
+  stateOfDownload(download) {
+    // Collapse state using the correct priority.
+    if (!download.stopped) {
+      return nsIDM.DOWNLOAD_DOWNLOADING;
+    }
+    if (download.succeeded) {
+      return nsIDM.DOWNLOAD_FINISHED;
+    }
+    if (download.error) {
+      if (download.error.becauseBlockedByParentalControls) {
+        return nsIDM.DOWNLOAD_BLOCKED_PARENTAL;
+      }
+      if (download.error.becauseBlockedByReputationCheck) {
+        return nsIDM.DOWNLOAD_DIRTY;
+      }
+      return nsIDM.DOWNLOAD_FAILED;
+    }
+    if (download.canceled) {
+      if (download.hasPartialData) {
+        return nsIDM.DOWNLOAD_PAUSED;
+      }
+      return nsIDM.DOWNLOAD_CANCELED;
+    }
+    return nsIDM.DOWNLOAD_NOTSTARTED;
+  },
+
+  /**
+   * Returns the highest number of bytes transferred or the known size of the
+   * given Download object, or -1 if the size is not available. Callers should
+   * use Download properties directly when possible.
+   */
+  maxBytesOfDownload(download) {
+    if (download.succeeded) {
+      // If the download succeeded, show the final size if available, otherwise
+      // use the last known number of bytes transferred.  The final size on disk
+      // will be available when bug 941063 is resolved.
+      return download.hasProgress ? download.totalBytes : download.currentBytes;
+    } else if (download.hasProgress) {
+      // If the final size and progress are known, use them.
+      return download.totalBytes;
+    } else {
+      // The download final size and progress percentage is unknown.
+      return -1;
+    }
+  },
+
+  /**
    * Given an iterable collection of DownloadDataItems, generates and returns
    * statistics about that collection.
    *
    * @param aDataItems An iterable collection of DownloadDataItems.
    *
    * @return Object whose properties are the generated statistics. Currently,
    *         we return the following properties:
    *
@@ -334,41 +383,44 @@ this.DownloadsCommon = {
       // it's still at Infinity by the time we're done iterating all
       // dataItems.
       slowestSpeed: Infinity,
       rawTimeLeft: -1,
       percentComplete: -1
     }
 
     for (let dataItem of aDataItems) {
+      let download = dataItem.download;
+      let state = DownloadsCommon.stateOfDownload(download);
+      let maxBytes = DownloadsCommon.maxBytesOfDownload(download);
+
       summary.numActive++;
-      switch (dataItem.state) {
+      switch (state) {
         case nsIDM.DOWNLOAD_PAUSED:
           summary.numPaused++;
           break;
         case nsIDM.DOWNLOAD_SCANNING:
           summary.numScanning++;
           break;
         case nsIDM.DOWNLOAD_DOWNLOADING:
           summary.numDownloading++;
-          if (dataItem.maxBytes > 0 && dataItem.download.speed > 0) {
-            let sizeLeft = dataItem.maxBytes - dataItem.download.currentBytes;
+          if (maxBytes > 0 && download.speed > 0) {
+            let sizeLeft = maxBytes - download.currentBytes;
             summary.rawTimeLeft = Math.max(summary.rawTimeLeft,
-                                           sizeLeft / dataItem.download.speed);
+                                           sizeLeft / download.speed);
             summary.slowestSpeed = Math.min(summary.slowestSpeed,
-                                            dataItem.download.speed);
+                                            download.speed);
           }
           break;
       }
       // Only add to total values if we actually know the download size.
-      if (dataItem.maxBytes > 0 &&
-          dataItem.state != nsIDM.DOWNLOAD_CANCELED &&
-          dataItem.state != nsIDM.DOWNLOAD_FAILED) {
-        summary.totalSize += dataItem.maxBytes;
-        summary.totalTransferred += dataItem.download.currentBytes;
+      if (maxBytes > 0 && state != nsIDM.DOWNLOAD_CANCELED &&
+                          state != nsIDM.DOWNLOAD_FAILED) {
+        summary.totalSize += maxBytes;
+        summary.totalTransferred += download.currentBytes;
       }
     }
 
     if (summary.numActive != 0 && summary.totalSize != 0 &&
         summary.numActive != summary.numScanning) {
       summary.percentComplete = (summary.totalTransferred /
                                  summary.totalSize) * 100;
     }
@@ -413,17 +465,17 @@ this.DownloadsCommon = {
     // In the last few seconds of downloading, we are always subtracting and
     // never adding to the time left.  Ensure that we never fall below one
     // second left until all downloads are actually finished.
     return aLastSeconds = Math.max(aSeconds, 1);
   },
 
   /**
    * Opens a downloaded file.
-   * If you've a dataItem, you should call dataItem.openLocalFile.
+   *
    * @param aFile
    *        the downloaded file to be opened.
    * @param aMimeInfo
    *        the mime type info object.  May be null.
    * @param aOwnerWindow
    *        the window with which this action is associated.
    */
   openDownloadedFile(aFile, aMimeInfo, aOwnerWindow) {
@@ -604,16 +656,17 @@ XPCOMUtils.defineLazyGetter(DownloadsCom
  * objects, one accessing non-private downloads, and the other accessing private
  * ones.
  */
 function DownloadsDataCtor(aPrivate) {
   this._isPrivate = aPrivate;
 
   // Contains all the available DownloadsDataItem objects.
   this.dataItems = new Set();
+  this.oldDownloadStates = new Map();
 
   // Array of view objects that should be notified when the available download
   // data changes.
   this._views = [];
 
   // Maps Download objects to DownloadDataItem objects.
   this._downloadToDataItemMap = new Map();
 }
@@ -632,17 +685,19 @@ DownloadsDataCtor.prototype = {
   },
   _dataLinkInitialized: false,
 
   /**
    * True if there are finished downloads that can be removed from the list.
    */
   get canRemoveFinished() {
     for (let dataItem of this.dataItems) {
-      if (!dataItem.inProgress) {
+      let download = dataItem.download;
+      // 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.
@@ -656,108 +711,108 @@ DownloadsDataCtor.prototype = {
 
   //////////////////////////////////////////////////////////////////////////////
   //// Integration with the asynchronous Downloads back-end
 
   onDownloadAdded(aDownload) {
     let dataItem = new DownloadsDataItem(aDownload);
     this._downloadToDataItemMap.set(aDownload, dataItem);
     this.dataItems.add(dataItem);
+    this.oldDownloadStates.set(aDownload,
+                               DownloadsCommon.stateOfDownload(aDownload));
 
     for (let view of this._views) {
       view.onDataItemAdded(dataItem, true);
     }
-
-    this._updateDataItemState(dataItem);
   },
 
   onDownloadChanged(aDownload) {
-    let dataItem = this._downloadToDataItemMap.get(aDownload);
-    if (!dataItem) {
+    let aDataItem = this._downloadToDataItemMap.get(aDownload);
+    if (!aDataItem) {
       Cu.reportError("Download doesn't exist.");
       return;
     }
 
-    this._updateDataItemState(dataItem);
+    let oldState = this.oldDownloadStates.get(aDownload);
+    let newState = DownloadsCommon.stateOfDownload(aDownload);
+    this.oldDownloadStates.set(aDownload, newState);
+
+    if (oldState != newState) {
+      if (aDownload.succeeded ||
+          (aDownload.canceled && !aDownload.hasPartialData) ||
+          aDownload.error) {
+        // Store the end time that may be displayed by the views.
+        aDownload.endTime = Date.now();
+
+        // This state transition code should actually be located in a Downloads
+        // API module (bug 941009).  Moreover, the fact that state is stored as
+        // annotations should be ideally hidden behind methods of
+        // nsIDownloadHistory (bug 830415).
+        if (!this._isPrivate) {
+          try {
+            let downloadMetaData = {
+              state: DownloadsCommon.stateOfDownload(aDownload),
+              endTime: aDownload.endTime,
+            };
+            if (aDownload.succeeded ||
+                (aDownload.error && aDownload.error.becauseBlocked)) {
+              downloadMetaData.fileSize =
+                DownloadsCommon.maxBytesOfDownload(aDataItem.download);
+            }
+  
+            PlacesUtils.annotations.setPageAnnotation(
+                          NetUtil.newURI(aDownload.source.url),
+                          "downloads/metaData",
+                          JSON.stringify(downloadMetaData), 0,
+                          PlacesUtils.annotations.EXPIRE_WITH_HISTORY);
+          } catch (ex) {
+            Cu.reportError(ex);
+          }
+        }
+      }
+
+      for (let view of this._views) {
+        try {
+          view.onDataItemStateChanged(aDataItem);
+        } catch (ex) {
+          Cu.reportError(ex);
+        }
+      }
+
+      if (aDownload.succeeded ||
+          (aDownload.error && aDownload.error.becauseBlocked)) {
+        this._notifyDownloadEvent("finish");
+      }
+    }
+
+    if (!aDownload.newDownloadNotified) {
+      aDownload.newDownloadNotified = true;
+      this._notifyDownloadEvent("start");
+    }
+
+    for (let view of this._views) {
+      view.onDataItemChanged(aDataItem);
+    }
   },
 
   onDownloadRemoved(aDownload) {
     let dataItem = this._downloadToDataItemMap.get(aDownload);
     if (!dataItem) {
       Cu.reportError("Download doesn't exist.");
       return;
     }
 
     this._downloadToDataItemMap.delete(aDownload);
     this.dataItems.delete(dataItem);
+    this.oldDownloadStates.delete(aDownload);
     for (let view of this._views) {
       view.onDataItemRemoved(dataItem);
     }
   },
 
-  /**
-   * Updates the given data item and sends related notifications.
-   */
-  _updateDataItemState(aDataItem) {
-    let oldState = aDataItem.state;
-    let wasInProgress = aDataItem.inProgress;
-    let wasDone = aDataItem.done;
-
-    aDataItem.updateFromDownload();
-
-    if (wasInProgress && !aDataItem.inProgress) {
-      aDataItem.endTime = Date.now();
-    }
-
-    if (oldState != aDataItem.state) {
-      for (let view of this._views) {
-        try {
-          view.onDataItemStateChanged(aDataItem, oldState);
-        } catch (ex) {
-          Cu.reportError(ex);
-        }
-      }
-
-      // This state transition code should actually be located in a Downloads
-      // API module (bug 941009).  Moreover, the fact that state is stored as
-      // annotations should be ideally hidden behind methods of
-      // nsIDownloadHistory (bug 830415).
-      if (!this._isPrivate && !aDataItem.inProgress) {
-        try {
-          let downloadMetaData = { state: aDataItem.state,
-                                   endTime: aDataItem.endTime };
-          if (aDataItem.done) {
-            downloadMetaData.fileSize = aDataItem.maxBytes;
-          }
-
-          PlacesUtils.annotations.setPageAnnotation(
-                        NetUtil.newURI(aDataItem.download.source.url),
-                        "downloads/metaData",
-                        JSON.stringify(downloadMetaData), 0,
-                        PlacesUtils.annotations.EXPIRE_WITH_HISTORY);
-        } catch (ex) {
-          Cu.reportError(ex);
-        }
-      }
-    }
-
-    if (!aDataItem.newDownloadNotified) {
-      aDataItem.newDownloadNotified = true;
-      this._notifyDownloadEvent("start");
-    }
-
-    if (!wasDone && aDataItem.done) {
-      this._notifyDownloadEvent("finish");
-    }
-
-    for (let view of this._views) {
-      view.onDataItemChanged(aDataItem);
-    }
-  },
-
   //////////////////////////////////////////////////////////////////////////////
   //// 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
@@ -866,64 +921,21 @@ XPCOMUtils.defineLazyGetter(this, "Downl
  *
  * The endTime property is initialized to the current date and time.
  *
  * @param aDownload
  *        The Download object with the current state.
  */
 function DownloadsDataItem(aDownload) {
   this.download = aDownload;
-  this.endTime = Date.now();
-  this.updateFromDownload();
+  this.download.endTime = Date.now();
 }
 
 DownloadsDataItem.prototype = {
-  /**
-   * Updates this object from the underlying Download object.
-   */
-  updateFromDownload() {
-    // Collapse state using the correct priority.
-    if (this.download.succeeded) {
-      this.state = nsIDM.DOWNLOAD_FINISHED;
-    } else if (this.download.error &&
-               this.download.error.becauseBlockedByParentalControls) {
-      this.state = nsIDM.DOWNLOAD_BLOCKED_PARENTAL;
-    } else if (this.download.error &&
-               this.download.error.becauseBlockedByReputationCheck) {
-      this.state = nsIDM.DOWNLOAD_DIRTY;
-    } else if (this.download.error) {
-      this.state = nsIDM.DOWNLOAD_FAILED;
-    } else if (this.download.canceled && this.download.hasPartialData) {
-      this.state = nsIDM.DOWNLOAD_PAUSED;
-    } else if (this.download.canceled) {
-      this.state = nsIDM.DOWNLOAD_CANCELED;
-    } else if (this.download.stopped) {
-      this.state = nsIDM.DOWNLOAD_NOTSTARTED;
-    } else {
-      this.state = nsIDM.DOWNLOAD_DOWNLOADING;
-    }
-
-    if (this.download.succeeded) {
-      // If the download succeeded, show the final size if available, otherwise
-      // use the last known number of bytes transferred.  The final size on disk
-      // will be available when bug 941063 is resolved.
-      this.maxBytes = this.download.hasProgress ?
-                             this.download.totalBytes :
-                             this.download.currentBytes;
-      this.percentComplete = 100;
-    } else if (this.download.hasProgress) {
-      // If the final size and progress are known, use them.
-      this.maxBytes = this.download.totalBytes;
-      this.percentComplete = this.download.progress;
-    } else {
-      // The download final size and progress percentage is unknown.
-      this.maxBytes = -1;
-      this.percentComplete = -1;
-    }
-  },
+  get state() DownloadsCommon.stateOfDownload(this.download),
 
   /**
    * Indicates whether the download is proceeding normally, and not finished
    * yet.  This includes paused downloads.  When this property is true, the
    * "progress" property represents the current progress of the download.
    */
   get inProgress() {
     return [
@@ -1254,19 +1266,20 @@ DownloadsIndicatorDataCtor.prototype = {
    *        DownloadsDataItem object that is being removed.
    */
   onDataItemRemoved(aDataItem) {
     this._itemCount--;
     this._updateViews();
   },
 
   // DownloadsView
-  onDataItemStateChanged(aDataItem, aOldState) {
-    if (aDataItem.state == nsIDM.DOWNLOAD_FINISHED ||
-        aDataItem.state == nsIDM.DOWNLOAD_FAILED) {
+  onDataItemStateChanged(aDataItem) {
+    let download = aDataItem.download;
+
+    if (download.succeeded || download.error) {
       this.attention = true;
     }
 
     // Since the state of a download changed, reset the estimated time left.
     this._lastRawTimeLeft = -1;
     this._lastTimeLeft = -1;
   },
 
@@ -1363,17 +1376,21 @@ DownloadsIndicatorDataCtor.prototype = {
    * interested in. This generator is passed off to summarizeDownloads in order
    * to generate statistics about the dataItems we care about - in this case,
    * it's all dataItems for active downloads.
    */
   _activeDataItems() {
     let dataItems = this._isPrivate ? PrivateDownloadsData.dataItems
                                     : DownloadsData.dataItems;
     for (let dataItem of dataItems) {
-      if (dataItem && dataItem.inProgress) {
+      if (!dataItem) {
+        continue;
+      }
+      let download = dataItem.download;
+      if (!download.stopped || (download.canceled && download.hasPartialData)) {
         yield dataItem;
       }
     }
   },
 
   /**
    * Computes aggregate values based on the current state of downloads.
    */
@@ -1507,17 +1524,17 @@ DownloadsSummaryData.prototype = {
 
   onDataItemRemoved(aDataItem) {
     let itemIndex = this._dataItems.indexOf(aDataItem);
     this._dataItems.splice(itemIndex, 1);
     this._updateViews();
   },
 
   // DownloadsView
-  onDataItemStateChanged(aOldState) {
+  onDataItemStateChanged() {
     // Since the state of a download changed, reset the estimated time left.
     this._lastRawTimeLeft = -1;
     this._lastTimeLeft = -1;
   },
 
   // DownloadsView
   onDataItemChanged() {
     this._updateViews();
--- a/browser/components/downloads/content/allDownloadsViewOverlay.js
+++ b/browser/components/downloads/content/allDownloadsViewOverlay.js
@@ -19,25 +19,101 @@ const DOWNLOAD_VIEW_SUPPORTED_COMMANDS =
   "downloadsCmd_openReferrer", "downloadsCmd_clearDownloads"];
 
 /**
  * Represents a download from the browser history. It implements part of the
  * interface of the Download object.
  *
  * @param url
  *        URI string for the download source.
+ * @param endTime
+ *        Timestamp with the end time for the download, used if there is no
+ *        additional metadata available.
  */
-function HistoryDownload(url) {
+function HistoryDownload(aPlacesNode) {
   // TODO (bug 829201): history downloads should get the referrer from Places.
-  this.source = { url };
+  this.source = { url: aPlacesNode.uri };
   this.target = { path: undefined, size: undefined };
+
+  // In case this download cannot obtain its end time from the Places metadata,
+  // use the time from the Places node, that is the start time of the download.
+  this.endTime = aPlacesNode.time / 1000;
 }
 
 HistoryDownload.prototype = {
   /**
+   * Pushes information from Places metadata into this object.
+   */
+  updateFromMetaData(aPlacesMetaData) {
+    try {
+      this.target.path = Cc["@mozilla.org/network/protocol;1?name=file"]
+                           .getService(Ci.nsIFileProtocolHandler)
+                           .getFileFromURLSpec(aPlacesMetaData.
+                                               targetFileURISpec).path;
+    } catch (ex) {
+      this.target.path = undefined;
+    }
+
+    try {
+      let metaData = JSON.parse(aPlacesMetaData.jsonDetails);
+      this.succeeded = metaData.state == nsIDM.DOWNLOAD_FINISHED;
+      this.error = metaData.state == nsIDM.DOWNLOAD_FAILED
+                   ? { message: "History download failed." }
+                   : metaData.state == nsIDM.DOWNLOAD_BLOCKED_PARENTAL
+                   ? { becauseBlockedByParentalControls: true }
+                   : metaData.state == nsIDM.DOWNLOAD_DIRTY
+                   ? { becauseBlockedByReputationCheck: true }
+                   : null;
+      this.canceled = metaData.state == nsIDM.DOWNLOAD_CANCELED ||
+                      metaData.state == nsIDM.DOWNLOAD_PAUSED;
+      this.endTime = metaData.endTime;
+      this.target.size = metaData.fileSize;
+    } catch (ex) {
+      // Metadata might be missing from a download that has started but hasn't
+      // stopped already. Normally, this state is overridden with the one from
+      // the corresponding in-progress session download. But if the browser is
+      // terminated abruptly and additionally the file with information about
+      // in-progress downloads is lost, we may end up using this state. We use
+      // the failed state to allow the download to be restarted.
+      //
+      // On the other hand, if the download is missing the target file
+      // annotation as well, it is just a very old one, and we can assume it
+      // succeeded.
+      this.succeeded = !this.target.path;
+      this.error = this.target.path ? { message: "Unstarted download." } : null;
+      this.canceled = false;
+      this.target.size = -1;
+    }
+
+    // This property is currently used to get the size of downloads, but will be
+    // replaced by download.target.size when available for session downloads.
+    this.totalBytes = this.target.size;
+    this.currentBytes = this.target.size;
+  },
+
+  /**
+   * History downloads are never in progress.
+   */
+  stopped: true,
+
+  /**
+   * No percentage indication is shown for history downloads.
+   */
+  hasProgress: false,
+
+  /**
+   * History downloads cannot be restarted using their partial data, even if
+   * they are indicated as paused in their Places metadata. The only way is to
+   * use the information from a persisted session download, that will be shown
+   * instead of the history download. In case this session download is not
+   * available, we show the history download as canceled, not paused.
+   */
+  hasPartialData: false,
+
+  /**
    * This method mimicks the "start" method of session downloads, and is called
    * when the user retries a history download.
    */
   start() {
     // In future we may try to download into the same original target uri, when
     // we have it.  Though that requires verifying the path is still valid and
     // may surprise the user if he wants to be requested every time.
     let browserWin = RecentWindow.getMostRecentBrowserWindow();
@@ -54,67 +130,21 @@ HistoryDownload.prototype = {
 /**
  * Represents a download from the browser history. It uses the same interface as
  * the DownloadsDataItem object.
  *
  * @param aPlacesNode
  *        The Places node for the history download.
  */
 function DownloadsHistoryDataItem(aPlacesNode) {
-  this.download = new HistoryDownload(aPlacesNode.uri);
-
-  // In case this download cannot obtain its end time from the Places metadata,
-  // use the time from the Places node, that is the start time of the download.
-  this.endTime = aPlacesNode.time / 1000;
+  this.download = new HistoryDownload(aPlacesNode);
 }
 
 DownloadsHistoryDataItem.prototype = {
   __proto__: DownloadsDataItem.prototype,
-
-  /**
-   * Pushes information from Places metadata into this object.
-   */
-  updateFromMetaData(aPlacesMetaData) {
-    try {
-      let targetFile = Cc["@mozilla.org/network/protocol;1?name=file"]
-                         .getService(Ci.nsIFileProtocolHandler)
-                         .getFileFromURLSpec(aPlacesMetaData.targetFileURISpec);
-      this.download.target.path = targetFile.path;
-    } catch (ex) {
-      this.download.target.path = undefined;
-    }
-
-    try {
-      let metaData = JSON.parse(aPlacesMetaData.jsonDetails);
-      this.state = metaData.state;
-      this.endTime = metaData.endTime;
-      this.download.target.size = metaData.fileSize;
-    } catch (ex) {
-      // Metadata might be missing from a download that has started but hasn't
-      // stopped already. Normally, this state is overridden with the one from
-      // the corresponding in-progress session download. But if the browser is
-      // terminated abruptly and additionally the file with information about
-      // in-progress downloads is lost, we may end up using this state. We use
-      // the failed state to allow the download to be restarted.
-      //
-      // On the other hand, if the download is missing the target file
-      // annotation as well, it is just a very old one, and we can assume it
-      // succeeded.
-      this.state = this.download.target.path ? nsIDM.DOWNLOAD_FAILED
-                                             : nsIDM.DOWNLOAD_FINISHED;
-      this.download.target.size = undefined;
-    }
-
-    // This property is currently used to get the size of downloads, but will be
-    // replaced by download.target.size when available for session downloads.
-    this.maxBytes = this.download.target.size;
-
-    // This is not displayed for history downloads, that are never in progress.
-    this.percentComplete = 100;
-  },
 };
 
 /**
  * A download element shell is responsible for handling the commands and the
  * displayed data for a single download view element.
  *
  * The shell may contain a session download, a history download, or both.  When
  * both a history and a current download are present, the current download gets
@@ -218,37 +248,28 @@ HistoryDownloadElementShell.prototype = 
     this._updateState();
   },
 
   get statusTextAndTip() {
     let status = this.rawStatusTextAndTip;
 
     // The base object would show extended progress information in the tooltip,
     // but we move this to the main view and never display a tooltip.
-    if (this.dataItem.state == nsIDM.DOWNLOAD_DOWNLOADING) {
+    if (!this.download.stopped) {
       status.text = status.tip;
     }
     status.tip = "";
 
     return status;
   },
 
   onStateChanged() {
-    // If a download just finished successfully, it means that the target file
-    // now exists and we can extract its specific icon.  To ensure that the icon
-    // is reloaded, we must change the URI used by the XUL image element, for
-    // example by adding a query parameter.  Since this URI has a "moz-icon"
-    // scheme, this only works if we add one of the parameters explicitly
-    // supported by the nsIMozIconURI interface.
-    if (this.dataItem.state == nsIDM.DOWNLOAD_FINISHED) {
-      this.element.setAttribute("image", this.image + "&state=normal");
-    }
-
-    // Update the user interface after switching states.
-    this.element.setAttribute("state", this.dataItem.state);
+    this.element.setAttribute("image", this.image);
+    this.element.setAttribute("state",
+                              DownloadsCommon.stateOfDownload(this.download));
 
     if (this.element.selected) {
       goUpdateDownloadCommands();
     } else {
       goUpdateCommand("downloadsCmd_clearDownloads");
     }
   },
 
@@ -272,41 +293,42 @@ HistoryDownloadElementShell.prototype = 
         }
 
         if (this._targetFileChecked) {
           return this._targetFileExists;
         }
 
         // If the target file information is not yet fetched,
         // temporarily assume that the file is in place.
-        return this.dataItem.state == nsIDM.DOWNLOAD_FINISHED;
+        return this.download.succeeded;
       case "downloadsCmd_show":
         // TODO: Bug 827010 - Handle part-file asynchronously.
-        if (this._sessionDataItem &&
-            this.dataItem.partFile && this.dataItem.partFile.exists()) {
-          return true;
+        if (this._sessionDataItem && this.download.target.partFilePath) {
+          let partFile = new FileUtils.File(this.download.target.partFilePath);
+          if (partFile.exists()) {
+            return true;
+          }
         }
 
         if (this._targetFileChecked) {
           return this._targetFileExists;
         }
 
         // If the target file information is not yet fetched,
         // temporarily assume that the file is in place.
-        return this.dataItem.state == nsIDM.DOWNLOAD_FINISHED;
+        return this.download.succeeded;
       case "downloadsCmd_pauseResume":
-        return this._sessionDataItem && this.dataItem.inProgress &&
-               this.dataItem.download.hasPartialData;
+        return this.download.hasPartialData && !this.download.error;
       case "downloadsCmd_retry":
-        return this.dataItem.canRetry;
+        return this.download.canceled || this.download.error;
       case "downloadsCmd_openReferrer":
         return !!this.download.source.referrer;
       case "cmd_delete":
-        // The behavior in this case is somewhat unexpected, so we disallow that.
-        return !this.dataItem.inProgress;
+        // We don't want in-progress downloads to be removed accidentally.
+        return this.download.stopped;
       case "downloadsCmd_cancel":
         return !!this._sessionDataItem;
     }
     return false;
   },
 
   /* nsIController */
   doCommand(aCommand) {
@@ -391,17 +413,18 @@ HistoryDownloadElementShell.prototype = 
           return "downloadsCmd_show";
         case nsIDM.DOWNLOAD_BLOCKED_PARENTAL:
         case nsIDM.DOWNLOAD_DIRTY:
         case nsIDM.DOWNLOAD_BLOCKED_POLICY:
           return "downloadsCmd_openReferrer";
       }
       return "";
     }
-    let command = getDefaultCommandForState(this.dataItem.state);
+    let command = getDefaultCommandForState(
+                            DownloadsCommon.stateOfDownload(this.download));
     if (command && this.isCommandEnabled(command)) {
       this.doCommand(command);
     }
   },
 
   /**
    * This method is called by the outer download view, after the controller
    * commands have already been updated. In case we did not check for the
@@ -620,18 +643,19 @@ DownloadsPlacesView.prototype = {
    *        To speed up the appending of multiple elements to the end of the
    *        list which are coming in a single batch (i.e. invalidateContainer),
    *        a document fragment may be passed to which the new elements would
    *        be appended. It's the caller's job to ensure the fragment is merged
    *        to the richlistbox at the end.
    */
   _addDownloadData(aDataItem, aPlacesNode, aNewest = false,
                    aDocumentFragment = null) {
+    let sessionDownload = aDataItem && aDataItem.download;
     let downloadURI = aPlacesNode ? aPlacesNode.uri
-                                  : aDataItem.download.source.url;
+                                  : sessionDownload.source.url;
     let shellsForURI = this._downloadElementsShellsForURI.get(downloadURI);
     if (!shellsForURI) {
       shellsForURI = new Set();
       this._downloadElementsShellsForURI.set(downloadURI, shellsForURI);
     }
 
     let newOrUpdatedShell = null;
 
@@ -673,17 +697,17 @@ DownloadsPlacesView.prototype = {
     if (shouldCreateShell) {
       // If we are adding a new history download here, it means there is no
       // associated session download, thus we must read the Places metadata,
       // because it will not be obscured by the session download.
       let historyDataItem = null;
       if (aPlacesNode) {
         let metaData = this._getCachedPlacesMetaDataFor(aPlacesNode.uri);
         historyDataItem = new DownloadsHistoryDataItem(aPlacesNode);
-        historyDataItem.updateFromMetaData(metaData);
+        historyDataItem.download.updateFromMetaData(metaData);
       }
       let shell = new HistoryDownloadElementShell(aDataItem, historyDataItem);
       shell.element._placesNode = aPlacesNode;
       newOrUpdatedShell = shell;
       shellsForURI.add(shell);
       if (aDataItem) {
         this._viewItemsForDataItems.set(aDataItem, shell);
       }
@@ -777,18 +801,19 @@ DownloadsPlacesView.prototype = {
           if (shellsForURI.size == 0)
             this._downloadElementsShellsForURI.delete(downloadURI);
         }
       }
     }
   },
 
   _removeSessionDownloadFromView(aDataItem) {
+    let download = aDataItem.download;
     let shells = this._downloadElementsShellsForURI
-                     .get(aDataItem.download.source.url);
+                     .get(download.source.url);
     if (shells.size == 0) {
       throw new Error("Should have had at leaat one shell for this uri");
     }
 
     let shell = this._viewItemsForDataItems.get(aDataItem);
     if (!shells.has(shell)) {
       throw new Error("Missing download element shell in shells list for url");
     }
@@ -796,27 +821,27 @@ DownloadsPlacesView.prototype = {
     // If there's more than one item for this download uri, we can let the
     // view item for this this particular data item go away.
     // If there's only one item for this download uri, we should only
     // keep it if it is associated with a history download.
     if (shells.size > 1 || !shell.historyDataItem) {
       this._removeElement(shell.element);
       shells.delete(shell);
       if (shells.size == 0) {
-        this._downloadElementsShellsForURI.delete(aDataItem.download.source.url);
+        this._downloadElementsShellsForURI.delete(download.source.url);
       }
     } else {
       // We have one download element shell containing both a session download
       // and a history download, and we are now removing the session download.
       // Previously, we did not use the Places metadata because it was obscured
       // by the session download. Since this is no longer the case, we have to
       // read the latest metadata before removing the session download.
       let url = shell.historyDataItem.download.source.url;
       let metaData = this._getPlacesMetaDataFor(url);
-      shell.historyDataItem.updateFromMetaData(metaData);
+      shell.historyDataItem.download.updateFromMetaData(metaData);
       shell.sessionDataItem = null;
       // Move it below the session-download items;
       if (this._lastSessionDownloadElement == shell.element) {
         this._lastSessionDownloadElement = shell.element.previousSibling;
       } else {
         let before = this._lastSessionDownloadElement ?
           this._lastSessionDownloadElement.nextSibling : this._richlistbox.firstChild;
         this._richlistbox.insertBefore(shell.element, before);
@@ -1141,17 +1166,19 @@ DownloadsPlacesView.prototype = {
   },
 
   _canClearDownloads() {
     // Downloads can be cleared if there's at least one removable download in
     // the list (either a history download or a completed session download).
     // Because history downloads are always removable and are listed after the
     // session downloads, check from bottom to top.
     for (let elt = this._richlistbox.lastChild; elt; elt = elt.previousSibling) {
-      if (!elt._shell.dataItem.inProgress) {
+      // Stopped, paused, and failed downloads with partial data are removed.
+      let download = elt._shell.download;
+      if (download.stopped && !(download.canceled && download.hasPartialData)) {
         return true;
       }
     }
     return false;
   },
 
   _copySelectedDownloadsToClipboard() {
     let urls = [for (element of this._richlistbox.selectedItems)
@@ -1238,24 +1265,26 @@ DownloadsPlacesView.prototype = {
   onContextMenu(aEvent) {
     let element = this._richlistbox.selectedItem;
     if (!element || !element._shell) {
       return false;
     }
 
     // Set the state attribute so that only the appropriate items are displayed.
     let contextMenu = document.getElementById("downloadsContextMenu");
-    let state = element._shell.dataItem.state;
-    contextMenu.setAttribute("state", state);
+    let download = element._shell.download;
+    contextMenu.setAttribute("state",
+                             DownloadsCommon.stateOfDownload(download));
 
-    if (state == nsIDM.DOWNLOAD_DOWNLOADING) {
-      // The resumable property of a download may change at any time, so
-      // ensure we update the related command now.
+    if (!download.stopped) {
+      // The hasPartialData property of a download may change at any time after
+      // it has started, so ensure we update the related command now.
       goUpdateCommand("downloadsCmd_pauseResume");
     }
+
     return true;
   },
 
   onKeyPress(aEvent) {
     let selectedElements = this._richlistbox.selectedItems;
     if (aEvent.keyCode == KeyEvent.DOM_VK_RETURN) {
       // In the content tree, opening bookmarks by pressing return is only
       // supported when a single item is selected. To be consistent, do the
--- a/browser/components/downloads/content/downloads.js
+++ b/browser/components/downloads/content/downloads.js
@@ -948,19 +948,20 @@ const DownloadsView = {
   },
 
   onDownloadDragStart(aEvent) {
     let element = this.richListBox.selectedItem;
     if (!element) {
       return;
     }
 
-    let localFile = DownloadsView.controllerForElement(element)
-                                 .dataItem.localFile;
-    if (!localFile.exists()) {
+    // We must check for existence synchronously because this is a DOM event.
+    let file = new FileUtils.File(DownloadsView.controllerForElement(element)
+                                               .download.target.path);
+    if (!file.exists()) {
       return;
     }
 
     let dataTransfer = aEvent.dataTransfer;
     dataTransfer.mozSetDataAt("application/x-moz-file", localFile, 0);
     dataTransfer.effectAllowed = "copyMove";
     var url = Services.io.newFileURI(localFile).spec;
     dataTransfer.setData("text/uri-list", url);
@@ -1004,34 +1005,27 @@ DownloadsViewItem.prototype = {
   dataItem: null,
 
   /**
    * The XUL element corresponding to the associated richlistbox item.
    */
   _element: null,
 
   onStateChanged() {
-    // If a download just finished successfully, it means that the target file
-    // now exists and we can extract its specific icon.  To ensure that the icon
-    // is reloaded, we must change the URI used by the XUL image element, for
-    // example by adding a query parameter.  Since this URI has a "moz-icon"
-    // scheme, this only works if we add one of the parameters explicitly
-    // supported by the nsIMozIconURI interface.
-    if (this.dataItem.state == Ci.nsIDownloadManager.DOWNLOAD_FINISHED) {
-      this.element.setAttribute("image", this.image + "&state=normal");
+    this.element.setAttribute("image", this.image);
+    this.element.setAttribute("state",
+                              DownloadsCommon.stateOfDownload(this.download));
 
+    if (this.download.succeeded) {
       // We assume the existence of the target of a download that just completed
       // successfully, without checking the condition in the background.  If the
       // panel is already open, this will take effect immediately.  If the panel
       // is opened later, a new background existence check will be performed.
       this.element.setAttribute("exists", "true");
     }
-
-    // Update the user interface after switching states.
-    this.element.setAttribute("state", this.dataItem.state);
   },
 
   onChanged() {
     this._updateProgress();
   },
 
   /**
    * Starts checking whether the target file of a finished download is still
@@ -1162,33 +1156,47 @@ DownloadsViewItemController.prototype = 
   //////////////////////////////////////////////////////////////////////////////
   //// Command dispatching
 
   /**
    * The DownloadDataItem controlled by this object.
    */
   dataItem: null,
 
+  get download() this.dataItem.download,
+
   isCommandEnabled(aCommand) {
     switch (aCommand) {
       case "downloadsCmd_open": {
-        return this.dataItem.download.succeeded &&
-               this.dataItem.localFile.exists();
+        if (!this.download.succeeded) {
+          return false;
+        }
+
+        let file = new FileUtils.File(this.download.target.path);
+        return file.exists();
       }
       case "downloadsCmd_show": {
-        return this.dataItem.localFile.exists() ||
-               this.dataItem.partFile.exists();
+        let file = new FileUtils.File(this.download.target.path);
+        if (file.exists()) {
+          return true;
+        }
+
+        if (!this.download.target.partFilePath) {
+          return false;
+        }
+
+        let partFile = new FileUtils.File(this.download.target.partFilePath);
+        return partFile.exists();
       }
       case "downloadsCmd_pauseResume":
-        return this.dataItem.inProgress &&
-               this.dataItem.download.hasPartialData;
+        return this.download.hasPartialData && !this.download.error;
       case "downloadsCmd_retry":
-        return this.dataItem.canRetry;
+        return this.download.canceled || this.download.error;
       case "downloadsCmd_openReferrer":
-        return !!this.dataItem.download.source.referrer;
+        return !!this.download.source.referrer;
       case "cmd_delete":
       case "downloadsCmd_cancel":
       case "downloadsCmd_copyLocation":
       case "downloadsCmd_doDefault":
         return true;
     }
     return false;
   },
@@ -1205,78 +1213,79 @@ DownloadsViewItemController.prototype = 
   /**
    * This object contains one key for each command that operates on this item.
    *
    * In commands, the "this" identifier points to the controller item.
    */
   commands: {
     cmd_delete() {
       Downloads.getList(Downloads.ALL)
-               .then(list => list.remove(this.dataItem.download))
-               .then(() => this.dataItem.download.finalize(true))
+               .then(list => list.remove(this.download))
+               .then(() => this.download.finalize(true))
                .catch(Cu.reportError);
       PlacesUtils.bhistory.removePage(
-                             NetUtil.newURI(this.dataItem.download.source.url));
+                             NetUtil.newURI(this.download.source.url));
     },
 
     downloadsCmd_cancel() {
-      this.dataItem.download.cancel().catch(() => {});
-      this.dataItem.download.removePartialData().catch(Cu.reportError);
+      this.download.cancel().catch(() => {});
+      this.download.removePartialData().catch(Cu.reportError);
     },
 
     downloadsCmd_open() {
-      this.dataItem.download.launch().catch(Cu.reportError);
+      this.download.launch().catch(Cu.reportError);
 
       // We explicitly close the panel here to give the user the feedback that
       // their click has been received, and we're handling the action.
       // Otherwise, we'd have to wait for the file-type handler to execute
       // before the panel would close. This also helps to prevent the user from
       // accidentally opening a file several times.
       DownloadsPanel.hidePanel();
     },
 
     downloadsCmd_show() {
-      DownloadsCommon.showDownloadedFile(this.dataItem.localFile);
+      let file = new FileUtils.File(this.download.target.path);
+      DownloadsCommon.showDownloadedFile(file);
 
       // We explicitly close the panel here to give the user the feedback that
       // their click has been received, and we're handling the action.
       // Otherwise, we'd have to wait for the operating system file manager
       // window to open before the panel closed. This also helps to prevent the
       // user from opening the containing folder several times.
       DownloadsPanel.hidePanel();
     },
 
     downloadsCmd_pauseResume() {
-      if (this.dataItem.download.stopped) {
-        this.dataItem.download.start();
+      if (this.download.stopped) {
+        this.download.start();
       } else {
-        this.dataItem.download.cancel();
+        this.download.cancel();
       }
     },
 
     downloadsCmd_retry() {
-      this.dataItem.download.start().catch(() => {});
+      this.download.start().catch(() => {});
     },
 
     downloadsCmd_openReferrer() {
-      openURL(this.dataItem.download.source.referrer);
+      openURL(this.download.source.referrer);
     },
 
     downloadsCmd_copyLocation() {
       let clipboard = Cc["@mozilla.org/widget/clipboardhelper;1"]
                       .getService(Ci.nsIClipboardHelper);
-      clipboard.copyString(this.dataItem.download.source.url, document);
+      clipboard.copyString(this.download.source.url, document);
     },
 
     downloadsCmd_doDefault() {
       const nsIDM = Ci.nsIDownloadManager;
 
       // Determine the default command for the current item.
       let defaultCommand = function () {
-        switch (this.dataItem.state) {
+        switch (DownloadsCommon.stateOfDownload(this.download)) {
           case nsIDM.DOWNLOAD_NOTSTARTED:       return "downloadsCmd_cancel";
           case nsIDM.DOWNLOAD_FINISHED:         return "downloadsCmd_open";
           case nsIDM.DOWNLOAD_FAILED:           return "downloadsCmd_retry";
           case nsIDM.DOWNLOAD_CANCELED:         return "downloadsCmd_retry";
           case nsIDM.DOWNLOAD_PAUSED:           return "downloadsCmd_pauseResume";
           case nsIDM.DOWNLOAD_QUEUED:           return "downloadsCmd_cancel";
           case nsIDM.DOWNLOAD_BLOCKED_PARENTAL: return "downloadsCmd_openReferrer";
           case nsIDM.DOWNLOAD_SCANNING:         return "downloadsCmd_show";
--- a/browser/components/downloads/content/downloadsViewCommon.js
+++ b/browser/components/downloads/content/downloadsViewCommon.js
@@ -67,22 +67,29 @@ DownloadElementShell.prototype = {
    * for executing commands in the user interface.
    */
   get download() this.dataItem.download,
 
   /**
    * URI string for the file type icon displayed in the download element.
    */
   get image() {
-    if (this.download.target.path) {
-      return "moz-icon://" + this.download.target.path + "?size=32";
+    if (!this.download.target.path) {
+      // Old history downloads may not have a target path.
+      return "moz-icon://.unknown?size=32";
     }
 
-    // Old history downloads may not have a target path.
-    return "moz-icon://.unknown?size=32";
+    // When a download that was previously in progress finishes successfully, it
+    // means that the target file now exists and we can extract its specific
+    // icon, for example from a Windows executable. To ensure that the icon is
+    // reloaded, however, we must change the URI used by the XUL image element,
+    // for example by adding a query parameter. This only works if we add one of
+    // the parameters explicitly supported by the nsIMozIconURI interface.
+    return "moz-icon://" + this.download.target.path + "?size=32" +
+           (this.download.succeeded ? "&state=normal" : "");
   },
 
   /**
    * The user-facing label for the download. This is normally the leaf name of
    * the download target file. In case this is a very old history download for
    * which the target file is unknown, the download source URI is displayed.
    */
   get displayName() {
@@ -106,44 +113,38 @@ DownloadElementShell.prototype = {
   },
 
   /**
    * Processes a major state change in the user interface, then proceeds with
    * the normal progress update. This function is not called for every progress
    * update in order to improve performance.
    */
   _updateState() {
-    this.element.setAttribute("state", this.dataItem.state);
     this.element.setAttribute("displayName", this.displayName);
     this.element.setAttribute("image", this.image);
+    this.element.setAttribute("state",
+                              DownloadsCommon.stateOfDownload(this.download));
 
     // Since state changed, reset the time left estimation.
     this.lastEstimatedSecondsLeft = Infinity;
 
     this._updateProgress();
   },
 
   /**
    * Updates the elements that change regularly for in-progress downloads,
    * namely the progress bar and the status line.
    */
   _updateProgress() {
-    if (this.dataItem.starting) {
-      // Before the download starts, the progress meter has its initial value.
+    // The progress bar is only displayed for in-progress downloads.
+    if (this.download.hasProgress) {
       this.element.setAttribute("progressmode", "normal");
-      this.element.setAttribute("progress", "0");
-    } else if (this.dataItem.state == Ci.nsIDownloadManager.DOWNLOAD_SCANNING ||
-               this.dataItem.percentComplete == -1) {
-      // We might not know the progress of a running download, and we don't know
-      // the remaining time during the malware scanning phase.
+      this.element.setAttribute("progress", this.download.progress);
+    } else {
       this.element.setAttribute("progressmode", "undetermined");
-    } else {
-      // This is a running download of which we know the progress.
-      this.element.setAttribute("progressmode", "normal");
-      this.element.setAttribute("progress", this.dataItem.percentComplete);
     }
 
     // Dispatch the ValueChange event for accessibility, if possible.
     if (this._progressElement) {
       let event = document.createEvent("Events");
       event.initEvent("ValueChange", true, true);
       this._progressElement.dispatchEvent(event);
     }
@@ -167,81 +168,70 @@ DownloadElementShell.prototype = {
    */
   get rawStatusTextAndTip() {
     const nsIDM = Ci.nsIDownloadManager;
     let s = DownloadsCommon.strings;
 
     let text = "";
     let tip = "";
 
-    if (this.dataItem.paused) {
-      let transfer = DownloadUtils.getTransferTotal(this.download.currentBytes,
-                                                    this.dataItem.maxBytes);
-
-      // We use the same XUL label to display both the state and the amount
-      // transferred, for example "Paused -  1.1 MB".
-      text = s.statusSeparatorBeforeNumber(s.statePaused, transfer);
-    } else if (this.dataItem.state == nsIDM.DOWNLOAD_DOWNLOADING) {
+    if (!this.download.stopped) {
+      let maxBytes = DownloadsCommon.maxBytesOfDownload(this.download);
       // By default, extended status information including the individual
       // download rate is displayed in the tooltip. The history view overrides
       // the getter and displays the detials in the main area instead.
       [text] = DownloadUtils.getDownloadStatusNoRate(
                                           this.download.currentBytes,
-                                          this.dataItem.maxBytes,
+                                          maxBytes,
                                           this.download.speed,
                                           this.lastEstimatedSecondsLeft);
       let newEstimatedSecondsLeft;
       [tip, newEstimatedSecondsLeft] = DownloadUtils.getDownloadStatus(
                                           this.download.currentBytes,
-                                          this.dataItem.maxBytes,
+                                          maxBytes,
                                           this.download.speed,
                                           this.lastEstimatedSecondsLeft);
       this.lastEstimatedSecondsLeft = newEstimatedSecondsLeft;
-    } else if (this.dataItem.starting) {
+    } else if (this.download.canceled && this.download.hasPartialData) {
+      let maxBytes = DownloadsCommon.maxBytesOfDownload(this.download);
+      let transfer = DownloadUtils.getTransferTotal(this.download.currentBytes,
+                                                    maxBytes);
+
+      // We use the same XUL label to display both the state and the amount
+      // transferred, for example "Paused -  1.1 MB".
+      text = s.statusSeparatorBeforeNumber(s.statePaused, transfer);
+    } else if (!this.download.succeeded && !this.download.canceled &&
+               !this.download.error) {
       text = s.stateStarting;
-    } else if (this.dataItem.state == nsIDM.DOWNLOAD_SCANNING) {
-      text = s.stateScanning;
     } else {
       let stateLabel;
-      switch (this.dataItem.state) {
-        case nsIDM.DOWNLOAD_FAILED:
-          stateLabel = s.stateFailed;
-          break;
-        case nsIDM.DOWNLOAD_CANCELED:
-          stateLabel = s.stateCanceled;
-          break;
-        case nsIDM.DOWNLOAD_BLOCKED_PARENTAL:
-          stateLabel = s.stateBlockedParentalControls;
-          break;
-        case nsIDM.DOWNLOAD_BLOCKED_POLICY:
-          stateLabel = s.stateBlockedPolicy;
-          break;
-        case nsIDM.DOWNLOAD_DIRTY:
-          stateLabel = s.stateDirty;
-          break;
-        case nsIDM.DOWNLOAD_FINISHED:
-          // For completed downloads, show the file size (e.g. "1.5 MB")
-          if (this.dataItem.maxBytes !== undefined &&
-              this.dataItem.maxBytes >= 0) {
-            let [size, unit] =
-                DownloadUtils.convertByteUnits(this.dataItem.maxBytes);
-            stateLabel = s.sizeWithUnits(size, unit);
-            break;
-          }
-          // Fallback to default unknown state.
-        default:
+
+      if (this.download.succeeded) {
+        // For completed downloads, show the file size (e.g. "1.5 MB")
+        let maxBytes = DownloadsCommon.maxBytesOfDownload(this.download);
+        if (maxBytes >= 0) {
+          let [size, unit] = DownloadUtils.convertByteUnits(maxBytes);
+          stateLabel = s.sizeWithUnits(size, unit);
+        } else {
           stateLabel = s.sizeUnknown;
-          break;
+        }
+      } else if (this.download.canceled) {
+        stateLabel = s.stateCanceled;
+      } else if (this.download.error.becauseBlockedByParentalControls) {
+        stateLabel = s.stateBlockedParentalControls;
+      } else if (this.download.error.becauseBlockedByReputationCheck) {
+        stateLabel = s.stateDirty;
+      } else {
+        stateLabel = s.stateFailed;
       }
 
-      let referrer = this.download.source.referrer ||
-                     this.download.source.url;
+      let referrer = this.download.source.referrer || this.download.source.url;
       let [displayHost, fullHost] = DownloadUtils.getURIHost(referrer);
 
-      let date = new Date(this.dataItem.endTime);
+      let date = new Date(this.download.endTime);
       let [displayDate, fullDate] = DownloadUtils.getReadableDates(date);
 
       let firstPart = s.statusSeparator(stateLabel, displayHost);
       text = s.statusSeparator(firstPart, displayDate);
       tip = s.statusSeparator(fullHost, fullDate);
     }
 
     return { text, tip: tip || text };
--- a/browser/components/downloads/test/browser/browser_basic_functionality.js
+++ b/browser/components/downloads/test/browser/browser_basic_functionality.js
@@ -44,12 +44,13 @@ add_task(function* test_basic_functional
   let richlistbox = document.getElementById("downloadsListBox");
   /* disabled for failing intermittently (bug 767828)
     is(richlistbox.children.length, DownloadData.length,
        "There is the correct number of richlistitems");
   */
   let itemCount = richlistbox.children.length;
   for (let i = 0; i < itemCount; i++) {
     let element = richlistbox.children[itemCount - i - 1];
-    let dataItem = DownloadsView.controllerForElement(element).dataItem;
-    is(dataItem.state, DownloadData[i].state, "Download states match up");
+    let download = DownloadsView.controllerForElement(element).download;
+    is(DownloadsCommon.stateOfDownload(download), DownloadData[i].state,
+       "Download states match up");
   }
 });