Bug 901360 - Part2 Use Downloads.jsm for about:downloads. r=wesj
authorJames Gilbertson <james.gilbertson@luniv.ca>
Thu, 30 Jan 2014 19:16:36 -0700
changeset 197273 b56da878caedd7c9e351b0bde1a2aa7e7db77f68
parent 197272 8487d4305be37c0942c43d8e75c0f70b77d0624d
child 197274 15ca8fe42018bd2307f663539096ea8431c2ebf0
push id3624
push userasasaki@mozilla.com
push dateMon, 09 Jun 2014 21:49:01 +0000
treeherdermozilla-beta@b1a5da15899a [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewerswesj
bugs901360
milestone31.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 901360 - Part2 Use Downloads.jsm for about:downloads. r=wesj
mobile/android/chrome/content/aboutDownloads.js
mobile/android/chrome/content/aboutDownloads.xhtml
mobile/android/chrome/content/browser.js
mobile/android/chrome/content/browser.xul
mobile/android/chrome/content/downloads.js
mobile/android/chrome/jar.mn
mobile/android/installer/package-manifest.in
mobile/android/themes/core/aboutDownloads.css
--- a/mobile/android/chrome/content/aboutDownloads.js
+++ b/mobile/android/chrome/content/aboutDownloads.js
@@ -1,617 +1,354 @@
 /* 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/. */
+ * 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/. */
 
-let Ci = Components.interfaces, Cc = Components.classes, Cu = Components.utils;
+"use strict";
+
+const Cu = Components.utils;
 
-Cu.import("resource://gre/modules/Services.jsm");
-Cu.import("resource://gre/modules/DownloadUtils.jsm");
-Cu.import("resource://gre/modules/XPCOMUtils.jsm");
-Cu.import("resource://gre/modules/PluralForm.jsm");
-Cu.import("resource://gre/modules/PrivateBrowsingUtils.jsm");
+Cu.import("resource://gre/modules/XPCOMUtils.jsm", this);
 
+XPCOMUtils.defineLazyModuleGetter(this, "Downloads", "resource://gre/modules/Downloads.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "DownloadUtils", "resource://gre/modules/DownloadUtils.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "OS", "resource://gre/modules/osfile.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "PluralForm", "resource://gre/modules/PluralForm.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "Services", "resource://gre/modules/Services.jsm");
 
-let gStrings = Services.strings.createBundle("chrome://browser/locale/aboutDownloads.properties");
+XPCOMUtils.defineLazyGetter(this, "strings",
+                            () => Services.strings.createBundle("chrome://browser/locale/aboutDownloads.properties"));
 
-let downloadTemplate =
-"<li downloadGUID='{guid}' class='list-item' role='button' state='{state}' contextmenu='downloadmenu'>" +
-  "<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>";
+function deleteDownload(download) {
+  download.finalize(true).then(null, Cu.reportError);
+  OS.File.remove(download.target.path).then(null, ex => {
+    if (!(ex instanceof OS.File.Error && ex.becauseNoSuchFile)) {
+      Cu.reportError(ex);
+    }
+  });
+}
+
+let contextMenu = {
+  _items: [],
+  _targetDownload: null,
 
-XPCOMUtils.defineLazyGetter(window, "gChromeWin", function ()
-  window.QueryInterface(Ci.nsIInterfaceRequestor)
-    .getInterface(Ci.nsIWebNavigation)
-    .QueryInterface(Ci.nsIDocShellTreeItem)
-    .rootTreeItem
-    .QueryInterface(Ci.nsIInterfaceRequestor)
-    .getInterface(Ci.nsIDOMWindow)
-    .QueryInterface(Ci.nsIDOMChromeWindow));
-
-
-var ContextMenus = {
-  target: null,
+  init: function () {
+    let element = document.getElementById("downloadmenu");
+    element.addEventListener("click",
+                             event => event.download = this._targetDownload,
+                             true);
 
-  init: function() {
-    document.addEventListener("contextmenu", this, false);
-    document.getElementById("contextmenu-open").addEventListener("click", this.open.bind(this), false);
-    document.getElementById("contextmenu-retry").addEventListener("click", this.retry.bind(this), false);
-    document.getElementById("contextmenu-remove").addEventListener("click", this.remove.bind(this), false);
-    document.getElementById("contextmenu-pause").addEventListener("click", this.pause.bind(this), false);
-    document.getElementById("contextmenu-resume").addEventListener("click", this.resume.bind(this), false);
-    document.getElementById("contextmenu-cancel").addEventListener("click", this.cancel.bind(this), false);
-    document.getElementById("contextmenu-removeall").addEventListener("click", this.removeAll.bind(this), false);
-    this.items = [
-      { name: "open", states: [Downloads._dlmgr.DOWNLOAD_FINISHED] },
-      { name: "retry", states: [Downloads._dlmgr.DOWNLOAD_FAILED, Downloads._dlmgr.DOWNLOAD_CANCELED] },
-      { name: "remove", states: [Downloads._dlmgr.DOWNLOAD_FINISHED,Downloads._dlmgr.DOWNLOAD_FAILED, Downloads._dlmgr.DOWNLOAD_CANCELED] },
-      { name: "removeall", states: [Downloads._dlmgr.DOWNLOAD_FINISHED,Downloads._dlmgr.DOWNLOAD_FAILED, Downloads._dlmgr.DOWNLOAD_CANCELED] },
-      { name: "pause", states: [Downloads._dlmgr.DOWNLOAD_DOWNLOADING] },
-      { name: "resume", states: [Downloads._dlmgr.DOWNLOAD_PAUSED] },
-      { name: "cancel", states: [Downloads._dlmgr.DOWNLOAD_DOWNLOADING, Downloads._dlmgr.DOWNLOAD_NOTSTARTED, Downloads._dlmgr.DOWNLOAD_QUEUED, Downloads._dlmgr.DOWNLOAD_PAUSED] },
+    this._items = [
+      new ContextMenuItem("open",
+                          download => download.succeeded,
+                          download => download.launch().then(null, Cu.reportError)),
+      new ContextMenuItem("retry",
+                          download => download.error ||
+                                      (download.canceled && !download.hasPartialData),
+                          download => download.start().then(null, Cu.reportError)),
+      new ContextMenuItem("remove",
+                          download => download.stopped,
+                          download => {
+                            Downloads.getList(Downloads.ALL)
+                                     .then(list => list.remove(download))
+                                     .then(null, Cu.reportError);
+                            deleteDownload(download);
+                          }),
+      new ContextMenuItem("pause",
+                          download => !download.stopped,
+                          download => download.cancel().then(null, Cu.reportError)),
+      new ContextMenuItem("resume",
+                          download => download.canceled && download.hasPartialData,
+                          download => download.start().then(null, Cu.reportError)),
+      new ContextMenuItem("cancel",
+                          download => !download.stopped ||
+                                      (download.canceled && download.hasPartialData),
+                          download => {
+                            download.cancel().then(null, Cu.reportError);
+                            download.removePartialData().then(null, Cu.reportError);
+                          }),
+      // following menu item is a global action
+      new ContextMenuItem("removeall",
+                          () => downloadLists.finished.length > 0,
+                          () => downloadLists.removeFinished())
     ];
   },
 
-  handleEvent: function(event) {
-    // store the target of context menu events so that we know which app to act on
-    this.target = event.target;
-    while (!this.target.hasAttribute("contextmenu")) {
-      this.target = this.target.parentNode;
-    }
-    if (!this.target)
-      return;
-
-    let state = parseInt(this.target.getAttribute("state"));
-    for (let i = 0; i < this.items.length; i++) {
-      var item = this.items[i];
-      let enabled = (item.states.indexOf(state) > -1);
-      if (enabled)
-        document.getElementById("contextmenu-" + item.name).removeAttribute("hidden");
-      else
-        document.getElementById("contextmenu-" + item.name).setAttribute("hidden", "true");
-    }
-  },
-
-  // Open shown only for downloads that completed successfully
-  open: function(event) {
-    Downloads.openDownload(this.target);
-    this.target = null;
-  },
-
-  // Retry shown when its failed, canceled, blocked(covered in failed, see _getState())
-  retry: function (event) {
-    Downloads.retryDownload(this.target);
-    this.target = null;
+  addContextMenuEventListener: function (element) {
+    element.addEventListener("contextmenu", this.onContextMenu.bind(this));
   },
 
-  // Remove shown when its canceled, finished, failed(failed includes blocked and dirty, see _getState())
-  remove: function (event) {
-    Downloads.removeDownload(this.target);
-    this.target = null;
-  },
+  onContextMenu: function (event) {
+    let target = event.target;
+    while (target && !target.download) {
+      target = target.parentNode;
+    }
+    if (!target) {
+      Cu.reportError("No download found for context menu target");
+      event.preventDefault();
+      return;
+    }
+
+    // capture the target download for menu items to use in a click event
+    this._targetDownload = target.download;
+    for (let item of this._items) {
+      item.updateVisibility(target.download);
+    }
+  }
+};
+
+function ContextMenuItem(name, isVisible, action) {
+  this.element = document.getElementById("contextmenu-" + name);
+  this.isVisible = isVisible;
+
+  this.element.addEventListener("click", event => action(event.download));
+}
 
-  // Pause shown when item is currently downloading
-  pause: function (event) {
-    Downloads.pauseDownload(this.target);
-    this.target = null;
-  },
+ContextMenuItem.prototype = {
+  updateVisibility: function (download) {
+    this.element.hidden = !this.isVisible(download);
+  }
+};
+
+function DownloadListView(type, listElementId) {
+  this.listElement = document.getElementById(listElementId);
+  contextMenu.addContextMenuEventListener(this.listElement);
+
+  this.items = new Map();
+
+  Downloads.getList(type)
+           .then(list => list.addView(this))
+           .then(null, Cu.reportError);
 
-  // Resume shown for paused items only
-  resume: function (event) {
-    Downloads.resumeDownload(this.target);
-    this.target = null;
+  window.addEventListener("unload", event => {
+    Downloads.getList(type)
+             .then(list => list.removeView(this))
+             .then(null, Cu.reportError);
+  });
+}
+
+DownloadListView.prototype = {
+  get finished() {
+    let finished = [];
+    for (let download of this.items.keys()) {
+      if (download.stopped && (!download.hasPartialData || download.error)) {
+        finished.push(download);
+      }
+    }
+
+    return finished;
   },
 
-  // Cancel shown when its downloading, notstarted, queued or paused
-  cancel: function (event) {
-    Downloads.cancelDownload(this.target);
-    this.target = null;
+  insertOrMoveItem: function (item) {
+    var compare = (a, b) => {
+      // active downloads always before stopped downloads
+      if (a.stopped != b.stopped) {
+        return b.stopped ? -1 : 1
+      }
+      // most recent downloads first
+      return b.startTime - a.startTime;
+    };
+
+    let insertLocation = this.listElement.firstChild;
+    while (insertLocation && compare(item.download, insertLocation.download) > 0) {
+      insertLocation = insertLocation.nextElementSibling;
+    }
+    this.listElement.insertBefore(item.element, insertLocation);
   },
 
-  removeAll: function(event) {
-    Downloads.removeAll();
-    this.target = null;
-  }
-}
+  onDownloadAdded: function (download) {
+    let item = new DownloadItem(download);
+    this.items.set(download, item);
+    this.insertOrMoveItem(item);
+  },
 
+  onDownloadChanged: function (download) {
+    let item = this.items.get(download);
+    if (!item) {
+      Cu.reportError("No DownloadItem found for download");
+      return;
+    }
 
-let Downloads = {
-  init: function dl_init() {
-    function onClick(evt) {
-      let target = evt.target;
-      while (target.nodeName != "li") {
-        target = target.parentNode;
-        if (!target)
-          return;
-      }
+    if (item.stateChanged) {
+      this.insertOrMoveItem(item);
+    }
 
-      Downloads.openDownload(target);
+    item.onDownloadChanged();
+  },
+
+  onDownloadRemoved: function (download) {
+    let item = this.items.get(download);
+    if (!item) {
+      Cu.reportError("No DownloadItem found for download");
+      return;
     }
 
-    this._normalList = document.getElementById("normal-downloads-list");
-    this._privateList = document.getElementById("private-downloads-list");
-
-    this._normalList.addEventListener("click", onClick, false);
-    this._privateList.addEventListener("click", onClick, false);
-
-    this._dlmgr = Cc["@mozilla.org/download-manager;1"].getService(Ci.nsIDownloadManager);
-    this._dlmgr.addPrivacyAwareListener(this);
-
-    Services.obs.addObserver(this, "last-pb-context-exited", false);
-    Services.obs.addObserver(this, "download-manager-remove-download-guid", false);
+    this.items.delete(download);
+    this.listElement.removeChild(item.element);
+  }
+};
 
-    // If we have private downloads, show them all immediately. If we were to
-    // add them asynchronously, there's a small chance we could get a
-    // "last-pb-context-exited" notification before downloads are added to the
-    // list, meaning we'd show private downloads without any private tabs open.
-    let privateEntries = this.getDownloads({ isPrivate: true });
-    this._stepAddEntries(privateEntries, this._privateList, privateEntries.length);
-
-    // Add non-private downloads
-    let normalEntries = this.getDownloads({ isPrivate: false });    
-    this._stepAddEntries(normalEntries, this._normalList, 1, this._scrollToSelectedDownload.bind(this));    
-    ContextMenus.init();    
+let downloadLists = {
+  init: function () {
+    this.publicDownloads = new DownloadListView(Downloads.PUBLIC, "public-downloads-list");
+    this.privateDownloads = new DownloadListView(Downloads.PRIVATE, "private-downloads-list");
   },
 
-  uninit: function dl_uninit() {
-    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);
-    contextmenus.remove(this.deleteAllMenuItem);
-
-    this._dlmgr.removeListener(this);
-    Services.obs.removeObserver(this, "last-pb-context-exited");
-    Services.obs.removeObserver(this, "download-manager-remove-download-guid");
+  get finished() {
+    return this.publicDownloads.finished.concat(this.privateDownloads.finished);
   },
 
-  onProgressChange: function(aWebProgress, aRequest, aCurSelfProgress, aMaxSelfProgress,
-                             aCurTotalProgress, aMaxTotalProgress, aDownload) { },
-  onDownloadStateChange: function(aState, aDownload) {
-    switch (aDownload.state) {
-      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:
-        // For all "completed" states, move them after active downloads
-        this._moveDownloadAfterActive(this._getElementForDownload(aDownload.guid));
+  removeFinished: function () {
+    let finished = this.finished;
+    if (finished.length == 0) {
+      return;
+    }
+
+    let title = strings.GetStringFromName("downloadAction.deleteAll");
+    let messageForm = strings.GetStringFromName("downloadMessage.deleteAll");
+    let message = PluralForm.get(finished.length, messageForm).replace("#1", finished.length);
+
+    if (Services.prompt.confirm(null, title, message)) {
+      Downloads.getList(Downloads.ALL)
+               .then(list => {
+                 for (let download of finished) {
+                   list.remove(download).then(null, Cu.reportError);
+                   deleteDownload(download);
+                 }
+               }, Cu.reportError);
+    }
+  }
+};
 
-      // Fall-through the rest
-      case Ci.nsIDownloadManager.DOWNLOAD_SCANNING:
-      case Ci.nsIDownloadManager.DOWNLOAD_QUEUED:
-      case Ci.nsIDownloadManager.DOWNLOAD_DOWNLOADING:
-        let item = this._getElementForDownload(aDownload.guid);
-        if (item)
-          this._updateDownloadRow(item, aDownload);
-        else
-          this._insertDownloadRow(aDownload);
-        break;
-    }
-  },
-  onStateChange: function(aWebProgress, aRequest, aState, aStatus, aDownload) { },
-  onSecurityChange: function(aWebProgress, aRequest, aState, aDownload) { },
+function DownloadItem(download) {
+  this._download = download;
+  this._updateFromDownload();
+
+  this._domain = DownloadUtils.getURIHost(download.source.url)[0];
+  this._fileName = this._htmlEscape(OS.Path.basename(download.target.path));
+  this._iconUrl = "moz-icon://" + this._fileName + "?size=64";
+  this._startDate = this._htmlEscape(DownloadUtils.getReadableDates(download.startTime)[0]);
+
+  this._element = this.createElement();
+}
 
-  observe: function (aSubject, aTopic, aData) {
-    switch (aTopic) {
-      case "last-pb-context-exited":
-        this._privateList.innerHTML = "";
-        break;
-      case "download-manager-remove-download-guid": {
-        let guid = aSubject.QueryInterface(Ci.nsISupportsCString).data;
-        this._removeItem(this._getElementForDownload(guid));
-        break;
-      }
-    }
+const kDownloadStatePropertyNames = [
+  "stopped",
+  "succeeded",
+  "canceled",
+  "error",
+  "startTime"
+];
+
+DownloadItem.prototype = {
+  _htmlEscape : function (s) {
+    s = s.replace(/&/g, "&amp;");
+    s = s.replace(/>/g, "&gt;");
+    s = s.replace(/</g, "&lt;");
+    s = s.replace(/"/g, "&quot;");
+    s = s.replace(/'/g, "&apos;");
+    return s;
   },
 
-  _moveDownloadAfterActive: function dl_moveDownloadAfterActive(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.nextElementSibling;
-      while (next && this._inProgress(next.getAttribute("state")))
-        next = next.nextElementSibling;
-      // Move the item
-      aItem.parentNode.insertBefore(aItem, next);
-    } catch (ex) {
-      this.logError("_moveDownloadAfterActive() " + ex);
+  _updateFromDownload: function () {
+    this._state = {};
+    kDownloadStatePropertyNames.forEach(
+      name => this._state[name] = this._download[name],
+      this);
+  },
+
+  get stateChanged() {
+    return kDownloadStatePropertyNames.some(
+      name => this._state[name] != this._download[name],
+      this);
+  },
+
+  get download() this._download,
+  get element() this._element,
+
+  createElement: function() {
+    let template = document.getElementById("download-item");
+    // TODO: use this once <template> is working
+    // let element = document.importNode(template.content, true);
+
+    // simulate a <template> node...
+    let element = template.cloneNode(true);
+    element.removeAttribute("id");
+    element.removeAttribute("style");
+
+    // launch the download if clicked
+    element.addEventListener("click", this.onClick.bind(this));
+
+    // set download as an expando property for the context menu
+    element.download = this.download;
+
+    // fill in template placeholders
+    this.updateElement(element);
+
+    return element;
+  },
+
+  updateElement: function (element) {
+    element.querySelector(".date").textContent = this.startDate;
+    element.querySelector(".domain").textContent = this.domain;
+    element.querySelector(".icon").src = this.iconUrl;
+    element.querySelector(".size").textContent = this.size;
+    element.querySelector(".state").textContent = this.stateDescription;
+    element.querySelector(".title").setAttribute("value", this.fileName);
+  },
+
+  onClick: function (event) {
+    if (this.download.succeeded) {
+      this.download.launch().then(null, Cu.reportError);
     }
   },
 
-  _inProgress: function dl_inProgress(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 dl_insertDownloadRow(aDownload) {
-    let updatedState = this._getState(aDownload.state);
-    let item = this._createItem(downloadTemplate, {
-      guid: aDownload.guid,
-      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
-    });
-    list = aDownload.isPrivate ? this._privateList : this._normalList;
-    list.insertAdjacentHTML("afterbegin", item);
-  },
-
-  _getDownloadSize: function dl_getDownloadSize(aSize) {
-    if (aSize > 0) {
-      let displaySize = DownloadUtils.convertByteUnits(aSize);
-      return displaySize.join(""); // [0] is size, [1] is units
-    }
-    return gStrings.GetStringFromName("downloadState.unknownSize");
-  },
-
-  // Not all states are displayed as-is on mobile, some are translated to a generic state
-  _getState: function dl_getState(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;
+  onDownloadChanged: function () {
+    this._updateFromDownload();
+    this.updateElement(this.element);
   },
 
-  // Note: This doesn't cover all states as some of the states are translated in _getState()
-  _getStateString: function dl_getStateString(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;
+  // template properties below
+  get domain() this._domain,
+  get fileName() this._fileName,
+  get id() this._id,
+  get iconUrl() this._iconUrl,
 
-      // Queued and Notstarted show up as "Starting..."
-      case this._dlmgr.DOWNLOAD_QUEUED:
-      case this._dlmgr.DOWNLOAD_NOTSTARTED:
-        str = "downloadState.starting";
-        break;
-
-      default:
-        return "";
+  get size() {
+    if (this.download.hasProgress) {
+      return DownloadUtils.convertByteUnits(this.download.totalBytes).join("");
     }
-    return gStrings.GetStringFromName(str);
-  },
-
-  _updateItem: function dl_updateItem(aItem, aValues) {
-    for (let i in aValues) {
-      aItem.querySelector("." + i).textContent = aValues[i];
-    }
-  },
-
-  _initStatement: function dv__initStatement(aIsPrivate) {
-    let dbConn = aIsPrivate ? this._dlmgr.privateDBConnection : this._dlmgr.DBConnection;
-    return dbConn.createStatement(
-      "SELECT guid, name, source, state, startTime, endTime, referrer, " +
-             "currBytes, maxBytes, state IN (?1, ?2, ?3, ?4, ?5) isActive " +
-      "FROM moz_downloads " +
-      "ORDER BY isActive DESC, endTime DESC, startTime DESC");
+    return strings.GetStringFromName("downloadState.unknownSize");
   },
 
-  _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;");
-      s = s.replace(/'/g, "&apos;");
-      return s;
-    }
-
-    let t = aTemplate;
-    for (let key in aValues) {
-      if (aValues.hasOwnProperty(key)) {
-        let regEx = new RegExp("{" + key + "}", "g");
-        let value = htmlEscape(aValues[key].toString());
-        t = t.replace(regEx, value);
-      }
-    }
-    return t;
-  },
-
-  _getEntry: function dv__getEntry(aStmt) {
-    try {
-      if (!aStmt.executeStep()) {
-        return null;
-      }
-
-      let updatedState = this._getState(aStmt.row.state);
-      // Try to get the attribute values from the statement
-
-      return {
-        guid: aStmt.row.guid,
-        target: aStmt.row.name,
-        icon: "moz-icon://" + aStmt.row.name + "?size=64",
-        date: DownloadUtils.getReadableDates(new Date(aStmt.row.endTime / 1000))[0],
-        domain: DownloadUtils.getURIHost(aStmt.row.source)[0],
-        size: this._getDownloadSize(aStmt.row.maxBytes),
-        displayState: this._getStateString(updatedState),
-        state: updatedState
-      };
-
-    } catch (e) {
-      // Something went wrong when stepping or getting values, so clear and quit
-      this.logError("_getEntry() " + e);
-      aStmt.reset();
-      return null;
-    }
-  },
-
-  _stepAddEntries: function dv__stepAddEntries(aEntries, aList, aNumItems, aCallback) {
-    
-    if (aEntries.length == 0){
-      if (aCallback)
-        aCallback();
-
-      return;
-    }
-
-    let attrs = aEntries.shift();
-    let item = this._createItem(downloadTemplate, attrs);
-    aList.insertAdjacentHTML("beforeend", item);
-
-    // Add another item to the list if we should; otherwise, let the UI update
-    // and continue later
-    if (aNumItems > 1) {
-      this._stepAddEntries(aEntries, aList, aNumItems - 1, aCallback);
-    } else {
-      // Use a shorter delay for earlier downloads to display them faster
-      let delay = Math.min(aList.itemCount * 10, 300);
-      setTimeout(function () {
-        this._stepAddEntries(aEntries, aList, 5, aCallback);
-      }.bind(this), delay);
-    }
+  get startDate() {
+    return this._startDate;
   },
 
-  getDownloads: function dl_getDownloads(aParams) {
-    aParams = aParams || {};
-    let stmt = this._initStatement(aParams.isPrivate);
-
-    stmt.reset();
-    stmt.bindInt32Parameter(0, Ci.nsIDownloadManager.DOWNLOAD_NOTSTARTED);
-    stmt.bindInt32Parameter(1, Ci.nsIDownloadManager.DOWNLOAD_DOWNLOADING);
-    stmt.bindInt32Parameter(2, Ci.nsIDownloadManager.DOWNLOAD_PAUSED);
-    stmt.bindInt32Parameter(3, Ci.nsIDownloadManager.DOWNLOAD_QUEUED);
-    stmt.bindInt32Parameter(4, Ci.nsIDownloadManager.DOWNLOAD_SCANNING);
-
-    let entries = [];
-    while (entry = this._getEntry(stmt)) {
-      entries.push(entry);
-    }
-
-    stmt.finalize();
-
-    return entries;
-  },
-
-  _getElementForDownload: function dl_getElementForDownload(aKey) {
-    return document.body.querySelector("li[downloadGUID='" + aKey + "']");
-  },
-
-  _getDownloadForElement: function dl_getDownloadForElement(aElement, aCallback) {
-    let guid = aElement.getAttribute("downloadGUID");
-    this._dlmgr.getDownloadByGUID(guid, function(status, download) {
-      if (!Components.isSuccessCode(status)) {
-        return;
+  get stateDescription() {
+    let name;
+    if (this.download.error) {
+      name = "downloadState.failed";
+    } else if (this.download.canceled) {
+      if (this.download.hasPartialData) {
+        name = "downloadState.paused";
+      } else {
+        name = "downloadState.canceled";
       }
-      aCallback(download);
-    });
-  },
-
-  _removeItem: function dl_removeItem(aItem) {
-    // Make sure we have an item to remove
-    if (!aItem)
-      return;
-
-    aItem.parentNode.removeChild(aItem);
-  },
-
-  openDownload: function dl_openDownload(aItem) {
-    this._getDownloadForElement(aItem, function(aDownload) {
-      if (aDownload.state !== Ci.nsIDownloadManager.DOWNLOAD_FINISHED) {
-        // Do not open unfinished downloads.
-        return;
-      }
-      try {
-        let f = aDownload.targetFile;
-        if (f) f.launch();
-      } catch (ex) {
-        this.logError("openDownload() " + ex, aDownload);
-      }
-    }.bind(this));
-  },
-
-  removeDownload: function dl_removeDownload(aItem) {
-    this._getDownloadForElement(aItem, function(aDownload) {
-      if (aDownload.targetFile) {
-        OS.File.remove(aDownload.targetFile.path).then(null, function onError(reason) {
-          if (!(reason instanceof OS.File.Error && reason.becauseNoSuchFile)) {
-            this.logError("removeDownload() " + reason, aDownload);
-          }
-        }.bind(this));
-      }
-
-      aDownload.remove();
-    }.bind(this));
-  },
-
-  removeAll: function dl_removeAll() {
-    let title = gStrings.GetStringFromName("downloadAction.deleteAll");
-    let messageForm = gStrings.GetStringFromName("downloadMessage.deleteAll");
-    let elements = document.body.querySelectorAll("li[state='" + this._dlmgr.DOWNLOAD_FINISHED + "']," +
-                                               "li[state='" + this._dlmgr.DOWNLOAD_CANCELED + "']," +
-                                               "li[state='" + this._dlmgr.DOWNLOAD_FAILED + "']");
-    let message = PluralForm.get(elements.length, messageForm)
-                            .replace("#1", elements.length);
-    let flags = Services.prompt.BUTTON_POS_0 * Services.prompt.BUTTON_TITLE_OK +
-                Services.prompt.BUTTON_POS_1 * Services.prompt.BUTTON_TITLE_CANCEL;
-    let choice = Services.prompt.confirmEx(null, title, message, flags,
-                                           null, null, null, null, {});
-    if (choice == 0) {
-      for (let i = 0; i < elements.length; i++) {
-        this.removeDownload(elements[i]);
+    } else if (!this.download.stopped) {
+      if (this.download.currentBytes > 0) {
+        name = "downloadState.downloading";
+      } else {
+        name = "downloadState.starting";
       }
     }
-  },
-
-  pauseDownload: function dl_pauseDownload(aItem) {
-    this._getDownloadForElement(aItem, function(aDownload) {
-      try {
-        aDownload.pause();
-        this._updateDownloadRow(aItem, aDownload);
-      } catch (ex) {
-        this.logError("Error: pauseDownload() " + ex, aDownload);
-      }
-    }.bind(this));
-  },
-
-  resumeDownload: function dl_resumeDownload(aItem) {
-    this._getDownloadForElement(aItem, function(aDownload) {
-      try {
-        aDownload.resume();
-        this._updateDownloadRow(aItem, aDownload);
-      } catch (ex) {
-        this.logError("resumeDownload() " + ex, aDownload);
-      }
-    }.bind(this));
-  },
-
-  retryDownload: function dl_retryDownload(aItem) {
-    this._getDownloadForElement(aItem, function(aDownload) {
-      try {
-        this._removeItem(aItem);
-        aDownload.retry();
-      } catch (ex) {
-        this.logError("retryDownload() " + ex, aDownload);
-      }
-    }.bind(this));
-  },
-
-  cancelDownload: function dl_cancelDownload(aItem) {
-    this._getDownloadForElement(aItem, function(aDownload) {
-      OS.File.remove(aDownload.targetFile.path).then(null, function onError(reason) {
-        if (!(reason instanceof OS.File.Error && reason.becauseNoSuchFile)) {
-          this.logError("cancelDownload() " + reason, aDownload);
-        }
-      }.bind(this));
-
-      aDownload.cancel();
-
-      this._updateDownloadRow(aItem, aDownload);
-    }.bind(this));
-  },
 
-  _updateDownloadRow: function dl_updateDownloadRow(aItem, aDownload) {
-    try {
-      let updatedState = this._getState(aDownload.state);
-      aItem.setAttribute("state", updatedState);
-      this._updateItem(aItem, {
-        size: this._getDownloadSize(aDownload.size),
-        displayState: this._getStateString(updatedState),
-        date: DownloadUtils.getReadableDates(new Date())[0]
-      });
-    } catch (ex) {
-      this.logError("_updateDownloadRow() " + ex, aDownload);
+    if (name) {
+      return strings.GetStringFromName(name);
     }
-  },
-  
-  /**
-   * In case a specific downloadId was passed while opening, scrolls the list to 
-   * the given elemenet
-   */
-
-  _scrollToSelectedDownload : function dl_scrollToSelected() {
-    let spec = document.location.href;
-    let pos = spec.indexOf("?");
-    let query = "";
-    if (pos >= 0)
-      query = spec.substring(pos + 1);
+    return "";
+  }
+};
 
-    // Just assume the query is "id=<id>"
-    let id = query.substring(3);
-    if (!id) {
-      return;
-    }    
-    downloadElement = this._getElementForDownload(id);
-    if (!downloadElement) {
-      return;
-    }
-
-    downloadElement.scrollIntoView();
-  },
-
-  /**
-   * Logs the error to the console.
-   *
-   * @param aMessage  error message to log
-   * @param aDownload (optional) if given, and if the download is private, the
-   *                  log message is suppressed
-   */
-  logError: function dl_logError(aMessage, aDownload) {
-    if (!aDownload || !aDownload.isPrivate) {
-      console.log("Error: " + aMessage);
-    }
-  },
-
-  QueryInterface: function (aIID) {
-    if (!aIID.equals(Ci.nsIDownloadProgressListener) &&
-        !aIID.equals(Ci.nsISupports))
-      throw Components.results.NS_ERROR_NO_INTERFACE;
-    return this;
-  }
-}
-
-document.addEventListener("DOMContentLoaded", Downloads.init.bind(Downloads), true);
-window.addEventListener("unload", Downloads.uninit.bind(Downloads), false);
-
-
+window.addEventListener("DOMContentLoaded", event => {
+    contextMenu.init();
+    downloadLists.init()
+});
--- a/mobile/android/chrome/content/aboutDownloads.xhtml
+++ b/mobile/android/chrome/content/aboutDownloads.xhtml
@@ -30,17 +30,33 @@
     <menuitem id="contextmenu-retry" label="&aboutDownloads.retry;"></menuitem>
     <menuitem id="contextmenu-remove" label="&aboutDownloads.remove;"></menuitem>
     <menuitem id="contextmenu-pause" label="&aboutDownloads.pause;"></menuitem>
     <menuitem id="contextmenu-resume" label="&aboutDownloads.resume;"></menuitem>
     <menuitem id="contextmenu-cancel" label="&aboutDownloads.cancel;"></menuitem>
     <menuitem id="contextmenu-removeall" label="&aboutDownloads.removeAll;"></menuitem>
   </menu>
 
+  <!--template id="download-item"-->
+    <li id="download-item" class="list-item" role="button" contextmenu="downloadmenu" style="display: none">
+      <img class="icon" src=""/>
+      <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=""/>
+          <div class="date"></div>
+        </div>
+        <div class="size"></div>
+        <div class="domain"></div>
+        <div class="state"></div>
+      </div>
+    </li>
+  <!--/template-->
+
   <div class="header">
     <div>&aboutDownloads.header;</div>
   </div>
   <ul id="private-downloads-list" class="list"></ul>
-  <ul id="normal-downloads-list" class="list"></ul>
+  <ul id="public-downloads-list" class="list"></ul>
   <span id="no-downloads-indicator">&aboutDownloads.empty;</span>
   <script type="application/javascript;version=1.8" src="chrome://browser/content/aboutDownloads.js"/>
 </body>
 </html>
--- a/mobile/android/chrome/content/browser.js
+++ b/mobile/android/chrome/content/browser.js
@@ -8,16 +8,17 @@
 let Cc = Components.classes;
 let Ci = Components.interfaces;
 let Cu = Components.utils;
 let Cr = Components.results;
 
 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
 Cu.import("resource://gre/modules/Services.jsm");
 Cu.import("resource://gre/modules/AddonManager.jsm");
+Cu.import("resource://gre/modules/DownloadNotifications.jsm");
 Cu.import("resource://gre/modules/FileUtils.jsm");
 Cu.import("resource://gre/modules/JNI.jsm");
 Cu.import('resource://gre/modules/Payment.jsm');
 Cu.import("resource://gre/modules/NotificationDB.jsm");
 Cu.import("resource://gre/modules/SpatialNavigation.jsm");
 Cu.import("resource://gre/modules/UITelemetry.jsm");
 
 #ifdef ACCESSIBILITY
@@ -355,17 +356,17 @@ var BrowserApp = {
     }, false);
 
     // When a restricted key is pressed in DOM full-screen mode, we should display
     // the "Press ESC to exit" warning message.
     window.addEventListener("MozShowFullScreenWarning", showFullScreenWarning, true);
 
     NativeWindow.init();
     LightWeightThemeWebInstaller.init();
-    Downloads.init();
+    DownloadNotifications.init();
     FormAssistant.init();
     IndexedDB.init();
     HealthReportStatusListener.init();
     XPInstallObserver.init();
     CharacterEncoding.init();
     ActivityObserver.init();
 #ifdef MOZ_ANDROID_SYNTHAPKS
     // TODO: replace with Android implementation of WebappOSUtils.isLaunchable.
@@ -643,17 +644,17 @@ var BrowserApp = {
         aTarget.mozRequestFullScreen();
       });
 
     NativeWindow.contextmenus.add(Strings.browser.GetStringFromName("contextmenu.mute"),
       NativeWindow.contextmenus.mediaContext("media-unmuted"),
       function(aTarget) {
         aTarget.muted = true;
       });
-  
+
     NativeWindow.contextmenus.add(Strings.browser.GetStringFromName("contextmenu.unmute"),
       NativeWindow.contextmenus.mediaContext("media-muted"),
       function(aTarget) {
         aTarget.muted = false;
       });
 
     NativeWindow.contextmenus.add(Strings.browser.GetStringFromName("contextmenu.copyImageLocation"),
       NativeWindow.contextmenus.imageLocationCopyableContext,
@@ -736,16 +737,17 @@ var BrowserApp = {
     if (Services.prefs.prefHasUserValue("plugins.click_to_play")) {
       Services.prefs.setIntPref("plugin.default.state", Ci.nsIPluginTag.STATE_ENABLED);
       Services.prefs.clearUserPref("plugins.click_to_play");
     }
   },
 
   shutdown: function shutdown() {
     NativeWindow.uninit();
+    DownloadNotifications.uninit();
     LightWeightThemeWebInstaller.uninit();
     FormAssistant.uninit();
     IndexedDB.uninit();
     ViewportHandler.uninit();
     XPInstallObserver.uninit();
     HealthReportStatusListener.uninit();
     CharacterEncoding.uninit();
     SearchEngines.uninit();
@@ -1805,17 +1807,17 @@ var NativeWindow = {
       sendMessageToJava({ type: "Menu:Remove", id: aId });
     },
 
     update: function(aId, aOptions) {
       if (!aOptions)
         return;
 
       sendMessageToJava({
-        type: "Menu:Update", 
+        type: "Menu:Update",
         id: aId,
         options: aOptions
       });
     }
   },
 
   doorhanger: {
     _callbacks: {},
@@ -1831,17 +1833,17 @@ var NativeWindow = {
    *                     to -1, the doorhanger will never automatically dismiss.
    *        persistWhileVisible:
    *                     A boolean. If true, a visible notification will always
    *                     persist across location changes.
    *        timeout:     A time in milliseconds. The notification will not
    *                     automatically dismiss before this time.
    *        checkbox:    A string to appear next to a checkbox under the notification
    *                     message. The button callback functions will be called with
-   *                     the checked state as an argument.                   
+   *                     the checked state as an argument.
    */
     show: function(aMessage, aValue, aButtons, aTabID, aOptions) {
       if (aButtons == null) {
         aButtons = [];
       }
 
       aButtons.forEach((function(aButton) {
         this._callbacks[this._callbacksId] = { cb: aButton.callback, prompt: this._promptId };
@@ -2228,17 +2230,17 @@ var NativeWindow = {
         this.menus = null;
         Services.obs.notifyObservers({target: target, x: x, y: y}, "context-menu-not-shown", "");
 
         if (SelectionHandler.canSelect(target)) {
           if (!SelectionHandler.startSelection(target, {
             mode: SelectionHandler.SELECT_AT_POINT,
             x: x,
             y: y
-          })) { 
+          })) {
             SelectionHandler.attachCaret(target);
           }
         }
       }
     },
 
     // Returns a title for a context menu. If no title attribute exists, will fall back to looking for a url
     _getTitle: function(node) {
@@ -3105,17 +3107,17 @@ Tab.prototype = {
 
     // We add in a bit of fudge just so that the end characters
     // don't accidentally get clipped. 15px is an arbitrary choice.
     gReflowPending = setTimeout(doChangeMaxLineBoxWidth,
                                 reflozTimeout,
                                 viewportWidth - 15);
   },
 
-  /** 
+  /**
    * Reloads the tab with the desktop mode setting.
    */
   reloadWithMode: function (aDesktopMode) {
     // Set desktop mode for tab and send change to Java
     if (this.desktopMode != aDesktopMode) {
       this.desktopMode = aDesktopMode;
       sendMessageToJava({
         type: "DesktopMode:Changed",
@@ -3735,17 +3737,17 @@ Tab.prototype = {
 
           // We use the sizes attribute if available
           // see http://www.whatwg.org/specs/web-apps/current-work/multipage/links.html#rel-icon
           if (target.hasAttribute("sizes")) {
             let sizes = target.getAttribute("sizes").toLowerCase();
 
             if (sizes == "any") {
               // Since Java expects an integer, use -1 to represent icons with sizes="any"
-              maxSize = -1; 
+              maxSize = -1;
             } else {
               let tokens = sizes.split(" ");
               tokens.forEach(function(token) {
                 // TODO: check for invalid tokens
                 let [w, h] = token.split("x");
                 maxSize = Math.max(maxSize, Math.max(w, h));
               });
             }
@@ -6575,17 +6577,17 @@ var IdentityHandler = {
    * Determine the identity of the page being displayed by examining its SSL cert
    * (if available). Return the data needed to update the UI.
    */
   checkIdentity: function checkIdentity(aState, aBrowser) {
     this._lastStatus = aBrowser.securityUI
                                .QueryInterface(Components.interfaces.nsISSLStatusProvider)
                                .SSLStatus;
 
-    // Don't pass in the actual location object, since it can cause us to 
+    // Don't pass in the actual location object, since it can cause us to
     // hold on to the window object too long.  Just pass in the fields we
     // care about. (bug 424829)
     let locationObj = {};
     try {
       let location = aBrowser.contentWindow.location;
       locationObj.host = location.host;
       locationObj.hostname = location.hostname;
       locationObj.port = location.port;
@@ -6625,17 +6627,17 @@ var IdentityHandler = {
       else if (iData.state) // State only
         supplemental += iData.state;
       else if (iData.country) // Country only
         supplemental += iData.country;
       result.supplemental = supplemental;
 
       return result;
     }
-    
+
     // Otherwise, we don't know the cert owner
     result.owner = Strings.browser.GetStringFromName("identity.ownerUnknown3");
 
     // Cache the override service the first time we need to check it
     if (!this._overrideService)
       this._overrideService = Cc["@mozilla.org/security/certoverride;1"].getService(Ci.nsICertOverrideService);
 
     // Check whether this site is a security exception. XPConnect does the right
@@ -7237,17 +7239,17 @@ var WebappsUI = {
       Cu.reportError("CreateShortcut: favicon image load error");
 
       // if the image failed to load, and it was not our default icon, attempt to
       // use our default as a fallback
       if (favicon.src != WebappsUI.DEFAULT_ICON) {
         favicon.src = WebappsUI.DEFAULT_ICON;
       }
     };
-  
+
     favicon.src = aIconURL;
   },
 
   createShortcut: function createShortcut(aTitle, aURL, aIconURL, aType) {
     this.makeBase64Icon(aIconURL, function _createShortcut(icon) {
       try {
         let shell = Cc["@mozilla.org/browser/shell-service;1"].createInstance(Ci.nsIShellService);
         shell.createShortcut(aTitle, aURL, icon, aType);
@@ -8414,8 +8416,26 @@ HTMLContextMenuItem.prototype = Object.c
         icon: elt.icon,
         label: elt.label,
         disabled: elt.disabled,
         menu: elt instanceof Ci.nsIDOMHTMLMenuElement
       };
     }
   },
 });
+
+/**
+ * CID of Downloads.jsm's implementation of nsITransfer.
+ */
+const kTransferCid = Components.ID("{1b4c85df-cbdd-4bb6-b04e-613caece083c}");
+
+/**
+ * Contract ID of the service implementing nsITransfer.
+ */
+const kTransferContractId = "@mozilla.org/transfer;1";
+
+// Override Toolkit's nsITransfer implementation with the one from the
+// JavaScript API for downloads.  This will eventually be removed when
+// nsIDownloadManager will not be available anymore (bug 851471).  The
+// old code in this module will be removed in bug 899110.
+Components.manager.QueryInterface(Ci.nsIComponentRegistrar)
+                  .registerFactory(kTransferCid, "",
+                                   kTransferContractId, null);
--- a/mobile/android/chrome/content/browser.xul
+++ b/mobile/android/chrome/content/browser.xul
@@ -6,13 +6,12 @@
 <?xml-stylesheet href="chrome://browser/content/browser.css" type="text/css"?>
 
 <window id="main-window"
         onload="BrowserApp.startup();"
         windowtype="navigator:browser"
         xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
 
   <script type="application/javascript" src="chrome://browser/content/browser.js"/>
-  <script type="application/javascript" src="chrome://browser/content/downloads.js"/>
 
   <deck id="browsers" flex="1"/>
 
 </window>
deleted file mode 100644
--- a/mobile/android/chrome/content/downloads.js
+++ /dev/null
@@ -1,298 +0,0 @@
-// -*- Mode: js2; tab-width: 2; indent-tabs-mode: nil; js2-basic-offset: 2; js2-skip-preprocessor-directives: t; -*-
-/* This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this
- * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
-
-"use strict";
-
-let Cu = Components.utils;
-Cu.import("resource://gre/modules/XPCOMUtils.jsm");
-
-function dump(a) {
-  Cc["@mozilla.org/consoleservice;1"].getService(Ci.nsIConsoleService).logStringMessage(a);
-}
-
-XPCOMUtils.defineLazyModuleGetter(this, "Notifications",
-                                  "resource://gre/modules/Notifications.jsm");
-
-const URI_GENERIC_ICON_DOWNLOAD = "drawable://alert_download";
-const URI_PAUSE_ICON = "drawable://pause";
-const URI_CANCEL_ICON = "drawable://close";
-const URI_RESUME_ICON = "drawable://play";
-
-
-XPCOMUtils.defineLazyModuleGetter(this, "OS", "resource://gre/modules/osfile.jsm");
-
-var Downloads = {
-  _initialized: false,
-  _dlmgr: null,
-  _progressAlert: null,
-  _privateDownloads: [],
-  _showingPrompt: false,
-  _downloadsIdMap: {},
-
-  _getLocalFile: function dl__getLocalFile(aFileURI) {
-    // if this is a URL, get the file from that
-    // XXX it's possible that using a null char-set here is bad
-    const fileUrl = Services.io.newURI(aFileURI, null, null).QueryInterface(Ci.nsIFileURL);
-    return fileUrl.file.clone().QueryInterface(Ci.nsILocalFile);
-  },
-
-  init: function dl_init() {
-    if (this._initialized)
-      return;
-    this._initialized = true;
-
-    // Monitor downloads and display alerts
-    this._dlmgr = Cc["@mozilla.org/download-manager;1"].getService(Ci.nsIDownloadManager);
-    this._progressAlert = new AlertDownloadProgressListener();
-    this._dlmgr.addPrivacyAwareListener(this._progressAlert);
-    Services.obs.addObserver(this, "last-pb-context-exited", true);
-  },
-
-  openDownload: function dl_openDownload(aDownload) {
-    let fileUri = aDownload.target.spec;
-    let guid = aDownload.guid;
-    let f = this._getLocalFile(fileUri);
-    try {
-      f.launch();
-    } catch (ex) {
-      // in case we are not able to open the file (i.e. there is no app able to handle it)
-      // we just open the browser tab showing it 
-      BrowserApp.addTab("about:downloads?id=" + guid);
-    }
-  },
-
-  cancelDownload: function dl_cancelDownload(aDownload) {
-    aDownload.cancel();
-    let fileURI = aDownload.target.spec;
-    let f = this._getLocalFile(fileURI);
-
-    OS.File.remove(f.path);
-  },
-
-  showCancelConfirmPrompt: function dl_showCancelConfirmPrompt(aDownload) {
-    if (this._showingPrompt)
-      return;
-    this._showingPrompt = true;
-    // Open a prompt that offers a choice to cancel the download
-    let title = Strings.browser.GetStringFromName("downloadCancelPromptTitle");
-    let message = Strings.browser.GetStringFromName("downloadCancelPromptMessage");
-    let flags = Services.prompt.BUTTON_POS_0 * Services.prompt.BUTTON_TITLE_YES +
-                Services.prompt.BUTTON_POS_1 * Services.prompt.BUTTON_TITLE_NO;
-    let choice = Services.prompt.confirmEx(null, title, message, flags,
-                                           null, null, null, null, {});
-    if (choice == 0)
-      this.cancelDownload(aDownload);
-    this._showingPrompt = false;
-  },
-
-  handleClickEvent: function dl_handleClickEvent(aDownload) {
-    // Only open the downloaded file if the download is complete
-    if (aDownload.state == Ci.nsIDownloadManager.DOWNLOAD_FINISHED)
-      this.openDownload(aDownload);
-    else if (aDownload.state == Ci.nsIDownloadManager.DOWNLOAD_DOWNLOADING ||
-                aDownload.state == Ci.nsIDownloadManager.DOWNLOAD_PAUSED)
-      this.showCancelConfirmPrompt(aDownload);
-  },
-
-  clickCallback: function dl_clickCallback(aDownloadId) {
-    this._dlmgr.getDownloadByGUID(aDownloadId, (function(status, download) {
-          if (Components.isSuccessCode(status))
-            this.handleClickEvent(download);
-        }).bind(this));
-  },
-
-  pauseClickCallback: function dl_buttonPauseCallback(aDownloadId) {
-    this._dlmgr.getDownloadByGUID(aDownloadId, (function(status, download) {
-          if (Components.isSuccessCode(status))
-            download.pause();
-        }).bind(this));
-  },
-
-  resumeClickCallback: function dl_buttonPauseCallback(aDownloadId) {
-    this._dlmgr.getDownloadByGUID(aDownloadId, (function(status, download) {
-          if (Components.isSuccessCode(status))
-            download.resume();
-        }).bind(this));
-  },
-
-  cancelClickCallback: function dl_buttonPauseCallback(aDownloadId) {
-    this._dlmgr.getDownloadByGUID(aDownloadId, (function(status, download) {
-          if (Components.isSuccessCode(status))
-            this.cancelDownload(download);
-        }).bind(this));
-  },
-
-  notificationCanceledCallback: function dl_notifCancelCallback(aId, aDownloadId) {
-    let notificationId = this._downloadsIdMap[aDownloadId];
-    if (notificationId && notificationId == aId)
-      delete this._downloadsIdMap[aDownloadId];
-  },
-
-  createNotification: function dl_createNotif(aDownload, aOptions) {
-    let notificationId = Notifications.create(aOptions);
-    this._downloadsIdMap[aDownload.guid] = notificationId;
-  },
-
-  updateNotification: function dl_updateNotif(aDownload, aOptions) {
-    let notificationId = this._downloadsIdMap[aDownload.guid];
-    if (notificationId)
-      Notifications.update(notificationId, aOptions);
-  },
-
-  cancelNotification: function dl_cleanNotif(aDownload) {
-    Notifications.cancel(this._downloadsIdMap[aDownload.guid]);
-    delete this._downloadsIdMap[aDownload.guid];
-  },
-
-  // observer for last-pb-context-exited
-  observe: function dl_observe(aSubject, aTopic, aData) {
-    let download;
-    while ((download = this._privateDownloads.pop())) {
-      try {
-        let notificationId = aDownload.guid;
-        Notifications.clear(notificationId);
-        Downloads.removeNotification(download);
-      } catch (e) {
-        dump("Error removing private download: " + e);
-      }
-    }
-  },
-
-  QueryInterface: function (aIID) {
-    if (!aIID.equals(Ci.nsISupports) &&
-        !aIID.equals(Ci.nsIObserver) &&
-        !aIID.equals(Ci.nsISupportsWeakReference))
-      throw Components.results.NS_ERROR_NO_INTERFACE;
-    return this;
-  }
-};
-
-const PAUSE_BUTTON = {
-  buttonId: "pause",
-  title : Strings.browser.GetStringFromName("alertDownloadsPause"),
-  icon : URI_PAUSE_ICON,
-  onClicked: function (aId, aCookie) {
-    Downloads.pauseClickCallback(aCookie);
-  }
-};
-
-const CANCEL_BUTTON = {
-  buttonId: "cancel",
-  title : Strings.browser.GetStringFromName("alertDownloadsCancel"),
-  icon : URI_CANCEL_ICON,
-  onClicked: function (aId, aCookie) {
-    Downloads.cancelClickCallback(aCookie);
-  }
-};
-
-const RESUME_BUTTON = {
-  buttonId: "resume",
-  title : Strings.browser.GetStringFromName("alertDownloadsResume"),
-  icon: URI_RESUME_ICON,
-  onClicked: function (aId, aCookie) {
-    Downloads.resumeClickCallback(aCookie);
-  }
-};
-
-function DownloadNotifOptions (aDownload, aTitle, aMessage) {
-  this.icon = URI_GENERIC_ICON_DOWNLOAD;
-  this.onCancel = function (aId, aCookie) {
-    Downloads.notificationCanceledCallback(aId, aCookie);
-  }
-  this.onClick = function (aId, aCookie) {
-    Downloads.clickCallback(aCookie);
-  }
-  this.title = aTitle;
-  this.message = aMessage;
-  this.buttons = null;
-  this.cookie = aDownload.guid;
-  this.persistent = true;
-}
-
-function DownloadProgressNotifOptions (aDownload, aButtons) {
-  DownloadNotifOptions.apply(this, [aDownload, aDownload.displayName, aDownload.percentComplete + "%"]);
-  this.ongoing = true;
-  this.progress = aDownload.percentComplete;
-  this.buttons = aButtons;
-}
-
-// AlertDownloadProgressListener is used to display progress in the alert notifications.
-function AlertDownloadProgressListener() { }
-
-AlertDownloadProgressListener.prototype = {
-  //////////////////////////////////////////////////////////////////////////////
-  //// nsIDownloadProgressListener
-  onProgressChange: function(aWebProgress, aRequest, aCurSelfProgress, aMaxSelfProgress, aCurTotalProgress, aMaxTotalProgress, aDownload) {
-    let strings = Strings.browser;
-    let availableSpace = -1;
-    try {
-      // diskSpaceAvailable is not implemented on all systems
-      let availableSpace = aDownload.targetFile.diskSpaceAvailable;
-    } catch(ex) { }
-    let contentLength = aDownload.size;
-    if (availableSpace > 0 && contentLength > 0 && contentLength > availableSpace) {
-      Downloads.updateNotification(aDownload, new DownloadNotifOptions(aDownload,
-                                                                        strings.GetStringFromName("alertDownloadsNoSpace"),
-                                                                        strings.GetStringFromName("alertDownloadsSize")));
-      aDownload.cancel();
-    }
-
-    if (aDownload.percentComplete == -1) {
-      // Undetermined progress is not supported yet
-      return;
-    }
-
-    Downloads.updateNotification(aDownload, new DownloadProgressNotifOptions(aDownload, [PAUSE_BUTTON, CANCEL_BUTTON]));
-  },
-
-  onDownloadStateChange: function(aState, aDownload) {
-    let state = aDownload.state;
-    switch (state) {
-      case Ci.nsIDownloadManager.DOWNLOAD_QUEUED: {
-        NativeWindow.toast.show(Strings.browser.GetStringFromName("alertDownloadsToast"), "long");
-        Downloads.createNotification(aDownload, new DownloadNotifOptions(aDownload,
-                                                                         Strings.browser.GetStringFromName("alertDownloadsStart2"),
-                                                                         aDownload.displayName));
-        break;
-      }
-      case Ci.nsIDownloadManager.DOWNLOAD_PAUSED: {
-        Downloads.updateNotification(aDownload, new DownloadProgressNotifOptions(aDownload, [RESUME_BUTTON, CANCEL_BUTTON]));
-        break;
-      }
-      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: {
-        Downloads.cancelNotification(aDownload);
-        if (aDownload.isPrivate) {
-          let index = Downloads._privateDownloads.indexOf(aDownload);
-          if (index != -1) {
-            Downloads._privateDownloads.splice(index, 1);
-          }
-        }
-
-        if (state == Ci.nsIDownloadManager.DOWNLOAD_FINISHED) {
-          Downloads.createNotification(aDownload, new DownloadNotifOptions(aDownload,
-                                                                Strings.browser.GetStringFromName("alertDownloadsDone2"),
-                                                                aDownload.displayName));
-        }
-        break;
-      }
-    }
-  },
-
-  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/android/chrome/jar.mn
+++ b/mobile/android/chrome/jar.mn
@@ -29,17 +29,16 @@ chrome.jar:
 * content/aboutApps.xhtml              (content/aboutApps.xhtml)
 * content/aboutApps.js                 (content/aboutApps.js)
   content/blockedSite.xhtml            (content/blockedSite.xhtml)
   content/languages.properties         (content/languages.properties)
   content/browser.xul                  (content/browser.xul)
 * content/browser.js                   (content/browser.js)
   content/bindings/checkbox.xml        (content/bindings/checkbox.xml)
   content/bindings/settings.xml        (content/bindings/settings.xml)
-  content/downloads.js                 (content/downloads.js)
   content/netError.xhtml               (content/netError.xhtml)
   content/SelectHelper.js              (content/SelectHelper.js)
   content/SelectionHandler.js          (content/SelectionHandler.js)
   content/dbg-browser-actors.js        (content/dbg-browser-actors.js)
 * content/WebappRT.js                  (content/WebappRT.js)
   content/InputWidgetHelper.js         (content/InputWidgetHelper.js)
   content/WebrtcUI.js                  (content/WebrtcUI.js)
   content/MemoryObserver.js            (content/MemoryObserver.js)
--- a/mobile/android/installer/package-manifest.in
+++ b/mobile/android/installer/package-manifest.in
@@ -422,16 +422,19 @@
 @BINPATH@/components/dom_webspeechsynth.xpt
 #endif
 
 #ifdef MOZ_DEBUG
 @BINPATH@/components/TestInterfaceJS.js
 @BINPATH@/components/TestInterfaceJS.manifest
 #endif
 
+@BINPATH@/components/Downloads.manifest
+@BINPATH@/components/DownloadLegacy.js
+
 ; Modules
 @BINPATH@/modules/*
 
 #ifdef MOZ_SAFE_BROWSING
 ; Safe Browsing
 @BINPATH@/components/nsURLClassifier.manifest
 @BINPATH@/components/nsUrlClassifierHashCompleter.js
 @BINPATH@/components/nsUrlClassifierListManager.js
--- a/mobile/android/themes/core/aboutDownloads.css
+++ b/mobile/android/themes/core/aboutDownloads.css
@@ -46,27 +46,27 @@ li:active div.details,
   color: gray;
 }
 
 .domain,
 .size {
   display: inline;
 }
 
-.displayState {
+.state {
   color: gray;
   margin-bottom: -3px; /* Prevent overflow that hides bottom border */
 }
 
 .size:after {
   content: " - ";
   white-space: pre;
 }
 
 #no-downloads-indicator {
   display: none;
 }
 
-#private-downloads-list:empty + #normal-downloads-list:empty + #no-downloads-indicator {
+#private-downloads-list:empty + #public-downloads-list:empty + #no-downloads-indicator {
   display: block;
   text-align: center;
   padding-top: 3.9em;
 }