Bug 474492: Update the downloads manager UI, r=gavin
authorMark Finkle <mfinkle@mozilla.com>
Wed, 22 Apr 2009 10:59:52 -0400
changeset 65177 75e5fa619b38b518f9452e1cf6ea2b8db1551886
parent 65176 99eacf8e2e89bc2e0aa4700ae7e9a5b400d3e78c
child 65178 03b7e5f7457ce63a13f0e656c0d0fc93d5be46bb
push id1
push userroot
push dateMon, 20 Oct 2014 17:29:22 +0000
reviewersgavin
bugs474492
Bug 474492: Update the downloads manager UI, r=gavin
mobile/chrome/content/bindings/downloads.xml
mobile/chrome/content/browser-ui.js
mobile/chrome/content/browser.css
mobile/chrome/content/browser.js
mobile/chrome/content/browser.xul
mobile/chrome/content/downloads.js
mobile/chrome/jar.mn
mobile/components/Makefile.in
mobile/components/downloadManagerUI.js
mobile/locales/en-US/chrome/browser.dtd
mobile/locales/en-US/chrome/browser.properties
new file mode 100644
--- /dev/null
+++ b/mobile/chrome/content/bindings/downloads.xml
@@ -0,0 +1,158 @@
+<?xml version="1.0"?>
+
+<!DOCTYPE bindings [
+<!ENTITY % browserDTD SYSTEM "chrome://browser/locale/browser.dtd">
+%browserDTD;
+]>
+
+<bindings
+    xmlns="http://www.mozilla.org/xbl"
+    xmlns:xbl="http://www.mozilla.org/xbl"
+    xmlns:xul="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
+
+  <binding id="download-base" extends="chrome://global/content/bindings/richlistbox.xml#richlistitem">
+    <implementation>
+      <field name="nsIDLMgr">Components.interfaces.nsIDownloadManager</field>
+
+      <property name="paused">
+        <getter>
+        <![CDATA[
+          return parseInt(this.getAttribute("state")) == this.nsIDLMgr.DOWNLOAD_PAUSED;
+        ]]>
+        </getter>
+      </property>
+      <property name="openable">
+        <getter>
+        <![CDATA[
+          return parseInt(this.getAttribute("state")) == this.nsIDLMgr.DOWNLOAD_FINISHED;
+        ]]>
+        </getter>
+      </property>
+      <property name="inProgress">
+        <getter>
+        <![CDATA[
+          var state = parseInt(this.getAttribute("state"));
+          return state == this.nsIDLMgr.DOWNLOAD_NOTSTARTED ||
+                 state == this.nsIDLMgr.DOWNLOAD_QUEUED ||
+                 state == this.nsIDLMgr.DOWNLOAD_DOWNLOADING ||
+                 state == this.nsIDLMgr.DOWNLOAD_PAUSED ||
+                 state == this.nsIDLMgr.DOWNLOAD_SCANNING;
+        ]]>
+        </getter>
+      </property>
+      <property name="removable">
+        <getter>
+        <![CDATA[
+          var state = parseInt(this.getAttribute("state"));
+          return state == this.nsIDLMgr.DOWNLOAD_FINISHED ||
+                 state == this.nsIDLMgr.DOWNLOAD_CANCELED ||
+                 state == this.nsIDLMgr.DOWNLOAD_BLOCKED_PARENTAL ||
+                 state == this.nsIDLMgr.DOWNLOAD_BLOCKED_POLICY ||
+                 state == this.nsIDLMgr.DOWNLOAD_DIRTY ||
+                 state == this.nsIDLMgr.DOWNLOAD_FAILED;
+        ]]>
+        </getter>
+      </property>
+    </implementation>
+  </binding>
+
+  <binding id="download-downloading" extends="chrome://browser/content/bindings/downloads.xml#download-base">
+   <content orient="horizontal" align="start">
+      <xul:image validate="always" xbl:inherits="src=iconURL"/>
+      <xul:vbox flex="1">
+        <xul:hbox align="center">
+          <xul:label class="title" xbl:inherits="value=target" crop="center" flex="1"/>
+          <xul:label class="normal" xbl:inherits="value=datetime"/>
+        </xul:hbox>
+        <xul:hbox align="center">
+          <xul:progressmeter anonid="progressmeter" mode="normal" value="0" flex="1" xbl:inherits="value=progress,mode=progressmode"/>
+          <xul:button class="download-pause" label="&downloadPause.label;"
+                      oncommand="DownloadsView.pauseDownload(document.getBindingParent(this));"/>
+          <xul:button class="download-cancel" label="&downloadCancel.label;"
+                      oncommand="DownloadsView.cancelDownload(document.getBindingParent(this));"/>
+        </xul:hbox>
+        <xul:label class="normal" xbl:inherits="value=status" crop="end"/>
+      </xul:vbox>
+    </content>
+  </binding>
+
+  <binding id="download-paused" extends="chrome://browser/content/bindings/downloads.xml#download-base">
+   <content orient="horizontal" align="start">
+      <xul:image validate="always" xbl:inherits="src=iconURL"/>
+      <xul:vbox flex="1">
+        <xul:hbox align="center">
+          <xul:label class="title" xbl:inherits="value=target" crop="center" flex="1"/>
+          <xul:label class="normal" xbl:inherits="value=datetime"/>
+        </xul:hbox>
+        <xul:hbox align="center">
+          <xul:progressmeter anonid="progressmeter" mode="normal" value="0" flex="1" xbl:inherits="value=progress,mode=progressmode"/>
+          <xul:button class="download-resume" label="&downloadResume.label;"
+                      oncommand="DownloadsView.resumeDownload(document.getBindingParent(this));"/>
+          <xul:button class="download-cancel" label="&downloadCancel.label;"
+                      oncommand="DownloadsView.cancelDownload(document.getBindingParent(this));"/>
+        </xul:hbox>
+        <xul:label class="normal" xbl:inherits="value=status" crop="end"/>
+      </xul:vbox>
+    </content>
+  </binding>
+
+  <binding id="download-retry" extends="chrome://browser/content/bindings/downloads.xml#download-base">
+   <content orient="horizontal" align="start">
+      <xul:image validate="always" xbl:inherits="src=iconURL"/>
+      <xul:vbox flex="1">
+        <xul:hbox align="center">
+          <xul:label class="title" xbl:inherits="value=target" crop="center" flex="1"/>
+          <xul:label class="normal" xbl:inherits="value=datetime"/>
+        </xul:hbox>
+        <xul:hbox>
+          <xul:label class="normal" xbl:inherits="value=status" crop="end" flex="1"/>
+          <xul:button class="download-retry" label="&downloadRetry.label;"
+                      oncommand="DownloadsView.retryDownload(document.getBindingParent(this));"/>
+        </xul:hbox>
+      </xul:vbox>
+    </content>
+  </binding>
+
+  <binding id="download-done" extends="chrome://browser/content/bindings/downloads.xml#download-base">
+   <content orient="horizontal" align="start">
+      <xul:image validate="always" xbl:inherits="src=iconURL"/>
+      <xul:vbox flex="1">
+        <xul:hbox align="center">
+          <xul:label class="title" xbl:inherits="value=target" crop="center" flex="1"/>
+          <xul:label class="normal" xbl:inherits="value=datetime"/>
+        </xul:hbox>
+        <xul:hbox>
+          <xul:label class="normal" xbl:inherits="value=status"/>
+        </xul:hbox>
+        <xul:hbox class="show-on-select" align="center">
+          <xul:button anonid="showpage-button" label="&downloadShowPage.label;"
+                      oncommand="DownloadsView.showPage(document.getBindingParent(this));"/>
+          <xul:spacer flex="1"/>
+          <xul:button anonid="show-button" label="&downloadShow.label;"
+                      oncommand="DownloadsView.showDownload(document.getBindingParent(this));"/>
+          <xul:button anonid="open-button" label="&downloadOpen.label;"
+                      oncommand="DownloadsView.openDownload(document.getBindingParent(this));"/>
+          <xul:image anonid="remove-button" class="close-button"
+                     onmousedown="DownloadsView.removeDownload(document.getBindingParent(this))"/>
+        </xul:hbox>
+      </xul:vbox>
+    </content>
+
+    <implementation>
+      <constructor>
+        <![CDATA[
+          let referrer = this.hasAttribute("referrer");
+          if (!referrer)
+            document.getAnonymousElementByAttribute(this, "anonid", "showpage-button").setAttribute("disabled", "true");
+
+          let file = DownloadsView._getLocalFile(this.getAttribute("file"));
+          if (!file.exists()) {
+            document.getAnonymousElementByAttribute(this, "anonid", "open-button").setAttribute("disabled", "true");
+            document.getAnonymousElementByAttribute(this, "anonid", "show-button").setAttribute("disabled", "true");
+          }
+        ]]>
+      </constructor>
+    </implementation>
+  </binding>
+
+</bindings>
--- a/mobile/chrome/content/browser-ui.js
+++ b/mobile/chrome/content/browser-ui.js
@@ -229,16 +229,19 @@ var BrowserUI = {
     this._autocompleteNavbuttons = document.getElementById("autocomplete_navbuttons");
 
     // XXX these really want to listen whatever is the current browser, not any browser
     let browsers = document.getElementById("browsers");
     browsers.addEventListener("DOMTitleChanged", this, true);
     browsers.addEventListener("DOMLinkAdded", this, true);
 
     document.getElementById("tabs").addEventListener("TabSelect", this, true);
+
+    ExtensionsView.init();
+    DownloadsView.init();
   },
 
   update : function(aState) {
     var icons = document.getElementById("urlbar-icons");
 
     switch (aState) {
       case TOOLBARSTATE_LOADED:
         icons.setAttribute("mode", "view");
@@ -449,21 +452,16 @@ var BrowserUI = {
   showPanel: function showPanel(aPage) {
     let panelUI = document.getElementById("panel-container");
     let container = document.getElementById("browser-container");
 
     panelUI.hidden = false;
     panelUI.width = container.boxObject.width;
     panelUI.height = container.boxObject.height;
 
-    ExtensionsView.init();
-    let dloads = document.getElementById("downloads-container");
-    if (!dloads.hasAttribute("src"))
-      dloads.setAttribute("src", "chrome://mozapps/content/downloads/downloads.xul");
-
     if (aPage != undefined)
       this.switchPane(aPage);
   },
 
   hidePanel: function hidePanel() {
     let panelUI = document.getElementById("panel-container");
     panelUI.hidden = true;
   },
--- a/mobile/chrome/content/browser.css
+++ b/mobile/chrome/content/browser.css
@@ -120,16 +120,33 @@ richlistitem[selected="true"] .hide-on-s
 
 richlistitem.section-header,
 richlistitem[selected="true"].section-header {
   font-weight: bold;
   color: #000;
   background-color: lightgray;
 }
 
+richlistitem[typeName="download"] {
+  -moz-binding: url("chrome://browser/content/bindings/downloads.xml#download-downloading");
+}
+
+richlistitem[typeName="download"][state="1"] {
+  -moz-binding: url("chrome://browser/content/bindings/downloads.xml#download-done");
+}
+
+richlistitem[typeName="download"][state="2"],
+richlistitem[typeName="download"][state="3"] {
+  -moz-binding: url("chrome://browser/content/bindings/downloads.xml#download-retry");
+}
+
+richlistitem[typeName="download"][state="4"] {
+  -moz-binding: url("chrome://browser/content/bindings/downloads.xml#download-paused");
+}
+
 /* addons states ----------------------------------------------------------- */
 .hide-on-enable,
 .show-on-uninstall,
 .show-on-install,
 .show-on-restart,
 richlistitem[isDisabled="true"] .hide-on-disable {
   display: none;
 }
--- a/mobile/chrome/content/browser.js
+++ b/mobile/chrome/content/browser.js
@@ -1008,16 +1008,22 @@ const gXPInstallObserver = {
     }
   }
 };
 
 function getNotificationBox(aWindow) {
   return Browser.getNotificationBox();
 }
 
+function showDownloadsManager(aWindowContext, aID, aReason) {
+  BrowserUI.show(UIMODE_PANEL);
+  BrowserUI.switchPane("downloads-container");
+  // TODO: select the download with aID
+}
+
 var AlertsHelper = {
   _timeoutID: -1,
   _listener: null,
   _cookie: "",
   _clickable: false,
 
   showAlertNotification: function ah_show(aImageURL, aTitle, aText, aTextClickable, aCookie, aListener) {
     this._clickable = aTextClickable || false;
--- a/mobile/chrome/content/browser.xul
+++ b/mobile/chrome/content/browser.xul
@@ -70,16 +70,17 @@
 
   <script type="application/x-javascript" src="chrome://global/content/globalOverlay.js"/>
   <script type="application/x-javascript" src="chrome://global/content/inlineSpellCheckUI.js"/>
   <script type="application/x-javascript" src="chrome://browser/content/commandUtil.js"/>
   <script type="application/x-javascript" src="chrome://browser/content/browser.js"/>
   <script type="application/x-javascript" src="chrome://browser/content/browser-ui.js"/>
   <script type="application/x-javascript" src="chrome://browser/content/sanitize.js"/>
   <script type="application/x-javascript" src="chrome://browser/content/extensions.js"/>
+  <script type="application/x-javascript" src="chrome://browser/content/downloads.js"/>
   <script type="application/x-javascript" src="chrome://browser/content/WidgetStack.js"/>
   <script type="application/x-javascript" src="chrome://browser/content/CanvasBrowser.js"/>
   <script type="application/x-javascript" src="chrome://browser/content/InputHandler.js"/>
 
   <stringbundleset id="stringbundleset">
     <stringbundle id="bundle_browser" src="chrome://browser/locale/browser.properties"/>
     <stringbundle id="bundle_brand" src="chrome://branding/locale/brand.properties"/>
   </stringbundleset>
@@ -310,17 +311,34 @@
                   <textbox id="addons-search-text" emptytext="&addonsSearch.emptytext;" type="search" searchbutton="false"
                            oncommand="ExtensionsView.getAddonsFromRepo(this.value);"/>
                 </hbox>
               </richlistitem>
             </richlistbox>
           </notificationbox>
         </vbox>
 
-        <iframe id="downloads-container" flex="1"/>
+        <vbox id="downloads-container" flex="1">
+          <vbox id="downloads-header">
+            <hbox align="center">
+              <label value="&downloadsHeader.label;" flex="1"/>
+              <radiogroup id="downloads-sort-mode" oncommand="DownloadsView.toggleMode();">
+                <radio label="&downloadsSortDate.label;" value="date" selected="true"/>
+                <radio label="&downloadsSortSite.label;" value="site"/>
+                <radio label="&downloadsSortName.label;" value="name"/>
+                <radio label="&downloadsSearch.label;" value="search"/>
+              </radiogroup>
+            </hbox>
+            <hbox id="downloads-search-box" pack="end" collapsed="true">
+              <textbox id="downloads-search-text" emptytext="&downloadsSearch.emptytext;" type="search" searchbutton="false"
+                       oncommand="DownloadsView.getDownloads();"/>
+            </hbox>
+          </vbox>
+          <richlistbox id="downloads-list" flex="1"/>
+        </vbox>
 
         <vbox id="prefs-container" flex="1">
           <hbox pack="center" id="buttons"/>
           <scrollbox orient="vertical" id="pref-list" seltype="single" flex="1">
             <vbox class="prefsection">
               <label value="&content.title;" crop="end" flex="1"/>
             </vbox>
             <richpref pref="permissions.default.image" title="&permissions.default.image.title;" type="boolint" on="1" off="2">
new file mode 100644
--- /dev/null
+++ b/mobile/chrome/content/downloads.js
@@ -0,0 +1,572 @@
+// -*- Mode: js2; tab-width: 2; indent-tabs-mode: nil; js2-basic-offset: 2; js2-skip-preprocessor-directives: t; -*-
+/* ***** BEGIN LICENSE BLOCK *****
+ * Version: MPL 1.1/GPL 2.0/LGPL 2.1
+ *
+ * The contents of this file are subject to the Mozilla Public License Version
+ * 1.1 (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ * http://www.mozilla.org/MPL/
+ *
+ * Software distributed under the License is distributed on an "AS IS" basis,
+ * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
+ * for the specific language governing rights and limitations under the
+ * License.
+ *
+ * The Original Code is Mozilla Mobile Browser.
+ *
+ * The Initial Developer of the Original Code is Mozilla Corporation.
+ * Portions created by the Initial Developer are Copyright (C) 2009
+ * the Initial Developer. All Rights Reserved.
+ *
+ * Contributor(s):
+ *   Mark Finkle <mfinkle@mozilla.com>
+ *
+ * Alternatively, the contents of this file may be used under the terms of
+ * either the GNU General Public License Version 2 or later (the "GPL"), or
+ * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
+ * in which case the provisions of the GPL or the LGPL are applicable instead
+ * of those above. If you wish to allow use of your version of this file only
+ * under the terms of either the GPL or the LGPL, and not to allow others to
+ * use your version of this file under the terms of the MPL, indicate your
+ * decision by deleting the provisions above and replace them with the notice
+ * and other provisions required by the GPL or the LGPL. If you do not delete
+ * the provisions above, a recipient may use your version of this file under
+ * the terms of any one of the MPL, the GPL or the LGPL.
+ *
+ * ***** END LICENSE BLOCK ***** */
+
+Components.utils.import("resource://gre/modules/DownloadUtils.jsm");
+
+const URI_GENERIC_ICON_DOWNLOAD = "chrome://mozapps/skin/downloads/downloadIcon.png";
+
+var DownloadsView = {
+  _pref: null,
+  _list: null,
+  _dlmgr: null,
+  _progress: null,
+
+  _initStatement: function dv__initStatement(aMode) {
+    aMode = aMode || "date";
+
+    if (this._stmt)
+      this._stmt.finalize();
+
+    let order = "endTime DESC, startTime DESC";
+    if (aMode == "name")
+      order = "name ASC";
+    else if (aMode == "site")
+      order = "REPLACE(REPLACE(REPLACE(source, \"http://\", \"\"), \"https://\", \"\"), \"ftp://\", \"\") ASC";
+
+    this._stmt = this._dlmgr.DBConnection.createStatement(
+      "SELECT id, target, name, source, state, startTime, endTime, referrer, " +
+             "currBytes, maxBytes, state IN (?1, ?2, ?3, ?4, ?5) isActive " +
+      "FROM moz_downloads " +
+      "ORDER BY isActive DESC, " + order);
+  },
+
+  _getLocalFile: function dv__getLocalFile(aFileURI) {
+    // if this is a URL, get the file from that
+    let ios = Cc["@mozilla.org/network/io-service;1"].getService(Ci.nsIIOService);
+
+    // XXX it's possible that using a null char-set here is bad
+    const fileUrl = ios.newURI(aFileURI, null, null).QueryInterface(Ci.nsIFileURL);
+    return fileUrl.file.clone().QueryInterface(Ci.nsILocalFile);
+  },
+
+  _getReferrerOrSource: function dv__getReferrerOrSource(aItem) {
+    // Give the referrer if we have it set, otherwise, provide the source
+    return aItem.getAttribute("referrer") || aItem.getAttribute("uri");
+  },
+
+  _createItem: function dv__createItem(aAttrs) {
+    let item = document.createElement("richlistitem");
+
+    // Copy the attributes from the argument into the item
+    for (let attr in aAttrs)
+      item.setAttribute(attr, aAttrs[attr]);
+
+    // Initialize other attributes
+    item.setAttribute("typeName", "download");
+    item.setAttribute("id", "dl-" + aAttrs.id);
+    item.setAttribute("downloadID", aAttrs.id);
+    item.setAttribute("iconURL", "moz-icon://" + aAttrs.file + "?size=32");
+    item.setAttribute("lastSeconds", Infinity);
+
+    // Initialize more complex attributes
+    this._updateTime(item);
+    this._updateStatus(item);
+
+    return item;
+  },
+
+  _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);
+  },
+
+  _clearList: function dv__clearList() {
+    // Clear the list by replacing with a shallow copy
+    let empty = this._list.cloneNode(false);
+    this._list.parentNode.replaceChild(empty, this._list);
+    this._list = empty;
+  },
+
+  get visible() {
+    let panel = document.getElementById("panel-container");
+    let items = document.getElementById("panel-items");
+    if (panel.hidden == false && items.selectedPanel.id == "downloads-container")
+      return true;
+    return false;
+  },
+
+  init: function dv_init() {
+    if (this._dlmgr)
+      return;
+
+    this._dlmgr = Cc["@mozilla.org/download-manager;1"].getService(Ci.nsIDownloadManager);
+    this._pref = Cc["@mozilla.org/preferences-service;1"].getService(Ci.nsIPrefBranch2);
+
+    this._progress = new DownloadProgressListener();
+    this._dlmgr.addListener(this._progress);
+
+    var os = Cc["@mozilla.org/observer-service;1"].getService(Ci.nsIObserverService);
+    os.addObserver(this, "download-manager-remove-download", false);
+
+    let self = this;
+    let panels = document.getElementById("panel-items");
+    panels.addEventListener("select",
+                            function(aEvent) {
+                              if (panels.selectedPanel.id == "downloads-container")
+                                self.show();
+                            },
+                            false);
+  },
+
+  show: function dv_show() {
+    if (this._list)
+      return;
+
+    this._list = document.getElementById("downloads-list");
+
+    this._initStatement();
+    this.getDownloads();
+  },
+
+  getDownloads: function dv_getDownloads() {
+    clearTimeout(this._timeoutID);
+    this._stmt.reset();
+
+    // Array of space-separated lower-case search terms
+    let search = document.getElementById("downloads-search-text");
+    this._searchTerms = search.value.trim().toLowerCase().split(/\s+/);
+
+    // Clear the list before adding items
+    this._clearList();
+
+    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() {
+      // Start building the list and select the first item
+      self._stepDownloads(1);
+      self._list.selectedIndex = 0;
+    }, 0);
+  },
+
+  _stepDownloads: function dv__stepDownloads(aNumItems) {
+    try {
+      // If we're done adding all items, we can quit
+      if (!this._stmt.executeStep()) {
+        // Send a notification that we finished, but wait for clear list to update
+        setTimeout(function() {
+          let os = Cc["@mozilla.org/observer-service;1"].getService(Ci.nsIObserverService);
+          os.notifyObservers(window, "download-manager-ui-done", null);
+        }, 0);
+        return;
+      }
+
+      // Try to get the attribute values from the statement
+      let attrs = {
+        id: this._stmt.getInt64(0),
+        file: this._stmt.getString(1),
+        target: this._stmt.getString(2),
+        uri: this._stmt.getString(3),
+        state: this._stmt.getInt32(4),
+        startTime: Math.round(this._stmt.getInt64(5) / 1000),
+        endTime: Math.round(this._stmt.getInt64(6) / 1000),
+        currBytes: this._stmt.getInt64(8),
+        maxBytes: this._stmt.getInt64(9)
+      };
+
+      // Only add the referrer if it's not null
+      let (referrer = this._stmt.getString(7)) {
+        if (referrer)
+          attrs.referrer = referrer;
+      }
+
+      // If the download is active, grab the real progress, otherwise default 100
+      let isActive = this._stmt.getInt32(10);
+      attrs.progress = isActive ? this._dlmgr.getDownload(attrs.id).percentComplete : 100;
+
+      // Make the item and add it to the end if it's active or matches the search
+      let item = this._createItem(attrs);
+      if (item && (isActive || this._matchesSearch(item))) {
+        // Add item to the end
+        this._list.appendChild(item);
+      }
+      else {
+        // We didn't add an item, so bump up the number of items to process, but
+        // not a whole number so that we eventually do pause for a chunk break
+        aNumItems += .9;
+      }
+    }
+    catch (e) {
+      // Something went wrong when stepping or getting values, so clear and quit
+      this._stmt.reset();
+      return;
+    }
+
+    // Add another item to the list if we should; otherwise, let the UI update
+    // and continue later
+    if (aNumItems > 1) {
+      this._stepDownloads(aNumItems - 1);
+    }
+    else {
+      // Use a shorter delay for earlier downloads to display them faster
+      let delay = Math.min(this._list.itemCount * 10, 300);
+      let self = this;
+      this._timeoutID = setTimeout(function() { self._stepDownloads(5); }, delay);
+    }
+  },
+
+  _matchesSearch: function dv__matchesSearch(aItem) {
+    const searchAttributes = ["target", "status", "datetime"];
+
+    // Return early if we have no search terms
+    if (this._searchTerms.length == 0)
+      return true;
+
+    // Search through the download attributes that are shown to the user and
+    // make it into one big string for easy combined searching
+    let combinedSearch = "";
+    for each (let attr in searchAttributes)
+      combinedSearch += aItem.getAttribute(attr).toLowerCase() + " ";
+
+    // Make sure each of the terms are found
+    for each (let term in this._searchTerms)
+      if (combinedSearch.search(term) == -1)
+        return false;
+
+    return true;
+  },
+
+  downloadStarted: function dv_downloadStarted(aDownload) {
+    let attrs = {
+      id: aDownload.id,
+      file: aDownload.target.spec,
+      target: aDownload.displayName,
+      uri: aDownload.source.spec,
+      state: aDownload.state,
+      progress: aDownload.percentComplete,
+      startTime: Math.round(aDownload.startTime / 1000),
+      endTime: Date.now(),
+      currBytes: aDownload.amountTransferred,
+      maxBytes: aDownload.size
+    };
+
+    // Make the item and add it to the beginning
+    let item = this._createItem(attrs);
+    if (item) {
+      // Add item to the beginning
+      this._list.insertBefore(item, this._list.firstChild);
+    }
+
+    if (this.visible)
+      return;
+
+    let strings = document.getElementById("bundle_browser");
+    var notifier = Cc["@mozilla.org/alerts-service;1"].getService(Ci.nsIAlertsService);
+    notifier.showAlertNotification(URI_GENERIC_ICON_DOWNLOAD, strings.getString("alertDownloads"),
+                                   strings.getFormattedString("alertDownloadsStart", [attrs.target]), true, "", this);
+  },
+
+  downloadCompleted: function dv_downloadCompleted(aDownload) {
+    let element = this.getElementForDownload(aDownload.id);
+
+    // Move the download below active if it should stay in the list
+    if (this._matchesSearch(element)) {
+      // Iterate down until we find a non-active download
+      let next = element.nextSibling;
+      while (next && next.inProgress)
+        next = next.nextSibling;
+
+      // Move the item
+      this._list.insertBefore(element, next);
+    }
+    else {
+      this._removeItem(element);
+    }
+
+    if (this.visible)
+      return;
+
+    let target = element.getAttribute("target");
+    let strings = document.getElementById("bundle_browser");
+    var notifier = Cc["@mozilla.org/alerts-service;1"].getService(Ci.nsIAlertsService);
+    notifier.showAlertNotification(URI_GENERIC_ICON_DOWNLOAD, strings.getString("alertDownloads"),
+                                   strings.getFormattedString("alertDownloadsDone", [target]), true, "", this);
+  },
+
+  _updateStatus: function dv__updateStatus(aItem) {
+    let strings = document.getElementById("bundle_browser");
+
+    let status = "";
+    let state = Number(aItem.getAttribute("state"));
+
+    // Display the file size, but show "Unknown" for negative sizes
+    let fileSize = Number(aItem.getAttribute("maxBytes"));
+    let sizeText = strings.getString("downloadsUnknownSize");
+    if (fileSize >= 0) {
+      let [size, unit] = DownloadUtils.convertByteUnits(fileSize);
+      sizeText = this._replaceInsert(strings.getString("downloadsKnownSize"), 1, size);
+      sizeText = this._replaceInsert(sizeText, 2, unit);
+    }
+
+    // Insert 1 is the download size or download state
+    status = this._replaceInsert(strings.getString("downloadsStatus"), 1, sizeText);
+
+    // Insert 2 is the eTLD + 1 or other variations of the host
+    let [displayHost, fullHost] = DownloadUtils.getURIHost(this._getReferrerOrSource(aItem));
+    status = this._replaceInsert(status, 2, displayHost);
+
+    aItem.setAttribute("status", status);
+  },
+
+  _updateTime: function dv__updateTime(aItem) {
+    // Don't bother updating for things that aren't finished
+    if (aItem.inProgress)
+      return;
+
+    let dts = Cc["@mozilla.org/intl/scriptabledateformat;1"].getService(Ci.nsIScriptableDateFormat);
+
+    // Figure out when today begins
+    let now = new Date();
+    let today = new Date(now.getFullYear(), now.getMonth(), now.getDate());
+
+    // Get the end time to display
+    let end = new Date(parseInt(aItem.getAttribute("endTime")));
+
+    // Figure out if the end time is from today, yesterday, this week, etc.
+    let dateTime;
+    if (end >= today) {
+      // Download finished after today started, show the time
+      dateTime = dts.FormatTime("", dts.timeFormatNoSeconds, end.getHours(), end.getMinutes(), 0);
+    }
+    else if (today - end < (24 * 60 * 60 * 1000)) {
+      // Download finished after yesterday started, show yesterday
+      dateTime = "Yesterday";//gStr.yesterday;
+    }
+    else if (today - end < (6 * 24 * 60 * 60 * 1000)) {
+      // Download finished after last week started, show day of week
+      dateTime = end.toLocaleFormat("%A");
+    }
+    else {
+      // Download must have been from some time ago.. show month/day
+      let month = end.toLocaleFormat("%B");
+      // Remove leading 0 by converting the date string to a number
+      let date = Number(end.toLocaleFormat("%d"));
+      //dateTime = this._replaceInsert(gStr.monthDate, 1, month);
+      dateTime = this._replaceInsert("#1 #2", 1, month);
+      dateTime = this._replaceInsert(dateTime, 2, date);
+    }
+
+    aItem.setAttribute("datetime", dateTime);
+  },
+
+  _replaceInsert: function dv__replaceInsert(aText, aIndex, aValue) {
+    return aText.replace("#" + aIndex, aValue);
+  },
+
+  toggleMode: function dv_toggleMode() {
+    let mode = document.getElementById("downloads-sort-mode");
+    if (mode.value == "search") {
+      document.getElementById("downloads-search-box").collapsed = false;
+      document.getElementById("downloads-search-text").value = "";
+    }
+    else {
+      document.getElementById("downloads-search-box").collapsed = true;
+      this._initStatement(mode.value);
+      this.getDownloads();
+    }
+  },
+
+  getElementForDownload: function dv_getElementFromDownload(aID) {
+    return document.getElementById("dl-" + aID);
+  },
+
+  openDownload: function dv_openDownload(aItem) {
+    let f = this._getLocalFile(aItem.getAttribute("file"));
+    try {
+      f.launch();
+    } catch (ex) { }
+
+    // TODO: add back the code for "dontAsk"?
+  },
+
+  showDownload: function dv_showDownload(aItem) {
+    let f = this._getLocalFile(aItem.getAttribute("file"));
+    try {
+      f.reveal();
+    } catch (ex) { }
+
+    // TODO: add back the extra code?
+  },
+
+  removeDownload: function dv_removeDownload(aItem) {
+    this._dlmgr.removeDownload(aItem.getAttribute("downloadID"));
+  },
+
+  cancelDownload: function dv_cancelDownload(aItem) {
+    this._dlmgr.cancelDownload(aItem.getAttribute("downloadID"));
+    var f = this._getLocalFile(aItem.getAttribute("file"));
+    if (f.exists())
+      f.remove(false);
+  },
+
+  pauseDownload: function dv_pauseDownload(aItem) {
+    this._dlmgr.pauseDownload(aItem.getAttribute("downloadID"));
+  },
+
+  resumeDownload: function dv_resumeDownload(aItem) {
+    this._dlmgr.resumeDownload(aItem.getAttribute("downloadID"));
+  },
+
+  retryDownload: function dv_retryDownload(aItem) {
+    this._removeItem(aItem);
+    this._dlmgr.retryDownload(aItem.getAttribute("downloadID"));
+  },
+
+  showPage: function dv_showPage(aItem) {
+    BrowserUI.goToURI(this._getReferrerOrSource(aItem));
+  },
+
+  observe: function (aSubject, aTopic, aData) {
+    switch (aTopic) {
+      case "download-manager-remove-download":
+        // A null subject here indicates "remove multiple", so we just rebuild.
+        if (!aSubject) {
+          // Rebuild the default view
+          this.getDownloads();
+          break;
+        }
+
+        // Otherwise, remove a single download
+        let id = aSubject.QueryInterface(Ci.nsISupportsPRUint32);
+        let element = this.getElementForDownload(id.data);
+        this._removeItem(element);
+        break;
+    }
+  },
+
+  QueryInterface: function (aIID) {
+    if (!aIID.equals(Ci.nsIObserver) &&
+        !aIID.equals(Ci.nsISupports))
+      throw Components.results.NS_ERROR_NO_INTERFACE;
+    return this;
+  }
+};
+
+function DownloadProgressListener() { }
+
+DownloadProgressListener.prototype = {
+  //////////////////////////////////////////////////////////////////////////////
+  //// nsIDownloadProgressListener
+  onDownloadStateChange: function dlPL_onDownloadStateChange(aState, aDownload) {
+    let state = aDownload.state;
+    switch (state) {
+      case Ci.nsIDownloadManager.DOWNLOAD_QUEUED:
+        DownloadsView.downloadStarted(aDownload);
+        break;
+
+      case Ci.nsIDownloadManager.DOWNLOAD_BLOCKED_POLICY:
+        DownloadsView.downloadStarted(aDownload);
+        // Should fall through, this is a final state but DOWNLOAD_QUEUED
+        // is skipped. See nsDownloadManager::AddDownload.
+      case Ci.nsIDownloadManager.DOWNLOAD_FAILED:
+      case Ci.nsIDownloadManager.DOWNLOAD_CANCELED:
+      case Ci.nsIDownloadManager.DOWNLOAD_BLOCKED_PARENTAL:
+      case Ci.nsIDownloadManager.DOWNLOAD_DIRTY:
+      case Ci.nsIDownloadManager.DOWNLOAD_FINISHED:
+        DownloadsView.downloadCompleted(aDownload);
+        break;
+    }
+
+    let element = DownloadsView.getElementForDownload(aDownload.id);
+
+    // We should eventually know the referrer at some point
+    let referrer = aDownload.referrer;
+    if (referrer && element.getAttribute("referrer") != referrer.spec)
+      element.setAttribute("referrer", referrer.spec);
+
+    // Update to the new state
+    element.setAttribute("state", state);
+    element.setAttribute("currBytes", aDownload.amountTransferred);
+    element.setAttribute("maxBytes", aDownload.size);
+    element.setAttribute("endTime", Date.now());
+
+    // Update ui text values after switching states
+    DownloadsView._updateTime(element);
+    DownloadsView._updateStatus(element);
+  },
+
+  onProgressChange: function(aWebProgress, aRequest, aCurSelfProgress, aMaxSelfProgress, aCurTotalProgress, aMaxTotalProgress, aDownload) {
+    let element = DownloadsView.getElementForDownload(aDownload.id);
+    if (!element)
+      return;
+
+    // Update this download's progressmeter
+    if (aDownload.percentComplete == -1) {
+      element.setAttribute("progressmode", "undetermined");
+    }
+    else {
+      element.setAttribute("progressmode", "normal");
+      element.setAttribute("progress", aDownload.percentComplete);
+    }
+
+    // Dispatch ValueChange for a11y
+    let event = document.createEvent("Events");
+    event.initEvent("ValueChange", true, true);
+    let progmeter = document.getAnonymousElementByAttribute(element, "anonid", "progressmeter");
+    if (progmeter)
+      progmeter.dispatchEvent(event);
+
+    // Update the progress so the status can be correctly updated
+    element.setAttribute("currBytes", aDownload.amountTransferred);
+    element.setAttribute("maxBytes", aDownload.size);
+
+    // Update the rest of the UI
+    DownloadsView._updateStatus(element);
+  },
+
+  onStateChange: function(aWebProgress, aRequest, aState, aStatus, aDownload) { },
+  onSecurityChange: function(aWebProgress, aRequest, aState, aDownload) { },
+
+  //////////////////////////////////////////////////////////////////////////////
+  //// nsISupports
+  QueryInterface: function (aIID) {
+    if (!aIID.equals(Ci.nsIDownloadProgressListener) &&
+        !aIID.equals(Ci.nsISupports))
+      throw Components.results.NS_ERROR_NO_INTERFACE;
+    return this;
+  }
+};
--- a/mobile/chrome/jar.mn
+++ b/mobile/chrome/jar.mn
@@ -6,20 +6,22 @@ browser.jar:
   content/browser.js                   (content/browser.js)
   content/browser-ui.js                (content/browser-ui.js)
   content/commandUtil.js               (content/commandUtil.js)
   content/bindings.xml                 (content/bindings.xml)
   content/tabs.xml                     (content/tabs.xml)
   content/checkbox.xml                 (content/checkbox.xml)
   content/notification.xml             (content/notification.xml)
   content/bindings/extensions.xml      (content/bindings/extensions.xml)
