diff --git a/mobile/android/base/GeckoApp.java b/mobile/android/base/GeckoApp.java
index 90e3a50eb53d..0b335185f92f 100644
--- a/mobile/android/base/GeckoApp.java
+++ b/mobile/android/base/GeckoApp.java
@@ -517,8 +517,7 @@ abstract public class GeckoApp
loadUrlInTab("about:addons");
return true;
case R.id.downloads:
- intent = new Intent(DownloadManager.ACTION_VIEW_DOWNLOADS);
- startActivity(intent);
+ loadUrlInTab("about:downloads");
return true;
case R.id.char_encoding:
GeckoAppShell.sendEventToGecko(GeckoEvent.createBroadcastEvent("CharEncoding:Get", null));
diff --git a/mobile/android/base/Makefile.in b/mobile/android/base/Makefile.in
index 3067577e442c..c827a807899c 100644
--- a/mobile/android/base/Makefile.in
+++ b/mobile/android/base/Makefile.in
@@ -178,7 +178,6 @@ FENNEC_PP_JAVA_FILES = \
FENNEC_PP_XML_FILES = \
res/layout/abouthome_content.xml \
res/menu/gecko_menu.xml \
- res/menu-v11/gecko_menu.xml \
$(NULL)
diff --git a/mobile/android/base/resources/menu-v11/gecko_menu.xml.in b/mobile/android/base/resources/menu-v11/gecko_menu.xml.in
deleted file mode 100644
index da6e285ecc81..000000000000
--- a/mobile/android/base/resources/menu-v11/gecko_menu.xml.in
+++ /dev/null
@@ -1,48 +0,0 @@
-
-
diff --git a/mobile/android/base/resources/menu/gecko_menu.xml.in b/mobile/android/base/resources/menu/gecko_menu.xml.in
index 6efa3b697a58..da6e285ecc81 100644
--- a/mobile/android/base/resources/menu/gecko_menu.xml.in
+++ b/mobile/android/base/resources/menu/gecko_menu.xml.in
@@ -28,8 +28,7 @@
android:title="@string/addons"/>
+ android:title="@string/downloads"/>
- " +
+ "" +
+ "
" +
+ "
" +
+ // This is a hack so that we can crop this label in its center
+ "
" +
+ "
{date}
" +
+ "
" +
+ "
{size}
" +
+ "
{domain}
" +
+ "
" +
+"";
+
+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));
+
+let Downloads = {
+ init: function () {
+ this._list = document.getElementById("downloads-list");
+ this._list.addEventListener("click", function (event) {
+ let target = event.target;
+ while (target && target.nodeName != "li") {
+ target = target.parentNode;
+ }
+
+ Downloads.openDownload(target);
+ }, false);
+
+ let contextmenus = gChromeWin.NativeWindow.contextmenus;
+ Downloads.openMenuItem = contextmenus.add(gStrings.GetStringFromName("downloadAction.open"),
+ contextmenus.SelectorContext("li[mozDownload]"),
+ function (aTarget) {
+ Downloads.openDownload(aTarget);
+ }
+ );
+ Downloads.removeMenuItem = contextmenus.add(gStrings.GetStringFromName("downloadAction.remove"),
+ contextmenus.SelectorContext("li[mozDownload]"),
+ function (aTarget) {
+ Downloads.removeDownload(aTarget);
+ }
+ );
+
+ Services.obs.addObserver(this, "dl-failed", false);
+ Services.obs.addObserver(this, "dl-done", false);
+ Services.obs.addObserver(this, "dl-cancel", false);
+
+ this.getDownloads();
+ },
+
+ uninit: function () {
+ let contextmenus = gChromeWin.NativeWindow.contextmenus;
+ contextmenus.remove(this.openMenuItem);
+ contextmenus.remove(this.removeMenuItem);
+
+ Services.obs.removeObserver(this, "dl-failed");
+ Services.obs.removeObserver(this, "dl-done");
+ Services.obs.removeObserver(this, "dl-cancel");
+ },
+
+ observe: function (aSubject, aTopic, aData) {
+ let download = aSubject.QueryInterface(Ci.nsIDownload);
+ switch (aTopic) {
+ case "dl-failed":
+ case "dl-cancel":
+ case "dl-done":
+ if (!this._getElementForDownload(download.id)) {
+ let item = this._createItem(downloadTemplate, {
+ id: download.id,
+ target: download.displayName,
+ icon: "moz-icon://" + download.displayName + "?size=64",
+ date: DownloadUtils.getReadableDates(new Date())[0],
+ domain: DownloadUtils.getURIHost(download.source.spec)[0],
+ size: DownloadUtils.convertByteUnits(download.size).join(""),
+ });
+ this._list.insertAdjacentHTML("afterbegin", item);
+ break;
+ }
+ }
+ },
+
+ _initStatement: function dv__initStatement() {
+ if (this._stmt)
+ this._stmt.finalize();
+
+ this._stmt = this._dlmgr.DBConnection.createStatement(
+ "SELECT id, name, source, state, startTime, endTime, referrer, " +
+ "currBytes, maxBytes, state IN (?1, ?2, ?3, ?4, ?5) isActive " +
+ "FROM moz_downloads " +
+ "WHERE NOT isActive " +
+ "ORDER BY endTime DESC");
+ },
+
+ _createItem: function (aTemplate, aValues) {
+ let t = aTemplate;
+ for (let i in aValues) {
+ let regEx = new RegExp("{" + i + "}", "g");
+ t = t.replace(regEx, aValues[i]);
+ }
+ return t;
+ },
+
+ _stepDownloads: function dv__stepDownloads(aNumItems) {
+ try {
+ if (!this._stmt.executeStep()) {
+ this._stmt.finalize();
+ this._stmt = null;
+ return;
+ }
+
+ // Try to get the attribute values from the statement
+ let attrs = {
+ id: this._stmt.row.id,
+ target: this._stmt.row.name,
+ icon: "moz-icon://" + this._stmt.row.name + "?size=64",
+ date: DownloadUtils.getReadableDates(new Date(this._stmt.row.endTime/1000))[0],
+ domain: DownloadUtils.getURIHost(this._stmt.row.source)[0],
+ size: DownloadUtils.convertByteUnits(this._stmt.row.maxBytes).join(""),
+ };
+
+ let item = this._createItem(downloadTemplate, attrs);
+ this._list.insertAdjacentHTML("beforeend", item);
+ } catch (e) {
+ // Something went wrong when stepping or getting values, so clear and quit
+ console.log("Error: " + e);
+ this._stmt.reset();
+ 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);
+ }
+ },
+
+ getDownloads: function () {
+ this._dlmgr = Cc["@mozilla.org/download-manager;1"].getService(Ci.nsIDownloadManager);
+
+ this._initStatement();
+
+ clearTimeout(this._timeoutID);
+
+ this._stmt.reset();
+ this._stmt.bindInt32Parameter(0, Ci.nsIDownloadManager.DOWNLOAD_NOTSTARTED);
+ this._stmt.bindInt32Parameter(1, Ci.nsIDownloadManager.DOWNLOAD_DOWNLOADING);
+ this._stmt.bindInt32Parameter(2, Ci.nsIDownloadManager.DOWNLOAD_PAUSED);
+ this._stmt.bindInt32Parameter(3, Ci.nsIDownloadManager.DOWNLOAD_QUEUED);
+ this._stmt.bindInt32Parameter(4, Ci.nsIDownloadManager.DOWNLOAD_SCANNING);
+
+ // Take a quick break before we actually start building the list
+ let self = this;
+ this._timeoutID = setTimeout(function () {
+ self._stepDownloads(1);
+ }, 0);
+ },
+
+ _getElementForDownload: function (aKey) {
+ return this._list.querySelector("li[downloadID='" + aKey + "']");
+ },
+
+ _getDownloadForElement: function (aElement) {
+ let id = parseInt(aElement.getAttribute("downloadID"));
+ return this._dlmgr.getDownload(id);
+ },
+
+ openDownload: function (aItem) {
+ let f = null;
+ try {
+ let download = this._getDownloadForElement(aItem);
+ f = download.targetFile;
+ } catch(ex) { }
+
+ try {
+ if (f) f.launch();
+ } catch (ex) { }
+ },
+
+ removeDownload: function (aItem) {
+ let f = null;
+ try {
+ let download = this._getDownloadForElement(aItem);
+ f = download.targetFile;
+ } catch(ex) {
+ // even if there is no file, pretend that there is so that we can remove
+ // it from the list
+ f = { leafName: "" };
+ }
+
+ this._dlmgr.removeDownload(aItem.getAttribute("downloadID"));
+
+ this._list.removeChild(aItem);
+
+ try {
+ if (f) f.remove(false);
+ } catch(ex) { }
+ },
+}
diff --git a/mobile/android/chrome/content/aboutDownloads.xhtml b/mobile/android/chrome/content/aboutDownloads.xhtml
new file mode 100644
index 000000000000..106bb89bee13
--- /dev/null
+++ b/mobile/android/chrome/content/aboutDownloads.xhtml
@@ -0,0 +1,34 @@
+
+
+
+%brandDTD;
+
+%globalDTD;
+
+%downloadsDTD;
+]>
+
+
+
+
+
+ &aboutDownloads.title;
+
+
+
+
+
+
+
+
+ &aboutDownloads.empty;
+
+
+
diff --git a/mobile/android/chrome/jar.mn b/mobile/android/chrome/jar.mn
index 05457fcc22ad..b727a87d8a79 100644
--- a/mobile/android/chrome/jar.mn
+++ b/mobile/android/chrome/jar.mn
@@ -8,6 +8,8 @@ chrome.jar:
content/aboutAddons.xhtml (content/aboutAddons.xhtml)
content/aboutAddons.js (content/aboutAddons.js)
content/aboutCertError.xhtml (content/aboutCertError.xhtml)
+ content/aboutDownloads.xhtml (content/aboutDownloads.xhtml)
+ content/aboutDownloads.js (content/aboutDownloads.js)
content/aboutHome.xhtml (content/aboutHome.xhtml)
* content/aboutRights.xhtml (content/aboutRights.xhtml)
* content/aboutApps.xhtml (content/aboutApps.xhtml)
diff --git a/mobile/android/components/AboutRedirector.js b/mobile/android/components/AboutRedirector.js
index cbf4f68c5e9f..3dc55c9fc9d6 100644
--- a/mobile/android/components/AboutRedirector.js
+++ b/mobile/android/components/AboutRedirector.js
@@ -89,6 +89,10 @@ let modules = {
apps: {
uri: "chrome://browser/content/aboutApps.xhtml",
privileged: true
+ },
+ downloads: {
+ uri: "chrome://browser/content/aboutDownloads.xhtml",
+ privileged: true
}
}
diff --git a/mobile/android/components/MobileComponents.manifest b/mobile/android/components/MobileComponents.manifest
index d575b29d2d28..09dbb57807df 100644
--- a/mobile/android/components/MobileComponents.manifest
+++ b/mobile/android/components/MobileComponents.manifest
@@ -8,6 +8,7 @@ contract @mozilla.org/network/protocol/about;1?what=rights {322ba47e-7047-4f71-a
contract @mozilla.org/network/protocol/about;1?what=certerror {322ba47e-7047-4f71-aebf-cb7d69325cd9}
contract @mozilla.org/network/protocol/about;1?what=home {322ba47e-7047-4f71-aebf-cb7d69325cd9}
contract @mozilla.org/network/protocol/about;1?what=apps {322ba47e-7047-4f71-aebf-cb7d69325cd9}
+contract @mozilla.org/network/protocol/about;1?what=downloads {322ba47e-7047-4f71-aebf-cb7d69325cd9}
#ifdef MOZ_SAFE_BROWSING
contract @mozilla.org/network/protocol/about;1?what=blocked {322ba47e-7047-4f71-aebf-cb7d69325cd9}
#endif
diff --git a/mobile/android/locales/en-US/chrome/aboutDownloads.dtd b/mobile/android/locales/en-US/chrome/aboutDownloads.dtd
new file mode 100644
index 000000000000..ea969c28d288
--- /dev/null
+++ b/mobile/android/locales/en-US/chrome/aboutDownloads.dtd
@@ -0,0 +1,3 @@
+
+
+
diff --git a/mobile/android/locales/en-US/chrome/aboutDownloads.properties b/mobile/android/locales/en-US/chrome/aboutDownloads.properties
new file mode 100644
index 000000000000..27a8b82eabac
--- /dev/null
+++ b/mobile/android/locales/en-US/chrome/aboutDownloads.properties
@@ -0,0 +1,2 @@
+downloadAction.open=Open
+downloadAction.remove=Delete
diff --git a/mobile/android/locales/jar.mn b/mobile/android/locales/jar.mn
index ff6eb31ef9c6..7febd25313a6 100644
--- a/mobile/android/locales/jar.mn
+++ b/mobile/android/locales/jar.mn
@@ -8,6 +8,8 @@
locale/@AB_CD@/browser/aboutApps.dtd (%chrome/aboutApps.dtd)
locale/@AB_CD@/browser/aboutApps.properties (%chrome/aboutApps.properties)
locale/@AB_CD@/browser/aboutCertError.dtd (%chrome/aboutCertError.dtd)
+ locale/@AB_CD@/browser/aboutDownloads.dtd (%chrome/aboutDownloads.dtd)
+ locale/@AB_CD@/browser/aboutDownloads.properties (%chrome/aboutDownloads.properties)
locale/@AB_CD@/browser/browser.properties (%chrome/browser.properties)
locale/@AB_CD@/browser/config.dtd (%chrome/config.dtd)
locale/@AB_CD@/browser/config.properties (%chrome/config.properties)
diff --git a/mobile/android/themes/core/aboutDownloads.css b/mobile/android/themes/core/aboutDownloads.css
new file mode 100644
index 000000000000..1a10102e6246
--- /dev/null
+++ b/mobile/android/themes/core/aboutDownloads.css
@@ -0,0 +1,120 @@
+/* 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/. */
+
+%filter substitution
+%include defines.inc
+
+html {
+ font-family: "Droid Sans",helvetica,arial,clean,sans-serif;
+ font-size: 18px;
+ background-image: url("chrome://browser/skin/images/about-bg-lightblue.png");
+ -moz-text-size-adjust: none;
+}
+
+body {
+ margin: 0;
+}
+
+li > a {
+ color: inherit;
+ text-decoration: none;
+}
+
+.header {
+ color: black;
+ padding: 10px;
+ -moz-padding-start: 25px;
+ font-size: 20px;
+ font-weight: bold;
+ border-bottom: 2px solid;
+ -moz-border-bottom-colors: #ff9100 #f27900;
+}
+
+.header > div {
+ width: 100%;
+ height: 45px;
+ display: -moz-box;
+ -moz-box-align: center;
+}
+
+ul {
+ padding: 0px;
+ margin: 0px;
+ width: 100%;
+}
+
+li {
+ color: black;
+ background-image: url("chrome://browser/skin/images/row-bg-normal.png");
+ border-bottom: 2px solid;
+ -moz-border-bottom-colors: #ffffff #bac2ac;
+ position: relative;
+ list-style-type: none;
+ list-style-image: none;
+ margin: 0px;
+ padding: 0px;
+ color: black;
+ height: 86px;
+ font-size: 18px !important;
+}
+
+li:active {
+ background-color: @color_background_highlight_overlay@;
+ background-image: none;
+}
+
+li:active .details {
+ background-image: none;
+}
+
+.icon {
+ width: 32px;
+ height: 32px;
+ position: absolute;
+ top: 27px;
+ left: 27px;
+}
+
+.details {
+ margin-left: 86px;
+ padding: 10px;
+ min-height: 66px;
+ background-image: url("chrome://browser/skin/images/row-bg-light.png");
+}
+
+.row {
+ display: -moz-box;
+ width: 100%;
+}
+
+.title {
+ font-size: 22px;
+ font-weight: bold;
+ overflow: hidden;
+ -moz-box-flex: 1;
+}
+
+.date {
+ color: gray;
+}
+
+.domain,
+.size {
+ display: inline;
+}
+
+.size:after {
+ content: " - ";
+ white-space: pre;
+}
+
+#no-downloads-indicator {
+ display: none;
+}
+
+#downloads-list:empty + #no-downloads-indicator {
+ display: block;
+ text-align: center;
+ padding-top: 70px;
+}
diff --git a/mobile/android/themes/core/jar.mn b/mobile/android/themes/core/jar.mn
index 9d912de5cebf..666627f14af8 100644
--- a/mobile/android/themes/core/jar.mn
+++ b/mobile/android/themes/core/jar.mn
@@ -6,6 +6,7 @@ chrome.jar:
skin/about.css (about.css)
skin/aboutAddons.css (aboutAddons.css)
skin/aboutApps.css (aboutApps.css)
+* skin/aboutDownloads.css (aboutDownloads.css)
* skin/browser.css (browser.css)
* skin/content.css (content.css)
skin/config.css (config.css)