Bug 741655 - Add more controls to download manager. r=bnicholson
authorThiyag Krishna <thiyagaraj@gmail.com>
Wed, 23 May 2012 20:56:39 -0400
changeset 94748 60d3cf56a65c7ebf4afd63adf90c302735800395
parent 94747 b0249578ce22294ed0ce005ddb77270d65b6b73d
child 94749 9b0c651c5559a030b123f05241b551742ba700fd
push id9780
push userryanvm@gmail.com
push dateThu, 24 May 2012 00:59:08 +0000
treeherdermozilla-inbound@d753a5f4e673 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersbnicholson
bugs741655
milestone15.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 741655 - Add more controls to download manager. r=bnicholson
mobile/android/chrome/content/aboutDownloads.js
mobile/android/locales/en-US/chrome/aboutDownloads.properties
mobile/android/themes/core/aboutDownloads.css
--- a/mobile/android/chrome/content/aboutDownloads.js
+++ b/mobile/android/chrome/content/aboutDownloads.js
@@ -6,26 +6,27 @@ let Ci = Components.interfaces, Cc = Com
 
 Cu.import("resource://gre/modules/Services.jsm");
 Cu.import("resource://gre/modules/DownloadUtils.jsm");
 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
 
 let gStrings = Services.strings.createBundle("chrome://browser/locale/aboutDownloads.properties");
 
 let downloadTemplate =
-"<li downloadID='{id}' role='button' mozDownload=''>" +
+"<li downloadID='{id}' role='button' state='{state}'>" +
   "<img class='icon' src='{icon}'/>" +
   "<div class='details'>" +
      "<div class='row'>" +
        // This is a hack so that we can crop this label in its center
        "<xul:label class='title' crop='center' value='{target}'/>" +
        "<div class='date'>{date}</div>" +
      "</div>" +
      "<div class='size'>{size}</div>" +
      "<div class='domain'>{domain}</div>" +
+     "<div class='displayState'>{displayState}</div>" +
   "</div>" +
 "</li>";
 
 XPCOMUtils.defineLazyGetter(window, "gChromeWin", function ()
   window.QueryInterface(Ci.nsIInterfaceRequestor)
     .getInterface(Ci.nsIWebNavigation)
     .QueryInterface(Ci.nsIDocShellTreeItem)
     .rootTreeItem
@@ -40,79 +41,243 @@ let Downloads = {
       let target = event.target;
       while (target && target.nodeName != "li") {
         target = target.parentNode;
       }
 
       Downloads.openDownload(target);
     }, false);
 
+    Services.obs.addObserver(this, "dl-start", false);
+    Services.obs.addObserver(this, "dl-failed", false);
+    Services.obs.addObserver(this, "dl-scanning", false);
+    Services.obs.addObserver(this, "dl-done", false);
+    Services.obs.addObserver(this, "dl-blocked", false);
+    Services.obs.addObserver(this, "dl-dirty", false);
+    Services.obs.addObserver(this, "dl-cancel", false);
+
+    this.getDownloads();
+
     let contextmenus = gChromeWin.NativeWindow.contextmenus;
+
+    // Open shown only for downloads that completed successfully
     Downloads.openMenuItem = contextmenus.add(gStrings.GetStringFromName("downloadAction.open"),
-                                              contextmenus.SelectorContext("li[mozDownload]"),
+                                              contextmenus.SelectorContext("li[state='" + this._dlmgr.DOWNLOAD_FINISHED + "']"),
       function (aTarget) {
         Downloads.openDownload(aTarget);
       }
     );