+  content/bindings/downloads.xml       (content/bindings/downloads.xml)
   content/browser.css                  (content/browser.css)
   content/scrollbars.css               (content/scrollbars.css)
   content/content.css                  (content/content.css)
   content/checkerboard.png             (content/checkerboard.png)
 % content branding %content/branding/
   content/preferences/richpref.xml     (content/preferences/richpref.xml)
 * content/sanitize.xul                 (content/sanitize.xul)
 * content/sanitize.js                  (content/sanitize.js)
   content/WidgetStack.js               (content/WidgetStack.js)
   content/CanvasBrowser.js             (content/CanvasBrowser.js)
   content/InputHandler.js              (content/InputHandler.js)
   content/extensions.js                (content/extensions.js)
+  content/downloads.js                 (content/downloads.js)
--- a/mobile/components/Makefile.in
+++ b/mobile/components/Makefile.in
@@ -49,16 +49,17 @@ XPIDL_MODULE = browsercompsbase
 #XPIDLSRCS = \
 #	$(NULL)
 
 EXTRA_COMPONENTS = \
 	aboutFirstrun.js \
 	geolocationPrompt.js \
 	alertsService.js \
 	xpiDialogService.js \
+	downloadManagerUI.js \
 	$(NULL)
 
 DIRS = protocols \
 	$(NULL)
 
 ifdef WINCE
 DIRS += phone
 endif