+    
+    // Retry shown when its failed, canceled, blocked(covered in failed, see _getState())
+    Downloads.retryMenuItem = contextmenus.add(gStrings.GetStringFromName("downloadAction.retry"),
+                                               contextmenus.SelectorContext("li[state='" + this._dlmgr.DOWNLOAD_FAILED + "']," +
+                                                                            "li[state='" + this._dlmgr.DOWNLOAD_CANCELED + "']"),
+      function (aTarget) {
+        Downloads.retryDownload(aTarget);
+      }
+    );
+    
+    // Remove shown when its canceled, finished, failed(failed includes blocked and dirty, see _getState())
     Downloads.removeMenuItem = contextmenus.add(gStrings.GetStringFromName("downloadAction.remove"),
-                                                contextmenus.SelectorContext("li[mozDownload]"),
+                                                contextmenus.SelectorContext("li[state='" + this._dlmgr.DOWNLOAD_CANCELED + "']," +
+                                                                             "li[state='" + this._dlmgr.DOWNLOAD_FINISHED + "']," +
+                                                                             "li[state='" + this._dlmgr.DOWNLOAD_FAILED + "']"),
       function (aTarget) {
         Downloads.removeDownload(aTarget);
       }
     );
 
-    Services.obs.addObserver(this, "dl-failed", false);
-    Services.obs.addObserver(this, "dl-done", false);
-    Services.obs.addObserver(this, "dl-cancel", false);
-
-    this.getDownloads();
+    // Pause shown when item is currently downloading
+    Downloads.pauseMenuItem = contextmenus.add(gStrings.GetStringFromName("downloadAction.pause"),
+                                               contextmenus.SelectorContext("li[state='" + this._dlmgr.DOWNLOAD_DOWNLOADING + "']"),
+      function (aTarget) {
+        Downloads.pauseDownload(aTarget);
+      }
+    );
+    
+    // Resume shown for paused items only
+    Downloads.resumeMenuItem = contextmenus.add(gStrings.GetStringFromName("downloadAction.resume"),
+                                                contextmenus.SelectorContext("li[state='" + this._dlmgr.DOWNLOAD_PAUSED + "']"),
+      function (aTarget) {
+        Downloads.resumeDownload(aTarget);
+      }
+    );
+    
+    // Cancel shown when its downloading, notstarted, queued or paused
+    Downloads.cancelMenuItem = contextmenus.add(gStrings.GetStringFromName("downloadAction.cancel"),
+                                                contextmenus.SelectorContext("li[state='" + this._dlmgr.DOWNLOAD_DOWNLOADING + "']," +
+                                                                             "li[state='" + this._dlmgr.DOWNLOAD_NOTSTARTED + "']," +
+                                                                             "li[state='" + this._dlmgr.DOWNLOAD_QUEUED + "']," +
+                                                                             "li[state='" + this._dlmgr.DOWNLOAD_PAUSED + "']"),
+      function (aTarget) {
+        Downloads.cancelDownload(aTarget);
+      }
+    );
   },
 
   uninit: function () {
     let contextmenus = gChromeWin.NativeWindow.contextmenus;
     contextmenus.remove(this.openMenuItem);
     contextmenus.remove(this.removeMenuItem);
+    contextmenus.remove(this.pauseMenuItem);
+    contextmenus.remove(this.resumeMenuItem);
+    contextmenus.remove(this.retryMenuItem);
+    contextmenus.remove(this.cancelMenuItem);
 
+    Services.obs.removeObserver(this, "dl-start");
     Services.obs.removeObserver(this, "dl-failed");
+    Services.obs.removeObserver(this, "dl-scanning");
     Services.obs.removeObserver(this, "dl-done");
+    Services.obs.removeObserver(this, "dl-blocked");
+    Services.obs.removeObserver(this, "dl-dirty");
     Services.obs.removeObserver(this, "dl-cancel");
   },
 
   observe: function (aSubject, aTopic, aData) {
     let download = aSubject.QueryInterface(Ci.nsIDownload);
     switch (aTopic) {
-      case "dl-failed":
+      case "dl-blocked":
+      case "dl-dirty":
       case "dl-cancel":
-        break;
       case "dl-done":
-        if (!this._getElementForDownload(download.id)) {
-          let item = this._createItem(downloadTemplate, {
-            id: download.id,
-            target: download.displayName,
-            icon: "moz-icon://" + download.displayName + "?size=64",
-            date: DownloadUtils.getReadableDates(new Date())[0],
-            domain: DownloadUtils.getURIHost(download.source.spec)[0],
-            size: DownloadUtils.convertByteUnits(download.size).join("")
-          });
-          this._list.insertAdjacentHTML("afterbegin", item);
-          break;
-        }
+      case "dl-failed":
+        // For all "completed" states, move them after active downloads
+        this._moveDownloadAfterActive(this._getElementForDownload(download.id));
+      
+      // Fall-through the rest
+      case "dl-start":
+      case "dl-scanning":
+        let item = this._getElementForDownload(download.id);
+        if (item)
+          this._updateDownloadRow(item);
+        else
+          this._insertDownloadRow(download);
+        break;
+    }
+  },
+
+  _moveDownloadAfterActive: function (aItem) {
+    // Move downloads that just reached a "completed" state below any active
+    try {
+      // Iterate down until we find a non-active download
+      let next = aItem.nextSibling;
+      while (next && this._inProgress(next.getAttribute("state")))
+        next = next.nextSibling;
+      // Move the item
+      this._list.insertBefore(aItem, next);
+    } catch (ex) {
+      console.log("ERROR: _moveDownloadAfterActive() : " + ex);
+    }
+  },
+  
+  _inProgress: function (aState) {
+    return [
+      this._dlmgr.DOWNLOAD_NOTSTARTED,
+      this._dlmgr.DOWNLOAD_QUEUED,
+      this._dlmgr.DOWNLOAD_DOWNLOADING,
+      this._dlmgr.DOWNLOAD_PAUSED,
+      this._dlmgr.DOWNLOAD_SCANNING,
+    ].indexOf(parseInt(aState)) != -1;
+  },
+
+  _insertDownloadRow: function (aDownload) {
+    let updatedState = this._getState(aDownload.state);
+    let item = this._createItem(downloadTemplate, {
+      id: aDownload.id,
+      target: aDownload.displayName,
+      icon: "moz-icon://" + aDownload.displayName + "?size=64",
+      date: DownloadUtils.getReadableDates(new Date())[0],
+      domain: DownloadUtils.getURIHost(aDownload.source.spec)[0],
+      size: this._getDownloadSize(aDownload.size),
+      displayState: this._getStateString(updatedState),
+      state: updatedState
+    });
+    this._list.insertAdjacentHTML("afterbegin", item);
+  },
+
+  _getDownloadSize: function (aSize) {
+    let displaySize = DownloadUtils.convertByteUnits(aSize);
+    if (displaySize[0] > 0) // [0] is size, [1] is units
+      return displaySize.join("");
+    else
+      return gStrings.GetStringFromName("downloadState.unknownSize");
+  },
+  
+  // Not all states are displayed as-is on mobile, some are translated to a generic state
+  _getState: function (aState) {
+    let str;
+    switch (aState) {
+      // Downloading and Scanning states show up as "Downloading"
+      case this._dlmgr.DOWNLOAD_DOWNLOADING:
+      case this._dlmgr.DOWNLOAD_SCANNING:
+        str = this._dlmgr.DOWNLOAD_DOWNLOADING;
+        break;
+        
+      // Failed, Dirty and Blocked states show up as "Failed"
+      case this._dlmgr.DOWNLOAD_FAILED:
+      case this._dlmgr.DOWNLOAD_DIRTY:
+      case this._dlmgr.DOWNLOAD_BLOCKED_POLICY:
+      case this._dlmgr.DOWNLOAD_BLOCKED_PARENTAL:
+        str = this._dlmgr.DOWNLOAD_FAILED;
+        break;
+        
+      /* QUEUED and NOTSTARTED are not translated as they 
+         dont fall under a common state but we still need
+         to display a common "status" on the UI */
+         
+      default:
+        str = aState;
+    }
+    return str;
+  },
+  
+  // Note: This doesn't cover all states as some of the states are translated in _getState()
+  _getStateString: function (aState) {
+    let str;
+    switch (aState) {
+      case this._dlmgr.DOWNLOAD_DOWNLOADING:
+        str = "downloadState.downloading";
+        break;
+      case this._dlmgr.DOWNLOAD_CANCELED:
+        str = "downloadState.canceled";
+        break;
+      case this._dlmgr.DOWNLOAD_FAILED:
+        str = "downloadState.failed";
+        break;
+      case this._dlmgr.DOWNLOAD_PAUSED:
+        str = "downloadState.paused";
+        break;
+        
+      // Queued and Notstarted show up as "Starting..."
+      case this._dlmgr.DOWNLOAD_QUEUED:
+      case this._dlmgr.DOWNLOAD_NOTSTARTED:
+        str = "downloadState.starting";
+        break;
+        
+      default:
+        return "";
+    }
+    return gStrings.GetStringFromName(str);
+  },
+
+  _updateItem: function (aItem, aValues) {
+    for (let i in aValues) {
+      aItem.querySelector("." + i).textContent = aValues[i];
     }
   },
 
   _initStatement: function dv__initStatement() {
     if (this._stmt)
       this._stmt.finalize();
 
     this._stmt = this._dlmgr.DBConnection.createStatement(
-      "SELECT id, name, source, startTime, endTime, referrer, " +
-             "currBytes, maxBytes " +
+      "SELECT id, name, source, state, startTime, endTime, referrer, " +
+             "currBytes, maxBytes, state IN (?1, ?2, ?3, ?4, ?5) isActive " +
       "FROM moz_downloads " +
-      "WHERE state = :download_state " +
-      "ORDER BY endTime DESC");
+      "ORDER BY isActive DESC, endTime DESC, startTime DESC");
   },
 
   _createItem: function _createItem(aTemplate, aValues) {
     function htmlEscape(s) {
       s = s.replace(/&/g, "&amp;");
       s = s.replace(/>/g, "&gt;");
       s = s.replace(/</g, "&lt;");
       s = s.replace(/"/g, "&quot;");
@@ -134,24 +299,27 @@ let Downloads = {
   _stepDownloads: function dv__stepDownloads(aNumItems) {
     try {
       if (!this._stmt.executeStep()) {
         this._stmt.finalize();
         this._stmt = null;
         return;
       }
   
+      let updatedState = this._getState(this._stmt.row.state);
       // Try to get the attribute values from the statement
       let attrs = {
         id: this._stmt.row.id,
         target: this._stmt.row.name,
         icon: "moz-icon://" + this._stmt.row.name + "?size=64",
         date: DownloadUtils.getReadableDates(new Date(this._stmt.row.endTime / 1000))[0],
         domain: DownloadUtils.getURIHost(this._stmt.row.source)[0],
-        size: DownloadUtils.convertByteUnits(this._stmt.row.maxBytes).join("")
+        size: this._getDownloadSize(this._stmt.row.maxBytes),
+        displayState: this._getStateString(updatedState),
+        state: updatedState
       };
 
       let item = this._createItem(downloadTemplate, attrs);
       this._list.insertAdjacentHTML("beforeend", item);
     } catch (e) {
       // Something went wrong when stepping or getting values, so clear and quit
       console.log("Error: " + e);
       this._stmt.reset();
@@ -173,17 +341,21 @@ let Downloads = {
   getDownloads: function () {
     this._dlmgr = Cc["@mozilla.org/download-manager;1"].getService(Ci.nsIDownloadManager);
 
     this._initStatement();
 
     clearTimeout(this._timeoutID);
 
     this._stmt.reset();
-    this._stmt.params.download_state = Ci.nsIDownloadManager.DOWNLOAD_FINISHED;
+    this._stmt.bindInt32Parameter(0, Ci.nsIDownloadManager.DOWNLOAD_NOTSTARTED);
+    this._stmt.bindInt32Parameter(1, Ci.nsIDownloadManager.DOWNLOAD_DOWNLOADING);
+    this._stmt.bindInt32Parameter(2, Ci.nsIDownloadManager.DOWNLOAD_PAUSED);
+    this._stmt.bindInt32Parameter(3, Ci.nsIDownloadManager.DOWNLOAD_QUEUED);
+    this._stmt.bindInt32Parameter(4, Ci.nsIDownloadManager.DOWNLOAD_SCANNING);
 
     // Take a quick break before we actually start building the list
     let self = this;
     this._timeoutID = setTimeout(function () {
       self._stepDownloads(1);
     }, 0);
   },
 
@@ -191,16 +363,26 @@ let Downloads = {
     return this._list.querySelector("li[downloadID='" + aKey + "']");
   },
 
   _getDownloadForElement: function (aElement) {
     let id = parseInt(aElement.getAttribute("downloadID"));
     return this._dlmgr.getDownload(id);
   },
 
+  _removeItem: function dv__removeItem(aItem) {
+    // Make sure we have an item to remove
+    if (!aItem)
+      return;
+  
+    let index = this._list.selectedIndex;
+    this._list.removeChild(aItem);
+    this._list.selectedIndex = Math.min(index, this._list.itemCount - 1);
+  },
+  
   openDownload: function (aItem) {
     let f = null;
     try {
       let download = this._getDownloadForElement(aItem);
       f = download.targetFile;
     } catch(ex) { }
 
     try {
@@ -222,9 +404,70 @@ let Downloads = {
     this._dlmgr.removeDownload(aItem.getAttribute("downloadID"));
 
     this._list.removeChild(aItem);
 
     try {
       if (f) f.remove(false);
     } catch(ex) { }
   },
+
+  pauseDownload: function (aItem) {
+    try {
+      let download = this._getDownloadForElement(aItem);
+      this._dlmgr.pauseDownload(aItem.getAttribute("downloadID"));
+      this._updateDownloadRow(aItem);
+    } catch (ex) {
+      console.log("Error: pauseDownload() " + ex);  
+    }
+
+  },
+
+  resumeDownload: function (aItem) {
+    try {
+      let download = this._getDownloadForElement(aItem);
+      this._dlmgr.resumeDownload(aItem.getAttribute("downloadID"));
+      this._updateDownloadRow(aItem);
+    } catch (ex) {
+      console.log("Error: resumeDownload() " + ex);  
+    }
+  },
+
+  retryDownload: function (aItem) {
+    try {
+      let download = this._getDownloadForElement(aItem);
+      this._removeItem(aItem);
+      this._dlmgr.retryDownload(aItem.getAttribute("downloadID"));
+    } catch (ex) {
+      console.log("Error: retryDownload() " + ex);  
+    }
+  },
+
+  cancelDownload: function (aItem) {
+    try {
+      this._dlmgr.cancelDownload(aItem.getAttribute("downloadID"));
+      let download = this._getDownloadForElement(aItem);
+      let f = download.targetFile;
+
+      if (f.exists())
+        f.remove(false);
+      
+      this._updateDownloadRow(aItem);
+    } catch (ex) {
+      console.log("Error: cancelDownload() " + ex);  
+    }
+  },
+  
+  _updateDownloadRow: function (aItem){
+    try {
+      let download = this._getDownloadForElement(aItem);
+      let updatedState = this._getState(download.state);
+      aItem.setAttribute("state", updatedState);
+      this._updateItem(aItem, {
+        size: this._getDownloadSize(download.size),
+        displayState: this._getStateString(updatedState),
+        date: DownloadUtils.getReadableDates(new Date())[0]
+      });
+    } catch (ex){
+       console.log("ERROR: _updateDownloadRow(): " + ex);
+    }
+  }
 }
--- a/mobile/android/locales/en-US/chrome/aboutDownloads.properties
+++ b/mobile/android/locales/en-US/chrome/aboutDownloads.properties
@@ -1,6 +1,20 @@
 # 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 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/.
+
 downloadAction.open=Open
 downloadAction.remove=Delete
+downloadAction.pause=Pause
+downloadAction.resume=Resume
+downloadAction.cancel=Cancel
+downloadAction.retry=Retry
+downloadState.downloading=Downloading…
+downloadState.canceled=Canceled
+downloadState.failed=Failed
+downloadState.paused=Paused
+downloadState.starting=Starting…
+downloadState.unknownSize=Unknown size
--- a/mobile/android/themes/core/aboutDownloads.css
+++ b/mobile/android/themes/core/aboutDownloads.css
@@ -100,16 +100,21 @@ li:active .details {
   color: gray;
 }
 
 .domain,
 .size {
   display: inline;
 }
 
+.displayState {
+  color:gray;
+  margin-bottom: -3px; /* Prevent overflow that hides bottom border */
+}
+
 .size:after {
   content: " - ";
   white-space: pre;
 }
 
 #no-downloads-indicator {
   display: none;
 }