new file mode 100644
--- /dev/null
+++ b/mobile/components/downloadManagerUI.js
@@ -0,0 +1,84 @@
+/* ***** BEGIN LICENSE BLOCK *****
+ * Version: MPL 1.1/GPL 2.0/LGPL 2.1
+ *
+ * The contents of this file are subject to the Mozilla Public License Version
+ * 1.1 (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ * http://www.mozilla.org/MPL/
+ *
+ * Software distributed under the License is distributed on an "AS IS" basis,
+ * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
+ * for the specific language governing rights and limitations under the
+ * License.
+ *
+ * The Original Code is Alerts Service.
+ *
+ * The Initial Developer of the Original Code is Mozilla Foundation.
+ * Portions created by the Initial Developer are Copyright (C) 2009
+ * the Initial Developer. All Rights Reserved.
+ *
+ * Contributor(s):
+ *   Mark Finkle <mfinkle@mozilla.com>
+ *
+ * Alternatively, the contents of this file may be used under the terms of
+ * either the GNU General Public License Version 2 or later (the "GPL"), or
+ * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
+ * in which case the provisions of the GPL or the LGPL are applicable instead
+ * of those above. If you wish to allow use of your version of this file only
+ * under the terms of either the GPL or the LGPL, and not to allow others to
+ * use your version of this file under the terms of the MPL, indicate your
+ * decision by deleting the provisions above and replace them with the notice
+ * and other provisions required by the GPL or the LGPL. If you do not delete
+ * the provisions above, a recipient may use your version of this file under
+ * the terms of any one of the MPL, the GPL or the LGPL.
+ *
+ * ***** END LICENSE BLOCK ***** */
+
+const Cc = Components.classes;
+const Ci = Components.interfaces;
+
+Components.utils.import("resource://gre/modules/XPCOMUtils.jsm");
+
+// -----------------------------------------------------------------------
+// Download Manager UI
+// -----------------------------------------------------------------------
+
+function DownloadManagerUI() { }
+
+DownloadManagerUI.prototype = {
+  classDescription: "Download Manager UI",
+  contractID: "@mozilla.org/download-manager-ui;1",
+  classID: Components.ID("{93db15b1-b408-453e-9a2b-6619e168324a}"),
+
+  show: function show(aWindowContext, aID, aReason) {
+    if (!aReason)
+      aReason = Ci.nsIDownloadManagerUI.REASON_USER_INTERACTED;
+
+    let wm = Cc["@mozilla.org/appshell/window-mediator;1"].getService(Ci.nsIWindowMediator);
+    let browser = wm.getMostRecentWindow("navigator:browser");
+    if (browser)
+      browser.showDownloadManager(aWindowContext, aID, aReason);
+  },
+
+  get visible() {
+    let wm = Cc["@mozilla.org/appshell/window-mediator;1"].getService(Ci.nsIWindowMediator);
+    let browser = wm.getMostRecentWindow("navigator:browser");
+    if (browser) {
+      return browser.DownloadsView.visible;
+    }
+    return false;
+  },
+
+  getAttention: function getAttention() {
+    if (this.visible)
+      this.show(null, null, null);
+    else
+      throw Cr.NS_ERROR_UNEXPECTED;
+  },
+
+  QueryInterface: XPCOMUtils.generateQI([Ci.nsIDownloadManagerUI])
+};
+
+function NSGetModule(aCompMgr, aFileSpec) {
+  return XPCOMUtils.generateModule([DownloadManagerUI]);
+}
--- a/mobile/locales/en-US/chrome/browser.dtd
+++ b/mobile/locales/en-US/chrome/browser.dtd
@@ -58,16 +58,31 @@
 
 <!ENTITY addonOptions.label        "Options">
 <!ENTITY addonEnable.label         "Enable">
 <!ENTITY addonDisable.label        "Disable">
 <!ENTITY addonInstall.label        "Install">
 <!ENTITY addonUninstall.label      "Uninstall">
 <!ENTITY addonCancel.label         "Cancel">
 
+<!ENTITY downloadsHeader.label     "Downloads">
+<!ENTITY downloadsSortDate.label   "By Date">
+<!ENTITY downloadsSortSite.label   "By Site">
+<!ENTITY downloadsSortName.label   "By Name">
+<!ENTITY downloadsSearch.label     "Search">
+<!ENTITY downloadsSearch.emptytext "Search for downloaded file">
+
+<!ENTITY downloadShowPage.label    "Go to Page">
+<!ENTITY downloadShow.label        "Find File">
+<!ENTITY downloadOpen.label        "Open File">
+<!ENTITY downloadCancel.label      "Cancel">
+<!ENTITY downloadPause.label       "Pause">
+<!ENTITY downloadResume.label      "Resume">
+<!ENTITY downloadRetry.label       "Retry">
+
 <!ENTITY identity.unverifiedsite2 "This web site does not supply identity information.">
 <!ENTITY identity.connectedTo "You are connected to">
 <!-- Localization note (identity.runBy) : This string appears between a
 domain name (above) and an organization name (below). E.g.
 
 example.com
 which is run by
 Example Enterprises, Inc.
--- a/mobile/locales/en-US/chrome/browser.properties
+++ b/mobile/locales/en-US/chrome/browser.properties
@@ -7,22 +7,35 @@ addonsRestart=Restart to complete change
 addonsRestartButton.label=Restart
 addonsSearchStart.label=Searching for add-ons...
 addonsSearchStart.button=Cancel
 addonsSearchNone.label=No matches found
 addonsSearchNone.button=OK
 addonsSearchFail.label=%S couldn't retrieve add-ons
 addonsSearchFail.button=OK
 
+# Download Manager
+# LOCALIZATION NOTE (Status): — is the "em dash" (long dash)
+# #1 download size for FINISHED or download state; #2 host (e.g., eTLD + 1, IP)
+downloadsStatus=#1 — #2
+downloadsUnknownSize=Unknown size
+# LOCALIZATION NOTE (KnownSize): #1 size number; #2 size unit
+downloadsKnownSize=#1 #2
+donwloadsYesterday=Yesterday
+# LOCALIZATION NOTE (MonthDate): #1 month name; #2 date number; e.g., January 22
+downloadsMonthDate=#1 #2
 
 # Alerts
 alertAddons=Add-ons
 alertAddonsStart=Installing addons
 alertAddonsDone=Installation complete
 alertAddonsFail=Installation failed
+alertDownloads=Downloads
+alertDownloadsStart=Downloading: %S
+alertDownloadsDone=%S has finished downloading
 
 # Popup Blocker
 popupWarning=%S prevented this site from opening a pop-up window.
 popupWarningMultiple=%S prevented this site from opening %S pop-up windows.
 popupButtonAlwaysAllow=Always allow
 popupButtonAlwaysAllow.accesskey=A
 popupButtonNeverWarn=Never tell me
 popupButtonNeverWarn.accesskey=N