Merge m-c to inbound. a=merge
authorRyan VanderMeulen <ryanvm@gmail.com>
Tue, 17 Feb 2015 14:27:23 -0500
changeset 256663 ebd50d4250b2bfc11b09abfa6e650138e99c4cda
parent 256662 4638a15529043d97b572c321351aa76cd3d6e48c (current diff)
parent 256585 b6c56fab513ddd109307a8d9720c53947bd1756e (diff)
child 256664 a3cabc94db732dd274679d3c6961dae652e4b563
push id4610
push userjlund@mozilla.com
push dateMon, 30 Mar 2015 18:32:55 +0000
treeherdermozilla-beta@4df54044d9ef [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersmerge
milestone38.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Merge m-c to inbound. a=merge
--- a/b2g/config/dolphin/sources.xml
+++ b/b2g/config/dolphin/sources.xml
@@ -10,17 +10,17 @@
   <!--original fetch url was git://codeaurora.org/-->
   <remote fetch="https://git.mozilla.org/external/caf" name="caf"/>
   <!--original fetch url was https://git.mozilla.org/releases-->
   <remote fetch="https://git.mozilla.org/releases" name="mozillaorg"/>
   <!-- B2G specific things. -->
   <project name="platform_build" path="build" remote="b2g" revision="cdaa0a4ac28c781709df8c318ed079e9e475503a">
     <copyfile dest="Makefile" src="core/root.mk"/>
   </project>
-  <project name="gaia" path="gaia" remote="mozillaorg" revision="ae02fbdeae77b2002cebe33c61aedeee4b9439fd"/>
+  <project name="gaia" path="gaia" remote="mozillaorg" revision="4f39e48b95fa00c8669b8707447542024bb55432"/>
   <project name="fake-libdvm" path="dalvik" remote="b2g" revision="d50ae982b19f42f0b66d08b9eb306be81687869f"/>
   <project name="gonk-misc" path="gonk-misc" remote="b2g" revision="2262d4a77d4f46ab230fd747bb91e9b77bad36cb"/>
   <project name="librecovery" path="librecovery" remote="b2g" revision="1b3591a50ed352fc6ddb77462b7b35d0bfa555a3"/>
   <project name="moztt" path="external/moztt" remote="b2g" revision="fe893bb760a3bb64375f62fdf4762a58c59df9ef"/>
   <project name="rilproxy" path="rilproxy" remote="b2g" revision="5ef30994f4778b4052e58a4383dbe7890048c87e"/>
   <project name="valgrind" path="external/valgrind" remote="b2g" revision="daa61633c32b9606f58799a3186395fd2bbb8d8c"/>
   <project name="vex" path="external/VEX" remote="b2g" revision="47f031c320888fe9f3e656602588565b52d43010"/>
   <project name="apitrace" path="external/apitrace" remote="apitrace" revision="8d0d11d190ccc50d7d66009bcc896ad4b42d3f0d"/>
--- a/b2g/config/emulator-ics/sources.xml
+++ b/b2g/config/emulator-ics/sources.xml
@@ -14,17 +14,17 @@
   <!--original fetch url was git://github.com/apitrace/-->
   <remote fetch="https://git.mozilla.org/external/apitrace" name="apitrace"/>
   <default remote="caf" revision="refs/tags/android-4.0.4_r2.1" sync-j="4"/>
   <!-- Gonk specific things and forks -->
   <project name="platform_build" path="build" remote="b2g" revision="6295c4eb38de793159368aa7f745ef3faf7208aa">
     <copyfile dest="Makefile" src="core/root.mk"/>
   </project>
   <project name="fake-dalvik" path="dalvik" remote="b2g" revision="ca1f327d5acc198bb4be62fa51db2c039032c9ce"/>
-  <project name="gaia.git" path="gaia" remote="mozillaorg" revision="ae02fbdeae77b2002cebe33c61aedeee4b9439fd"/>
+  <project name="gaia.git" path="gaia" remote="mozillaorg" revision="4f39e48b95fa00c8669b8707447542024bb55432"/>
   <project name="gonk-misc" path="gonk-misc" remote="b2g" revision="2262d4a77d4f46ab230fd747bb91e9b77bad36cb"/>
   <project name="rilproxy" path="rilproxy" remote="b2g" revision="5ef30994f4778b4052e58a4383dbe7890048c87e"/>
   <project name="platform_hardware_ril" path="hardware/ril" remote="b2g" revision="eb1795a9002eb142ac58c8d68f8f4ba094af07ca"/>
   <project name="platform_external_qemu" path="external/qemu" remote="b2g" revision="2c31ac3a31a340b40ecd9c291df9b9613d3afa72"/>
   <project name="moztt" path="external/moztt" remote="b2g" revision="fe893bb760a3bb64375f62fdf4762a58c59df9ef"/>
   <project name="apitrace" path="external/apitrace" remote="apitrace" revision="8d0d11d190ccc50d7d66009bcc896ad4b42d3f0d"/>
   <!-- Stock Android things -->
   <project name="platform/abi/cpp" path="abi/cpp" revision="dd924f92906085b831bf1cbbc7484d3c043d613c"/>
--- a/b2g/config/emulator-jb/sources.xml
+++ b/b2g/config/emulator-jb/sources.xml
@@ -12,17 +12,17 @@
   <!--original fetch url was https://git.mozilla.org/releases-->
   <remote fetch="https://git.mozilla.org/releases" name="mozillaorg"/>
   <!-- B2G specific things. -->
   <project name="platform_build" path="build" remote="b2g" revision="ac778ae59be38aea284a04c89640b1a11c26a5de">
     <copyfile dest="Makefile" src="core/root.mk"/>
   </project>
   <project name="rilproxy" path="rilproxy" remote="b2g" revision="5ef30994f4778b4052e58a4383dbe7890048c87e"/>
   <project name="fake-libdvm" path="dalvik" remote="b2g" revision="d50ae982b19f42f0b66d08b9eb306be81687869f"/>
-  <project name="gaia" path="gaia" remote="mozillaorg" revision="ae02fbdeae77b2002cebe33c61aedeee4b9439fd"/>
+  <project name="gaia" path="gaia" remote="mozillaorg" revision="4f39e48b95fa00c8669b8707447542024bb55432"/>
   <project name="gonk-misc" path="gonk-misc" remote="b2g" revision="2262d4a77d4f46ab230fd747bb91e9b77bad36cb"/>
   <project name="moztt" path="external/moztt" remote="b2g" revision="fe893bb760a3bb64375f62fdf4762a58c59df9ef"/>
   <project name="apitrace" path="external/apitrace" remote="apitrace" revision="8d0d11d190ccc50d7d66009bcc896ad4b42d3f0d"/>
   <project name="valgrind" path="external/valgrind" remote="b2g" revision="daa61633c32b9606f58799a3186395fd2bbb8d8c"/>
   <project name="vex" path="external/VEX" remote="b2g" revision="47f031c320888fe9f3e656602588565b52d43010"/>
   <!-- Stock Android things -->
   <project groups="linux" name="platform/prebuilts/clang/linux-x86/3.1" path="prebuilts/clang/linux-x86/3.1" revision="5c45f43419d5582949284eee9cef0c43d866e03b"/>
   <project groups="linux" name="platform/prebuilts/clang/linux-x86/3.2" path="prebuilts/clang/linux-x86/3.2" revision="3748b4168e7bd8d46457d4b6786003bc6a5223ce"/>
--- a/b2g/config/emulator-kk/sources.xml
+++ b/b2g/config/emulator-kk/sources.xml
@@ -10,17 +10,17 @@
   <!--original fetch url was git://codeaurora.org/-->
   <remote fetch="https://git.mozilla.org/external/caf" name="caf"/>
   <!--original fetch url was https://git.mozilla.org/releases-->
   <remote fetch="https://git.mozilla.org/releases" name="mozillaorg"/>
   <!-- B2G specific things. -->
   <project name="platform_build" path="build" remote="b2g" revision="cdaa0a4ac28c781709df8c318ed079e9e475503a">
     <copyfile dest="Makefile" src="core/root.mk"/>
   </project>
-  <project name="gaia" path="gaia" remote="mozillaorg" revision="ae02fbdeae77b2002cebe33c61aedeee4b9439fd"/>
+  <project name="gaia" path="gaia" remote="mozillaorg" revision="4f39e48b95fa00c8669b8707447542024bb55432"/>
   <project name="fake-libdvm" path="dalvik" remote="b2g" revision="d50ae982b19f42f0b66d08b9eb306be81687869f"/>
   <project name="gonk-misc" path="gonk-misc" remote="b2g" revision="2262d4a77d4f46ab230fd747bb91e9b77bad36cb"/>
   <project name="librecovery" path="librecovery" remote="b2g" revision="1b3591a50ed352fc6ddb77462b7b35d0bfa555a3"/>
   <project name="moztt" path="external/moztt" remote="b2g" revision="fe893bb760a3bb64375f62fdf4762a58c59df9ef"/>
   <project name="rilproxy" path="rilproxy" remote="b2g" revision="5ef30994f4778b4052e58a4383dbe7890048c87e"/>
   <project name="valgrind" path="external/valgrind" remote="b2g" revision="daa61633c32b9606f58799a3186395fd2bbb8d8c"/>
   <project name="vex" path="external/VEX" remote="b2g" revision="47f031c320888fe9f3e656602588565b52d43010"/>
   <project name="apitrace" path="external/apitrace" remote="apitrace" revision="8d0d11d190ccc50d7d66009bcc896ad4b42d3f0d"/>
--- a/b2g/config/emulator/sources.xml
+++ b/b2g/config/emulator/sources.xml
@@ -14,17 +14,17 @@
   <!--original fetch url was git://github.com/apitrace/-->
   <remote fetch="https://git.mozilla.org/external/apitrace" name="apitrace"/>
   <default remote="caf" revision="refs/tags/android-4.0.4_r2.1" sync-j="4"/>
   <!-- Gonk specific things and forks -->
   <project name="platform_build" path="build" remote="b2g" revision="6295c4eb38de793159368aa7f745ef3faf7208aa">
     <copyfile dest="Makefile" src="core/root.mk"/>
   </project>
   <project name="fake-dalvik" path="dalvik" remote="b2g" revision="ca1f327d5acc198bb4be62fa51db2c039032c9ce"/>
-  <project name="gaia.git" path="gaia" remote="mozillaorg" revision="ae02fbdeae77b2002cebe33c61aedeee4b9439fd"/>
+  <project name="gaia.git" path="gaia" remote="mozillaorg" revision="4f39e48b95fa00c8669b8707447542024bb55432"/>
   <project name="gonk-misc" path="gonk-misc" remote="b2g" revision="2262d4a77d4f46ab230fd747bb91e9b77bad36cb"/>
   <project name="rilproxy" path="rilproxy" remote="b2g" revision="5ef30994f4778b4052e58a4383dbe7890048c87e"/>
   <project name="platform_hardware_ril" path="hardware/ril" remote="b2g" revision="eb1795a9002eb142ac58c8d68f8f4ba094af07ca"/>
   <project name="platform_external_qemu" path="external/qemu" remote="b2g" revision="2c31ac3a31a340b40ecd9c291df9b9613d3afa72"/>
   <project name="moztt" path="external/moztt" remote="b2g" revision="fe893bb760a3bb64375f62fdf4762a58c59df9ef"/>
   <project name="apitrace" path="external/apitrace" remote="apitrace" revision="8d0d11d190ccc50d7d66009bcc896ad4b42d3f0d"/>
   <!-- Stock Android things -->
   <project name="platform/abi/cpp" path="abi/cpp" revision="dd924f92906085b831bf1cbbc7484d3c043d613c"/>
--- a/b2g/config/flame-kk/sources.xml
+++ b/b2g/config/flame-kk/sources.xml
@@ -10,17 +10,17 @@
   <!--original fetch url was git://codeaurora.org/-->
   <remote fetch="https://git.mozilla.org/external/caf" name="caf"/>
   <!--original fetch url was https://git.mozilla.org/releases-->
   <remote fetch="https://git.mozilla.org/releases" name="mozillaorg"/>
   <!-- B2G specific things. -->
   <project name="platform_build" path="build" remote="b2g" revision="cdaa0a4ac28c781709df8c318ed079e9e475503a">
     <copyfile dest="Makefile" src="core/root.mk"/>
   </project>
-  <project name="gaia" path="gaia" remote="mozillaorg" revision="ae02fbdeae77b2002cebe33c61aedeee4b9439fd"/>
+  <project name="gaia" path="gaia" remote="mozillaorg" revision="4f39e48b95fa00c8669b8707447542024bb55432"/>
   <project name="fake-libdvm" path="dalvik" remote="b2g" revision="d50ae982b19f42f0b66d08b9eb306be81687869f"/>
   <project name="gonk-misc" path="gonk-misc" remote="b2g" revision="2262d4a77d4f46ab230fd747bb91e9b77bad36cb"/>
   <project name="librecovery" path="librecovery" remote="b2g" revision="1b3591a50ed352fc6ddb77462b7b35d0bfa555a3"/>
   <project name="moztt" path="external/moztt" remote="b2g" revision="fe893bb760a3bb64375f62fdf4762a58c59df9ef"/>
   <project name="rilproxy" path="rilproxy" remote="b2g" revision="5ef30994f4778b4052e58a4383dbe7890048c87e"/>
   <project name="valgrind" path="external/valgrind" remote="b2g" revision="daa61633c32b9606f58799a3186395fd2bbb8d8c"/>
   <project name="vex" path="external/VEX" remote="b2g" revision="47f031c320888fe9f3e656602588565b52d43010"/>
   <project name="apitrace" path="external/apitrace" remote="apitrace" revision="8d0d11d190ccc50d7d66009bcc896ad4b42d3f0d"/>
--- a/b2g/config/flame/sources.xml
+++ b/b2g/config/flame/sources.xml
@@ -12,17 +12,17 @@
   <!--original fetch url was https://git.mozilla.org/releases-->
   <remote fetch="https://git.mozilla.org/releases" name="mozillaorg"/>
   <!-- B2G specific things. -->
   <project name="platform_build" path="build" remote="b2g" revision="ac778ae59be38aea284a04c89640b1a11c26a5de">
     <copyfile dest="Makefile" src="core/root.mk"/>
   </project>
   <project name="librecovery" path="librecovery" remote="b2g" revision="1b3591a50ed352fc6ddb77462b7b35d0bfa555a3"/>
   <project name="fake-libdvm" path="dalvik" remote="b2g" revision="d50ae982b19f42f0b66d08b9eb306be81687869f"/>
-  <project name="gaia" path="gaia" remote="mozillaorg" revision="ae02fbdeae77b2002cebe33c61aedeee4b9439fd"/>
+  <project name="gaia" path="gaia" remote="mozillaorg" revision="4f39e48b95fa00c8669b8707447542024bb55432"/>
   <project name="gonk-misc" path="gonk-misc" remote="b2g" revision="2262d4a77d4f46ab230fd747bb91e9b77bad36cb"/>
   <project name="moztt" path="external/moztt" remote="b2g" revision="fe893bb760a3bb64375f62fdf4762a58c59df9ef"/>
   <project name="apitrace" path="external/apitrace" remote="apitrace" revision="8d0d11d190ccc50d7d66009bcc896ad4b42d3f0d"/>
   <project name="valgrind" path="external/valgrind" remote="b2g" revision="daa61633c32b9606f58799a3186395fd2bbb8d8c"/>
   <project name="vex" path="external/VEX" remote="b2g" revision="47f031c320888fe9f3e656602588565b52d43010"/>
   <!-- Stock Android things -->
   <project groups="linux" name="platform/prebuilts/clang/linux-x86/3.1" path="prebuilts/clang/linux-x86/3.1" revision="e95b4ce22c825da44d14299e1190ea39a5260bde"/>
   <project groups="linux" name="platform/prebuilts/clang/linux-x86/3.2" path="prebuilts/clang/linux-x86/3.2" revision="471afab478649078ad7c75ec6b252481a59e19b8"/>
--- a/b2g/config/gaia.json
+++ b/b2g/config/gaia.json
@@ -1,9 +1,9 @@
 {
     "git": {
-        "git_revision": "ae02fbdeae77b2002cebe33c61aedeee4b9439fd", 
+        "git_revision": "4f39e48b95fa00c8669b8707447542024bb55432", 
         "remote": "https://git.mozilla.org/releases/gaia.git", 
         "branch": ""
     }, 
-    "revision": "62d026a98ea42f2b93de000e8d0d4f1254f86730", 
+    "revision": "e0816d2581cdc2d0581f625c06811128c87c0c48", 
     "repo_path": "integration/gaia-central"
 }
--- a/b2g/config/nexus-4/sources.xml
+++ b/b2g/config/nexus-4/sources.xml
@@ -12,17 +12,17 @@
   <!--original fetch url was https://git.mozilla.org/releases-->
   <remote fetch="https://git.mozilla.org/releases" name="mozillaorg"/>
   <!-- B2G specific things. -->
   <project name="platform_build" path="build" remote="b2g" revision="ac778ae59be38aea284a04c89640b1a11c26a5de">
     <copyfile dest="Makefile" src="core/root.mk"/>
   </project>
   <project name="rilproxy" path="rilproxy" remote="b2g" revision="5ef30994f4778b4052e58a4383dbe7890048c87e"/>
   <project name="fake-libdvm" path="dalvik" remote="b2g" revision="d50ae982b19f42f0b66d08b9eb306be81687869f"/>
-  <project name="gaia" path="gaia" remote="mozillaorg" revision="ae02fbdeae77b2002cebe33c61aedeee4b9439fd"/>
+  <project name="gaia" path="gaia" remote="mozillaorg" revision="4f39e48b95fa00c8669b8707447542024bb55432"/>
   <project name="gonk-misc" path="gonk-misc" remote="b2g" revision="2262d4a77d4f46ab230fd747bb91e9b77bad36cb"/>
   <project name="moztt" path="external/moztt" remote="b2g" revision="fe893bb760a3bb64375f62fdf4762a58c59df9ef"/>
   <project name="apitrace" path="external/apitrace" remote="apitrace" revision="8d0d11d190ccc50d7d66009bcc896ad4b42d3f0d"/>
   <project name="valgrind" path="external/valgrind" remote="b2g" revision="daa61633c32b9606f58799a3186395fd2bbb8d8c"/>
   <project name="vex" path="external/VEX" remote="b2g" revision="47f031c320888fe9f3e656602588565b52d43010"/>
   <!-- Stock Android things -->
   <project groups="linux" name="platform/prebuilts/clang/linux-x86/3.1" path="prebuilts/clang/linux-x86/3.1" revision="5c45f43419d5582949284eee9cef0c43d866e03b"/>
   <project groups="linux" name="platform/prebuilts/clang/linux-x86/3.2" path="prebuilts/clang/linux-x86/3.2" revision="3748b4168e7bd8d46457d4b6786003bc6a5223ce"/>
--- a/b2g/config/nexus-5-l/sources.xml
+++ b/b2g/config/nexus-5-l/sources.xml
@@ -10,17 +10,17 @@
   <!--original fetch url was git://codeaurora.org/-->
   <remote fetch="https://git.mozilla.org/external/caf" name="caf"/>
   <!--original fetch url was https://git.mozilla.org/releases-->
   <remote fetch="https://git.mozilla.org/releases" name="mozillaorg"/>
   <!-- B2G specific things. -->
   <project name="platform_build" path="build" remote="b2g" revision="7f2ee9f4cb926684883fc2a2e407045fd9db2199">
     <copyfile dest="Makefile" src="core/root.mk"/>
   </project>
-  <project name="gaia" path="gaia" remote="mozillaorg" revision="ae02fbdeae77b2002cebe33c61aedeee4b9439fd"/>
+  <project name="gaia" path="gaia" remote="mozillaorg" revision="4f39e48b95fa00c8669b8707447542024bb55432"/>
   <project name="fake-libdvm" path="dalvik" remote="b2g" revision="d50ae982b19f42f0b66d08b9eb306be81687869f"/>
   <project name="gonk-misc" path="gonk-misc" remote="b2g" revision="2262d4a77d4f46ab230fd747bb91e9b77bad36cb"/>
   <project name="librecovery" path="librecovery" remote="b2g" revision="1b3591a50ed352fc6ddb77462b7b35d0bfa555a3"/>
   <project name="moztt" path="external/moztt" remote="b2g" revision="fe893bb760a3bb64375f62fdf4762a58c59df9ef"/>
   <project name="rilproxy" path="rilproxy" remote="b2g" revision="5ef30994f4778b4052e58a4383dbe7890048c87e"/>
   <project name="valgrind" path="external/valgrind" remote="b2g" revision="daa61633c32b9606f58799a3186395fd2bbb8d8c"/>
   <project name="vex" path="external/VEX" remote="b2g" revision="47f031c320888fe9f3e656602588565b52d43010"/>
   <project name="apitrace" path="external/apitrace" remote="apitrace" revision="8d0d11d190ccc50d7d66009bcc896ad4b42d3f0d"/>
--- a/browser/base/content/tabbrowser.xml
+++ b/browser/base/content/tabbrowser.xml
@@ -3204,17 +3204,18 @@
                                           browser: browser,
                                           editFlags: aMessage.data.editFlags,
                                           spellInfo: spellInfo,
                                           principal: aMessage.data.principal,
                                           customMenuItems: aMessage.data.customMenuItems,
                                           addonInfo: aMessage.data.addonInfo };
               let popup = browser.ownerDocument.getElementById("contentAreaContextMenu");
               let event = gContextMenuContentData.event;
-              popup.openPopupAtScreen(event.screenX, event.screenY, true);
+              let pos = browser.mapScreenCoordinatesFromContent(event.screenX, event.screenY);
+              popup.openPopupAtScreen(pos.x, pos.y, true);
               break;
             }
             case "DOMWebNotificationClicked": {
               let tab = this.getTabForBrowser(browser);
               if (!tab)
                 return;
               this.selectedTab = tab;
               window.focus();
--- a/browser/components/downloads/DownloadsCommon.jsm
+++ b/browser/components/downloads/DownloadsCommon.jsm
@@ -16,25 +16,19 @@ this.EXPORTED_SYMBOLS = [
  * This file includes the following constructors and global objects:
  *
  * DownloadsCommon
  * This object is exposed directly to the consumers of this JavaScript module,
  * and provides shared methods for all the instances of the user interface.
  *
  * DownloadsData
  * Retrieves the list of past and completed downloads from the underlying
- * Download Manager data, and provides asynchronous notifications allowing
+ * Downloads API data, and provides asynchronous notifications allowing
  * to build a consistent view of the available data.
  *
- * DownloadsDataItem
- * Represents a single item in the list of downloads.  This object either wraps
- * an existing nsIDownload from the Download Manager, or provides the same
- * information read directly from the downloads database, with the possibility
- * of querying the nsIDownload lazily, for performance reasons.
- *
  * DownloadsIndicatorData
  * This object registers itself with DownloadsData as a view, and transforms the
  * notifications it receives into overall status data, that is then broadcast to
  * the registered download status indicators.
  */
 
 ////////////////////////////////////////////////////////////////////////////////
 //// Globals
@@ -52,16 +46,18 @@ XPCOMUtils.defineLazyModuleGetter(this, 
 XPCOMUtils.defineLazyModuleGetter(this, "PluralForm",
                                   "resource://gre/modules/PluralForm.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "Downloads",
                                   "resource://gre/modules/Downloads.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "DownloadUIHelper",
                                   "resource://gre/modules/DownloadUIHelper.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "DownloadUtils",
                                   "resource://gre/modules/DownloadUtils.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "FileUtils",
+                                  "resource://gre/modules/FileUtils.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "OS",
                                   "resource://gre/modules/osfile.jsm")
 XPCOMUtils.defineLazyModuleGetter(this, "PlacesUtils",
                                   "resource://gre/modules/PlacesUtils.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "PrivateBrowsingUtils",
                                   "resource://gre/modules/PrivateBrowsingUtils.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "RecentWindow",
                                   "resource:///modules/RecentWindow.jsm");
@@ -87,21 +83,16 @@ const kDownloadsStringsRequiringFormatti
   statusSeparatorBeforeNumber: true,
   fileExecutableSecurityWarning: true
 };
 
 const kDownloadsStringsRequiringPluralForm = {
   otherDownloads2: true
 };
 
-XPCOMUtils.defineLazyGetter(this, "DownloadsLocalFileCtor", function () {
-  return Components.Constructor("@mozilla.org/file/local;1",
-                                "nsILocalFile", "initWithPath");
-});
-
 const kPartialDownloadSuffix = ".part";
 
 const kPrefBranch = Services.prefs.getBranch("browser.download.");
 
 let PrefObserver = {
   QueryInterface: XPCOMUtils.generateQI([Ci.nsIObserver,
                                          Ci.nsISupportsWeakReference]),
   getPref(name) {
@@ -299,84 +290,118 @@ this.DownloadsCommon = {
       }
       return this._summary = new DownloadsSummaryData(false, aNumToExclude);
     }
   },
   _summary: null,
   _privateSummary: null,
 
   /**
-   * Given an iterable collection of DownloadDataItems, generates and returns
+   * Returns the legacy state integer value for the provided Download object.
+   */
+  stateOfDownload(download) {
+    // Collapse state using the correct priority.
+    if (!download.stopped) {
+      return nsIDM.DOWNLOAD_DOWNLOADING;
+    }
+    if (download.succeeded) {
+      return nsIDM.DOWNLOAD_FINISHED;
+    }
+    if (download.error) {
+      if (download.error.becauseBlockedByParentalControls) {
+        return nsIDM.DOWNLOAD_BLOCKED_PARENTAL;
+      }
+      if (download.error.becauseBlockedByReputationCheck) {
+        return nsIDM.DOWNLOAD_DIRTY;
+      }
+      return nsIDM.DOWNLOAD_FAILED;
+    }
+    if (download.canceled) {
+      if (download.hasPartialData) {
+        return nsIDM.DOWNLOAD_PAUSED;
+      }
+      return nsIDM.DOWNLOAD_CANCELED;
+    }
+    return nsIDM.DOWNLOAD_NOTSTARTED;
+  },
+
+  /**
+   * Helper function required because the Downloads Panel and the Downloads View
+   * don't share the controller yet.
+   */
+  removeAndFinalizeDownload(download) {
+    Downloads.getList(Downloads.ALL)
+             .then(list => list.remove(download))
+             .then(() => download.finalize(true))
+             .catch(Cu.reportError);
+  },
+
+  /**
+   * Given an iterable collection of Download objects, generates and returns
    * statistics about that collection.
    *
-   * @param aDataItems An iterable collection of DownloadDataItems.
+   * @param downloads An iterable collection of Download objects.
    *
    * @return Object whose properties are the generated statistics. Currently,
    *         we return the following properties:
    *
    *         numActive       : The total number of downloads.
    *         numPaused       : The total number of paused downloads.
-   *         numScanning     : The total number of downloads being scanned.
    *         numDownloading  : The total number of downloads being downloaded.
    *         totalSize       : The total size of all downloads once completed.
    *         totalTransferred: The total amount of transferred data for these
    *                           downloads.
    *         slowestSpeed    : The slowest download rate.
    *         rawTimeLeft     : The estimated time left for the downloads to
    *                           complete.
    *         percentComplete : The percentage of bytes successfully downloaded.
    */
-  summarizeDownloads(aDataItems) {
+  summarizeDownloads(downloads) {
     let summary = {
       numActive: 0,
       numPaused: 0,
-      numScanning: 0,
       numDownloading: 0,
       totalSize: 0,
       totalTransferred: 0,
       // slowestSpeed is Infinity so that we can use Math.min to
       // find the slowest speed. We'll set this to 0 afterwards if
       // it's still at Infinity by the time we're done iterating all
-      // dataItems.
+      // download.
       slowestSpeed: Infinity,
       rawTimeLeft: -1,
       percentComplete: -1
     }
 
-    for (let dataItem of aDataItems) {
+    for (let download of downloads) {
       summary.numActive++;
-      switch (dataItem.state) {
-        case nsIDM.DOWNLOAD_PAUSED:
-          summary.numPaused++;
-          break;
-        case nsIDM.DOWNLOAD_SCANNING:
-          summary.numScanning++;
-          break;
-        case nsIDM.DOWNLOAD_DOWNLOADING:
-          summary.numDownloading++;
-          if (dataItem.maxBytes > 0 && dataItem.speed > 0) {
-            let sizeLeft = dataItem.maxBytes - dataItem.currBytes;
-            summary.rawTimeLeft = Math.max(summary.rawTimeLeft,
-                                           sizeLeft / dataItem.speed);
-            summary.slowestSpeed = Math.min(summary.slowestSpeed,
-                                            dataItem.speed);
-          }
-          break;
+
+      if (!download.stopped) {
+        summary.numDownloading++;
+        if (download.hasProgress && download.speed > 0) {
+          let sizeLeft = download.totalBytes - download.currentBytes;
+          summary.rawTimeLeft = Math.max(summary.rawTimeLeft,
+                                         sizeLeft / download.speed);
+          summary.slowestSpeed = Math.min(summary.slowestSpeed,
+                                          download.speed);
+        }
+      } else if (download.canceled && download.hasPartialData) {
+        summary.numPaused++;
       }
+
       // Only add to total values if we actually know the download size.
-      if (dataItem.maxBytes > 0 &&
-          dataItem.state != nsIDM.DOWNLOAD_CANCELED &&
-          dataItem.state != nsIDM.DOWNLOAD_FAILED) {
-        summary.totalSize += dataItem.maxBytes;
-        summary.totalTransferred += dataItem.currBytes;
+      if (download.succeeded) {
+        summary.totalSize += download.target.size;
+        summary.totalTransferred += download.target.size;
+      } else if (download.hasProgress) {
+        summary.totalSize += download.totalBytes;
+        summary.totalTransferred += download.currentBytes;
       }
     }
 
-    if (summary.numActive != 0 && summary.totalSize != 0 &&
-        summary.numActive != summary.numScanning) {
+    if (summary.totalSize != 0) {
       summary.percentComplete = (summary.totalTransferred /
                                  summary.totalSize) * 100;
     }
 
     if (summary.slowestSpeed == Infinity) {
       summary.slowestSpeed = 0;
     }
 
@@ -416,17 +441,17 @@ this.DownloadsCommon = {
     // In the last few seconds of downloading, we are always subtracting and
     // never adding to the time left.  Ensure that we never fall below one
     // second left until all downloads are actually finished.
     return aLastSeconds = Math.max(aSeconds, 1);
   },
 
   /**
    * Opens a downloaded file.
-   * If you've a dataItem, you should call dataItem.openLocalFile.
+   *
    * @param aFile
    *        the downloaded file to be opened.
    * @param aMimeInfo
    *        the mime type info object.  May be null.
    * @param aOwnerWindow
    *        the window with which this action is associated.
    */
   openDownloadedFile(aFile, aMimeInfo, aOwnerWindow) {
@@ -475,17 +500,16 @@ this.DownloadsCommon = {
           .getService(Ci.nsIExternalProtocolService)
           .loadUrl(NetUtil.newURI(aFile));
       }
     }).then(null, Cu.reportError);
   },
 
   /**
    * Show a downloaded file in the system file manager.
-   * If you have a dataItem, use dataItem.showLocalFile.
    *
    * @param aFile
    *        a downloaded file.
    */
   showDownloadedFile(aFile) {
     if (!(aFile instanceof Ci.nsIFile)) {
       throw new Error("aFile must be a nsIFile object");
     }
@@ -606,25 +630,22 @@ XPCOMUtils.defineLazyGetter(DownloadsCom
  *
  * Note that DownloadsData and PrivateDownloadsData are two equivalent singleton
  * objects, one accessing non-private downloads, and the other accessing private
  * ones.
  */
 function DownloadsDataCtor(aPrivate) {
   this._isPrivate = aPrivate;
 
-  // Contains all the available DownloadsDataItem objects.
-  this.dataItems = new Set();
+  // Contains all the available Download objects and their integer state.
+  this.oldDownloadStates = new Map();
 
   // Array of view objects that should be notified when the available download
   // data changes.
   this._views = [];
-
-  // Maps Download objects to DownloadDataItem objects.
-  this._downloadToDataItemMap = new Map();
 }
 
 DownloadsDataCtor.prototype = {
   /**
    * Starts receiving events for current downloads.
    */
   initializeDataLink() {
     if (!this._dataLinkInitialized) {
@@ -632,21 +653,28 @@ DownloadsDataCtor.prototype = {
                                                           : Downloads.PUBLIC);
       promiseList.then(list => list.addView(this)).then(null, Cu.reportError);
       this._dataLinkInitialized = true;
     }
   },
   _dataLinkInitialized: false,
 
   /**
+   * Iterator for all the available Download objects. This is empty until the
+   * data has been loaded using the JavaScript API for downloads.
+   */
+  get downloads() this.oldDownloadStates.keys(),
+
+  /**
    * True if there are finished downloads that can be removed from the list.
    */
   get canRemoveFinished() {
-    for (let dataItem of this.dataItems) {
-      if (!dataItem.inProgress) {
+    for (let download of this.downloads) {
+      // Stopped, paused, and failed downloads with partial data are removed.
+      if (download.stopped && !(download.canceled && download.hasPartialData)) {
         return true;
       }
     }
     return false;
   },
 
   /**
    * Asks the back-end to remove finished downloads from the list.
@@ -656,108 +684,97 @@ DownloadsDataCtor.prototype = {
                                                         : Downloads.PUBLIC);
     promiseList.then(list => list.removeFinished())
                .then(null, Cu.reportError);
   },
 
   //////////////////////////////////////////////////////////////////////////////
   //// Integration with the asynchronous Downloads back-end
 
-  onDownloadAdded(aDownload) {
-    let dataItem = new DownloadsDataItem(aDownload);
-    this._downloadToDataItemMap.set(aDownload, dataItem);
-    this.dataItems.add(dataItem);
+  onDownloadAdded(download) {
+    // Download objects do not store the end time of downloads, as the Downloads
+    // API does not need to persist this information for all platforms. Once a
+    // download terminates on a Desktop browser, it becomes a history download,
+    // for which the end time is stored differently, as a Places annotation.
+    download.endTime = Date.now();
+
+    this.oldDownloadStates.set(download,
+                               DownloadsCommon.stateOfDownload(download));
 
     for (let view of this._views) {
-      view.onDataItemAdded(dataItem, true);
-    }
-
-    this._updateDataItemState(dataItem);
-  },
-
-  onDownloadChanged(aDownload) {
-    let dataItem = this._downloadToDataItemMap.get(aDownload);
-    if (!dataItem) {
-      Cu.reportError("Download doesn't exist.");
-      return;
-    }
-
-    this._updateDataItemState(dataItem);
-  },
-
-  onDownloadRemoved(aDownload) {
-    let dataItem = this._downloadToDataItemMap.get(aDownload);
-    if (!dataItem) {
-      Cu.reportError("Download doesn't exist.");
-      return;
-    }
-
-    this._downloadToDataItemMap.delete(aDownload);
-    this.dataItems.delete(dataItem);
-    for (let view of this._views) {
-      view.onDataItemRemoved(dataItem);
+      view.onDownloadAdded(download, true);
     }
   },
 
-  /**
-   * Updates the given data item and sends related notifications.
-   */
-  _updateDataItemState(aDataItem) {
-    let oldState = aDataItem.state;
-    let wasInProgress = aDataItem.inProgress;
-    let wasDone = aDataItem.done;
+  onDownloadChanged(download) {
+    let oldState = this.oldDownloadStates.get(download);
+    let newState = DownloadsCommon.stateOfDownload(download);
+    this.oldDownloadStates.set(download, newState);
+
+    if (oldState != newState) {
+      if (download.succeeded ||
+          (download.canceled && !download.hasPartialData) ||
+          download.error) {
+        // Store the end time that may be displayed by the views.
+        download.endTime = Date.now();
 
-    aDataItem.updateFromDownload();
+        // This state transition code should actually be located in a Downloads
+        // API module (bug 941009).  Moreover, the fact that state is stored as
+        // annotations should be ideally hidden behind methods of
+        // nsIDownloadHistory (bug 830415).
+        if (!this._isPrivate) {
+          try {
+            let downloadMetaData = {
+              state: DownloadsCommon.stateOfDownload(download),
+              endTime: download.endTime,
+            };
+            if (download.succeeded) {
+              downloadMetaData.fileSize = download.target.size;
+            }
+  
+            PlacesUtils.annotations.setPageAnnotation(
+                          NetUtil.newURI(download.source.url),
+                          "downloads/metaData",
+                          JSON.stringify(downloadMetaData), 0,
+                          PlacesUtils.annotations.EXPIRE_WITH_HISTORY);
+          } catch (ex) {
+            Cu.reportError(ex);
+          }
+        }
+      }
 
-    if (wasInProgress && !aDataItem.inProgress) {
-      aDataItem.endTime = Date.now();
-    }
-
-    if (oldState != aDataItem.state) {
       for (let view of this._views) {
         try {
-          view.onDataItemStateChanged(aDataItem, oldState);
+          view.onDownloadStateChanged(download);
         } catch (ex) {
           Cu.reportError(ex);
         }
       }
 
-      // This state transition code should actually be located in a Downloads
-      // API module (bug 941009).  Moreover, the fact that state is stored as
-      // annotations should be ideally hidden behind methods of
-      // nsIDownloadHistory (bug 830415).
-      if (!this._isPrivate && !aDataItem.inProgress) {
-        try {
-          let downloadMetaData = { state: aDataItem.state,
-                                   endTime: aDataItem.endTime };
-          if (aDataItem.done) {
-            downloadMetaData.fileSize = aDataItem.maxBytes;
-          }
-
-          PlacesUtils.annotations.setPageAnnotation(
-                        NetUtil.newURI(aDataItem.uri), "downloads/metaData",
-                        JSON.stringify(downloadMetaData), 0,
-                        PlacesUtils.annotations.EXPIRE_WITH_HISTORY);
-        } catch (ex) {
-          Cu.reportError(ex);
-        }
+      if (download.succeeded ||
+          (download.error && download.error.becauseBlocked)) {
+        this._notifyDownloadEvent("finish");
       }
     }
 
-    if (!aDataItem.newDownloadNotified) {
-      aDataItem.newDownloadNotified = true;
+    if (!download.newDownloadNotified) {
+      download.newDownloadNotified = true;
       this._notifyDownloadEvent("start");
     }
 
-    if (!wasDone && aDataItem.done) {
-      this._notifyDownloadEvent("finish");
+    for (let view of this._views) {
+      view.onDownloadChanged(download);
     }
+  },
+
+  onDownloadRemoved(download) {
+    this.oldDownloadStates.delete(download);
 
     for (let view of this._views) {
-      view.onDataItemChanged(aDataItem);
+      view.onDownloadRemoved(download);
     }
   },
 
   //////////////////////////////////////////////////////////////////////////////
   //// Registration of views
 
   /**
    * Adds an object to be notified when the available download data changes.
@@ -792,19 +809,19 @@ DownloadsDataCtor.prototype = {
    *        DownloadsView object to be initialized.
    */
   _updateView(aView) {
     // Indicate to the view that a batch loading operation is in progress.
     aView.onDataLoadStarting();
 
     // Sort backwards by start time, ensuring that the most recent
     // downloads are added first regardless of their state.
-    let loadedItemsArray = [...this.dataItems];
-    loadedItemsArray.sort((a, b) => b.startTime - a.startTime);
-    loadedItemsArray.forEach(dataItem => aView.onDataItemAdded(dataItem, false));
+    let downloadsArray = [...this.downloads];
+    downloadsArray.sort((a, b) => b.startTime - a.startTime);
+    downloadsArray.forEach(download => aView.onDownloadAdded(download, false));
 
     // Notify the view that all data is available.
     aView.onDataLoadCompleted();
   },
 
   //////////////////////////////////////////////////////////////////////////////
   //// Notifications sent to the most recent browser window only
 
@@ -857,252 +874,16 @@ XPCOMUtils.defineLazyGetter(this, "Priva
   return new DownloadsDataCtor(true);
 });
 
 XPCOMUtils.defineLazyGetter(this, "DownloadsData", function() {
   return new DownloadsDataCtor(false);
 });
 
 ////////////////////////////////////////////////////////////////////////////////
-//// DownloadsDataItem
-
-/**
- * Represents a single item in the list of downloads.
- *
- * The endTime property is initialized to the current date and time.
- *
- * @param aDownload
- *        The Download object with the current state.
- */
-function DownloadsDataItem(aDownload) {
-  this._download = aDownload;
-
-  this.file = aDownload.target.path;
-  this.target = OS.Path.basename(aDownload.target.path);
-  this.uri = aDownload.source.url;
-  this.endTime = Date.now();
-
-  this.updateFromDownload();
-}
-
-DownloadsDataItem.prototype = {
-  /**
-   * Updates this object from the underlying Download object.
-   */
-  updateFromDownload() {
-    // Collapse state using the correct priority.
-    if (this._download.succeeded) {
-      this.state = nsIDM.DOWNLOAD_FINISHED;
-    } else if (this._download.error &&
-               this._download.error.becauseBlockedByParentalControls) {
-      this.state = nsIDM.DOWNLOAD_BLOCKED_PARENTAL;
-    } else if (this._download.error &&
-               this._download.error.becauseBlockedByReputationCheck) {
-      this.state = nsIDM.DOWNLOAD_DIRTY;
-    } else if (this._download.error) {
-      this.state = nsIDM.DOWNLOAD_FAILED;
-    } else if (this._download.canceled && this._download.hasPartialData) {
-      this.state = nsIDM.DOWNLOAD_PAUSED;
-    } else if (this._download.canceled) {
-      this.state = nsIDM.DOWNLOAD_CANCELED;
-    } else if (this._download.stopped) {
-      this.state = nsIDM.DOWNLOAD_NOTSTARTED;
-    } else {
-      this.state = nsIDM.DOWNLOAD_DOWNLOADING;
-    }
-
-    this.referrer = this._download.source.referrer;
-    this.startTime = this._download.startTime;
-    this.currBytes = this._download.currentBytes;
-    this.resumable = this._download.hasPartialData;
-    this.speed = this._download.speed;
-
-    if (this._download.succeeded) {
-      // If the download succeeded, show the final size if available, otherwise
-      // use the last known number of bytes transferred.  The final size on disk
-      // will be available when bug 941063 is resolved.
-      this.maxBytes = this._download.hasProgress ?
-                             this._download.totalBytes :
-                             this._download.currentBytes;
-      this.percentComplete = 100;
-    } else if (this._download.hasProgress) {
-      // If the final size and progress are known, use them.
-      this.maxBytes = this._download.totalBytes;
-      this.percentComplete = this._download.progress;
-    } else {
-      // The download final size and progress percentage is unknown.
-      this.maxBytes = -1;
-      this.percentComplete = -1;
-    }
-  },
-
-  /**
-   * Indicates whether the download is proceeding normally, and not finished
-   * yet.  This includes paused downloads.  When this property is true, the
-   * "progress" property represents the current progress of the download.
-   */
-  get inProgress() {
-    return [
-      nsIDM.DOWNLOAD_NOTSTARTED,
-      nsIDM.DOWNLOAD_QUEUED,
-      nsIDM.DOWNLOAD_DOWNLOADING,
-      nsIDM.DOWNLOAD_PAUSED,
-      nsIDM.DOWNLOAD_SCANNING,
-    ].indexOf(this.state) != -1;
-  },
-
-  /**
-   * This is true during the initial phases of a download, before the actual
-   * download of data bytes starts.
-   */
-  get starting() {
-    return this.state == nsIDM.DOWNLOAD_NOTSTARTED ||
-           this.state == nsIDM.DOWNLOAD_QUEUED;
-  },
-
-  /**
-   * Indicates whether the download is paused.
-   */
-  get paused() {
-    return this.state == nsIDM.DOWNLOAD_PAUSED;
-  },
-
-  /**
-   * Indicates whether the download is in a final state, either because it
-   * completed successfully or because it was blocked.
-   */
-  get done() {
-    return [
-      nsIDM.DOWNLOAD_FINISHED,
-      nsIDM.DOWNLOAD_BLOCKED_PARENTAL,
-      nsIDM.DOWNLOAD_BLOCKED_POLICY,
-      nsIDM.DOWNLOAD_DIRTY,
-    ].indexOf(this.state) != -1;
-  },
-
-  /**
-   * Indicates whether the download is finished and can be opened.
-   */
-  get openable() {
-    return this.state == nsIDM.DOWNLOAD_FINISHED;
-  },
-
-  /**
-   * Indicates whether the download stopped because of an error, and can be
-   * resumed manually.
-   */
-  get canRetry() {
-    return this.state == nsIDM.DOWNLOAD_CANCELED ||
-           this.state == nsIDM.DOWNLOAD_FAILED;
-  },
-
-  /**
-   * Returns the nsILocalFile for the download target.
-   *
-   * @throws if the native path is not valid.  This can happen if the same
-   *         profile is used on different platforms, for example if a native
-   *         Windows path is stored and then the item is accessed on a Mac.
-   */
-  get localFile() {
-    return this._getFile(this.file);
-  },
-
-  /**
-   * Returns the nsILocalFile for the partially downloaded target.
-   *
-   * @throws if the native path is not valid.  This can happen if the same
-   *         profile is used on different platforms, for example if a native
-   *         Windows path is stored and then the item is accessed on a Mac.
-   */
-  get partFile() {
-    return this._getFile(this.file + kPartialDownloadSuffix);
-  },
-
-  /**
-   * Returns an nsILocalFile for aFilename. aFilename might be a file URL or
-   * a native path.
-   *
-   * @param aFilename the filename of the file to retrieve.
-   * @return an nsILocalFile for the file.
-   * @throws if the native path is not valid.  This can happen if the same
-   *         profile is used on different platforms, for example if a native
-   *         Windows path is stored and then the item is accessed on a Mac.
-   * @note This function makes no guarantees about the file's existence -
-   *       callers should check that the returned file exists.
-   */
-  _getFile(aFilename) {
-    // The download database may contain targets stored as file URLs or native
-    // paths.  This can still be true for previously stored items, even if new
-    // items are stored using their file URL.  See also bug 239948 comment 12.
-    if (aFilename.startsWith("file:")) {
-      // Assume the file URL we obtained from the downloads database or from the
-      // "spec" property of the target has the UTF-8 charset.
-      let fileUrl = NetUtil.newURI(aFilename).QueryInterface(Ci.nsIFileURL);
-      return fileUrl.file.clone().QueryInterface(Ci.nsILocalFile);
-    } else {
-      // The downloads database contains a native path.  Try to create a local
-      // file, though this may throw an exception if the path is invalid.
-      return new DownloadsLocalFileCtor(aFilename);
-    }
-  },
-
-  /**
-   * Open the target file for this download.
-   */
-  openLocalFile() {
-    this._download.launch().then(null, Cu.reportError);
-  },
-
-  /**
-   * Show the downloaded file in the system file manager.
-   */
-  showLocalFile() {
-    DownloadsCommon.showDownloadedFile(this.localFile);
-  },
-
-  /**
-   * Resumes the download if paused, pauses it if active.
-   * @throws if the download is not resumable or if has already done.
-   */
-  togglePauseResume() {
-    if (this._download.stopped) {
-      this._download.start();
-    } else {
-      this._download.cancel();
-    }
-  },
-
-  /**
-   * Attempts to retry the download.
-   * @throws if we cannot.
-   */
-  retry() {
-    this._download.start();
-  },
-
-  /**
-   * Cancels the download.
-   */
-  cancel() {
-    this._download.cancel();
-    this._download.removePartialData().then(null, Cu.reportError);
-  },
-
-  /**
-   * Remove the download.
-   */
-  remove() {
-    Downloads.getList(Downloads.ALL)
-             .then(list => list.remove(this._download))
-             .then(() => this._download.finalize(true))
-             .then(null, Cu.reportError);
-  },
-};
-
-////////////////////////////////////////////////////////////////////////////////
 //// DownloadsViewPrototype
 
 /**
  * A prototype for an object that registers itself with DownloadsData as soon
  * as a view is registered with it.
  */
 const DownloadsViewPrototype = {
   //////////////////////////////////////////////////////////////////////////////
@@ -1202,65 +983,67 @@ const DownloadsViewPrototype = {
   onDataLoadCompleted() {
     this._loading = false;
   },
 
   /**
    * Called when a new download data item is available, either during the
    * asynchronous data load or when a new download is started.
    *
-   * @param aDataItem
-   *        DownloadsDataItem object that was just added.
-   * @param aNewest
+   * @param download
+   *        Download object that was just added.
+   * @param newest
    *        When true, indicates that this item is the most recent and should be
    *        added in the topmost position.  This happens when a new download is
    *        started.  When false, indicates that the item is the least recent
    *        with regard to the items that have been already added. The latter
    *        generally happens during the asynchronous data load.
    *
    * @note Subclasses should override this.
    */
-  onDataItemAdded(aDataItem, aNewest) {
+  onDownloadAdded(download, newest) {
+    throw Components.results.NS_ERROR_NOT_IMPLEMENTED;
+  },
+
+  /**
+   * Called when the overall state of a Download has changed. In particular,
+   * this is called only once when the download succeeds or is blocked
+   * permanently, and is never called if only the current progress changed.
+   *
+   * The onDownloadChanged notification will always be sent afterwards.
+   *
+   * @note Subclasses should override this.
+   */
+  onDownloadStateChanged(download) {
+    throw Components.results.NS_ERROR_NOT_IMPLEMENTED;
+  },
+
+  /**
+   * Called every time any state property of a Download may have changed,
+   * including progress properties.
+   *
+   * Note that progress notification changes are throttled at the Downloads.jsm
+   * API level, and there is no throttling mechanism in the front-end.
+   *
+   * @note Subclasses should override this.
+   */
+  onDownloadChanged(download) {
     throw Components.results.NS_ERROR_NOT_IMPLEMENTED;
   },
 
   /**
    * Called when a data item is removed, ensures that the widget associated with
    * the view item is removed from the user interface.
    *
-   * @param aDataItem
-   *        DownloadsDataItem object that is being removed.
+   * @param download
+   *        Download object that is being removed.
    *
    * @note Subclasses should override this.
    */
-  onDataItemRemoved(aDataItem) {
-    throw Components.results.NS_ERROR_NOT_IMPLEMENTED;
-  },
-
-  /**
-   * Called when the "state" property of a DownloadsDataItem has changed.
-   *
-   * The onDataItemChanged notification will be sent afterwards.
-   *
-   * @note Subclasses should override this.
-   */
-  onDataItemStateChanged(aDataItem) {
-    throw Components.results.NS_ERROR_NOT_IMPLEMENTED;
-  },
-
-  /**
-   * Called every time any state property of a DownloadsDataItem may have
-   * changed, including progress properties and the "state" property.
-   *
-   * Note that progress notification changes are throttled at the Downloads.jsm
-   * API level, and there is no throttling mechanism in the front-end.
-   *
-   * @note Subclasses should override this.
-   */
-  onDataItemChanged(aDataItem) {
+  onDownloadRemoved(download) {
     throw Components.results.NS_ERROR_NOT_IMPLEMENTED;
   },
 
   /**
    * Private function used to refresh the internal properties being sent to
    * each registered view.
    *
    * @note Subclasses should override this.
@@ -1311,68 +1094,42 @@ DownloadsIndicatorDataCtor.prototype = {
     if (this._views.length == 0) {
       this._itemCount = 0;
     }
   },
 
   //////////////////////////////////////////////////////////////////////////////
   //// Callback functions from DownloadsData
 
-  /**
-   * Called after data loading finished.
-   */
   onDataLoadCompleted() {
     DownloadsViewPrototype.onDataLoadCompleted.call(this);
     this._updateViews();
   },
 
-  /**
-   * Called when a new download data item is available, either during the
-   * asynchronous data load or when a new download is started.
-   *
-   * @param aDataItem
-   *        DownloadsDataItem object that was just added.
-   * @param aNewest
-   *        When true, indicates that this item is the most recent and should be
-   *        added in the topmost position.  This happens when a new download is
-   *        started.  When false, indicates that the item is the least recent
-   *        with regard to the items that have been already added. The latter
-   *        generally happens during the asynchronous data load.
-   */
-  onDataItemAdded(aDataItem, aNewest) {
+  onDownloadAdded(download, newest) {
     this._itemCount++;
     this._updateViews();
   },
 
-  /**
-   * Called when a data item is removed, ensures that the widget associated with
-   * the view item is removed from the user interface.
-   *
-   * @param aDataItem
-   *        DownloadsDataItem object that is being removed.
-   */
-  onDataItemRemoved(aDataItem) {
-    this._itemCount--;
-    this._updateViews();
-  },
-
-  // DownloadsView
-  onDataItemStateChanged(aDataItem, aOldState) {
-    if (aDataItem.state == nsIDM.DOWNLOAD_FINISHED ||
-        aDataItem.state == nsIDM.DOWNLOAD_FAILED) {
+  onDownloadStateChanged(download) {
+    if (download.succeeded || download.error) {
       this.attention = true;
     }
 
     // Since the state of a download changed, reset the estimated time left.
     this._lastRawTimeLeft = -1;
     this._lastTimeLeft = -1;
   },
 
-  // DownloadsView
-  onDataItemChanged() {
+  onDownloadChanged(download) {
+    this._updateViews();
+  },
+
+  onDownloadRemoved(download) {
+    this._itemCount--;
     this._updateViews();
   },
 
   //////////////////////////////////////////////////////////////////////////////
   //// Propagation of properties to our views
 
   // The following properties are updated by _refreshProperties and are then
   // propagated to the views.  See _refreshProperties for details.
@@ -1450,37 +1207,37 @@ DownloadsIndicatorDataCtor.prototype = {
    * Last number of seconds estimated until all in-progress downloads with a
    * known size and speed will finish.  This value is stored to allow smoothing
    * in case of small variations.  This is set to -1 if the previous value is
    * unknown.
    */
   _lastTimeLeft: -1,
 
   /**
-   * A generator function for the dataItems that this summary is currently
+   * A generator function for the Download objects this summary is currently
    * interested in. This generator is passed off to summarizeDownloads in order
-   * to generate statistics about the dataItems we care about - in this case,
-   * it's all dataItems for active downloads.
+   * to generate statistics about the downloads we care about - in this case,
+   * it's all active downloads.
    */
-  _activeDataItems() {
-    let dataItems = this._isPrivate ? PrivateDownloadsData.dataItems
-                                    : DownloadsData.dataItems;
-    for (let dataItem of dataItems) {
-      if (dataItem && dataItem.inProgress) {
-        yield dataItem;
+  * _activeDownloads() {
+    let downloads = this._isPrivate ? PrivateDownloadsData.downloads
+                                    : DownloadsData.downloads;
+    for (let download of downloads) {
+      if (!download.stopped || (download.canceled && download.hasPartialData)) {
+        yield download;
       }
     }
   },
 
   /**
    * Computes aggregate values based on the current state of downloads.
    */
   _refreshProperties() {
     let summary =
-      DownloadsCommon.summarizeDownloads(this._activeDataItems());
+      DownloadsCommon.summarizeDownloads(this._activeDownloads());
 
     // Determine if the indicator should be shown or get attention.
     this._hasDownloads = (this._itemCount > 0);
 
     // If all downloads are paused, show the progress indicator as paused.
     this._paused = summary.numActive > 0 &&
                    summary.numActive == summary.numPaused;
 
@@ -1531,17 +1288,17 @@ XPCOMUtils.defineLazyGetter(this, "Downl
  */
 function DownloadsSummaryData(aIsPrivate, aNumToExclude) {
   this._numToExclude = aNumToExclude;
   // Since we can have multiple instances of DownloadsSummaryData, we
   // override these values from the prototype so that each instance can be
   // completely separated from one another.
   this._loading = false;
 
-  this._dataItems = [];
+  this._downloads = [];
 
   // Floating point value indicating the last number of seconds estimated until
   // the longest download will finish.  We need to store this value so that we
   // don't continuously apply smoothing if the actual download state has not
   // changed.  This is set to -1 if the previous value is unknown.
   this._lastRawTimeLeft = -1;
 
   // Last number of seconds estimated until all in-progress downloads with a
@@ -1570,57 +1327,55 @@ DownloadsSummaryData.prototype = {
    *
    * @param aView
    *        DownloadsSummary view to be removed.
    */
   removeView(aView) {
     DownloadsViewPrototype.removeView.call(this, aView);
 
     if (this._views.length == 0) {
-      // Clear out our collection of DownloadDataItems. If we ever have
+      // Clear out our collection of Download objects. If we ever have
       // another view registered with us, this will get re-populated.
-      this._dataItems = [];
+      this._downloads = [];
     }
   },
 
   //////////////////////////////////////////////////////////////////////////////
   //// Callback functions from DownloadsData - see the documentation in
   //// DownloadsViewPrototype for more information on what these functions
   //// are used for.
 
   onDataLoadCompleted() {
     DownloadsViewPrototype.onDataLoadCompleted.call(this);
     this._updateViews();
   },
 
-  onDataItemAdded(aDataItem, aNewest) {
-    if (aNewest) {
-      this._dataItems.unshift(aDataItem);
+  onDownloadAdded(download, newest) {
+    if (newest) {
+      this._downloads.unshift(download);
     } else {
-      this._dataItems.push(aDataItem);
+      this._downloads.push(download);
     }
 
     this._updateViews();
   },
 
-  onDataItemRemoved(aDataItem) {
-    let itemIndex = this._dataItems.indexOf(aDataItem);
-    this._dataItems.splice(itemIndex, 1);
-    this._updateViews();
-  },
-
-  // DownloadsView
-  onDataItemStateChanged(aOldState) {
+  onDownloadStateChanged() {
     // Since the state of a download changed, reset the estimated time left.
     this._lastRawTimeLeft = -1;
     this._lastTimeLeft = -1;
   },
 
-  // DownloadsView
-  onDataItemChanged() {
+  onDownloadChanged() {
+    this._updateViews();
+  },
+
+  onDownloadRemoved(download) {
+    let itemIndex = this._downloads.indexOf(download);
+    this._downloads.splice(itemIndex, 1);
     this._updateViews();
   },
 
   //////////////////////////////////////////////////////////////////////////////
   //// Propagation of properties to our views
 
   /**
    * Computes aggregate values and propagates the changes to our views.
@@ -1647,37 +1402,37 @@ DownloadsSummaryData.prototype = {
     aView.description = this._description;
     aView.details = this._details;
   },
 
   //////////////////////////////////////////////////////////////////////////////
   //// Property updating based on current download status
 
   /**
-   * A generator function for the dataItems that this summary is currently
+   * A generator function for the Download objects this summary is currently
    * interested in. This generator is passed off to summarizeDownloads in order
-   * to generate statistics about the dataItems we care about - in this case,
-   * it's the dataItems in this._dataItems after the first few to exclude,
+   * to generate statistics about the downloads we care about - in this case,
+   * it's the downloads in this._downloads after the first few to exclude,
    * which was set when constructing this DownloadsSummaryData instance.
    */
-  _dataItemsForSummary() {
-    if (this._dataItems.length > 0) {
-      for (let i = this._numToExclude; i < this._dataItems.length; ++i) {
-        yield this._dataItems[i];
+  * _downloadsForSummary() {
+    if (this._downloads.length > 0) {
+      for (let i = this._numToExclude; i < this._downloads.length; ++i) {
+        yield this._downloads[i];
       }
     }
   },
 
   /**
    * Computes aggregate values based on the current state of downloads.
    */
   _refreshProperties() {
     // Pre-load summary with default values.
     let summary =
-      DownloadsCommon.summarizeDownloads(this._dataItemsForSummary());
+      DownloadsCommon.summarizeDownloads(this._downloadsForSummary());
 
     this._description = DownloadsCommon.strings
                                        .otherDownloads2(summary.numActive);
     this._percentComplete = summary.percentComplete;
 
     // If all downloads are paused, show the progress indicator as paused.
     this._showingProgress = summary.numDownloading > 0 ||
                             summary.numPaused > 0;
new file mode 100755
--- /dev/null
+++ b/browser/components/downloads/DownloadsViewUI.jsm
@@ -0,0 +1,232 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+/*
+ * This module is imported by code that uses the "download.xml" binding, and
+ * provides prototypes for objects that handle input and display information.
+ */
+
+"use strict";
+
+this.EXPORTED_SYMBOLS = [
+  "DownloadsViewUI",
+];
+
+const { classes: Cc, interfaces: Ci, utils: Cu, results: Cr } = Components;
+
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+
+XPCOMUtils.defineLazyModuleGetter(this, "DownloadUtils",
+                                  "resource://gre/modules/DownloadUtils.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "DownloadsCommon",
+                                  "resource:///modules/DownloadsCommon.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "OS",
+                                  "resource://gre/modules/osfile.jsm");
+
+this.DownloadsViewUI = {};
+
+/**
+ * A download element shell is responsible for handling the commands and the
+ * displayed data for a single element that uses the "download.xml" binding.
+ *
+ * The information to display is obtained through the associated Download object
+ * from the JavaScript API for downloads, and commands are executed using a
+ * combination of Download methods and DownloadsCommon.jsm helper functions.
+ *
+ * Specialized versions of this shell must be defined, and they are required to
+ * implement the "download" property or getter. Currently these objects are the
+ * HistoryDownloadElementShell and the DownloadsViewItem for the panel. The
+ * history view may use a HistoryDownload object in place of a Download object.
+ */
+this.DownloadsViewUI.DownloadElementShell = function () {}
+
+this.DownloadsViewUI.DownloadElementShell.prototype = {
+  /**
+   * The richlistitem for the download, initialized by the derived object.
+   */
+  element: null,
+
+  /**
+   * URI string for the file type icon displayed in the download element.
+   */
+  get image() {
+    if (!this.download.target.path) {
+      // Old history downloads may not have a target path.
+      return "moz-icon://.unknown?size=32";
+    }
+
+    // When a download that was previously in progress finishes successfully, it
+    // means that the target file now exists and we can extract its specific
+    // icon, for example from a Windows executable. To ensure that the icon is
+    // reloaded, however, we must change the URI used by the XUL image element,
+    // for example by adding a query parameter. This only works if we add one of
+    // the parameters explicitly supported by the nsIMozIconURI interface.
+    return "moz-icon://" + this.download.target.path + "?size=32" +
+           (this.download.succeeded ? "&state=normal" : "");
+  },
+
+  /**
+   * The user-facing label for the download. This is normally the leaf name of
+   * the download target file. In case this is a very old history download for
+   * which the target file is unknown, the download source URI is displayed.
+   */
+  get displayName() {
+    if (!this.download.target.path) {
+      return this.download.source.url;
+    }
+    return OS.Path.basename(this.download.target.path);
+  },
+
+  /**
+   * The progress element for the download, or undefined in case the XBL binding
+   * has not been applied yet.
+   */
+  get _progressElement() {
+    if (!this.__progressElement) {
+      // If the element is not available now, we will try again the next time.
+      this.__progressElement =
+           this.element.ownerDocument.getAnonymousElementByAttribute(
+                                         this.element, "anonid",
+                                         "progressmeter");
+    }
+    return this.__progressElement;
+  },
+
+  /**
+   * Processes a major state change in the user interface, then proceeds with
+   * the normal progress update. This function is not called for every progress
+   * update in order to improve performance.
+   */
+  _updateState() {
+    this.element.setAttribute("displayName", this.displayName);
+    this.element.setAttribute("image", this.image);
+    this.element.setAttribute("state",
+                              DownloadsCommon.stateOfDownload(this.download));
+
+    // Since state changed, reset the time left estimation.
+    this.lastEstimatedSecondsLeft = Infinity;
+
+    this._updateProgress();
+  },
+
+  /**
+   * Updates the elements that change regularly for in-progress downloads,
+   * namely the progress bar and the status line.
+   */
+  _updateProgress() {
+    if (this.download.succeeded) {
+      // We only need to add or remove this attribute for succeeded downloads.
+      if (this.download.target.exists) {
+        this.element.setAttribute("exists", "true");
+      } else {
+        this.element.removeAttribute("exists");
+      }
+    }
+
+    // The progress bar is only displayed for in-progress downloads.
+    if (this.download.hasProgress) {
+      this.element.setAttribute("progressmode", "normal");
+      this.element.setAttribute("progress", this.download.progress);
+    } else {
+      this.element.setAttribute("progressmode", "undetermined");
+    }
+
+    // Dispatch the ValueChange event for accessibility, if possible.
+    if (this._progressElement) {
+      let event = this.element.ownerDocument.createEvent("Events");
+      event.initEvent("ValueChange", true, true);
+      this._progressElement.dispatchEvent(event);
+    }
+
+    let status = this.statusTextAndTip;
+    this.element.setAttribute("status", status.text);
+    this.element.setAttribute("statusTip", status.tip);
+  },
+
+  lastEstimatedSecondsLeft: Infinity,
+
+  /**
+   * Returns the text for the status line and the associated tooltip. These are
+   * returned by a single property because they are computed together. The
+   * result may be overridden by derived objects.
+   */
+  get statusTextAndTip() this.rawStatusTextAndTip,
+
+  /**
+   * Derived objects may call this to get the status text.
+   */
+  get rawStatusTextAndTip() {
+    const nsIDM = Ci.nsIDownloadManager;
+    let s = DownloadsCommon.strings;
+
+    let text = "";
+    let tip = "";
+
+    if (!this.download.stopped) {
+      let totalBytes = this.download.hasProgress ? this.download.totalBytes
+                                                 : -1;
+      // By default, extended status information including the individual
+      // download rate is displayed in the tooltip. The history view overrides
+      // the getter and displays the datails in the main area instead.
+      [text] = DownloadUtils.getDownloadStatusNoRate(
+                                          this.download.currentBytes,
+                                          totalBytes,
+                                          this.download.speed,
+                                          this.lastEstimatedSecondsLeft);
+      let newEstimatedSecondsLeft;
+      [tip, newEstimatedSecondsLeft] = DownloadUtils.getDownloadStatus(
+                                          this.download.currentBytes,
+                                          totalBytes,
+                                          this.download.speed,
+                                          this.lastEstimatedSecondsLeft);
+      this.lastEstimatedSecondsLeft = newEstimatedSecondsLeft;
+    } else if (this.download.canceled && this.download.hasPartialData) {
+      let totalBytes = this.download.hasProgress ? this.download.totalBytes
+                                                 : -1;
+      let transfer = DownloadUtils.getTransferTotal(this.download.currentBytes,
+                                                    totalBytes);
+
+      // We use the same XUL label to display both the state and the amount
+      // transferred, for example "Paused -  1.1 MB".
+      text = s.statusSeparatorBeforeNumber(s.statePaused, transfer);
+    } else if (!this.download.succeeded && !this.download.canceled &&
+               !this.download.error) {
+      text = s.stateStarting;
+    } else {
+      let stateLabel;
+
+      if (this.download.succeeded) {
+        // For completed downloads, show the file size (e.g. "1.5 MB").
+        if (this.download.target.size !== undefined) {
+          let [size, unit] =
+            DownloadUtils.convertByteUnits(this.download.target.size);
+          stateLabel = s.sizeWithUnits(size, unit);
+        } else {
+          // History downloads may not have a size defined.
+          stateLabel = s.sizeUnknown;
+        }
+      } else if (this.download.canceled) {
+        stateLabel = s.stateCanceled;
+      } else if (this.download.error.becauseBlockedByParentalControls) {
+        stateLabel = s.stateBlockedParentalControls;
+      } else if (this.download.error.becauseBlockedByReputationCheck) {
+        stateLabel = s.stateDirty;
+      } else {
+        stateLabel = s.stateFailed;
+      }
+
+      let referrer = this.download.source.referrer || this.download.source.url;
+      let [displayHost, fullHost] = DownloadUtils.getURIHost(referrer);
+
+      let date = new Date(this.download.endTime);
+      let [displayDate, fullDate] = DownloadUtils.getReadableDates(date);
+
+      let firstPart = s.statusSeparator(stateLabel, displayHost);
+      text = s.statusSeparator(firstPart, displayDate);
+      tip = s.statusSeparator(fullHost, fullDate);
+    }
+
+    return { text, tip: tip || text };
+  },
+};
--- a/browser/components/downloads/content/allDownloadsViewOverlay.js
+++ b/browser/components/downloads/content/allDownloadsViewOverlay.js
@@ -1,668 +1,407 @@
 /* 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/. */
 
-/**
- * THE PLACES VIEW IMPLEMENTED IN THIS FILE HAS A VERY PARTICULAR USE CASE.
- * IT IS HIGHLY RECOMMENDED NOT TO EXTEND IT FOR ANY OTHER USE CASES OR RELY
- * ON IT AS AN API.
- */
-
-let Cu = Components.utils;
-let Ci = Components.interfaces;
-let Cc = Components.classes;
+let { classes: Cc, interfaces: Ci, utils: Cu, results: Cr } = Components;
 
-Cu.import("resource://gre/modules/XPCOMUtils.jsm");
-Cu.import("resource://gre/modules/Services.jsm");
-Cu.import("resource://gre/modules/NetUtil.jsm");
-Cu.import("resource://gre/modules/DownloadUtils.jsm");
-Cu.import("resource:///modules/DownloadsCommon.jsm");
-Cu.import("resource://gre/modules/PlacesUtils.jsm");
-Cu.import("resource://gre/modules/osfile.jsm");
-
-XPCOMUtils.defineLazyModuleGetter(this, "PrivateBrowsingUtils",
-                                  "resource://gre/modules/PrivateBrowsingUtils.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "DownloadUtils",
+                                  "resource://gre/modules/DownloadUtils.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "DownloadsCommon",
+                                  "resource:///modules/DownloadsCommon.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "DownloadsViewUI",
+                                  "resource:///modules/DownloadsViewUI.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "FileUtils",
+                                  "resource://gre/modules/FileUtils.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "NetUtil",
+                                  "resource://gre/modules/NetUtil.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "OS",
+                                  "resource://gre/modules/osfile.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "PlacesUtils",
+                                  "resource://gre/modules/PlacesUtils.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "Promise",
+                                  "resource://gre/modules/Promise.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "RecentWindow",
                                   "resource:///modules/RecentWindow.jsm");
-XPCOMUtils.defineLazyModuleGetter(this, "FileUtils",
-                                  "resource://gre/modules/FileUtils.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "Services",
+                                  "resource://gre/modules/Services.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "Task",
+                                  "resource://gre/modules/Task.jsm");
 
 const nsIDM = Ci.nsIDownloadManager;
 
 const DESTINATION_FILE_URI_ANNO  = "downloads/destinationFileURI";
 const DOWNLOAD_META_DATA_ANNO    = "downloads/metaData";
 
 const DOWNLOAD_VIEW_SUPPORTED_COMMANDS =
  ["cmd_delete", "cmd_copy", "cmd_paste", "cmd_selectAll",
   "downloadsCmd_pauseResume", "downloadsCmd_cancel",
   "downloadsCmd_open", "downloadsCmd_show", "downloadsCmd_retry",
   "downloadsCmd_openReferrer", "downloadsCmd_clearDownloads"];
 
-const NOT_AVAILABLE = Number.MAX_VALUE;
+/**
+ * Represents a download from the browser history. It implements part of the
+ * interface of the Download object.
+ *
+ * @param aPlacesNode
+ *        The Places node from which the history download should be initialized.
+ */
+function HistoryDownload(aPlacesNode) {
+  // TODO (bug 829201): history downloads should get the referrer from Places.
+  this.source = {
+    url: aPlacesNode.uri,
+  };
+  this.target = {
+    path: undefined,
+    exists: false,
+    size: undefined,
+  };
+
+  // In case this download cannot obtain its end time from the Places metadata,
+  // use the time from the Places node, that is the start time of the download.
+  this.endTime = aPlacesNode.time / 1000;
+}
+
+HistoryDownload.prototype = {
+  /**
+   * Pushes information from Places metadata into this object.
+   */
+  updateFromMetaData(metaData) {
+    try {
+      this.target.path = Cc["@mozilla.org/network/protocol;1?name=file"]
+                           .getService(Ci.nsIFileProtocolHandler)
+                           .getFileFromURLSpec(metaData.targetFileSpec).path;
+    } catch (ex) {
+      this.target.path = undefined;
+    }
+
+    if ("state" in metaData) {
+      this.succeeded = metaData.state == nsIDM.DOWNLOAD_FINISHED;
+      this.error = metaData.state == nsIDM.DOWNLOAD_FAILED
+                   ? { message: "History download failed." }
+                   : metaData.state == nsIDM.DOWNLOAD_BLOCKED_PARENTAL
+                   ? { becauseBlockedByParentalControls: true }
+                   : metaData.state == nsIDM.DOWNLOAD_DIRTY
+                   ? { becauseBlockedByReputationCheck: true }
+                   : null;
+      this.canceled = metaData.state == nsIDM.DOWNLOAD_CANCELED ||
+                      metaData.state == nsIDM.DOWNLOAD_PAUSED;
+      this.endTime = metaData.endTime;
+
+      // Normal history downloads are assumed to exist until the user interface
+      // is refreshed, at which point these values may be updated.
+      this.target.exists = true;
+      this.target.size = metaData.fileSize;
+    } else {
+      // Metadata might be missing from a download that has started but hasn't
+      // stopped already. Normally, this state is overridden with the one from
+      // the corresponding in-progress session download. But if the browser is
+      // terminated abruptly and additionally the file with information about
+      // in-progress downloads is lost, we may end up using this state. We use
+      // the failed state to allow the download to be restarted.
+      //
+      // On the other hand, if the download is missing the target file
+      // annotation as well, it is just a very old one, and we can assume it
+      // succeeded.
+      this.succeeded = !this.target.path;
+      this.error = this.target.path ? { message: "Unstarted download." } : null;
+      this.canceled = false;
+
+      // These properties may be updated if the user interface is refreshed.
+      this.exists = false;
+      this.target.size = undefined;
+    }
+  },
+
+  /**
+   * History downloads are never in progress.
+   */
+  stopped: true,
+
+  /**
+   * No percentage indication is shown for history downloads.
+   */
+  hasProgress: false,
+
+  /**
+   * History downloads cannot be restarted using their partial data, even if
+   * they are indicated as paused in their Places metadata. The only way is to
+   * use the information from a persisted session download, that will be shown
+   * instead of the history download. In case this session download is not
+   * available, we show the history download as canceled, not paused.
+   */
+  hasPartialData: false,
+
+  /**
+   * This method mimicks the "start" method of session downloads, and is called
+   * when the user retries a history download.
+   *
+   * At present, we always ask the user for a new target path when retrying a
+   * history download. In the future we may consider reusing the known target
+   * path if the folder still exists and the file name is not already used,
+   * except when the user preferences indicate that the target path should be
+   * requested every time a new download is started.
+   */
+  start() {
+    let browserWin = RecentWindow.getMostRecentBrowserWindow();
+    let initiatingDoc = browserWin ? browserWin.document : document;
+
+    // Do not suggest a file name if we don't know the original target.
+    let leafName = this.target.path ? OS.Path.basename(this.target.path) : null;
+    DownloadURL(this.source.url, leafName, initiatingDoc);
+
+    return Promise.resolve();
+  },
+
+  /**
+   * This method mimicks the "refresh" method of session downloads, except that
+   * it cannot notify that the data changed to the Downloads View.
+   */
+  refresh: Task.async(function* () {
+    try {
+      this.target.size = (yield OS.File.stat(this.target.path)).size;
+      this.target.exists = true;
+    } catch (ex) {
+      // We keep the known file size from the metadata, if any.
+      this.target.exists = false;
+    }
+  }),
+};
 
 /**
  * A download element shell is responsible for handling the commands and the
- * displayed data for a single download view element. The download element
- * could represent either a past download (for which we get data from places)  or
- * a "session" download (using a data-item object. See DownloadsCommon.jsm), or both.
+ * displayed data for a single download view element.
  *
- * Once initialized with either a data item or a places node, the created richlistitem
- * can be accessed through the |element| getter, and can then be inserted/removed from
- * a richlistbox.
+ * The shell may contain a session download, a history download, or both.  When
+ * both a history and a session download are present, the session download gets
+ * priority and its information is displayed.
  *
- * The shell doesn't take care of inserting the item, or removing it when it's no longer
- * valid. That's the caller (a DownloadsPlacesView object) responsibility.
+ * On construction, a new richlistitem is created, and can be accessed through
+ * the |element| getter. The shell doesn't insert the item in a richlistbox, the
+ * caller must do it and remove the element when it's no longer needed.
  *
- * The caller is also responsible for "passing over" notifications. The
- * DownloadsPlacesView object implements onDataItemStateChanged and
- * onDataItemChanged of the DownloadsView pseudo interface, and registers as a
- * Places result observer.
+ * The caller is also responsible for forwarding status notifications for
+ * session downloads, calling the onStateChanged and onChanged methods.
  *
- * @param [optional] aDataItem
- *        The data item of a the session download. Required if aPlacesNode is not set
- * @param [optional] aPlacesNode
- *        The places node for a past download. Required if aDataItem is not set.
- * @param [optional] aAnnotations
- *        Map containing annotations values, to speed up the initial loading.
+ * @param [optional] aSessionDownload
+ *        The session download, required if aHistoryDownload is not set.
+ * @param [optional] aHistoryDownload
+ *        The history download, required if aSessionDownload is not set.
  */
-function DownloadElementShell(aDataItem, aPlacesNode, aAnnotations) {
-  this._element = document.createElement("richlistitem");
-  this._element._shell = this;
+function HistoryDownloadElementShell(aSessionDownload, aHistoryDownload) {
+  this.element = document.createElement("richlistitem");
+  this.element._shell = this;
 
-  this._element.classList.add("download");
-  this._element.classList.add("download-state");
+  this.element.classList.add("download");
+  this.element.classList.add("download-state");
 
-  if (aAnnotations) {
-    this._annotations = aAnnotations;
+  if (aSessionDownload) {
+    this.sessionDownload = aSessionDownload;
   }
-  if (aDataItem) {
-    this.dataItem = aDataItem;
-  }
-  if (aPlacesNode) {
-    this.placesNode = aPlacesNode;
+  if (aHistoryDownload) {
+    this.historyDownload = aHistoryDownload;
   }
 }
 
-DownloadElementShell.prototype = {
-  // The richlistitem for the download
-  get element() this._element,
+HistoryDownloadElementShell.prototype = {
+  __proto__: DownloadsViewUI.DownloadElementShell.prototype,
 
   /**
-   * Manages the "active" state of the shell.  By default all the shells
-   * without a dataItem are inactive, thus their UI is not updated.  They must
-   * be activated when entering the visible area.  Session downloads are
-   * always active since they always have a dataItem.
+   * Manages the "active" state of the shell.  By default all the shells without
+   * a session download are inactive, thus their UI is not updated.  They must
+   * be activated when entering the visible area.  Session downloads are always
+   * active.
    */
   ensureActive() {
     if (!this._active) {
       this._active = true;
-      this._element.setAttribute("active", true);
+      this.element.setAttribute("active", true);
       this._updateUI();
     }
   },
   get active() !!this._active,
 
-  // The data item for the download
-  _dataItem: null,
-  get dataItem() this._dataItem,
+  /**
+   * Overrides the base getter to return the Download or HistoryDownload object
+   * for displaying information and executing commands in the user interface.
+   */
+  get download() this._sessionDownload || this._historyDownload,
 
-  set dataItem(aValue) {
-    if (this._dataItem != aValue) {
-      if (!aValue && !this._placesNode) {
-        throw new Error("Should always have either a dataItem or a placesNode");
+  _sessionDownload: null,
+  get sessionDownload() this._sessionDownload,
+  set sessionDownload(aValue) {
+    if (this._sessionDownload != aValue) {
+      if (!aValue && !this._historyDownload) {
+        throw new Error("Should always have either a Download or a HistoryDownload");
       }
 
-      this._dataItem = aValue;
-      if (!this.active) {
-        this.ensureActive();
-      } else {
-        this._updateUI();
-      }
+      this._sessionDownload = aValue;
+
+      this.ensureActive();
+      this._updateUI();
     }
     return aValue;
   },
 
-  _placesNode: null,
-  get placesNode() this._placesNode,
-  set placesNode(aValue) {
-    if (this._placesNode != aValue) {
-      if (!aValue && !this._dataItem) {
-        throw new Error("Should always have either a dataItem or a placesNode");
+  _historyDownload: null,
+  get historyDownload() this._historyDownload,
+  set historyDownload(aValue) {
+    if (this._historyDownload != aValue) {
+      if (!aValue && !this._sessionDownload) {
+        throw new Error("Should always have either a Download or a HistoryDownload");
       }
 
-      // Preserve the annotations map if this is the first loading and we got
-      // cached values.
-      if (this._placesNode || !this._annotations) {
-        this._annotations = new Map();
-      }
+      this._historyDownload = aValue;
 
-      this._placesNode = aValue;
-
-      // We don't need to update the UI if we had a data item, because
+      // We don't need to update the UI if we had a session data item, because
       // the places information isn't used in this case.
-      if (!this._dataItem && this.active) {
+      if (!this._sessionDownload) {
         this._updateUI();
       }
     }
     return aValue;
   },
 
-  // The download uri (as a string)
-  get downloadURI() {
-    if (this._dataItem) {
-      return this._dataItem.uri;
-    }
-    if (this._placesNode) {
-      return this._placesNode.uri;
-    }
-    throw new Error("Unexpected download element state");
-  },
-
-  get _downloadURIObj() {
-    if (!("__downloadURIObj" in this)) {
-      this.__downloadURIObj = NetUtil.newURI(this.downloadURI);
-    }
-    return this.__downloadURIObj;
-  },
-
-  _getIcon() {
-    let metaData = this.getDownloadMetaData();
-    if ("filePath" in metaData) {
-      return "moz-icon://" + metaData.filePath + "?size=32";
-    }
-
-    if (this._placesNode) {
-      return "moz-icon://.unknown?size=32";
-    }
-
-    // Assert unreachable.
-    if (this._dataItem) {
-      throw new Error("Session-download items should always have a target file uri");
-    }
-
-    throw new Error("Unexpected download element state");
-  },
-
-  // Helper for getting a places annotation set for the download.
-  _getAnnotation(aAnnotation, aDefaultValue) {
-    let value;
-    if (this._annotations.has(aAnnotation)) {
-      value = this._annotations.get(aAnnotation);
-    }
-
-    // If the value is cached, or we know it doesn't exist, avoid a database
-    // lookup.
-    if (value === undefined) {
-      try {
-        value = PlacesUtils.annotations.getPageAnnotation(
-          this._downloadURIObj, aAnnotation);
-      } catch (ex) {
-        value = NOT_AVAILABLE;
-      }
-    }
-
-    if (value === NOT_AVAILABLE) {
-      if (aDefaultValue === undefined) {
-        throw new Error("Could not get required annotation '" + aAnnotation +
-                        "' for download with url '" + this.downloadURI + "'");
-      }
-      value = aDefaultValue;
-    }
-
-    this._annotations.set(aAnnotation, value);
-    return value;
-  },
-
-  _fetchTargetFileInfo(aUpdateMetaDataAndStatusUI = false) {
-    if (this._targetFileInfoFetched) {
-      throw new Error("_fetchTargetFileInfo should not be called if the information was already fetched");
-    }
+  _updateUI() {
+    // There is nothing to do if the item has always been invisible.
     if (!this.active) {
-      throw new Error("Trying to _fetchTargetFileInfo on an inactive download shell");
-    }
-
-    let path = this.getDownloadMetaData().filePath;
-
-    // In previous version, the target file annotations were not set,
-    // so we cannot tell where is the file.
-    if (path === undefined) {
-      this._targetFileInfoFetched = true;
-      this._targetFileExists = false;
-      if (aUpdateMetaDataAndStatusUI) {
-        this._metaData = null;
-        this._updateDownloadStatusUI();
-      }
-      // Here we don't need to update the download commands,
-      // as the state is unknown as it was.
       return;
     }
 
-    OS.File.stat(path).then(
-      fileInfo => {
-        this._targetFileInfoFetched = true;
-        this._targetFileExists = true;
-        this._targetFileSize = fileInfo.size;
-        if (aUpdateMetaDataAndStatusUI) {
-          this._metaData = null;
-          this._updateDownloadStatusUI();
-        }
-        if (this._element.selected) {
-          goUpdateDownloadCommands();
-        }
-      },
-
-      aReason => {
-        if (aReason instanceof OS.File.Error && aReason.becauseNoSuchFile) {
-          this._targetFileInfoFetched = true;
-          this._targetFileExists = false;
-        } else {
-          Cu.reportError("Could not fetch info for target file (reason: " +
-                         aReason + ")");
-        }
-
-        if (aUpdateMetaDataAndStatusUI) {
-          this._metaData = null;
-          this._updateDownloadStatusUI();
-        }
-
-        if (this._element.selected) {
-          goUpdateDownloadCommands();
-        }
-      }
-    );
-  },
-
-  _getAnnotatedMetaData() {
-    return JSON.parse(this._getAnnotation(DOWNLOAD_META_DATA_ANNO));
-  },
-
-  _extractFilePathAndNameFromFileURI(aFileURI) {
-    let file = Cc["@mozilla.org/network/protocol;1?name=file"]
-                .getService(Ci.nsIFileProtocolHandler)
-                .getFileFromURLSpec(aFileURI);
-    return [file.path, file.leafName];
-  },
+    // Since the state changed, we may need to check the target file again.
+    this._targetFileChecked = false;
 
-  /**
-   * Retrieve the meta data object for the download.  The following fields
-   * may be set.
-   *
-   * - state - any download state defined in nsIDownloadManager.  If this field
-   *   is not set, the download state is unknown.
-   * - endTime: the end time of the download.
-   * - filePath: the downloaded file path on the file system, when it
-   *   was downloaded.  The file may not exist.  This is set for session
-   *   downloads that have a local file set, and for history downloads done
-   *   after the landing of bug 591289.
-   * - fileName: the downloaded file name on the file system. Set if filePath
-   *   is set.
-   * - displayName: the user-facing label for the download.  This is always
-   *   set.  If available, it's set to the downloaded file name.  If not, this
-   *   means the download does not have Places metadata because it is very old,
-   *   and in this rare case the download uri is used.
-   * - fileSize (only set for downloads which completed successfully):
-   *   the downloaded file size.  For downloads done after the landing of
-   *   bug 826991, this value is "static" - that is, it does not necessarily
-   *   mean that the file is in place and has this size.
-   */
-  getDownloadMetaData() {
-    if (!this._metaData) {
-      if (this._dataItem) {
-        this._metaData = {
-          state:       this._dataItem.state,
-          endTime:     this._dataItem.endTime,
-          fileName:    this._dataItem.target,
-          displayName: this._dataItem.target
-        };
-        if (this._dataItem.done) {
-          this._metaData.fileSize = this._dataItem.maxBytes;
-        }
-        if (this._dataItem.localFile) {
-          this._metaData.filePath = this._dataItem.localFile.path;
-        }
-      } else {
-        try {
-          this._metaData = this._getAnnotatedMetaData();
-        } catch (ex) {
-          this._metaData = {};
-          if (this._targetFileInfoFetched && this._targetFileExists) {
-            this._metaData.state = this._targetFileSize > 0 ?
-              nsIDM.DOWNLOAD_FINISHED : nsIDM.DOWNLOAD_FAILED;
-            this._metaData.fileSize = this._targetFileSize;
-          }
-
-          // This is actually the start-time, but it's the best we can get.
-          this._metaData.endTime = this._placesNode.time / 1000;
-        }
-
-        try {
-          let targetFileURI = this._getAnnotation(DESTINATION_FILE_URI_ANNO);
-          [this._metaData.filePath, this._metaData.fileName] =
-            this._extractFilePathAndNameFromFileURI(targetFileURI);
-          this._metaData.displayName = this._metaData.fileName;
-        } catch (ex) {
-          this._metaData.displayName = this.downloadURI;
-        }
-      }
-    }
-    return this._metaData;
+    this._updateState();
   },
 
-  _getStatusText() {
-    let s = DownloadsCommon.strings;
-    if (this._dataItem && this._dataItem.inProgress) {
-      if (this._dataItem.paused) {
-        let transfer =
-          DownloadUtils.getTransferTotal(this._dataItem.currBytes,
-                                         this._dataItem.maxBytes);
-
-         // We use the same XUL label to display both the state and the amount
-         // transferred, for example "Paused -  1.1 MB".
-         return s.statusSeparatorBeforeNumber(s.statePaused, transfer);
-      }
-      if (this._dataItem.state == nsIDM.DOWNLOAD_DOWNLOADING) {
-        let [status, newEstimatedSecondsLeft] =
-          DownloadUtils.getDownloadStatus(this.dataItem.currBytes,
-                                          this.dataItem.maxBytes,
-                                          this.dataItem.speed,
-                                          this._lastEstimatedSecondsLeft || Infinity);
-        this._lastEstimatedSecondsLeft = newEstimatedSecondsLeft;
-        return status;
-      }
-      if (this._dataItem.starting) {
-        return s.stateStarting;
-      }
-      if (this._dataItem.state == nsIDM.DOWNLOAD_SCANNING) {
-        return s.stateScanning;
-      }
-
-      throw new Error("_getStatusText called with a bogus download state");
-    }
+  get statusTextAndTip() {
+    let status = this.rawStatusTextAndTip;
 
-    // This is a not-in-progress or history download.
-    let stateLabel = "";
-    let state = this.getDownloadMetaData().state;
-    switch (state) {
-      case nsIDM.DOWNLOAD_FAILED:
-        stateLabel = s.stateFailed;
-        break;
-      case nsIDM.DOWNLOAD_CANCELED:
-        stateLabel = s.stateCanceled;
-        break;
-      case nsIDM.DOWNLOAD_BLOCKED_PARENTAL:
-        stateLabel = s.stateBlockedParentalControls;
-        break;
-      case nsIDM.DOWNLOAD_BLOCKED_POLICY:
-        stateLabel = s.stateBlockedPolicy;
-        break;
-      case nsIDM.DOWNLOAD_DIRTY:
-        stateLabel = s.stateDirty;
-        break;
-      case nsIDM.DOWNLOAD_FINISHED:{
-        // For completed downloads, show the file size (e.g. "1.5 MB")
-        let metaData = this.getDownloadMetaData();
-        if ("fileSize" in metaData) {
-          let [size, unit] = DownloadUtils.convertByteUnits(metaData.fileSize);
-          stateLabel = s.sizeWithUnits(size, unit);
-          break;
-        }
-        // Fallback to default unknown state.
-      }
-      default:
-        stateLabel = s.sizeUnknown;
-        break;
+    // The base object would show extended progress information in the tooltip,
+    // but we move this to the main view and never display a tooltip.
+    if (!this.download.stopped) {
+      status.text = status.tip;
     }
-
-    // TODO (bug 829201): history downloads should get the referrer from Places.
-    let referrer = this._dataItem && this._dataItem.referrer ||
-                   this.downloadURI;
-    let [displayHost, fullHost] = DownloadUtils.getURIHost(referrer);
+    status.tip = "";
 
-    let date = new Date(this.getDownloadMetaData().endTime);
-    let [displayDate, fullDate] = DownloadUtils.getReadableDates(date);
-
-    // We use the same XUL label to display the state, the host name, and the
-    // end time.
-    let firstPart = s.statusSeparator(stateLabel, displayHost);
-    return s.statusSeparator(firstPart, displayDate);
+    return status;
   },
 
-  // The progressmeter element for the download
-  get _progressElement() {
-    if (!("__progressElement" in this)) {
-      this.__progressElement =
-        document.getAnonymousElementByAttribute(this._element, "anonid",
-                                                "progressmeter");
-    }
-    return this.__progressElement;
-  },
-
-  // Updates the download state attribute (and by that hide/unhide the
-  // appropriate buttons and context menu items), the status text label,
-  // and the progress meter.
-  _updateDownloadStatusUI() {
-    if (!this.active) {
-      throw new Error("_updateDownloadStatusUI called for an inactive item.");
-    }
-
-    let state = this.getDownloadMetaData().state;
-    if (state !== undefined) {
-      this._element.setAttribute("state", state);
-    }
-
-    this._element.setAttribute("status", this._getStatusText());
-
-    // For past-downloads, we're done. For session-downloads, we may also need
-    // to update the progress-meter.
-    if (!this._dataItem) {
-      return;
-    }
-
-    // Copied from updateProgress in downloads.js.
-    if (this._dataItem.starting) {
-      // Before the download starts, the progress meter has its initial value.
-      this._element.setAttribute("progressmode", "normal");
-      this._element.setAttribute("progress", "0");
-    } else if (this._dataItem.state == nsIDM.DOWNLOAD_SCANNING ||
-               this._dataItem.percentComplete == -1) {
-      // We might not know the progress of a running download, and we don't know
-      // the remaining time during the malware scanning phase.
-      this._element.setAttribute("progressmode", "undetermined");
-    } else {
-      // This is a running download of which we know the progress.
-      this._element.setAttribute("progressmode", "normal");
-      this._element.setAttribute("progress", this._dataItem.percentComplete);
-    }
+  onStateChanged() {
+    this.element.setAttribute("image", this.image);
+    this.element.setAttribute("state",
+                              DownloadsCommon.stateOfDownload(this.download));
 
-    // Dispatch the ValueChange event for accessibility, if possible.
-    if (this._progressElement) {
-      let event = document.createEvent("Events");
-      event.initEvent("ValueChange", true, true);
-      this._progressElement.dispatchEvent(event);
-    }
-  },
-
-  _updateUI() {
-    if (!this.active) {
-      throw new Error("Trying to _updateUI on an inactive download shell");
-    }
-
-    this._metaData = null;
-    this._targetFileInfoFetched = false;
-
-    let metaData = this.getDownloadMetaData();
-    this._element.setAttribute("displayName", metaData.displayName);
-    this._element.setAttribute("image", this._getIcon());
-
-    // For history downloads done in past releases, the downloads/metaData
-    // annotation is not set, and therefore we cannot tell the download
-    // state without the target file information.
-    if (this._dataItem || this.getDownloadMetaData().state !== undefined) {
-      this._updateDownloadStatusUI();
-    } else {
-      this._fetchTargetFileInfo(true);
-    }
-  },
-
-  onStateChanged(aOldState) {
-    let metaData = this.getDownloadMetaData();
-    metaData.state = this.dataItem.state;
-    if (aOldState != nsIDM.DOWNLOAD_FINISHED && aOldState != metaData.state) {
-      // See comment in DVI_onStateChange in downloads.js (the panel-view)
-      this._element.setAttribute("image", this._getIcon() + "&state=normal");
-      metaData.fileSize = this._dataItem.maxBytes;
-      if (this._targetFileInfoFetched) {
-        this._targetFileInfoFetched = false;
-        this._fetchTargetFileInfo();
-      }
-    }
-
-    this._updateDownloadStatusUI();
-
-    if (this._element.selected) {
+    if (this.element.selected) {
       goUpdateDownloadCommands();
     } else {
       goUpdateCommand("downloadsCmd_clearDownloads");
     }
   },
 
   onChanged() {
-    this._updateDownloadStatusUI();
+    this._updateProgress();
   },
 
   /* nsIController */
   isCommandEnabled(aCommand) {
     // The only valid command for inactive elements is cmd_delete.
     if (!this.active && aCommand != "cmd_delete") {
       return false;
     }
     switch (aCommand) {
       case "downloadsCmd_open":
-        // We cannot open a session download file unless it's done ("openable").
-        // If it's finished, we need to make sure the file was not removed,
-        // as we do for past downloads.
-        if (this._dataItem && !this._dataItem.openable) {
-          return false;
-        }
-
-        if (this._targetFileInfoFetched) {
-          return this._targetFileExists;
-        }
-
-        // If the target file information is not yet fetched,
-        // temporarily assume that the file is in place.
-        return this.getDownloadMetaData().state == nsIDM.DOWNLOAD_FINISHED;
+        // This property is false if the download did not succeed.
+        return this.download.target.exists;
       case "downloadsCmd_show":
         // TODO: Bug 827010 - Handle part-file asynchronously.
-        if (this._dataItem &&
-            this._dataItem.partFile && this._dataItem.partFile.exists()) {
-          return true;
-        }
-
-        if (this._targetFileInfoFetched) {
-          return this._targetFileExists;
+        if (this._sessionDownload && this.download.target.partFilePath) {
+          let partFile = new FileUtils.File(this.download.target.partFilePath);
+          if (partFile.exists()) {
+            return true;
+          }
         }
 
-        // If the target file information is not yet fetched,
-        // temporarily assume that the file is in place.
-        return this.getDownloadMetaData().state == nsIDM.DOWNLOAD_FINISHED;
+        // This property is false if the download did not succeed.
+        return this.download.target.exists;
       case "downloadsCmd_pauseResume":
-        return this._dataItem && this._dataItem.inProgress &&
-               this._dataItem.resumable;
+        return this.download.hasPartialData && !this.download.error;
       case "downloadsCmd_retry":
-        // An history download can always be retried.
-        return !this._dataItem || this._dataItem.canRetry;
+        return this.download.canceled || this.download.error;
       case "downloadsCmd_openReferrer":
-        return this._dataItem && !!this._dataItem.referrer;
+        return !!this.download.source.referrer;
       case "cmd_delete":
-        // The behavior in this case is somewhat unexpected, so we disallow that.
-        if (this._placesNode && this._dataItem && this._dataItem.inProgress) {
-          return false;
-        }
-        return true;
+        // We don't want in-progress downloads to be removed accidentally.
+        return this.download.stopped;
       case "downloadsCmd_cancel":
-        return this._dataItem != null;
+        return !!this._sessionDownload;
     }
     return false;
   },
 
-  _retryAsHistoryDownload() {
-    // In future we may try to download into the same original target uri, when
-    // we have it.  Though that requires verifying the path is still valid and
-    // may surprise the user if he wants to be requested every time.
-    let browserWin = RecentWindow.getMostRecentBrowserWindow();
-    let initiatingDoc = browserWin ? browserWin.document : document;
-    DownloadURL(this.downloadURI, this.getDownloadMetaData().fileName,
-                initiatingDoc);
-  },
-
   /* nsIController */
   doCommand(aCommand) {
     switch (aCommand) {
       case "downloadsCmd_open": {
-        let file = this._dataItem ?
-          this.dataItem.localFile :
-          new FileUtils.File(this.getDownloadMetaData().filePath);
-
+        let file = new FileUtils.File(this.download.target.path);
         DownloadsCommon.openDownloadedFile(file, null, window);
         break;
       }
       case "downloadsCmd_show": {
-        if (this._dataItem) {
-          this._dataItem.showLocalFile();
-        } else {
-          let file = new FileUtils.File(this.getDownloadMetaData().filePath);
-          DownloadsCommon.showDownloadedFile(file);
-        }
+        let file = new FileUtils.File(this.download.target.path);
+        DownloadsCommon.showDownloadedFile(file);
         break;
       }
       case "downloadsCmd_openReferrer": {
-        openURL(this._dataItem.referrer);
+        openURL(this.download.source.referrer);
         break;
       }
       case "downloadsCmd_cancel": {
-        this._dataItem.cancel();
+        this.download.cancel().catch(() => {});
+        this.download.removePartialData().catch(Cu.reportError);
         break;
       }
       case "cmd_delete": {
-        if (this._dataItem) {
-          this._dataItem.remove();
+        if (this._sessionDownload) {
+          DownloadsCommon.removeAndFinalizeDownload(this.download);
         }
-        if (this._placesNode) {
-          PlacesUtils.bhistory.removePage(this._downloadURIObj);
+        if (this._historyDownload) {
+          let uri = NetUtil.newURI(this.download.source.url);
+          PlacesUtils.bhistory.removePage(uri);
         }
         break;
       }
       case "downloadsCmd_retry": {
-        if (this._dataItem) {
-          this._dataItem.retry();
-        } else {
-          this._retryAsHistoryDownload();
-        }
+        // Errors when retrying are already reported as download failures.
+        this.download.start().catch(() => {});
         break;
       }
       case "downloadsCmd_pauseResume": {
-        this._dataItem.togglePauseResume();
+        // This command is only enabled for session downloads.
+        if (this.download.stopped) {
+          this.download.start();
+        } else {
+          this.download.cancel();
+        }
         break;
       }
     }
   },
 
   // Returns whether or not the download handled by this shell should
   // show up in the search results for the given term.  Both the display
   // name for the download and the url are searched.
   matchesSearchTerm(aTerm) {
     if (!aTerm) {
       return true;
     }
     aTerm = aTerm.toLowerCase();
-    return this.getDownloadMetaData().displayName.toLowerCase().contains(aTerm) ||
-           this.downloadURI.toLowerCase().contains(aTerm);
+    return this.displayName.toLowerCase().contains(aTerm) ||
+           this.download.source.url.toLowerCase().contains(aTerm);
   },
 
   // Handles return keypress on the element (the keypress listener is
   // set in the DownloadsPlacesView object).
   doDefaultCommand() {
     function getDefaultCommandForState(aState) {
       switch (aState) {
         case nsIDM.DOWNLOAD_FINISHED:
@@ -679,43 +418,69 @@ DownloadElementShell.prototype = {
           return "downloadsCmd_show";
         case nsIDM.DOWNLOAD_BLOCKED_PARENTAL:
         case nsIDM.DOWNLOAD_DIRTY:
         case nsIDM.DOWNLOAD_BLOCKED_POLICY:
           return "downloadsCmd_openReferrer";
       }
       return "";
     }
-    let command = getDefaultCommandForState(this.getDownloadMetaData().state);
+    let state = DownloadsCommon.stateOfDownload(this.download);
+    let command = getDefaultCommandForState(state);
     if (command && this.isCommandEnabled(command)) {
       this.doCommand(command);
     }
   },
 
   /**
-   * At the first time an item is selected, we don't yet have
-   * the target file information.  Thus the call to goUpdateDownloadCommands
-   * in DPV_onSelect would result in best-guess enabled/disabled result.
-   * That way we let the user perform command immediately. However, once
-   * we have the target file information, we can update the commands
-   * appropriately (_fetchTargetFileInfo() calls goUpdateDownloadCommands).
+   * This method is called by the outer download view, after the controller
+   * commands have already been updated. In case we did not check for the
+   * existence of the target file already, we can do it now and then update
+   * the commands as needed.
    */
   onSelect() {
     if (!this.active) {
       return;
     }
-    if (!this._targetFileInfoFetched) {
-      this._fetchTargetFileInfo();
+
+    // If this is a history download for which no target file information is
+    // available, we cannot retrieve information about the target file.
+    if (!this.download.target.path) {
+      return;
+    }
+
+    // Start checking for existence.  This may be done twice if onSelect is
+    // called again before the information is collected.
+    if (!this._targetFileChecked) {
+      this._checkTargetFileOnSelect().catch(Cu.reportError);
     }
-  }
+  },
+
+  _checkTargetFileOnSelect: Task.async(function* () {
+    try {
+      yield this.download.refresh();
+    } finally {
+      // Do not try to check for existence again if this failed once.
+      this._targetFileChecked = true;
+    }
+
+    // Update the commands only if the element is still selected.
+    if (this.element.selected) {
+      goUpdateDownloadCommands();
+    }
+
+    // Ensure the interface has been updated based on the new values. We need to
+    // do this because history downloads can't trigger update notifications.
+    this._updateProgress();
+  }),
 };
 
 /**
  * A Downloads Places View is a places view designed to show a places query
- * for history downloads alongside the current "session"-downloads.
+ * for history downloads alongside the session downloads.
  *
  * As we don't use the places controller, some methods implemented by other
  * places views are not implemented by this view.
  *
  * A richlistitem in this view can represent either a past download or a session
  * download, or both. Session downloads are shown first in the view, and as long
  * as they exist they "collapses" their history "counterpart" (So we don't show two
  * items for every download).
@@ -724,17 +489,17 @@ function DownloadsPlacesView(aRichListBo
   this._richlistbox = aRichListBox;
   this._richlistbox._placesView = this;
   window.controllers.insertControllerAt(0, this);
 
   // Map download URLs to download element shells regardless of their type
   this._downloadElementsShellsForURI = new Map();
 
   // Map download data items to their element shells.
-  this._viewItemsForDataItems = new WeakMap();
+  this._viewItemsForDownloads = new WeakMap();
 
   // Points to the last session download element. We keep track of this
   // in order to keep all session downloads above past downloads.
   this._lastSessionDownloadElement = null;
 
   this._searchTerm = "";
 
   this._active = aActive;
@@ -767,83 +532,142 @@ DownloadsPlacesView.prototype = {
   get active() this._active,
   set active(val) {
     this._active = val;
     if (this._active)
       this._ensureVisibleElementsAreActive();
     return this._active;
   },
 
-  _getAnnotationsFor(aURI) {
-    if (!this._cachedAnnotations) {
-      this._cachedAnnotations = new Map();
-      for (let name of [ DESTINATION_FILE_URI_ANNO,
-                         DOWNLOAD_META_DATA_ANNO ]) {
-        let results = PlacesUtils.annotations.getAnnotationsWithName(name);
-        for (let result of results) {
-          let url = result.uri.spec;
-          if (!this._cachedAnnotations.has(url)) {
-            this._cachedAnnotations.set(url, new Map());
-          }
-          let m = this._cachedAnnotations.get(url);
-          m.set(result.annotationName, result.annotationValue);
+  /**
+   * This cache exists in order to optimize the load of the Downloads View, when
+   * Places annotations for history downloads must be read. In fact, annotations
+   * are stored in a single table, and reading all of them at once is much more
+   * efficient than an individual query.
+   *
+   * When this property is first requested, it reads the annotations for all the
+   * history downloads and stores them indefinitely.
+   *
+   * The historical annotations are not expected to change for the duration of
+   * the session, except in the case where a session download is running for the
+   * same URI as a history download. To ensure we don't use stale data, URIs
+   * corresponding to session downloads are permanently removed from the cache.
+   * This is a very small mumber compared to history downloads.
+   *
+   * This property returns a Map from each download source URI found in Places
+   * annotations to an object with the format:
+   *
+   * { targetFileSpec, state, endTime, fileSize, ... }
+   *
+   * The targetFileSpec property is the value of "downloads/destinationFileURI",
+   * while the other properties are taken from "downloads/metaData". Any of the
+   * properties may be missing from the object.
+   */
+  get _cachedPlacesMetaData() {
+    if (!this.__cachedPlacesMetaData) {
+      this.__cachedPlacesMetaData = new Map();
+
+      // Read the metadata annotations first, but ignore invalid JSON.
+      for (let result of PlacesUtils.annotations.getAnnotationsWithName(
+                                                 DOWNLOAD_META_DATA_ANNO)) {
+        try {
+          this.__cachedPlacesMetaData.set(result.uri.spec,
+                                          JSON.parse(result.annotationValue));
+        } catch (ex) {}
+      }
+
+      // Add the target file annotations to the metadata.
+      for (let result of PlacesUtils.annotations.getAnnotationsWithName(
+                                                 DESTINATION_FILE_URI_ANNO)) {
+        let metaData = this.__cachedPlacesMetaData.get(result.uri.spec);
+        if (!metaData) {
+          metaData = {};
+          this.__cachedPlacesMetaData.set(result.uri.spec, metaData);
         }
+        metaData.targetFileSpec = result.annotationValue;
       }
     }
 
-    let annotations = this._cachedAnnotations.get(aURI);
-    if (!annotations) {
-      // There are no annotations for this entry, that means it is quite old.
-      // Make up a fake annotations entry with default values.
-      annotations = new Map();
-      annotations.set(DESTINATION_FILE_URI_ANNO, NOT_AVAILABLE);
-    }
-    // The meta-data annotation has been added recently, so it's likely missing.
-    if (!annotations.has(DOWNLOAD_META_DATA_ANNO)) {
-      annotations.set(DOWNLOAD_META_DATA_ANNO, NOT_AVAILABLE);
-    }
-    return annotations;
+    return this.__cachedPlacesMetaData;
+  },
+  __cachedPlacesMetaData: null,
+
+  /**
+   * Reads current metadata from Places annotations for the specified URI, and
+   * returns an object with the format:
+   *
+   * { targetFileSpec, state, endTime, fileSize, ... }
+   *
+   * The targetFileSpec property is the value of "downloads/destinationFileURI",
+   * while the other properties are taken from "downloads/metaData". Any of the
+   * properties may be missing from the object.
+   */
+  _getPlacesMetaDataFor(spec) {
+    let metaData = {};
+
+    try {
+      let uri = NetUtil.newURI(spec);
+      try {
+        metaData = JSON.parse(PlacesUtils.annotations.getPageAnnotation(
+                                          uri, DOWNLOAD_META_DATA_ANNO));
+      } catch (ex) {}
+      metaData.targetFileSpec = PlacesUtils.annotations.getPageAnnotation(
+                                            uri, DESTINATION_FILE_URI_ANNO);
+    } catch (ex) {}
+
+    return metaData;
   },
 
   /**
    * Given a data item for a session download, or a places node for a past
    * download, updates the view as necessary.
    *  1. If the given data is a places node, we check whether there are any
    *     elements for the same download url. If there are, then we just reset
    *     their places node. Otherwise we add a new download element.
    *  2. If the given data is a data item, we first check if there's a history
    *     download in the list that is not associated with a data item. If we
    *     found one, we use it for the data item as well and reposition it
    *     alongside the other session downloads. If we don't, then we go ahead
    *     and create a new element for the download.
    *
-   * @param aDataItem
-   *        The data item of a session download. Set to null for history
-   *        downloads data.
+   * @param [optional] sessionDownload
+   *        A Download object, or null for history downloads.
    * @param [optional] aPlacesNode
-   *        The places node for a history download. Required if there's no data
-   *        item.
+   *        The Places node for a history download, or null for session downloads.
    * @param [optional] aNewest
-   *        @see onDataItemAdded. Ignored for history downloads.
+   *        @see onDownloadAdded. Ignored for history downloads.
    * @param [optional] aDocumentFragment
    *        To speed up the appending of multiple elements to the end of the
    *        list which are coming in a single batch (i.e. invalidateContainer),
    *        a document fragment may be passed to which the new elements would
    *        be appended. It's the caller's job to ensure the fragment is merged
    *        to the richlistbox at the end.
    */
-  _addDownloadData(aDataItem, aPlacesNode, aNewest = false,
+  _addDownloadData(sessionDownload, aPlacesNode, aNewest = false,
                    aDocumentFragment = null) {
-    let downloadURI = aPlacesNode ? aPlacesNode.uri : aDataItem.uri;
+    let downloadURI = aPlacesNode ? aPlacesNode.uri
+                                  : sessionDownload.source.url;
     let shellsForURI = this._downloadElementsShellsForURI.get(downloadURI);
     if (!shellsForURI) {
       shellsForURI = new Set();
       this._downloadElementsShellsForURI.set(downloadURI, shellsForURI);
     }
 
+    // When a session download is attached to a shell, we ensure not to keep
+    // stale metadata around for the corresponding history download. This
+    // prevents stale state from being used if the view is rebuilt.
+    //
+    // Note that we will eagerly load the data in the cache at this point, even
+    // if we have seen no history download. The case where no history download
+    // will appear at all is rare enough in normal usage, so we can apply this
+    // simpler solution rather than keeping a list of cache items to ignore.
+    if (sessionDownload) {
+      this._cachedPlacesMetaData.delete(sessionDownload.source.url);
+    }
+
     let newOrUpdatedShell = null;
 
     // Trivial: if there are no shells for this download URI, we always
     // need to create one.
     let shouldCreateShell = shellsForURI.size == 0;
 
     // However, if we do have shells for this download uri, there are
     // few options:
@@ -851,70 +675,88 @@ DownloadsPlacesView.prototype = {
     //    no data item). In this case, we update this shell and move it
     //    if necessary
     // 2) There are multiple shells, indicating multiple downloads for
     //    the same download uri are running. In this case we create
     //    another shell for the download (so we have one shell for each data
     //    item).
     //
     // Note: If a cancelled session download is already in the list, and the
-    // download is retired, onDataItemAdded is called again for the same
+    // download is retried, onDownloadAdded is called again for the same
     // data item. Thus, we also check that we make sure we don't have a view item
     // already.
     if (!shouldCreateShell &&
-        aDataItem && !this._viewItemsForDataItems.has(aDataItem)) {
+        sessionDownload && !this._viewItemsForDownloads.has(sessionDownload)) {
       // If there's a past-download-only shell for this download-uri with no
       // associated data item, use it for the new data item. Otherwise, go ahead
       // and create another shell.
       shouldCreateShell = true;
       for (let shell of shellsForURI) {
-        if (!shell.dataItem) {
+        if (!shell.sessionDownload) {
           shouldCreateShell = false;
-          shell.dataItem = aDataItem;
+          shell.sessionDownload = sessionDownload;
           newOrUpdatedShell = shell;
-          this._viewItemsForDataItems.set(aDataItem, shell);
+          this._viewItemsForDownloads.set(sessionDownload, shell);
           break;
         }
       }
     }
 
     if (shouldCreateShell) {
-      // Bug 836271: The annotations for a url should be cached only when the
-      // places node is available, i.e. when we know we we'd be notified for
-      // annotation changes. 
-      // Otherwise we may cache NOT_AVILABLE values first for a given session
-      // download, and later use these NOT_AVILABLE values when a history
-      // download for the same URL is added.
-      let cachedAnnotations = aPlacesNode ? this._getAnnotationsFor(downloadURI) : null;
-      let shell = new DownloadElementShell(aDataItem, aPlacesNode, cachedAnnotations);
+      // If we are adding a new history download here, it means there is no
+      // associated session download, thus we must read the Places metadata,
+      // because it will not be obscured by the session download.
+      let historyDownload = null;
+      if (aPlacesNode) {
+        let metaData = this._cachedPlacesMetaData.get(aPlacesNode.uri) ||
+                       this._getPlacesMetaDataFor(aPlacesNode.uri);
+        historyDownload = new HistoryDownload(aPlacesNode);
+        historyDownload.updateFromMetaData(metaData);
+      }
+      let shell = new HistoryDownloadElementShell(sessionDownload,
+                                                  historyDownload);
+      shell.element._placesNode = aPlacesNode;
       newOrUpdatedShell = shell;
       shellsForURI.add(shell);
-      if (aDataItem) {
-        this._viewItemsForDataItems.set(aDataItem, shell);
+      if (sessionDownload) {
+        this._viewItemsForDownloads.set(sessionDownload, shell);
       }
     } else if (aPlacesNode) {
+      // We are updating information for a history download for which we have
+      // at least one download element shell already. There are two cases:
+      // 1) There are one or more download element shells for this source URI,
+      //    each with an associated session download. We update the Places node
+      //    because we may need it later, but we don't need to read the Places
+      //    metadata until the last session download is removed.
+      // 2) Occasionally, we may receive a duplicate notification for a history
+      //    download with no associated session download. We have exactly one
+      //    download element shell in this case, but the metdata cannot have
+      //    changed, just the reference to the Places node object is different.
+      // So, we update all the node references and keep the metadata intact.
       for (let shell of shellsForURI) {
-        if (shell.placesNode != aPlacesNode) {
-          shell.placesNode = aPlacesNode;
+        if (!shell.historyDownload) {
+          // Create the element to host the metadata when needed.
+          shell.historyDownload = new HistoryDownload(aPlacesNode);
         }
+        shell.element._placesNode = aPlacesNode;
       }
     }
 
     if (newOrUpdatedShell) {
       if (aNewest) {
         this._richlistbox.insertBefore(newOrUpdatedShell.element,
                                        this._richlistbox.firstChild);
         if (!this._lastSessionDownloadElement) {
           this._lastSessionDownloadElement = newOrUpdatedShell.element;
         }
         // Some operations like retrying an history download move an element to
         // the top of the richlistbox, along with other session downloads.
         // More generally, if a new download is added, should be made visible.
         this._richlistbox.ensureElementIsVisible(newOrUpdatedShell.element);
-      } else if (aDataItem) {
+      } else if (sessionDownload) {
         let before = this._lastSessionDownloadElement ?
           this._lastSessionDownloadElement.nextSibling : this._richlistbox.firstChild;
         this._richlistbox.insertBefore(newOrUpdatedShell.element, before);
         this._lastSessionDownloadElement = newOrUpdatedShell.element;
       } else {
         let appendTo = aDocumentFragment || this._richlistbox;
         appendTo.appendChild(newOrUpdatedShell.element);
       }
@@ -954,51 +796,60 @@ DownloadsPlacesView.prototype = {
     goUpdateCommand("downloadsCmd_clearDownloads");
   },
 
   _removeHistoryDownloadFromView(aPlacesNode) {
     let downloadURI = aPlacesNode.uri;
     let shellsForURI = this._downloadElementsShellsForURI.get(downloadURI);
     if (shellsForURI) {
       for (let shell of shellsForURI) {
-        if (shell.dataItem) {
-          shell.placesNode = null;
+        if (shell.sessionDownload) {
+          shell.historyDownload = null;
         } else {
           this._removeElement(shell.element);
           shellsForURI.delete(shell);
           if (shellsForURI.size == 0)
             this._downloadElementsShellsForURI.delete(downloadURI);
         }
       }
     }
   },
 
-  _removeSessionDownloadFromView(aDataItem) {
-    let shells = this._downloadElementsShellsForURI.get(aDataItem.uri);
+  _removeSessionDownloadFromView(download) {
+    let shells = this._downloadElementsShellsForURI
+                     .get(download.source.url);
     if (shells.size == 0) {
       throw new Error("Should have had at leaat one shell for this uri");
     }
 
-    let shell = this._viewItemsForDataItems.get(aDataItem);
+    let shell = this._viewItemsForDownloads.get(download);
     if (!shells.has(shell)) {
       throw new Error("Missing download element shell in shells list for url");
     }
 
     // If there's more than one item for this download uri, we can let the
     // view item for this this particular data item go away.
     // If there's only one item for this download uri, we should only
     // keep it if it is associated with a history download.
-    if (shells.size > 1 || !shell.placesNode) {
+    if (shells.size > 1 || !shell.historyDownload) {
       this._removeElement(shell.element);
       shells.delete(shell);
       if (shells.size == 0) {
-        this._downloadElementsShellsForURI.delete(aDataItem.uri);
+        this._downloadElementsShellsForURI.delete(download.source.url);
       }
     } else {
-      shell.dataItem = null;
+      // We have one download element shell containing both a session download
+      // and a history download, and we are now removing the session download.
+      // Previously, we did not use the Places metadata because it was obscured
+      // by the session download. Since this is no longer the case, we have to
+      // read the latest metadata before removing the session download.
+      let url = shell.historyDownload.source.url;
+      let metaData = this._getPlacesMetaDataFor(url);
+      shell.historyDownload.updateFromMetaData(metaData);
+      shell.sessionDownload = null;
       // Move it below the session-download items;
       if (this._lastSessionDownloadElement == shell.element) {
         this._lastSessionDownloadElement = shell.element.previousSibling;
       } else {
         let before = this._lastSessionDownloadElement ?
           this._lastSessionDownloadElement.nextSibling : this._richlistbox.firstChild;
         this._richlistbox.insertBefore(shell.element, before);
       }
@@ -1101,24 +952,19 @@ DownloadsPlacesView.prototype = {
       delete this._resultNode;
       delete this._result;
     }
 
     return val;
   },
 
   get selectedNodes() {
-    let placesNodes = [];
-    let selectedElements = this._richlistbox.selectedItems;
-    for (let elt of selectedElements) {
-      if (elt._shell.placesNode) {
-        placesNodes.push(elt._shell.placesNode);
-      }
-    }
-    return placesNodes;
+    return [for (element of this._richlistbox.selectedItems)
+            if (element._placesNode)
+            element._placesNode];
   },
 
   get selectedNode() {
     let selectedNodes = this.selectedNodes;
     return selectedNodes.length == 1 ? selectedNodes[0] : null;
   },
 
   get hasSelection() this.selectedNodes.length > 0,
@@ -1138,18 +984,18 @@ DownloadsPlacesView.prototype = {
     let suppressOnSelect = this._richlistbox.suppressOnSelect;
     this._richlistbox.suppressOnSelect = true;
     try {
       // Remove the invalidated history downloads from the list and unset the
       // places node for data downloads.
       // Loop backwards since _removeHistoryDownloadFromView may removeChild().
       for (let i = this._richlistbox.childNodes.length - 1; i >= 0; --i) {
         let element = this._richlistbox.childNodes[i];
-        if (element._shell.placesNode) {
-          this._removeHistoryDownloadFromView(element._shell.placesNode);
+        if (element._placesNode) {
+          this._removeHistoryDownloadFromView(element._placesNode);
         }
       }
     } finally {
       this._richlistbox.suppressOnSelect = suppressOnSelect;
     }
 
     if (aContainer.childCount > 0) {
       let elementsToAppendFragment = document.createDocumentFragment();
@@ -1263,32 +1109,30 @@ DownloadsPlacesView.prototype = {
     }
   },
 
   onDataLoadStarting() {},
   onDataLoadCompleted() {
     this._ensureInitialSelection();
   },
 
-  onDataItemAdded(aDataItem, aNewest) {
-    this._addDownloadData(aDataItem, null, aNewest);
-  },
-
-  onDataItemRemoved(aDataItem) {
-    this._removeSessionDownloadFromView(aDataItem);
+  onDownloadAdded(download, newest) {
+    this._addDownloadData(download, null, newest);
   },
 
-  // DownloadsView
-  onDataItemStateChanged(aDataItem, aOldState) {
-    this._viewItemsForDataItems.get(aDataItem).onStateChanged(aOldState);
+  onDownloadStateChanged(download) {
+    this._viewItemsForDownloads.get(download).onStateChanged();
   },
 
-  // DownloadsView
-  onDataItemChanged(aDataItem) {
-    this._viewItemsForDataItems.get(aDataItem).onChanged();
+  onDownloadChanged(download) {
+    this._viewItemsForDownloads.get(download).onChanged();
+  },
+
+  onDownloadRemoved(download) {
+    this._removeSessionDownloadFromView(download);
   },
 
   supportsCommand(aCommand) {
     if (DOWNLOAD_VIEW_SUPPORTED_COMMANDS.indexOf(aCommand) != -1) {
       // The clear-downloads command may be performed by the toolbar-button,
       // which can be focused on OS X.  Thus enable this command even if the
       // richlistbox is not focused.
       // For other commands, be prudent and disable them unless the richlistview
@@ -1321,29 +1165,32 @@ DownloadsPlacesView.prototype = {
   },
 
   _canClearDownloads() {
     // Downloads can be cleared if there's at least one removable download in
     // the list (either a history download or a completed session download).
     // Because history downloads are always removable and are listed after the
     // session downloads, check from bottom to top.
     for (let elt = this._richlistbox.lastChild; elt; elt = elt.previousSibling) {
-      if (elt._shell.placesNode || !elt._shell.dataItem.inProgress) {
+      // Stopped, paused, and failed downloads with partial data are removed.
+      let download = elt._shell.download;
+      if (download.stopped && !(download.canceled && download.hasPartialData)) {
         return true;
       }
     }
     return false;
   },
 
   _copySelectedDownloadsToClipboard() {
-    let selectedElements = this._richlistbox.selectedItems;
-    let urls = [e._shell.downloadURI for each (e in selectedElements)];
+    let urls = [for (element of this._richlistbox.selectedItems)
+                element._shell.download.source.url];
 
-    Cc["@mozilla.org/widget/clipboardhelper;1"].
-    getService(Ci.nsIClipboardHelper).copyString(urls.join("\n"), document);
+    Cc["@mozilla.org/widget/clipboardhelper;1"]
+      .getService(Ci.nsIClipboardHelper)
+      .copyString(urls.join("\n"), document);
   },
 
   _getURLFromClipboardData() {
     let trans = Cc["@mozilla.org/widget/transferable;1"].
                 createInstance(Ci.nsITransferable);
     trans.init(null);
 
     let flavors = ["text/x-moz-url", "text/unicode"];
@@ -1417,28 +1264,26 @@ DownloadsPlacesView.prototype = {
   onContextMenu(aEvent) {
     let element = this._richlistbox.selectedItem;
     if (!element || !element._shell) {
       return false;
     }
 
     // Set the state attribute so that only the appropriate items are displayed.
     let contextMenu = document.getElementById("downloadsContextMenu");
-    let state = element._shell.getDownloadMetaData().state;
-    if (state !== undefined) {
-      contextMenu.setAttribute("state", state);
-    } else {
-      contextMenu.removeAttribute("state");
+    let download = element._shell.download;
+    contextMenu.setAttribute("state",
+                             DownloadsCommon.stateOfDownload(download));
+
+    if (!download.stopped) {
+      // The hasPartialData property of a download may change at any time after
+      // it has started, so ensure we update the related command now.
+      goUpdateCommand("downloadsCmd_pauseResume");
     }
 
-    if (state == nsIDM.DOWNLOAD_DOWNLOADING) {
-      // The resumable property of a download may change at any time, so
-      // ensure we update the related command now.
-      goUpdateCommand("downloadsCmd_pauseResume");
-    }
     return true;
   },
 
   onKeyPress(aEvent) {
     let selectedElements = this._richlistbox.selectedItems;
     if (aEvent.keyCode == KeyEvent.DOM_VK_RETURN) {
       // In the content tree, opening bookmarks by pressing return is only
       // supported when a single item is selected. To be consistent, do the
@@ -1494,21 +1339,23 @@ DownloadsPlacesView.prototype = {
   onDragStart(aEvent) {
     // TODO Bug 831358: Support d&d for multiple selection.
     // For now, we just drag the first element.
     let selectedItem = this._richlistbox.selectedItem;
     if (!selectedItem) {
       return;
     }
 
-    let metaData = selectedItem._shell.getDownloadMetaData();
-    if (!("filePath" in metaData)) {
+    let targetPath = selectedItem._shell.download.target.path;
+    if (!targetPath) {
       return;
     }
-    let file = new FileUtils.File(metaData.filePath);
+
+    // We must check for existence synchronously because this is a DOM event.
+    let file = new FileUtils.File(targetPath);
     if (!file.exists()) {
       return;
     }
 
     let dt = aEvent.dataTransfer;
     dt.mozSetDataAt("application/x-moz-file", file, 0);
     let url = Services.io.newFileURI(file).spec;
     dt.setData("text/uri-list", url);
--- a/browser/components/downloads/content/download.xml
+++ b/browser/components/downloads/content/download.xml
@@ -31,17 +31,17 @@
              download items. An element in the summary has the same min-width
              on a description, and we don't want the panel to change size if the
              summary isn't being displayed, so we ensure that items share the
              same minimum width.
              -->
         <xul:description class="downloadTarget"
                          crop="center"
                          style="min-width: &downloadsSummary.minWidth2;"
-                         xbl:inherits="value=target,tooltiptext=target"/>
+                         xbl:inherits="value=displayName,tooltiptext=displayName"/>
         <xul:progressmeter anonid="progressmeter"
                            class="downloadProgress"
                            min="0"
                            max="100"
                            xbl:inherits="mode=progressmode,value=progress"/>
         <xul:description class="downloadDetails"
                          crop="end"
                          xbl:inherits="value=status,tooltiptext=statusTip"/>
--- a/browser/components/downloads/content/downloads.js
+++ b/browser/components/downloads/content/downloads.js
@@ -59,31 +59,30 @@
  * We end up capturing the tab/down key events, and preventing their default
  * behaviour. We then set a "keyfocus" attribute on the panel, which allows
  * us to draw a ring around the currently focused element. If the panel is
  * closed or the mouse moves over the panel, we remove the attribute.
  */
 
 "use strict";
 
-////////////////////////////////////////////////////////////////////////////////
-//// Globals
+let { classes: Cc, interfaces: Ci, utils: Cu, results: Cr } = Components;
 
-XPCOMUtils.defineLazyModuleGetter(this, "DownloadUtils",
-                                  "resource://gre/modules/DownloadUtils.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "DownloadsCommon",
                                   "resource:///modules/DownloadsCommon.jsm");
-XPCOMUtils.defineLazyModuleGetter(this, "OS",
-                                  "resource://gre/modules/osfile.jsm");
-XPCOMUtils.defineLazyModuleGetter(this, "PrivateBrowsingUtils",
-                                  "resource://gre/modules/PrivateBrowsingUtils.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "DownloadsViewUI",
+                                  "resource:///modules/DownloadsViewUI.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "FileUtils",
+                                  "resource://gre/modules/FileUtils.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "NetUtil",
+                                  "resource://gre/modules/NetUtil.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "PlacesUtils",
                                   "resource://gre/modules/PlacesUtils.jsm");
-XPCOMUtils.defineLazyModuleGetter(this, "NetUtil",
-                                  "resource://gre/modules/NetUtil.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "Services",
+                                  "resource://gre/modules/Services.jsm");
 
 ////////////////////////////////////////////////////////////////////////////////
 //// DownloadsPanel
 
 /**
  * Main entry point for the downloads panel interface.
  */
 const DownloadsPanel = {
@@ -550,17 +549,17 @@ const DownloadsPanel = {
         return;
       }
 
       // When the panel is opened, we check if the target files of visible items
       // still exist, and update the allowed items interactions accordingly.  We
       // do these checks on a background thread, and don't prevent the panel to
       // be displayed while these checks are being performed.
       for (let viewItem of DownloadsView._visibleViewItems.values()) {
-        viewItem.verifyTargetExists();
+        viewItem.download.refresh().catch(Cu.reportError);
       }
 
       DownloadsCommon.log("Opening downloads panel popup.");
       this.panel.openPopup(anchor, "bottomcenter topright", 0, 0, false, null);
     });
   },
 };
 
@@ -660,37 +659,37 @@ const DownloadsView = {
   kItemCountLimit: 3,
 
   /**
    * Indicates whether we are still loading downloads data asynchronously.
    */
   loading: false,
 
   /**
-   * Ordered array of all DownloadsDataItem objects.  We need to keep this array
-   * because only a limited number of items are shown at once, and if an item
-   * that is currently visible is removed from the list, we might need to take
-   * another item from the array and make it appear at the bottom.
+   * Ordered array of all Download objects.  We need to keep this array because
+   * only a limited number of items are shown at once, and if an item that is
+   * currently visible is removed from the list, we might need to take another
+   * item from the array and make it appear at the bottom.
    */
-  _dataItems: [],
+  _downloads: [],
 
   /**
-   * Associates the visible DownloadsDataItem objects with their corresponding
+   * Associates the visible Download objects with their corresponding
    * DownloadsViewItem object.  There is a limited number of view items in the
    * panel at any given time.
    */
   _visibleViewItems: new Map(),
 
   /**
    * Called when the number of items in the list changes.
    */
   _itemCountChanged() {
     DownloadsCommon.log("The downloads item count has changed - we are tracking",
-                        this._dataItems.length, "downloads in total.");
-    let count = this._dataItems.length;
+                        this._downloads.length, "downloads in total.");
+    let count = this._downloads.length;
     let hiddenCount = count - this.kItemCountLimit;
 
     if (count > 0) {
       DownloadsCommon.log("Setting the panel's hasdownloads attribute to true.");
       DownloadsPanel.panel.setAttribute("hasdownloads", "true");
     } else {
       DownloadsCommon.log("Removing the panel's hasdownloads attribute.");
       DownloadsPanel.panel.removeAttribute("hasdownloads");
@@ -745,140 +744,138 @@ const DownloadsView = {
     // loaded.  This ensures that the interface is visible, if still required.
     DownloadsPanel.onViewLoadCompleted();
   },
 
   /**
    * Called when a new download data item is available, either during the
    * asynchronous data load or when a new download is started.
    *
-   * @param aDataItem
-   *        DownloadsDataItem object that was just added.
+   * @param aDownload
+   *        Download object that was just added.
    * @param aNewest
    *        When true, indicates that this item is the most recent and should be
    *        added in the topmost position.  This happens when a new download is
    *        started.  When false, indicates that the item is the least recent
    *        and should be appended.  The latter generally happens during the
    *        asynchronous data load.
    */
-  onDataItemAdded(aDataItem, aNewest) {
+  onDownloadAdded(download, aNewest) {
     DownloadsCommon.log("A new download data item was added - aNewest =",
                         aNewest);
 
     if (aNewest) {
-      this._dataItems.unshift(aDataItem);
+      this._downloads.unshift(download);
     } else {
-      this._dataItems.push(aDataItem);
+      this._downloads.push(download);
     }
 
-    let itemsNowOverflow = this._dataItems.length > this.kItemCountLimit;
+    let itemsNowOverflow = this._downloads.length > this.kItemCountLimit;
     if (aNewest || !itemsNowOverflow) {
       // The newly added item is visible in the panel and we must add the
       // corresponding element.  This is either because it is the first item, or
       // because it was added at the bottom but the list still doesn't overflow.
-      this._addViewItem(aDataItem, aNewest);
+      this._addViewItem(download, aNewest);
     }
     if (aNewest && itemsNowOverflow) {
       // If the list overflows, remove the last item from the panel to make room
       // for the new one that we just added at the top.
-      this._removeViewItem(this._dataItems[this.kItemCountLimit]);
+      this._removeViewItem(this._downloads[this.kItemCountLimit]);
     }
 
     // For better performance during batch loads, don't update the count for
     // every item, because the interface won't be visible until load finishes.
     if (!this.loading) {
       this._itemCountChanged();
     }
   },
 
+  onDownloadStateChanged(download) {
+    let viewItem = this._visibleViewItems.get(download);
+    if (viewItem) {
+      viewItem.onStateChanged();
+    }
+  },
+
+  onDownloadChanged(download) {
+    let viewItem = this._visibleViewItems.get(download);
+    if (viewItem) {
+      viewItem.onChanged();
+    }
+  },
+
   /**
    * Called when a data item is removed.  Ensures that the widget associated
    * with the view item is removed from the user interface.
    *
-   * @param aDataItem
-   *        DownloadsDataItem object that is being removed.
+   * @param download
+   *        Download object that is being removed.
    */
-  onDataItemRemoved(aDataItem) {
+  onDownloadRemoved(download) {
     DownloadsCommon.log("A download data item was removed.");
 
-    let itemIndex = this._dataItems.indexOf(aDataItem);
-    this._dataItems.splice(itemIndex, 1);
+    let itemIndex = this._downloads.indexOf(download);
+    this._downloads.splice(itemIndex, 1);
 
     if (itemIndex < this.kItemCountLimit) {
       // The item to remove is visible in the panel.
-      this._removeViewItem(aDataItem);
-      if (this._dataItems.length >= this.kItemCountLimit) {
+      this._removeViewItem(download);
+      if (this._downloads.length >= this.kItemCountLimit) {
         // Reinsert the next item into the panel.
-        this._addViewItem(this._dataItems[this.kItemCountLimit - 1], false);
+        this._addViewItem(this._downloads[this.kItemCountLimit - 1], false);
       }
     }
 
     this._itemCountChanged();
   },
 
-  // DownloadsView
-  onDataItemStateChanged(aDataItem, aOldState) {
-    let viewItem = this._visibleViewItems.get(aDataItem);
-    if (viewItem) {
-      viewItem.onStateChanged(aOldState);
-    }
-  },
-
-  // DownloadsView
-  onDataItemChanged(aDataItem) {
-    let viewItem = this._visibleViewItems.get(aDataItem);
-    if (viewItem) {
-      viewItem.onChanged();
-    }
-  },
-
   /**
    * Associates each richlistitem for a download with its corresponding
    * DownloadsViewItemController object.
    */
   _controllersForElements: new Map(),
 
   controllerForElement(element) {
     return this._controllersForElements.get(element);
   },
 
   /**
    * Creates a new view item associated with the specified data item, and adds
    * it to the top or the bottom of the list.
    */
-  _addViewItem(aDataItem, aNewest)
+  _addViewItem(download, aNewest)
   {
     DownloadsCommon.log("Adding a new DownloadsViewItem to the downloads list.",
                         "aNewest =", aNewest);
 
     let element = document.createElement("richlistitem");
-    let viewItem = new DownloadsViewItem(aDataItem, element);
-    this._visibleViewItems.set(aDataItem, viewItem);
-    let viewItemController = new DownloadsViewItemController(aDataItem);
+    let viewItem = new DownloadsViewItem(download, element);
+    this._visibleViewItems.set(download, viewItem);
+    let viewItemController = new DownloadsViewItemController(download);
     this._controllersForElements.set(element, viewItemController);
     if (aNewest) {
       this.richListBox.insertBefore(element, this.richListBox.firstChild);
     } else {
       this.richListBox.appendChild(element);
     }
   },
 
   /**
    * Removes the view item associated with the specified data item.
    */
-  _removeViewItem(aDataItem) {
+  _removeViewItem(download) {
     DownloadsCommon.log("Removing a DownloadsViewItem from the downloads list.");
-    let element = this._visibleViewItems.get(aDataItem)._element;
+    let element = this._visibleViewItems.get(download).element;
     let previousSelectedIndex = this.richListBox.selectedIndex;
     this.richListBox.removeChild(element);
     if (previousSelectedIndex != -1) {
       this.richListBox.selectedIndex = Math.min(previousSelectedIndex,
                                                 this.richListBox.itemCount - 1);
     }
-    this._visibleViewItems.delete(aDataItem);
+    this._visibleViewItems.delete(download);
     this._controllersForElements.delete(element);
   },
 
   //////////////////////////////////////////////////////////////////////////////
   //// User interface event functions
 
   /**
    * Helper function to do commands on a specific download item.
@@ -964,19 +961,20 @@ const DownloadsView = {
   },
 
   onDownloadDragStart(aEvent) {
     let element = this.richListBox.selectedItem;
     if (!element) {
       return;
     }
 
-    let localFile = DownloadsView.controllerForElement(element)
-                                 .dataItem.localFile;
-    if (!localFile.exists()) {
+    // We must check for existence synchronously because this is a DOM event.
+    let file = new FileUtils.File(DownloadsView.controllerForElement(element)
+                                               .download.target.path);
+    if (!file.exists()) {
       return;
     }
 
     let dataTransfer = aEvent.dataTransfer;
     dataTransfer.mozSetDataAt("application/x-moz-file", localFile, 0);
     dataTransfer.effectAllowed = "copyMove";
     var url = Services.io.newFileURI(localFile).spec;
     dataTransfer.setData("text/uri-list", url);
@@ -989,255 +987,48 @@ const DownloadsView = {
 
 ////////////////////////////////////////////////////////////////////////////////
 //// DownloadsViewItem
 
 /**
  * Builds and updates a single item in the downloads list widget, responding to
  * changes in the download state and real-time data.
  *
- * @param aDataItem
- *        DownloadsDataItem to be associated with the view item.
+ * @param download
+ *        Download object to be associated with the view item.
  * @param aElement
  *        XUL element corresponding to the single download item in the view.
  */
-function DownloadsViewItem(aDataItem, aElement) {
-  this._element = aElement;
-  this.dataItem = aDataItem;
-
-  this.lastEstimatedSecondsLeft = Infinity;
-
-  // Set the URI that represents the correct icon for the target file.  As soon
-  // as bug 239948 comment 12 is handled, the "file" property will be always a
-  // file URL rather than a file name.  At that point we should remove the "//"
-  // (double slash) from the icon URI specification (see test_moz_icon_uri.js).
-  this.image = "moz-icon://" + this.dataItem.file + "?size=32";
+function DownloadsViewItem(download, aElement) {
+  this.download = download;
+  this.element = aElement;
+  this.element._shell = this;
 
-  let attributes = {
-    "type": "download",
-    "class": "download-state",
-    "state": this.dataItem.state,
-    "progress": this.dataItem.inProgress ? this.dataItem.percentComplete : 100,
-    "target": this.dataItem.target,
-    "image": this.image
-  };
+  this.element.setAttribute("type", "download");
+  this.element.classList.add("download-state");
 
-  for (let attributeName in attributes) {
-    this._element.setAttribute(attributeName, attributes[attributeName]);
-  }
-
-  // Initialize more complex attributes.
-  this._updateProgress();
-  this._updateStatusLine();
-  this.verifyTargetExists();
+  this._updateState();
 }
 
 DownloadsViewItem.prototype = {
-  /**
-   * The DownloadDataItem associated with this view item.
-   */
-  dataItem: null,
+  __proto__: DownloadsViewUI.DownloadElementShell.prototype,
 
   /**
    * The XUL element corresponding to the associated richlistbox item.
    */
   _element: null,
 
-  /**
-   * The inner XUL element for the progress bar, or null if not available.
-   */
-  _progressElement: null,
-
-  //////////////////////////////////////////////////////////////////////////////
-  //// Callback functions from DownloadsData
-
-  /**
-   * Called when the download state might have changed.  Sometimes the state of
-   * the download might be the same as before, if the data layer received
-   * multiple events for the same download.
-   */
-  onStateChanged(aOldState) {
-    // If a download just finished successfully, it means that the target file
-    // now exists and we can extract its specific icon.  To ensure that the icon
-    // is reloaded, we must change the URI used by the XUL image element, for
-    // example by adding a query parameter.  Since this URI has a "moz-icon"
-    // scheme, this only works if we add one of the parameters explicitly
-    // supported by the nsIMozIconURI interface.
-    if (aOldState != Ci.nsIDownloadManager.DOWNLOAD_FINISHED &&
-        aOldState != this.dataItem.state) {
-      this._element.setAttribute("image", this.image + "&state=normal");
-
-      // We assume the existence of the target of a download that just completed
-      // successfully, without checking the condition in the background.  If the
-      // panel is already open, this will take effect immediately.  If the panel
-      // is opened later, a new background existence check will be performed.
-      this._element.setAttribute("exists", "true");
-    }
-
-    // Update the user interface after switching states.
-    this._element.setAttribute("state", this.dataItem.state);
-  },
-
-  /**
-   * Called when the download progress has changed.
-   */
-  onChanged() {
-    this._updateProgress();
-    this._updateStatusLine();
-  },
-
-  //////////////////////////////////////////////////////////////////////////////
-  //// Functions for updating the user interface
-
-  /**
-   * Updates the progress bar.
-   */
-  _updateProgress() {
-    if (this.dataItem.starting) {
-      // Before the download starts, the progress meter has its initial value.
-      this._element.setAttribute("progressmode", "normal");
-      this._element.setAttribute("progress", "0");
-    } else if (this.dataItem.state == Ci.nsIDownloadManager.DOWNLOAD_SCANNING ||
-               this.dataItem.percentComplete == -1) {
-      // We might not know the progress of a running download, and we don't know
-      // the remaining time during the malware scanning phase.
-      this._element.setAttribute("progressmode", "undetermined");
-    } else {
-      // This is a running download of which we know the progress.
-      this._element.setAttribute("progressmode", "normal");
-      this._element.setAttribute("progress", this.dataItem.percentComplete);
-    }
-
-    // Find the progress element as soon as the download binding is accessible.
-    if (!this._progressElement) {
-      this._progressElement =
-           document.getAnonymousElementByAttribute(this._element, "anonid",
-                                                   "progressmeter");
-    }
-
-    // Dispatch the ValueChange event for accessibility, if possible.
-    if (this._progressElement) {
-      let event = document.createEvent("Events");
-      event.initEvent("ValueChange", true, true);
-      this._progressElement.dispatchEvent(event);
-    }
+  onStateChanged() {
+    this.element.setAttribute("image", this.image);
+    this.element.setAttribute("state",
+                              DownloadsCommon.stateOfDownload(this.download));
   },
 
-  /**
-   * Updates the main status line, including bytes transferred, bytes total,
-   * download rate, and time remaining.
-   */
-  _updateStatusLine() {
-    const nsIDM = Ci.nsIDownloadManager;
-
-    let status = "";
-    let statusTip = "";
-
-    if (this.dataItem.paused) {
-      let transfer = DownloadUtils.getTransferTotal(this.dataItem.currBytes,
-                                                    this.dataItem.maxBytes);
-
-      // We use the same XUL label to display both the state and the amount
-      // transferred, for example "Paused -  1.1 MB".
-      status = DownloadsCommon.strings.statusSeparatorBeforeNumber(
-                                            DownloadsCommon.strings.statePaused,
-                                            transfer);
-    } else if (this.dataItem.state == nsIDM.DOWNLOAD_DOWNLOADING) {
-      // We don't show the rate for each download in order to reduce clutter.
-      // The remaining time per download is likely enough information for the
-      // panel.
-      [status] =
-        DownloadUtils.getDownloadStatusNoRate(this.dataItem.currBytes,
-                                              this.dataItem.maxBytes,
-                                              this.dataItem.speed,
-                                              this.lastEstimatedSecondsLeft);
-
-      // We are, however, OK with displaying the rate in the tooltip.
-      let newEstimatedSecondsLeft;
-      [statusTip, newEstimatedSecondsLeft] =
-        DownloadUtils.getDownloadStatus(this.dataItem.currBytes,
-                                        this.dataItem.maxBytes,
-                                        this.dataItem.speed,
-                                        this.lastEstimatedSecondsLeft);
-      this.lastEstimatedSecondsLeft = newEstimatedSecondsLeft;
-    } else if (this.dataItem.starting) {
-      status = DownloadsCommon.strings.stateStarting;
-    } else if (this.dataItem.state == nsIDM.DOWNLOAD_SCANNING) {
-      status = DownloadsCommon.strings.stateScanning;
-    } else if (!this.dataItem.inProgress) {
-      let stateLabel = function () {
-        let s = DownloadsCommon.strings;
-        switch (this.dataItem.state) {
-          case nsIDM.DOWNLOAD_FAILED:           return s.stateFailed;
-          case nsIDM.DOWNLOAD_CANCELED:         return s.stateCanceled;
-          case nsIDM.DOWNLOAD_BLOCKED_PARENTAL: return s.stateBlockedParentalControls;
-          case nsIDM.DOWNLOAD_BLOCKED_POLICY:   return s.stateBlockedPolicy;
-          case nsIDM.DOWNLOAD_DIRTY:            return s.stateDirty;
-          case nsIDM.DOWNLOAD_FINISHED:         return this._fileSizeText;
-        }
-        return null;
-      }.apply(this);
-
-      let [displayHost, fullHost] =
-        DownloadUtils.getURIHost(this.dataItem.referrer || this.dataItem.uri);
-
-      let end = new Date(this.dataItem.endTime);
-      let [displayDate, fullDate] = DownloadUtils.getReadableDates(end);
-
-      // We use the same XUL label to display the state, the host name, and the
-      // end time, for example "Canceled - 222.net - 11:15" or "1.1 MB -
-      // website2.com - Yesterday".  We show the full host and the complete date
-      // in the tooltip.
-      let firstPart = DownloadsCommon.strings.statusSeparator(stateLabel,
-                                                              displayHost);
-      status = DownloadsCommon.strings.statusSeparator(firstPart, displayDate);
-      statusTip = DownloadsCommon.strings.statusSeparator(fullHost, fullDate);
-    }
-
-    this._element.setAttribute("status", status);
-    this._element.setAttribute("statusTip", statusTip || status);
-  },
-
-  /**
-   * Localized string representing the total size of completed downloads, for
-   * example "1.5 MB" or "Unknown size".
-   */
-  get _fileSizeText() {
-    // Display the file size, but show "Unknown" for negative sizes.
-    let fileSize = this.dataItem.maxBytes;
-    if (fileSize < 0) {
-      return DownloadsCommon.strings.sizeUnknown;
-    }
-    let [size, unit] = DownloadUtils.convertByteUnits(fileSize);
-    return DownloadsCommon.strings.sizeWithUnits(size, unit);
-  },
-
-  //////////////////////////////////////////////////////////////////////////////
-  //// Functions called by the panel
-
-  /**
-   * Starts checking whether the target file of a finished download is still
-   * available on disk, and sets an attribute that controls how the item is
-   * presented visually.
-   *
-   * The existence check is executed on a background thread.
-   */
-  verifyTargetExists() {
-    // We don't need to check if the download is not finished successfully.
-    if (!this.dataItem.openable) {
-      return;
-    }
-
-    OS.File.exists(this.dataItem.localFile.path).then(aExists => {
-      if (aExists) {
-        this._element.setAttribute("exists", "true");
-      } else {
-        this._element.removeAttribute("exists");
-      }
-    }).catch(Cu.reportError);
+  onChanged() {
+    this._updateProgress();
   },
 };
 
 ////////////////////////////////////////////////////////////////////////////////
 //// DownloadsViewController
 
 /**
  * Handles part of the user interaction events raised by the downloads list
@@ -1329,44 +1120,50 @@ const DownloadsViewController = {
 
 ////////////////////////////////////////////////////////////////////////////////
 //// DownloadsViewItemController
 
 /**
  * Handles all the user interaction events, in particular the "commands",
  * related to a single item in the downloads list widgets.
  */
-function DownloadsViewItemController(aDataItem) {
-  this.dataItem = aDataItem;
+function DownloadsViewItemController(download) {
+  this.download = download;
 }
 
 DownloadsViewItemController.prototype = {
-  //////////////////////////////////////////////////////////////////////////////
-  //// Command dispatching
-
-  /**
-   * The DownloadDataItem controlled by this object.
-   */
-  dataItem: null,
-
   isCommandEnabled(aCommand) {
     switch (aCommand) {
       case "downloadsCmd_open": {
-        return this.dataItem.openable && this.dataItem.localFile.exists();
+        if (!this.download.succeeded) {
+          return false;
+        }
+
+        let file = new FileUtils.File(this.download.target.path);
+        return file.exists();
       }
       case "downloadsCmd_show": {
-        return this.dataItem.localFile.exists() ||
-               this.dataItem.partFile.exists();
+        let file = new FileUtils.File(this.download.target.path);
+        if (file.exists()) {
+          return true;
+        }
+
+        if (!this.download.target.partFilePath) {
+          return false;
+        }
+
+        let partFile = new FileUtils.File(this.download.target.partFilePath);
+        return partFile.exists();
       }
       case "downloadsCmd_pauseResume":
-        return this.dataItem.inProgress && this.dataItem.resumable;
+        return this.download.hasPartialData && !this.download.error;
       case "downloadsCmd_retry":
-        return this.dataItem.canRetry;
+        return this.download.canceled || this.download.error;
       case "downloadsCmd_openReferrer":
-        return !!this.dataItem.referrer;
+        return !!this.download.source.referrer;
       case "cmd_delete":
       case "downloadsCmd_cancel":
       case "downloadsCmd_copyLocation":
       case "downloadsCmd_doDefault":
         return true;
     }
     return false;
   },
@@ -1382,70 +1179,77 @@ DownloadsViewItemController.prototype = 
 
   /**
    * This object contains one key for each command that operates on this item.
    *
    * In commands, the "this" identifier points to the controller item.
    */
   commands: {
     cmd_delete() {
-      this.dataItem.remove();
-      PlacesUtils.bhistory.removePage(NetUtil.newURI(this.dataItem.uri));
+      DownloadsCommon.removeAndFinalizeDownload(this.download);
+      PlacesUtils.bhistory.removePage(
+                             NetUtil.newURI(this.download.source.url));
     },
 
     downloadsCmd_cancel() {
-      this.dataItem.cancel();
+      this.download.cancel().catch(() => {});
+      this.download.removePartialData().catch(Cu.reportError);
     },
 
     downloadsCmd_open() {
-      this.dataItem.openLocalFile();
+      this.download.launch().catch(Cu.reportError);
 
       // We explicitly close the panel here to give the user the feedback that
       // their click has been received, and we're handling the action.
       // Otherwise, we'd have to wait for the file-type handler to execute
       // before the panel would close. This also helps to prevent the user from
       // accidentally opening a file several times.
       DownloadsPanel.hidePanel();
     },
 
     downloadsCmd_show() {
-      this.dataItem.showLocalFile();
+      let file = new FileUtils.File(this.download.target.path);
+      DownloadsCommon.showDownloadedFile(file);
 
       // We explicitly close the panel here to give the user the feedback that
       // their click has been received, and we're handling the action.
       // Otherwise, we'd have to wait for the operating system file manager
       // window to open before the panel closed. This also helps to prevent the
       // user from opening the containing folder several times.
       DownloadsPanel.hidePanel();
     },
 
     downloadsCmd_pauseResume() {
-      this.dataItem.togglePauseResume();
+      if (this.download.stopped) {
+        this.download.start();
+      } else {
+        this.download.cancel();
+      }
     },
 
     downloadsCmd_retry() {
-      this.dataItem.retry();
+      this.download.start().catch(() => {});
     },
 
     downloadsCmd_openReferrer() {
-      openURL(this.dataItem.referrer);
+      openURL(this.download.source.referrer);
     },
 
     downloadsCmd_copyLocation() {
       let clipboard = Cc["@mozilla.org/widget/clipboardhelper;1"]
                       .getService(Ci.nsIClipboardHelper);
-      clipboard.copyString(this.dataItem.uri, document);
+      clipboard.copyString(this.download.source.url, document);
     },
 
     downloadsCmd_doDefault() {
       const nsIDM = Ci.nsIDownloadManager;
 
       // Determine the default command for the current item.
       let defaultCommand = function () {
-        switch (this.dataItem.state) {
+        switch (DownloadsCommon.stateOfDownload(this.download)) {
           case nsIDM.DOWNLOAD_NOTSTARTED:       return "downloadsCmd_cancel";
           case nsIDM.DOWNLOAD_FINISHED:         return "downloadsCmd_open";
           case nsIDM.DOWNLOAD_FAILED:           return "downloadsCmd_retry";
           case nsIDM.DOWNLOAD_CANCELED:         return "downloadsCmd_retry";
           case nsIDM.DOWNLOAD_PAUSED:           return "downloadsCmd_pauseResume";
           case nsIDM.DOWNLOAD_QUEUED:           return "downloadsCmd_cancel";
           case nsIDM.DOWNLOAD_BLOCKED_PARENTAL: return "downloadsCmd_openReferrer";
           case nsIDM.DOWNLOAD_SCANNING:         return "downloadsCmd_show";
--- a/browser/components/downloads/moz.build
+++ b/browser/components/downloads/moz.build
@@ -8,9 +8,10 @@ XPCSHELL_TESTS_MANIFESTS += ['test/unit/
 BROWSER_CHROME_MANIFESTS += ['test/browser/browser.ini']
 
 JAR_MANIFESTS += ['jar.mn']
 
 EXTRA_JS_MODULES += [
     'DownloadsCommon.jsm',
     'DownloadsLogger.jsm',
     'DownloadsTaskbar.jsm',
+    'DownloadsViewUI.jsm',
 ]
--- a/browser/components/downloads/test/browser/browser_basic_functionality.js
+++ b/browser/components/downloads/test/browser/browser_basic_functionality.js
@@ -44,12 +44,13 @@ add_task(function* test_basic_functional
   let richlistbox = document.getElementById("downloadsListBox");
   /* disabled for failing intermittently (bug 767828)
     is(richlistbox.children.length, DownloadData.length,
        "There is the correct number of richlistitems");
   */
   let itemCount = richlistbox.children.length;
   for (let i = 0; i < itemCount; i++) {
     let element = richlistbox.children[itemCount - i - 1];
-    let dataItem = DownloadsView.controllerForElement(element).dataItem;
-    is(dataItem.state, DownloadData[i].state, "Download states match up");
+    let download = DownloadsView.controllerForElement(element).download;
+    is(DownloadsCommon.stateOfDownload(download), DownloadData[i].state,
+       "Download states match up");
   }
 });
--- a/browser/components/loop/content/js/conversationViews.js
+++ b/browser/components/loop/content/js/conversationViews.js
@@ -893,17 +893,17 @@ loop.conversationViews = (function(mozL1
         "local-stream-audio": !this.props.video.enabled
       });
 
       return (
         React.createElement("div", {className: "video-layout-wrapper"}, 
           React.createElement("div", {className: "conversation"}, 
             React.createElement("div", {className: "media nested"}, 
               React.createElement("div", {className: "video_wrapper remote_wrapper"}, 
-                React.createElement("div", {className: "video_inner remote remote-stream"})
+                React.createElement("div", {className: "video_inner remote focus-stream"})
               ), 
               React.createElement("div", {className: localStreamClasses})
             ), 
             React.createElement(loop.shared.views.ConversationToolbar, {
               video: this.props.video, 
               audio: this.props.audio, 
               publishStream: this.publishStream, 
               hangup: this.hangup})
--- a/browser/components/loop/content/js/conversationViews.jsx
+++ b/browser/components/loop/content/js/conversationViews.jsx
@@ -893,17 +893,17 @@ loop.conversationViews = (function(mozL1
         "local-stream-audio": !this.props.video.enabled
       });
 
       return (
         <div className="video-layout-wrapper">
           <div className="conversation">
             <div className="media nested">
               <div className="video_wrapper remote_wrapper">
-                <div className="video_inner remote remote-stream"></div>
+                <div className="video_inner remote focus-stream"></div>
               </div>
               <div className={localStreamClasses}></div>
             </div>
             <loop.shared.views.ConversationToolbar
               video={this.props.video}
               audio={this.props.audio}
               publishStream={this.publishStream}
               hangup={this.hangup} />
--- a/browser/components/loop/content/js/roomViews.js
+++ b/browser/components/loop/content/js/roomViews.js
@@ -268,17 +268,17 @@ loop.roomViews = (function(mozL10n) {
         default: {
           return (
             React.createElement("div", {className: "room-conversation-wrapper"}, 
               this._renderInvitationOverlay(), 
               React.createElement("div", {className: "video-layout-wrapper"}, 
                 React.createElement("div", {className: "conversation room-conversation"}, 
                   React.createElement("div", {className: "media nested"}, 
                     React.createElement("div", {className: "video_wrapper remote_wrapper"}, 
-                      React.createElement("div", {className: "video_inner remote remote-stream"})
+                      React.createElement("div", {className: "video_inner remote focus-stream"})
                     ), 
                     React.createElement("div", {className: localStreamClasses}), 
                     React.createElement("div", {className: "screen hide"})
                   ), 
                   React.createElement(sharedViews.ConversationToolbar, {
                     dispatcher: this.props.dispatcher, 
                     video: {enabled: !this.state.videoMuted, visible: true}, 
                     audio: {enabled: !this.state.audioMuted, visible: true}, 
--- a/browser/components/loop/content/js/roomViews.jsx
+++ b/browser/components/loop/content/js/roomViews.jsx
@@ -268,17 +268,17 @@ loop.roomViews = (function(mozL10n) {
         default: {
           return (
             <div className="room-conversation-wrapper">
               {this._renderInvitationOverlay()}
               <div className="video-layout-wrapper">
                 <div className="conversation room-conversation">
                   <div className="media nested">
                     <div className="video_wrapper remote_wrapper">
-                      <div className="video_inner remote remote-stream"></div>
+                      <div className="video_inner remote focus-stream"></div>
                     </div>
                     <div className={localStreamClasses}></div>
                     <div className="screen hide"></div>
                   </div>
                   <sharedViews.ConversationToolbar
                     dispatcher={this.props.dispatcher}
                     video={{enabled: !this.state.videoMuted, visible: true}}
                     audio={{enabled: !this.state.audioMuted, visible: true}}
--- a/browser/components/loop/content/shared/css/conversation.css
+++ b/browser/components/loop/content/shared/css/conversation.css
@@ -12,46 +12,48 @@
   border: 1px solid #5a5a5a;
   border-left: 0;
   border-right: 0;
   background: rgba(0,0,0,.70);
 }
 
 /* desktop version */
 .fx-embedded .conversation-toolbar {
+  /* required to have dropdowns float atop the .room-invitation-overlay: */
+  z-index: 1020;
   position: absolute;
   top: 0;
   left: 0;
   right: 0;
   /* note that .room-invitation-overlay top matches this */
   height: 26px;
 }
 
 /* standalone version */
 .standalone .conversation-toolbar {
   padding: 20px;
   height: 64px;
 }
 
-.standalone .remote-stream {
+.standalone .focus-stream {
   /* Set at maximum height, minus height of conversation toolbar */
   height: calc(100% - 64px);
 }
 
-.standalone .in-call .remote-stream {
+.standalone .in-call .focus-stream {
   height: 100%;
 }
 
-.conversation-toolbar li {
+.conversation-toolbar > li {
   float: left;
   font-size: 0; /* prevents vertical bottom padding added to buttons in google
                    chrome */
 }
 
-  .standalone .conversation-toolbar li {
+  .standalone .conversation-toolbar > li {
     /* XXX Might make sense to use relative units here.
      */
     margin-right: 16px;
   }
 
 .btn-screen-share-entry {
   float: right !important;
   border-left: 1px solid #5a5a5a;
@@ -160,17 +162,18 @@
   }
 }
 
 /* Common media control buttons behavior */
 .conversation-toolbar .transparent-button {
   background-color: transparent;
   opacity: 1;
 }
-.conversation-toolbar .transparent-button:hover {
+.conversation-toolbar .transparent-button:hover,
+.conversation-toolbar .transparent-button.menu-showing {
   background-color: rgba(255,255,255,.35);
   opacity: 1;
 }
 .conversation-toolbar .media-control.muted {
   background-color: #0096DD;
   opacity: 1;
 }
 
@@ -203,56 +206,79 @@
   }
   .btn-mute-video.muted {
     background-image: url(../img/facemute-14x14@2x.png);
   }
 }
 
 /* Screen share button */
 .btn-screen-share {
-  /* XXX Replace this with the real button: bug 1126286 */
+  position: relative;
   background-image: url(../img/icons-16x16.svg#screen-white);
   background-size: 16px 16px;
+  width: 42px;
+}
+
+/* Make room for the chevron. */
+.conversation-toolbar .btn-screen-share:not(.active) {
+  background-position: 5px center;
 }
 
 .btn-screen-share.active {
   background-image: url(../img/icons-16x16.svg#screenmute-white);
   background-color: #6CB23E;
   opacity: 1;
 }
 
 .btn-screen-share.disabled {
-  /* XXX Add css here for disabled state: bug 1126286 */
+  background-image: url(../img/icons-16x16.svg#screen-disabled);
+}
+
+.btn-screen-share .chevron {
+  background-image: url(../img/icons-10x10.svg#dropdown-white);
+  background-size: 10px 10px;
+  position: absolute;
+  right: 2px;
+  top: 8px;
+  width: 10px;
+  height: 10px;
+}
+
+.btn-screen-share.disabled .chevron {
+  background-image: url(../img/icons-10x10.svg#dropdown-disabled);
 }
 
 .fx-embedded .remote_wrapper {
   position: absolute;
   top: 0px;
   right: 0px;
   bottom: 0px;
   left: 0px;
 }
 
-.standalone .local-stream {
+.standalone .local-stream,
+.standalone .remote-inset-stream {
   /* required to have it superimposed to the control toolbar */
   z-index: 1001;
 }
 
-.standalone .room-conversation .local-stream {
+.standalone .room-conversation .local-stream,
+.standalone .room-conversation .remote-inset-stream {
   box-shadow: none;
 }
 
 /* Side by side video elements */
 
-.conversation .media.side-by-side .remote-stream {
+.conversation .media.side-by-side .focus-stream {
   width: 50%;
   float: left;
 }
 
-.conversation .media.side-by-side .local-stream {
+.conversation .media.side-by-side .local-stream,
+.conversation .media.side-by-side .remote-inset-stream {
   width: 50%;
 }
 
 /* Call ended view */
 .call-ended p {
   text-align: center;
 }
 
@@ -356,19 +382,20 @@
 
   .native-dropdown-menu li:hover,
   .native-dropdown-large-parent li:hover,
   .native-dropdown-large-parent li:hover button {
     color: #fff;
     background-color: #111;
   }
 
-.conversation-window-dropdown li {
+.conversation-window-dropdown > li {
   padding: 2px;
-  font-size: .9em;
+  font-size: .7rem;
+  white-space: nowrap;
 }
 
 /* Expired call url page */
 
 .expired-url-info {
   width: 400px;
   margin: 0 auto;
 }
@@ -497,17 +524,17 @@
   right: 0px;
   bottom: 0px;
   height: 100%;
   width: 100%;
   max-width: none;
   max-height: none;
 }
 
-.conversation .media.nested .remote-stream {
+.conversation .media.nested .focus-stream {
   display: inline-block;
   position: absolute; /* workaround for lack of object-fit; see bug 1020445 */
   width: 100%;
   top: 0;
   bottom: 0;
   left: 0;
   right: 0;
 }
@@ -670,17 +697,18 @@ html, .fx-embedded, #main,
     left: 0;
     right: 0;
   }
 
   .fx-embedded .local-stream {
     position: fixed;
   }
 
-  .standalone .local-stream {
+  .standalone .local-stream,
+  .standalone .remote-inset-stream {
     position: absolute;
     right: 15px;
     bottom: 15px;
     width: 20%;
     height: 20%;
     max-width: 400px;
     max-height: 300px;
   }
@@ -716,16 +744,20 @@ html, .fx-embedded, #main,
     height: 100%;
     width: auto;
   }
 
   .standalone .media.nested {
     min-height: 500px;
   }
 
+  .standalone .remote-inset-stream {
+    display: none;
+  }
+
   .standalone .local-stream {
     flex: 1;
     min-width: 120px;
     min-height: 150px;
     width: 100%;
     box-shadow: none;
   }
 
@@ -963,17 +995,17 @@ html, .fx-embedded, #main,
   }
   .standalone .room-conversation .video_wrapper.remote_wrapper {
     width: 100%;
   }
   .standalone .conversation-toolbar {
     height: 38px;
     padding: 8px;
   }
-  .standalone .remote-stream {
+  .standalone .focus-stream {
     /* Set at maximum height, minus height of conversation toolbar */
     height: 100%;
   }
 
   .standalone .media.nested {
     /* This forces the remote video stream to fit within wrapper's height */
     min-height: 0px;
   }
--- a/browser/components/loop/content/shared/img/icons-10x10.svg
+++ b/browser/components/loop/content/shared/img/icons-10x10.svg
@@ -21,17 +21,21 @@ use[id$="-hover"] {
   fill: #444;
 }
 
 use[id$="-active"] {
   fill: #0095dd;
 }
 
 use[id$="-white"] {
-  fill: rgba(255, 255, 255, 0.8);
+  fill: rgba(255,255,255,0.8);
+}
+
+use[id$="-disabled"] {
+  fill: rgba(255,255,255,0.4);
 }
 </style>
 <defs style="display:none">
   <polygon id="close-shape" fill-rule="evenodd" clip-rule="evenodd" points="10,1.717 8.336,0.049 5.024,3.369 1.663,0 0,1.668 
     3.36,5.037 0.098,8.307 1.762,9.975 5.025,6.705 8.311,10 9.975,8.332 6.688,5.037"/>
   <path id="dropdown-shape" fill-rule="evenodd" clip-rule="evenodd" d="M9,3L4.984,7L1,3H9z"/>
   <polygon id="expand-shape" fill-rule="evenodd" clip-rule="evenodd" points="10,0 4.838,0 6.506,1.669 0,8.175 1.825,10 8.331,3.494 
     10,5.162"/>
--- a/browser/components/loop/content/shared/img/icons-16x16.svg
+++ b/browser/components/loop/content/shared/img/icons-16x16.svg
@@ -27,16 +27,20 @@ use[id$="-active"] {
 
 use[id$="-red"] {
   fill: #d74345
 }
 
 use[id$="-white"] {
   fill: #fff;
 }
+
+use[id$="-disabled"] {
+  fill: rgba(255,255,255,.6);
+}
 </style>
 <defs style="display:none">
   <path id="audio-shape" fill-rule="evenodd" clip-rule="evenodd" d="M11.429,6.857v2.286c0,1.894-1.535,3.429-3.429,3.429
     c-1.894,0-3.429-1.535-3.429-3.429V6.857H3.429v2.286c0,2.129,1.458,3.913,3.429,4.422v1.293H6.286
     c-0.746,0-1.379,0.477-1.615,1.143h6.658c-0.236-0.665-0.869-1.143-1.615-1.143H9.143v-1.293c1.971-0.508,3.429-2.292,3.429-4.422
     V6.857H11.429z M8,12c1.578,0,2.857-1.279,2.857-2.857V2.857C10.857,1.279,9.578,0,8,0C6.422,0,5.143,1.279,5.143,2.857v6.286
     C5.143,10.721,6.422,12,8,12z"/>
   <path id="block-shape" fill-rule="evenodd" clip-rule="evenodd" d="M8,0C3.582,0,0,3.582,0,8c0,4.418,3.582,8,8,8
@@ -176,10 +180,11 @@ use[id$="-white"] {
 <use id="unblock"             xlink:href="#unblock-shape"/>
 <use id="unblock-hover"       xlink:href="#unblock-shape"/>
 <use id="unblock-active"      xlink:href="#unblock-shape"/>
 <use id="video"               xlink:href="#video-shape"/>
 <use id="video-hover"         xlink:href="#video-shape"/>
 <use id="video-active"        xlink:href="#video-shape"/>
 <use id="tour"                xlink:href="#tour-shape"/>
 <use id="screen-white"        xlink:href="#screen-shape"/>
+<use id="screen-disabled"     xlink:href="#screen-shape"/>
 <use id="screenmute-white"    xlink:href="#screenmute-shape"/>
 </svg>
--- a/browser/components/loop/content/shared/js/mixins.js
+++ b/browser/components/loop/content/shared/js/mixins.js
@@ -96,16 +96,41 @@ loop.shared.mixins = (function() {
 
     _onBodyClick: function() {
       this.setState({showMenu: false});
     },
 
     componentDidMount: function() {
       this.documentBody.addEventListener("click", this._onBodyClick);
       this.documentBody.addEventListener("blur", this.hideDropdownMenu);
+
+      var menu = this.refs.menu;
+      if (!menu) {
+        return;
+      }
+
+      // Correct the position of the menu if necessary.
+      var menuNode = menu.getDOMNode();
+      var menuNodeRect = menuNode.getBoundingClientRect();
+      var bodyRect = {
+        height: this.documentBody.offsetHeight,
+        width: this.documentBody.offsetWidth
+      };
+
+      // First we check the vertical overflow.
+      var y = menuNodeRect.top + menuNodeRect.height;
+      if (y >= bodyRect.height) {
+        menuNode.style.marginTop = bodyRect.height - y + "px";
+      }
+
+      // Then we check the horizontal overflow.
+      var x = menuNodeRect.left + menuNodeRect.width;
+      if (x >= bodyRect.width) {
+        menuNode.style.marginLeft = bodyRect.width - x + "px";
+      }
     },
 
     componentWillUnmount: function() {
       this.documentBody.removeEventListener("click", this._onBodyClick);
       this.documentBody.removeEventListener("blur", this.hideDropdownMenu);
     },
 
     showDropdownMenu: function() {
@@ -270,24 +295,27 @@ loop.shared.mixins = (function() {
      *     offsetX: 20,
      *     offsetY: 0
      *   }
      *
      * Note: This expects a class on the element that has the name "remote" or the
      *       same name as the possible video types (currently only "screen").
      * Note: Once we support multiple remote video streams, this function will
      *       need to be updated.
+     *
+     * @param {string} videoType The video type according to the sdk, e.g. "camera" or
+     *                           "screen".
      * @return {Object} contains the remote stream dimension properties of its
      *                  container node, the stream itself and offset of the stream
      *                  relative to its container node in pixels.
      */
-    getRemoteVideoDimensions: function() {
+    getRemoteVideoDimensions: function(videoType) {
       var remoteVideoDimensions;
 
-      Object.keys(this._videoDimensionsCache.remote).forEach(function(videoType) {
+      if (videoType in this._videoDimensionsCache.remote) {
         var node = this._getElement("." + (videoType === "camera" ? "remote" : videoType));
         var width = node.offsetWidth;
         // If the width > 0 then we record its real size by taking its aspect
         // ratio in account. Due to the 'contain' fit-mode, the stream will be
         // centered inside the video element.
         // We'll need to deal with more than one remote video stream whenever
         // that becomes something we need to support.
         if (width) {
@@ -322,17 +350,17 @@ loop.shared.mixins = (function() {
             var leadingAxisSize = remoteVideoDimensions[slaveAxis] * ratio[leadingAxis];
 
             remoteVideoDimensions.streamWidth = leadingAxis === "height" ?
               remoteVideoDimensions.width : leadingAxisSize;
             remoteVideoDimensions.streamHeight = leadingAxis === "width" ?
               remoteVideoDimensions.height: leadingAxisSize;
           }
         }
-      }, this);
+      }
 
       // Supply some sensible defaults for the remoteVideoDimensions if no remote
       // stream is connected (yet).
       if (!remoteVideoDimensions) {
         var node = this._getElement(".remote");
         var width = node.offsetWidth;
         var height = node.offsetHeight;
         remoteVideoDimensions = {
@@ -382,25 +410,31 @@ loop.shared.mixins = (function() {
         }
         if (remoteStreamParent) {
           remoteStreamParent.style.height = "100%";
         }
         if (screenShareStreamParent) {
           screenShareStreamParent.style.height = "100%";
         }
 
-        // Update the position and dimensions of the containers of local video
-        // streams, if necessary. The consumer of this mixin should implement the
-        // actual updating mechanism.
+        // Update the position and dimensions of the containers of local and remote
+        // video streams, if necessary. The consumer of this mixin should implement
+        // the actual updating mechanism.
         Object.keys(this._videoDimensionsCache.local).forEach(function(videoType) {
           var ratio = this._videoDimensionsCache.local[videoType].aspectRatio;
           if (videoType == "camera" && this.updateLocalCameraPosition) {
             this.updateLocalCameraPosition(ratio);
           }
         }, this);
+        Object.keys(this._videoDimensionsCache.remote).forEach(function(videoType) {
+          var ratio = this._videoDimensionsCache.remote[videoType].aspectRatio;
+          if (videoType == "camera" && this.updateRemoteCameraPosition) {
+            this.updateRemoteCameraPosition(ratio);
+          }
+        }, this);
       }.bind(this), 0);
     },
 
     /**
      * Returns the default configuration for publishing media on the sdk.
      *
      * @param {Object} options An options object containing:
      * - publishVideo A boolean set to true to publish video when the stream is initiated.
--- a/browser/components/loop/content/shared/js/views.js
+++ b/browser/components/loop/content/shared/js/views.js
@@ -80,56 +80,79 @@ loop.shared.views = (function(_, l10n) {
    *
    * Required props:
    * - {loop.Dispatcher} dispatcher  The dispatcher instance
    * - {Boolean}         visible     Set to true to display the button
    * - {String}          state       One of the screen sharing states, see
    *                                 loop.shared.utils.SCREEN_SHARE_STATES
    */
   var ScreenShareControlButton = React.createClass({displayName: "ScreenShareControlButton",
+    mixins: [sharedMixins.DropdownMenuMixin],
+
     propTypes: {
       dispatcher: React.PropTypes.instanceOf(loop.Dispatcher).isRequired,
       visible: React.PropTypes.bool.isRequired,
       state: React.PropTypes.string.isRequired,
     },
 
     handleClick: function() {
       if (this.props.state === SCREEN_SHARE_STATES.ACTIVE) {
         this.props.dispatcher.dispatch(
           new sharedActions.EndScreenShare({}));
       } else {
-        this.props.dispatcher.dispatch(
-          new sharedActions.StartScreenShare({}));
+        this.toggleDropdownMenu();
       }
     },
 
+    _handleShareWindows: function() {
+      this.props.dispatcher.dispatch(new sharedActions.StartScreenShare({}));
+    },
+
     _getTitle: function() {
       var prefix = this.props.state === SCREEN_SHARE_STATES.ACTIVE ?
         "active" : "inactive";
 
       return l10n.get(prefix + "_screenshare_button_title");
     },
 
     render: function() {
       if (!this.props.visible) {
         return null;
       }
 
-      var screenShareClasses = React.addons.classSet({
+      var cx = React.addons.classSet;
+
+      var isActive = this.props.state === SCREEN_SHARE_STATES.ACTIVE;
+      var screenShareClasses = cx({
         "btn": true,
         "btn-screen-share": true,
         "transparent-button": true,
-        "active": this.props.state === SCREEN_SHARE_STATES.ACTIVE,
+        "menu-showing": this.state.showMenu,
+        "active": isActive,
         "disabled": this.props.state === SCREEN_SHARE_STATES.PENDING
       });
+      var dropdownMenuClasses = cx({
+        "native-dropdown-menu": true,
+        "conversation-window-dropdown": true,
+        "visually-hidden": !this.state.showMenu
+      });
 
       return (
-        React.createElement("button", {className: screenShareClasses, 
-                onClick: this.handleClick, 
-                title: this._getTitle()})
+        React.createElement("div", null, 
+          React.createElement("button", {className: screenShareClasses, 
+                  onClick: this.handleClick, 
+                  title: this._getTitle()}, 
+            isActive ? null : React.createElement("span", {className: "chevron"})
+          ), 
+          React.createElement("ul", {ref: "menu", className: dropdownMenuClasses}, 
+            React.createElement("li", {onClick: this._handleShareWindows}, 
+              l10n.get("share_windows_button_title")
+            )
+          )
+        )
       );
     }
   });
 
   /**
    * Conversation controls.
    */
   var ConversationToolbar = React.createClass({displayName: "ConversationToolbar",
@@ -353,17 +376,17 @@ loop.shared.views = (function(_, l10n) {
         "local-stream-audio": !this.state.video.enabled
       });
       /* jshint ignore:start */
       return (
         React.createElement("div", {className: "video-layout-wrapper"}, 
           React.createElement("div", {className: "conversation in-call"}, 
             React.createElement("div", {className: "media nested"}, 
               React.createElement("div", {className: "video_wrapper remote_wrapper"}, 
-                React.createElement("div", {className: "video_inner remote remote-stream"})
+                React.createElement("div", {className: "video_inner remote focus-stream"})
               ), 
               React.createElement("div", {className: localStreamClasses})
             ), 
             React.createElement(ConversationToolbar, {video: this.state.video, 
                                  audio: this.state.audio, 
                                  publishStream: this.publishStream, 
                                  hangup: this.hangup})
           )
--- a/browser/components/loop/content/shared/js/views.jsx
+++ b/browser/components/loop/content/shared/js/views.jsx
@@ -80,56 +80,79 @@ loop.shared.views = (function(_, l10n) {
    *
    * Required props:
    * - {loop.Dispatcher} dispatcher  The dispatcher instance
    * - {Boolean}         visible     Set to true to display the button
    * - {String}          state       One of the screen sharing states, see
    *                                 loop.shared.utils.SCREEN_SHARE_STATES
    */
   var ScreenShareControlButton = React.createClass({
+    mixins: [sharedMixins.DropdownMenuMixin],
+
     propTypes: {
       dispatcher: React.PropTypes.instanceOf(loop.Dispatcher).isRequired,
       visible: React.PropTypes.bool.isRequired,
       state: React.PropTypes.string.isRequired,
     },
 
     handleClick: function() {
       if (this.props.state === SCREEN_SHARE_STATES.ACTIVE) {
         this.props.dispatcher.dispatch(
           new sharedActions.EndScreenShare({}));
       } else {
-        this.props.dispatcher.dispatch(
-          new sharedActions.StartScreenShare({}));
+        this.toggleDropdownMenu();
       }
     },
 
+    _handleShareWindows: function() {
+      this.props.dispatcher.dispatch(new sharedActions.StartScreenShare({}));
+    },
+
     _getTitle: function() {
       var prefix = this.props.state === SCREEN_SHARE_STATES.ACTIVE ?
         "active" : "inactive";
 
       return l10n.get(prefix + "_screenshare_button_title");
     },
 
     render: function() {
       if (!this.props.visible) {
         return null;
       }
 
-      var screenShareClasses = React.addons.classSet({
+      var cx = React.addons.classSet;
+
+      var isActive = this.props.state === SCREEN_SHARE_STATES.ACTIVE;
+      var screenShareClasses = cx({
         "btn": true,
         "btn-screen-share": true,
         "transparent-button": true,
-        "active": this.props.state === SCREEN_SHARE_STATES.ACTIVE,
+        "menu-showing": this.state.showMenu,
+        "active": isActive,
         "disabled": this.props.state === SCREEN_SHARE_STATES.PENDING
       });
+      var dropdownMenuClasses = cx({
+        "native-dropdown-menu": true,
+        "conversation-window-dropdown": true,
+        "visually-hidden": !this.state.showMenu
+      });
 
       return (
-        <button className={screenShareClasses}
-                onClick={this.handleClick}
-                title={this._getTitle()}></button>
+        <div>
+          <button className={screenShareClasses}
+                  onClick={this.handleClick}
+                  title={this._getTitle()}>
+            {isActive ? null : <span className="chevron"/>}
+          </button>
+          <ul ref="menu" className={dropdownMenuClasses}>
+            <li onClick={this._handleShareWindows}>
+              {l10n.get("share_windows_button_title")}
+            </li>
+          </ul>
+        </div>
       );
     }
   });
 
   /**
    * Conversation controls.
    */
   var ConversationToolbar = React.createClass({
@@ -353,17 +376,17 @@ loop.shared.views = (function(_, l10n) {
         "local-stream-audio": !this.state.video.enabled
       });
       /* jshint ignore:start */
       return (
         <div className="video-layout-wrapper">
           <div className="conversation in-call">
             <div className="media nested">
               <div className="video_wrapper remote_wrapper">
-                <div className="video_inner remote remote-stream"></div>
+                <div className="video_inner remote focus-stream"></div>
               </div>
               <div className={localStreamClasses}></div>
             </div>
             <ConversationToolbar video={this.state.video}
                                  audio={this.state.audio}
                                  publishStream={this.publishStream}
                                  hangup={this.hangup} />
           </div>
--- a/browser/components/loop/standalone/content/js/standaloneRoomViews.js
+++ b/browser/components/loop/standalone/content/js/standaloneRoomViews.js
@@ -258,16 +258,26 @@ loop.standaloneRoomViews = (function(moz
 
       if (this.state.roomState !== ROOM_STATES.JOINED &&
           nextState.roomState === ROOM_STATES.JOINED) {
         // This forces the video size to update - creating the publisher
         // first, and then connecting to the session doesn't seem to set the
         // initial size correctly.
         this.updateVideoContainer();
       }
+
+      // When screen sharing stops.
+      if (this.state.receivingScreenShare && !nextState.receivingScreenShare) {
+        // Remove the custom screenshare styles on the remote camera.
+        var node = this._getElement(".remote");
+        node.removeAttribute("style");
+
+        // Force the video sizes to update.
+        this.updateVideoContainer();
+      }
     },
 
     joinRoom: function() {
       this.props.dispatcher.dispatch(new sharedActions.JoinRoom());
     },
 
     leaveRoom: function() {
       this.props.dispatcher.dispatch(new sharedActions.LeaveRoom());
@@ -312,18 +322,20 @@ loop.standaloneRoomViews = (function(moz
         node.style.width = (targetWidth * ratio.width) + "px";
         node.style.height = (targetWidth * ratio.height) + "px";
         node.style.left = "auto";
       } else {
         // The local camera view should be a quarter of the size of the remote stream
         // and positioned to overlap with the remote stream at a quarter of its width.
 
         // Now position the local camera view correctly with respect to the remote
-        // video stream.
-        var remoteVideoDimensions = this.getRemoteVideoDimensions();
+        // video stream or the screen share stream.
+        var remoteVideoDimensions = this.getRemoteVideoDimensions(
+          this.state.receivingScreenShare ? "screen" : "camera");
+
         targetWidth = remoteVideoDimensions.streamWidth * LOCAL_STREAM_SIZE;
 
         var realWidth = targetWidth * ratio.width;
         var realHeight = targetWidth * ratio.height;
 
         // If we've hit the min size limits, then limit at the minimum.
         if (realWidth < SDK_MIN_SIZE) {
           realWidth = SDK_MIN_SIZE;
@@ -341,16 +353,63 @@ loop.standaloneRoomViews = (function(moz
         // ratio.
         node.style.left = (offsetX - (realWidth * LOCAL_STREAM_OVERLAP)) + "px";
         node.style.width = realWidth + "px";
         node.style.height = realHeight + "px";
       }
     },
 
     /**
+     * Specifically updates the remote camera stream size and position, if
+     * a screen share is being received. It is slaved from the position of the
+     * local stream.
+     * This method gets called from `updateVideoContainer`, which is defined in
+     * the `MediaSetupMixin`.
+     *
+     * @param  {Object} ratio Aspect ratio of the remote camera stream
+     */
+    updateRemoteCameraPosition: function(ratio) {
+      // Nothing to do for screenshare
+      if (!this.state.receivingScreenShare) {
+        return;
+      }
+      // XXX For the time being, if we're a narrow screen, aka mobile, we don't display
+      // the remote media (bug 1133534).
+      if (window.matchMedia && window.matchMedia("screen and (max-width:640px)").matches) {
+        return;
+      }
+
+      // 10px separation between the two streams.
+      var LOCAL_REMOTE_SEPARATION = 10;
+
+      var node = this._getElement(".remote");
+      var localNode = this._getElement(".local");
+
+      // Match the width to the local video.
+      node.style.width = localNode.offsetWidth + "px";
+
+      // The height is then determined from the aspect ratio
+      var height = ((localNode.offsetWidth / ratio.width) * ratio.height);
+      node.style.height = height + "px";
+
+      node.style.right = "auto";
+      node.style.bottom = "auto";
+
+      // Now position the local camera view correctly with respect to the remote
+      // video stream.
+
+      // The top is measured from the top of the element down the screen,
+      // so subtract the height of the video and the separation distance.
+      node.style.top = (localNode.offsetTop - height - LOCAL_REMOTE_SEPARATION) + "px";
+
+      // Match the left-hand sides.
+      node.style.left = localNode.offsetLeft + "px";
+    },
+
+    /**
      * Checks if current room is active.
      *
      * @return {Boolean}
      */
     _roomIsActive: function() {
       return this.state.roomState === ROOM_STATES.JOINED            ||
              this.state.roomState === ROOM_STATES.SESSION_CONNECTED ||
              this.state.roomState === ROOM_STATES.HAS_PARTICIPANTS;
@@ -362,24 +421,24 @@ loop.standaloneRoomViews = (function(moz
         local: true,
         "local-stream": true,
         "local-stream-audio": this.state.videoMuted
       });
 
       var remoteStreamClasses = React.addons.classSet({
         "video_inner": true,
         "remote": true,
-        "remote-stream": true,
-        hide: this.state.receivingScreenShare
+        "focus-stream": !this.state.receivingScreenShare,
+        "remote-inset-stream": this.state.receivingScreenShare
       });
 
       var screenShareStreamClasses = React.addons.classSet({
         "screen": true,
-        "remote-stream": true,
-        hide: !this.state.receivingScreenShare
+        "focus-stream": this.state.receivingScreenShare,
+        hide: !this.state.receivingScreenShare,
       });
 
       return (
         React.createElement("div", {className: "room-conversation-wrapper"}, 
           React.createElement("div", {className: "beta-logo"}), 
           React.createElement(StandaloneRoomHeader, null), 
           React.createElement(StandaloneRoomInfoArea, {roomState: this.state.roomState, 
                                   failureReason: this.state.failureReason, 
--- a/browser/components/loop/standalone/content/js/standaloneRoomViews.jsx
+++ b/browser/components/loop/standalone/content/js/standaloneRoomViews.jsx
@@ -258,16 +258,26 @@ loop.standaloneRoomViews = (function(moz
 
       if (this.state.roomState !== ROOM_STATES.JOINED &&
           nextState.roomState === ROOM_STATES.JOINED) {
         // This forces the video size to update - creating the publisher
         // first, and then connecting to the session doesn't seem to set the
         // initial size correctly.
         this.updateVideoContainer();
       }
+
+      // When screen sharing stops.
+      if (this.state.receivingScreenShare && !nextState.receivingScreenShare) {
+        // Remove the custom screenshare styles on the remote camera.
+        var node = this._getElement(".remote");
+        node.removeAttribute("style");
+
+        // Force the video sizes to update.
+        this.updateVideoContainer();
+      }
     },
 
     joinRoom: function() {
       this.props.dispatcher.dispatch(new sharedActions.JoinRoom());
     },
 
     leaveRoom: function() {
       this.props.dispatcher.dispatch(new sharedActions.LeaveRoom());
@@ -312,18 +322,20 @@ loop.standaloneRoomViews = (function(moz
         node.style.width = (targetWidth * ratio.width) + "px";
         node.style.height = (targetWidth * ratio.height) + "px";
         node.style.left = "auto";
       } else {
         // The local camera view should be a quarter of the size of the remote stream
         // and positioned to overlap with the remote stream at a quarter of its width.
 
         // Now position the local camera view correctly with respect to the remote
-        // video stream.
-        var remoteVideoDimensions = this.getRemoteVideoDimensions();
+        // video stream or the screen share stream.
+        var remoteVideoDimensions = this.getRemoteVideoDimensions(
+          this.state.receivingScreenShare ? "screen" : "camera");
+
         targetWidth = remoteVideoDimensions.streamWidth * LOCAL_STREAM_SIZE;
 
         var realWidth = targetWidth * ratio.width;
         var realHeight = targetWidth * ratio.height;
 
         // If we've hit the min size limits, then limit at the minimum.
         if (realWidth < SDK_MIN_SIZE) {
           realWidth = SDK_MIN_SIZE;
@@ -341,16 +353,63 @@ loop.standaloneRoomViews = (function(moz
         // ratio.
         node.style.left = (offsetX - (realWidth * LOCAL_STREAM_OVERLAP)) + "px";
         node.style.width = realWidth + "px";
         node.style.height = realHeight + "px";
       }
     },
 
     /**
+     * Specifically updates the remote camera stream size and position, if
+     * a screen share is being received. It is slaved from the position of the
+     * local stream.
+     * This method gets called from `updateVideoContainer`, which is defined in
+     * the `MediaSetupMixin`.
+     *
+     * @param  {Object} ratio Aspect ratio of the remote camera stream
+     */
+    updateRemoteCameraPosition: function(ratio) {
+      // Nothing to do for screenshare
+      if (!this.state.receivingScreenShare) {
+        return;
+      }
+      // XXX For the time being, if we're a narrow screen, aka mobile, we don't display
+      // the remote media (bug 1133534).
+      if (window.matchMedia && window.matchMedia("screen and (max-width:640px)").matches) {
+        return;
+      }
+
+      // 10px separation between the two streams.
+      var LOCAL_REMOTE_SEPARATION = 10;
+
+      var node = this._getElement(".remote");
+      var localNode = this._getElement(".local");
+
+      // Match the width to the local video.
+      node.style.width = localNode.offsetWidth + "px";
+
+      // The height is then determined from the aspect ratio
+      var height = ((localNode.offsetWidth / ratio.width) * ratio.height);
+      node.style.height = height + "px";
+
+      node.style.right = "auto";
+      node.style.bottom = "auto";
+
+      // Now position the local camera view correctly with respect to the remote
+      // video stream.
+
+      // The top is measured from the top of the element down the screen,
+      // so subtract the height of the video and the separation distance.
+      node.style.top = (localNode.offsetTop - height - LOCAL_REMOTE_SEPARATION) + "px";
+
+      // Match the left-hand sides.
+      node.style.left = localNode.offsetLeft + "px";
+    },
+
+    /**
      * Checks if current room is active.
      *
      * @return {Boolean}
      */
     _roomIsActive: function() {
       return this.state.roomState === ROOM_STATES.JOINED            ||
              this.state.roomState === ROOM_STATES.SESSION_CONNECTED ||
              this.state.roomState === ROOM_STATES.HAS_PARTICIPANTS;
@@ -362,24 +421,24 @@ loop.standaloneRoomViews = (function(moz
         local: true,
         "local-stream": true,
         "local-stream-audio": this.state.videoMuted
       });
 
       var remoteStreamClasses = React.addons.classSet({
         "video_inner": true,
         "remote": true,
-        "remote-stream": true,
-        hide: this.state.receivingScreenShare
+        "focus-stream": !this.state.receivingScreenShare,
+        "remote-inset-stream": this.state.receivingScreenShare
       });
 
       var screenShareStreamClasses = React.addons.classSet({
         "screen": true,
-        "remote-stream": true,
-        hide: !this.state.receivingScreenShare
+        "focus-stream": this.state.receivingScreenShare,
+        hide: !this.state.receivingScreenShare,
       });
 
       return (
         <div className="room-conversation-wrapper">
           <div className="beta-logo" />
           <StandaloneRoomHeader />
           <StandaloneRoomInfoArea roomState={this.state.roomState}
                                   failureReason={this.state.failureReason}
--- a/browser/components/loop/test/shared/mixins_test.js
+++ b/browser/components/loop/test/shared/mixins_test.js
@@ -263,32 +263,32 @@ describe("loop.shared.mixins", function(
             height: 480
           }
         };
       });
 
       it("should fetch the correct stream sizes for leading axis width and full",
         function() {
           remoteVideoDimensions = {
-            camera: {
+            screen: {
               width: 240,
               height: 320
             }
           };
-          remoteElement = {
+          screenShareElement = {
             offsetWidth: 480,
             offsetHeight: 700
           };
 
           view.updateVideoDimensions(localVideoDimensions, remoteVideoDimensions);
-          var result = view.getRemoteVideoDimensions();
+          var result = view.getRemoteVideoDimensions("screen");
 
-          expect(result.width).eql(remoteElement.offsetWidth);
-          expect(result.height).eql(remoteElement.offsetHeight);
-          expect(result.streamWidth).eql(remoteElement.offsetWidth);
+          expect(result.width).eql(screenShareElement.offsetWidth);
+          expect(result.height).eql(screenShareElement.offsetHeight);
+          expect(result.streamWidth).eql(screenShareElement.offsetWidth);
           // The real height of the stream accounting for the aspect ratio.
           expect(result.streamHeight).eql(640);
           expect(result.offsetX).eql(0);
           // The remote element height (700) minus the stream height (640) split in 2.
           expect(result.offsetY).eql(30);
         });
 
       it("should fetch the correct stream sizes for leading axis width and not full",
@@ -300,49 +300,49 @@ describe("loop.shared.mixins", function(
             }
           };
           remoteElement = {
             offsetWidth: 640,
             offsetHeight: 480
           };
 
           view.updateVideoDimensions(localVideoDimensions, remoteVideoDimensions);
-          var result = view.getRemoteVideoDimensions();
+          var result = view.getRemoteVideoDimensions("camera");
 
           expect(result.width).eql(remoteElement.offsetWidth);
           expect(result.height).eql(remoteElement.offsetHeight);
           // Aspect ratio modified from the height.
           expect(result.streamWidth).eql(360);
           expect(result.streamHeight).eql(remoteElement.offsetHeight);
           // The remote element width (640) minus the stream width (360) split in 2.
           expect(result.offsetX).eql(140);
           expect(result.offsetY).eql(0);
         });
 
       it("should fetch the correct stream sizes for leading axis height and full",
         function() {
           remoteVideoDimensions = {
-            camera: {
+            screen: {
               width: 320,
               height: 240
             }
           };
-          remoteElement = {
+          screenShareElement = {
             offsetWidth: 700,
             offsetHeight: 480
           };
 
           view.updateVideoDimensions(localVideoDimensions, remoteVideoDimensions);
-          var result = view.getRemoteVideoDimensions();
+          var result = view.getRemoteVideoDimensions("screen");
 
-          expect(result.width).eql(remoteElement.offsetWidth);
-          expect(result.height).eql(remoteElement.offsetHeight);
+          expect(result.width).eql(screenShareElement.offsetWidth);
+          expect(result.height).eql(screenShareElement.offsetHeight);
           // The real width of the stream accounting for the aspect ratio.
           expect(result.streamWidth).eql(640);
-          expect(result.streamHeight).eql(remoteElement.offsetHeight);
+          expect(result.streamHeight).eql(screenShareElement.offsetHeight);
           // The remote element width (700) minus the stream width (640) split in 2.
           expect(result.offsetX).eql(30);
           expect(result.offsetY).eql(0);
         });
 
       it("should fetch the correct stream sizes for leading axis height and not full",
         function() {
           remoteVideoDimensions = {
@@ -352,17 +352,17 @@ describe("loop.shared.mixins", function(
             }
           };
           remoteElement = {
             offsetWidth: 480,
             offsetHeight: 640
           };
 
           view.updateVideoDimensions(localVideoDimensions, remoteVideoDimensions);
-          var result = view.getRemoteVideoDimensions();
+          var result = view.getRemoteVideoDimensions("camera");
 
           expect(result.width).eql(remoteElement.offsetWidth);
           expect(result.height).eql(remoteElement.offsetHeight);
           expect(result.streamWidth).eql(remoteElement.offsetWidth);
           // Aspect ratio modified from the width.
           expect(result.streamHeight).eql(360);
           expect(result.offsetX).eql(0);
           // The remote element width (640) minus the stream width (360) split in 2.
--- a/browser/components/loop/test/shared/views_test.js
+++ b/browser/components/loop/test/shared/views_test.js
@@ -113,58 +113,77 @@ describe("loop.shared.views", function()
     it("should render a disabled share button when share is pending", function() {
       var comp = TestUtils.renderIntoDocument(
         React.createElement(sharedViews.ScreenShareControlButton, {
           dispatcher: dispatcher,
           visible: true,
           state: SCREEN_SHARE_STATES.PENDING
         }));
 
-      expect(comp.getDOMNode().classList.contains("active")).eql(false);
-      expect(comp.getDOMNode().classList.contains("disabled")).eql(true);
+      var node = comp.getDOMNode().querySelector(".btn-screen-share");
+      expect(node.classList.contains("active")).eql(false);
+      expect(node.classList.contains("disabled")).eql(true);
     });
 
     it("should render an active share button", function() {
       var comp = TestUtils.renderIntoDocument(
         React.createElement(sharedViews.ScreenShareControlButton, {
           dispatcher: dispatcher,
           visible: true,
           state: SCREEN_SHARE_STATES.ACTIVE
         }));
 
-      expect(comp.getDOMNode().classList.contains("active")).eql(true);
-      expect(comp.getDOMNode().classList.contains("disabled")).eql(false);
+      var node = comp.getDOMNode().querySelector(".btn-screen-share");
+      expect(node.classList.contains("active")).eql(true);
+      expect(node.classList.contains("disabled")).eql(false);
     });
 
-    it("should dispatch a StartScreenShare action on click when the state is not active",
+    it("should show the screenshare dropdown on click when the state is not active",
        function() {
         var comp = TestUtils.renderIntoDocument(
           React.createElement(sharedViews.ScreenShareControlButton, {
             dispatcher: dispatcher,
             visible: true,
             state: SCREEN_SHARE_STATES.INACTIVE
           }));
 
-        TestUtils.Simulate.click(comp.getDOMNode());
+        expect(comp.state.showMenu).eql(false);
+
+        TestUtils.Simulate.click(comp.getDOMNode().querySelector(".btn-screen-share"));
+
+        expect(comp.state.showMenu).eql(true);
+      });
+
+    it("should dispatch a StartScreenShare action on option click in screenshare dropdown",
+      function() {
+        var comp = TestUtils.renderIntoDocument(
+          React.createElement(sharedViews.ScreenShareControlButton, {
+            dispatcher: dispatcher,
+            visible: true,
+            state: SCREEN_SHARE_STATES.INACTIVE
+          }));
+
+        TestUtils.Simulate.click(comp.getDOMNode().querySelector(
+          ".conversation-window-dropdown > li"));
 
         sinon.assert.calledOnce(dispatcher.dispatch);
         sinon.assert.calledWithExactly(dispatcher.dispatch,
           new sharedActions.StartScreenShare({}));
       });
 
     it("should dispatch a EndScreenShare action on click when the state is active",
       function() {
         var comp = TestUtils.renderIntoDocument(
           React.createElement(sharedViews.ScreenShareControlButton, {
             dispatcher: dispatcher,
             visible: true,
             state: SCREEN_SHARE_STATES.ACTIVE
           }));
 
-        TestUtils.Simulate.click(comp.getDOMNode());
+        TestUtils.Simulate.click(comp.getDOMNode().querySelector(".btn-screen-share"));
 
         sinon.assert.calledOnce(dispatcher.dispatch);
         sinon.assert.calledWithExactly(dispatcher.dispatch,
           new sharedActions.EndScreenShare({}));
       });
   });
 
   describe("ConversationToolbar", function() {
--- a/browser/components/loop/test/standalone/standaloneRoomViews_test.js
+++ b/browser/components/loop/test/standalone/standaloneRoomViews_test.js
@@ -240,16 +240,100 @@ describe("loop.standaloneRoomViews", fun
           height: 1
         });
 
         expect(localElement.style.width).eql("120px");
         expect(localElement.style.left).eql("610px");
       });
     });
 
+    describe("Remote Stream Size Position", function() {
+      var view, localElement, remoteElement;
+
+      beforeEach(function() {
+        sandbox.stub(window, "matchMedia").returns({
+          matches: false
+        });
+        view = mountTestComponent();
+
+        localElement = {
+          style: {}
+        };
+        remoteElement = {
+          style: {},
+          removeAttribute: sinon.spy()
+        };
+
+        sandbox.stub(view, "_getElement", function(className) {
+          return className === ".local" ? localElement : remoteElement;
+        });
+
+        view.setState({"receivingScreenShare": true});
+      });
+
+      it("should do nothing if not receiving screenshare", function() {
+        view.setState({"receivingScreenShare": false});
+        remoteElement.style.width = "10px";
+
+        view.updateRemoteCameraPosition({
+          width: 1,
+          height: 0.75
+        });
+
+        expect(remoteElement.style.width).eql("10px");
+      });
+
+      it("should be the same width as the local video", function() {
+        localElement.offsetWidth = 100;
+
+        view.updateRemoteCameraPosition({
+          width: 1,
+          height: 0.75
+        });
+
+        expect(remoteElement.style.width).eql("100px");
+      });
+
+      it("should be the same left edge as the local video", function() {
+        localElement.offsetLeft = 50;
+
+        view.updateRemoteCameraPosition({
+          width: 1,
+          height: 0.75
+        });
+
+        expect(remoteElement.style.left).eql("50px");
+      });
+
+      it("should have a height determined by the aspect ratio", function() {
+        localElement.offsetWidth = 100;
+
+        view.updateRemoteCameraPosition({
+          width: 1,
+          height: 0.75
+        });
+
+        expect(remoteElement.style.height).eql("75px");
+      });
+
+      it("should have the top be set such that the bottom is 10px above the local video", function() {
+        localElement.offsetWidth = 100;
+        localElement.offsetTop = 200;
+
+        view.updateRemoteCameraPosition({
+          width: 1,
+          height: 0.75
+        });
+
+        // 200 (top) - 75 (height) - 10 (spacing) = 115
+        expect(remoteElement.style.top).eql("115px");
+      });
+
+    });
+
     describe("#render", function() {
       var view;
 
       beforeEach(function() {
         view = mountTestComponent();
       });
 
       describe("Empty room message", function() {
--- a/browser/components/loop/ui/index.html
+++ b/browser/components/loop/ui/index.html
@@ -47,19 +47,20 @@
     <script src="../content/shared/js/fxOSActiveRoomStore.js"></script>
     <script src="../content/shared/js/activeRoomStore.js"></script>
     <script src="../content/shared/js/feedbackStore.js"></script>
     <script src="../content/shared/js/views.js"></script>
     <script src="../content/shared/js/feedbackViews.js"></script>
     <script src="../content/js/roomViews.js"></script>
     <script src="../content/js/conversationViews.js"></script>
     <script src="../content/js/client.js"></script>
-    <script src="../content/js/fxOSMarketplace.js"></script>
-    <script src="../content/js/webapp.js"></script>
-    <script src="../content/js/standaloneRoomViews.js"></script>
+    <script src="../standalone/content/js/multiplexGum.js"></script>
+    <script src="../standalone/content/js/webapp.js"></script>
+    <script src="../standalone/content/js/standaloneRoomViews.js"></script>
+    <script src="../standalone/content/js/fxOSMarketplace.js"></script>
     <script type="text/javascript;version=1.8" src="../content/js/contacts.js"></script>
     <script>
       if (!loop.contacts) {
         // For browsers that don't support ES6 without special flags (all but Fx
         // at the moment), we shim the contacts namespace with its most barebone
         // implementation.
         loop.contacts = {
           ContactsList: React.createClass({render: function() {
--- a/browser/components/loop/ui/ui-showcase.css
+++ b/browser/components/loop/ui/ui-showcase.css
@@ -165,26 +165,38 @@
 
 /* Rooms edge cases */
 .standalone .room-conversation .remote_wrapper {
   background: none;
 }
 
 /* SVG icons showcase */
 
+.svg-icons h3 {
+  clear: left;
+}
+
+.svg-icon-list {
+  display: block;
+  margin: .5rem 0;
+  clear: left;
+}
+
 .svg-icon-entry {
   width: 180px;
   float: left;
+  background-color: rgba(255,0,255,.1)
 }
 
 .svg-icon-entry > p {
   float: left;
   margin-right: .5rem;
 }
 
 .svg-icon {
   display: inline-block;
   width:  16px;
   height: 16px;
+  margin-left: .5rem;
   background-repeat: no-repeat;
   background-size: 16px 16px;
   background-position: center;
 }
--- a/browser/components/loop/ui/ui-showcase.js
+++ b/browser/components/loop/ui/ui-showcase.js
@@ -119,45 +119,64 @@
     level: "error",
     message: "Could Not Authenticate",
     details: "Did you change your password?",
     detailsButtonLabel: "Retry",
   });
 
   var SVGIcon = React.createClass({displayName: "SVGIcon",
     render: function() {
+      var sizeUnit = this.props.size.split("x")[0] + "px";
       return (
         React.createElement("span", {className: "svg-icon", style: {
-          "background-image": "url(/content/shared/img/icons-16x16.svg#" + this.props.shapeId + ")"
+          "backgroundImage": "url(../content/shared/img/icons-" + this.props.size +
+                              ".svg#" + this.props.shapeId + ")",
+          "backgroundSize": sizeUnit + " " + sizeUnit
         }})
       );
     }
   });
 
   var SVGIcons = React.createClass({displayName: "SVGIcons",
-    shapes: [
-      "audio", "audio-hover", "audio-active", "block",
-      "block-red", "block-hover", "block-active", "contacts", "contacts-hover",
-      "contacts-active", "copy", "checkmark", "google", "google-hover",
-      "google-active", "history", "history-hover", "history-active", "leave",
-      "precall", "precall-hover", "precall-active", "settings", "settings-hover",
-      "settings-active", "tag", "tag-hover", "tag-active", "trash", "unblock",
-      "unblock-hover", "unblock-active", "video", "video-hover", "video-active"
-    ],
+    shapes: {
+      "10x10": ["close", "close-active", "close-disabled", "dropdown",
+        "dropdown-white", "dropdown-active", "dropdown-disabled", "expand",
+        "expand-active", "expand-disabled", "minimize", "minimize-active",
+        "minimize-disabled"
+      ],
+      "14x14": ["audio", "audio-active", "audio-disabled", "facemute",
+        "facemute-active", "facemute-disabled", "hangup", "hangup-active",
+        "hangup-disabled", "incoming", "incoming-active", "incoming-disabled",
+        "link", "link-active", "link-disabled", "mute", "mute-active",
+        "mute-disabled", "pause", "pause-active", "pause-disabled", "video",
+        "video-white", "video-active", "video-disabled", "volume", "volume-active",
+        "volume-disabled"
+      ],
+      "16x16": ["audio", "audio-hover", "audio-active", "block", "block-red",
+        "block-hover", "block-active", "contacts", "contacts-hover", "contacts-active",
+        "copy", "checkmark", "google", "google-hover", "google-active", "history",
+        "history-hover", "history-active", "leave", "precall", "precall-hover",
+        "precall-active", "screen-white", "screenmute-white", "settings",
+        "settings-hover", "settings-active", "tag", "tag-hover", "tag-active",
+        "trash", "unblock", "unblock-hover", "unblock-active", "video", "video-hover",
+        "video-active", "tour"
+      ]
+    },
 
     render: function() {
+      var icons = this.shapes[this.props.size].map(function(shapeId, i) {
+        return (
+          React.createElement("li", {key: this.props.size + "-" + i, className: "svg-icon-entry"}, 
+            React.createElement("p", null, React.createElement(SVGIcon, {shapeId: shapeId, size: this.props.size})), 
+            React.createElement("p", null, shapeId)
+          )
+        );
+      }, this);
       return (
-        React.createElement("div", {className: "svg-icon-list"}, 
-          this.shapes.map(function(shapeId, i) {
-            return React.createElement("div", {key: i, className: "svg-icon-entry"}, 
-              React.createElement("p", null, React.createElement(SVGIcon, {shapeId: shapeId})), 
-              React.createElement("p", null, shapeId)
-            );
-          }, this)
-        )
+        React.createElement("ul", {className: "svg-icon-list"}, icons)
       );
     }
   });
 
   var Example = React.createClass({displayName: "Example",
     makeId: function(prefix) {
       return (prefix || "") + this.props.summary.toLowerCase().replace(/\s/g, "-");
     },
@@ -166,28 +185,28 @@
       var cx = React.addons.classSet;
       return (
         React.createElement("div", {className: "example"}, 
           React.createElement("h3", {id: this.makeId()}, 
             this.props.summary, 
             React.createElement("a", {href: this.makeId("#")}, " ¶")
           ), 
           React.createElement("div", {className: cx({comp: true, dashed: this.props.dashed}), 
-               style: this.props.style || {}}, 
+               style: this.props.style}, 
             this.props.children
           )
         )
       );
     }
   });
 
   var Section = React.createClass({displayName: "Section",
     render: function() {
       return (
-        React.createElement("section", {id: this.props.name}, 
+        React.createElement("section", {id: this.props.name, className: this.props.className}, 
           React.createElement("h1", null, this.props.name), 
           this.props.children
         )
       );
     }
   });
 
   var ShowCase = React.createClass({displayName: "ShowCase",
@@ -652,19 +671,25 @@
                   dispatcher: dispatcher, 
                   activeRoomStore: activeRoomStore, 
                   roomState: ROOM_STATES.FAILED, 
                   isFirefox: false})
               )
             )
           ), 
 
-          React.createElement(Section, {name: "SVG icons preview"}, 
+          React.createElement(Section, {name: "SVG icons preview", className: "svg-icons"}, 
+            React.createElement(Example, {summary: "10x10"}, 
+              React.createElement(SVGIcons, {size: "10x10"})
+            ), 
+            React.createElement(Example, {summary: "14x14"}, 
+              React.createElement(SVGIcons, {size: "14x14"})
+            ), 
             React.createElement(Example, {summary: "16x16"}, 
-              React.createElement(SVGIcons, null)
+              React.createElement(SVGIcons, {size: "16x16"})
             )
           )
 
         )
       );
     }
   });
 
--- a/browser/components/loop/ui/ui-showcase.jsx
+++ b/browser/components/loop/ui/ui-showcase.jsx
@@ -119,45 +119,64 @@
     level: "error",
     message: "Could Not Authenticate",
     details: "Did you change your password?",
     detailsButtonLabel: "Retry",
   });
 
   var SVGIcon = React.createClass({
     render: function() {
+      var sizeUnit = this.props.size.split("x")[0] + "px";
       return (
         <span className="svg-icon" style={{
-          "background-image": "url(/content/shared/img/icons-16x16.svg#" + this.props.shapeId + ")"
+          "backgroundImage": "url(../content/shared/img/icons-" + this.props.size +
+                              ".svg#" + this.props.shapeId + ")",
+          "backgroundSize": sizeUnit + " " + sizeUnit
         }} />
       );
     }
   });
 
   var SVGIcons = React.createClass({
-    shapes: [
-      "audio", "audio-hover", "audio-active", "block",
-      "block-red", "block-hover", "block-active", "contacts", "contacts-hover",
-      "contacts-active", "copy", "checkmark", "google", "google-hover",
-      "google-active", "history", "history-hover", "history-active", "leave",
-      "precall", "precall-hover", "precall-active", "settings", "settings-hover",
-      "settings-active", "tag", "tag-hover", "tag-active", "trash", "unblock",
-      "unblock-hover", "unblock-active", "video", "video-hover", "video-active"
-    ],
+    shapes: {
+      "10x10": ["close", "close-active", "close-disabled", "dropdown",
+        "dropdown-white", "dropdown-active", "dropdown-disabled", "expand",
+        "expand-active", "expand-disabled", "minimize", "minimize-active",
+        "minimize-disabled"
+      ],
+      "14x14": ["audio", "audio-active", "audio-disabled", "facemute",
+        "facemute-active", "facemute-disabled", "hangup", "hangup-active",
+        "hangup-disabled", "incoming", "incoming-active", "incoming-disabled",
+        "link", "link-active", "link-disabled", "mute", "mute-active",
+        "mute-disabled", "pause", "pause-active", "pause-disabled", "video",
+        "video-white", "video-active", "video-disabled", "volume", "volume-active",
+        "volume-disabled"
+      ],
+      "16x16": ["audio", "audio-hover", "audio-active", "block", "block-red",
+        "block-hover", "block-active", "contacts", "contacts-hover", "contacts-active",
+        "copy", "checkmark", "google", "google-hover", "google-active", "history",
+        "history-hover", "history-active", "leave", "precall", "precall-hover",
+        "precall-active", "screen-white", "screenmute-white", "settings",
+        "settings-hover", "settings-active", "tag", "tag-hover", "tag-active",
+        "trash", "unblock", "unblock-hover", "unblock-active", "video", "video-hover",
+        "video-active", "tour"
+      ]
+    },
 
     render: function() {
+      var icons = this.shapes[this.props.size].map(function(shapeId, i) {
+        return (
+          <li key={this.props.size + "-" + i} className="svg-icon-entry">
+            <p><SVGIcon shapeId={shapeId} size={this.props.size} /></p>
+            <p>{shapeId}</p>
+          </li>
+        );
+      }, this);
       return (
-        <div className="svg-icon-list">{
-          this.shapes.map(function(shapeId, i) {
-            return <div key={i} className="svg-icon-entry">
-              <p><SVGIcon shapeId={shapeId} /></p>
-              <p>{shapeId}</p>
-            </div>;
-          }, this)
-        }</div>
+        <ul className="svg-icon-list">{icons}</ul>
       );
     }
   });
 
   var Example = React.createClass({
     makeId: function(prefix) {
       return (prefix || "") + this.props.summary.toLowerCase().replace(/\s/g, "-");
     },
@@ -166,28 +185,28 @@
       var cx = React.addons.classSet;
       return (
         <div className="example">
           <h3 id={this.makeId()}>
             {this.props.summary}
             <a href={this.makeId("#")}>&nbsp;¶</a>
           </h3>
           <div className={cx({comp: true, dashed: this.props.dashed})}
-               style={this.props.style || {}}>
+               style={this.props.style}>
             {this.props.children}
           </div>
         </div>
       );
     }
   });
 
   var Section = React.createClass({
     render: function() {
       return (
-        <section id={this.props.name}>
+        <section id={this.props.name} className={this.props.className}>
           <h1>{this.props.name}</h1>
           {this.props.children}
         </section>
       );
     }
   });
 
   var ShowCase = React.createClass({
@@ -652,19 +671,25 @@
                   dispatcher={dispatcher}
                   activeRoomStore={activeRoomStore}
                   roomState={ROOM_STATES.FAILED}
                   isFirefox={false} />
               </div>
             </Example>
           </Section>
 
-          <Section name="SVG icons preview">
+          <Section name="SVG icons preview" className="svg-icons">
+            <Example summary="10x10">
+              <SVGIcons size="10x10"/>
+            </Example>
+            <Example summary="14x14">
+              <SVGIcons size="14x14" />
+            </Example>
             <Example summary="16x16">
-              <SVGIcons />
+              <SVGIcons size="16x16"/>
             </Example>
           </Section>
 
         </ShowCase>
       );
     }
   });
 
--- a/browser/components/search/test/browser_webapi.js
+++ b/browser/components/search/test/browser_webapi.js
@@ -1,9 +1,16 @@
 let ROOT = getRootDirectory(gTestPath).replace("chrome://mochitests/content", "http://example.com");
+const searchBundle = Services.strings.createBundle("chrome://global/locale/search/search.properties");
+const brandBundle = Services.strings.createBundle("chrome://branding/locale/brand.properties");
+const brandName = brandBundle.GetStringFromName("brandShortName");
+
+function getString(key, ...params) {
+  return searchBundle.formatStringFromName(key, params, params.length);
+}
 
 function AddSearchProvider(...args) {
   return gBrowser.addTab(ROOT + "webapi.html?AddSearchProvider:" + encodeURIComponent(JSON.stringify(args)));
 }
 
 function addSearchEngine(...args) {
   return gBrowser.addTab(ROOT + "webapi.html?addSearchEngine:" + encodeURIComponent(JSON.stringify(args)));
 }
@@ -27,148 +34,148 @@ function promiseDialogOpened() {
   });
 }
 
 add_task(function* test_working_AddSearchProvider() {
   gBrowser.selectedTab = AddSearchProvider(ROOT + "testEngine.xml");
 
   let dialog = yield promiseDialogOpened();
   is(dialog.args.promptType, "confirmEx", "Should see the confirmation dialog.");
-  is(dialog.args.text, "Add \"Foo\" to the list of engines available in the search bar?\n\nFrom: example.com",
+  is(dialog.args.text, getString("addEngineConfirmation", "Foo", "example.com"),
      "Should have seen the right install message");
   dialog.document.documentElement.cancelDialog();
 
   gBrowser.removeCurrentTab();
 });
 
 add_task(function* test_HTTP_AddSearchProvider() {
   gBrowser.selectedTab = AddSearchProvider(ROOT.replace("http:", "HTTP:") + "testEngine.xml");
 
   let dialog = yield promiseDialogOpened();
   is(dialog.args.promptType, "confirmEx", "Should see the confirmation dialog.");
-  is(dialog.args.text, "Add \"Foo\" to the list of engines available in the search bar?\n\nFrom: example.com",
+  is(dialog.args.text, getString("addEngineConfirmation", "Foo", "example.com"),
      "Should have seen the right install message");
   dialog.document.documentElement.cancelDialog();
 
   gBrowser.removeCurrentTab();
 });
 
 add_task(function* test_relative_AddSearchProvider() {
   gBrowser.selectedTab = AddSearchProvider("testEngine.xml");
 
   let dialog = yield promiseDialogOpened();
   is(dialog.args.promptType, "confirmEx", "Should see the confirmation dialog.");
-  is(dialog.args.text, "Add \"Foo\" to the list of engines available in the search bar?\n\nFrom: example.com",
+  is(dialog.args.text, getString("addEngineConfirmation", "Foo", "example.com"),
      "Should have seen the right install message");
   dialog.document.documentElement.cancelDialog();
 
   gBrowser.removeCurrentTab();
 });
 
 add_task(function* test_invalid_AddSearchProvider() {
   gBrowser.selectedTab = AddSearchProvider("z://foobar");
 
   let dialog = yield promiseDialogOpened();
   is(dialog.args.promptType, "alert", "Should see the alert dialog.");
-  is(dialog.args.text, "This search engine isn't supported by Nightly and can't be installed.",
+  is(dialog.args.text, getString("error_invalid_engine_msg", brandName),
      "Should have seen the right error message")
   dialog.document.documentElement.acceptDialog();
 
   gBrowser.removeCurrentTab();
 });
 
 add_task(function* test_missing_AddSearchProvider() {
   let url = ROOT + "foobar.xml";
   gBrowser.selectedTab = AddSearchProvider(url);
 
   let dialog = yield promiseDialogOpened();
   is(dialog.args.promptType, "alert", "Should see the alert dialog.");
-  is(dialog.args.text, "Nightly could not download the search plugin from:\n" + url,
+  is(dialog.args.text, getString("error_loading_engine_msg2", brandName, url),
      "Should have seen the right error message")
   dialog.document.documentElement.acceptDialog();
 
   gBrowser.removeCurrentTab();
 });
 
 add_task(function* test_working_addSearchEngine_xml() {
   gBrowser.selectedTab = addSearchEngine(ROOT + "testEngine.xml", "", "", "");
 
   let dialog = yield promiseDialogOpened();
   is(dialog.args.promptType, "confirmEx", "Should see the confirmation dialog.");
-  is(dialog.args.text, "Add \"Foo\" to the list of engines available in the search bar?\n\nFrom: example.com",
+  is(dialog.args.text, getString("addEngineConfirmation", "Foo", "example.com"),
      "Should have seen the right install message");
   dialog.document.documentElement.cancelDialog();
 
   gBrowser.removeCurrentTab();
 });
 
 add_task(function* test_working_addSearchEngine_src() {
   gBrowser.selectedTab = addSearchEngine(ROOT + "testEngine.src", "", "", "");
 
   let dialog = yield promiseDialogOpened();
   is(dialog.args.promptType, "confirmEx", "Should see the confirmation dialog.");
-  is(dialog.args.text, "Add \"Test Sherlock\" to the list of engines available in the search bar?\n\nFrom: example.com",
+  is(dialog.args.text, getString("addEngineConfirmation", "Test Sherlock", "example.com"),
      "Should have seen the right install message");
   dialog.document.documentElement.cancelDialog();
 
   gBrowser.removeCurrentTab();
 });
 
 add_task(function* test_relative_addSearchEngine_xml() {
   gBrowser.selectedTab = addSearchEngine("testEngine.xml", "", "", "");
 
   let dialog = yield promiseDialogOpened();
   is(dialog.args.promptType, "confirmEx", "Should see the confirmation dialog.");
-  is(dialog.args.text, "Add \"Foo\" to the list of engines available in the search bar?\n\nFrom: example.com",
+  is(dialog.args.text, getString("addEngineConfirmation", "Foo", "example.com"),
      "Should have seen the right install message");
   dialog.document.documentElement.cancelDialog();
 
   gBrowser.removeCurrentTab();
 });
 
 add_task(function* test_relative_addSearchEngine_src() {
   gBrowser.selectedTab = addSearchEngine("testEngine.src", "", "", "");
 
   let dialog = yield promiseDialogOpened();
   is(dialog.args.promptType, "confirmEx", "Should see the confirmation dialog.");
-  is(dialog.args.text, "Add \"Test Sherlock\" to the list of engines available in the search bar?\n\nFrom: example.com",
+  is(dialog.args.text, getString("addEngineConfirmation", "Test Sherlock", "example.com"),
      "Should have seen the right install message");
   dialog.document.documentElement.cancelDialog();
 
   gBrowser.removeCurrentTab();
 });
 
 add_task(function* test_invalid_addSearchEngine() {
   gBrowser.selectedTab = addSearchEngine("z://foobar", "", "", "");
 
   let dialog = yield promiseDialogOpened();
   is(dialog.args.promptType, "alert", "Should see the alert dialog.");
-  is(dialog.args.text, "This search engine isn't supported by Nightly and can't be installed.",
+  is(dialog.args.text, getString("error_invalid_engine_msg", brandName),
      "Should have seen the right error message")
   dialog.document.documentElement.acceptDialog();
 
   gBrowser.removeCurrentTab();
 });
 
 add_task(function* test_invalid_icon_addSearchEngine() {
   gBrowser.selectedTab = addSearchEngine(ROOT + "testEngine.src", "z://foobar", "", "");
 
   let dialog = yield promiseDialogOpened();
   is(dialog.args.promptType, "alert", "Should see the alert dialog.");
-  is(dialog.args.text, "This search engine isn't supported by Nightly and can't be installed.",
+  is(dialog.args.text, getString("error_invalid_engine_msg", brandName),
      "Should have seen the right error message")
   dialog.document.documentElement.acceptDialog();
 
   gBrowser.removeCurrentTab();
 });
 
 add_task(function* test_missing_addSearchEngine() {
   let url = ROOT + "foobar.xml";
   gBrowser.selectedTab = addSearchEngine(url, "", "", "");
 
   let dialog = yield promiseDialogOpened();
   is(dialog.args.promptType, "alert", "Should see the alert dialog.");
-  is(dialog.args.text, "Nightly could not download the search plugin from:\n" + url,
+  is(dialog.args.text, getString("error_loading_engine_msg2", brandName, url),
      "Should have seen the right error message")
   dialog.document.documentElement.acceptDialog();
 
   gBrowser.removeCurrentTab();
 });
--- a/browser/locales/en-US/chrome/browser/loop/loop.properties
+++ b/browser/locales/en-US/chrome/browser/loop/loop.properties
@@ -180,16 +180,17 @@ incoming_call_block_button=Block
 hangup_button_title=Hang up
 hangup_button_caption2=Exit
 mute_local_audio_button_title=Mute your audio
 unmute_local_audio_button_title=Unmute your audio
 mute_local_video_button_title=Mute your video
 unmute_local_video_button_title=Unmute your video
 active_screenshare_button_title=Stop sharing
 inactive_screenshare_button_title=Share your screen
+share_windows_button_title=Share other Windows
 
 ## LOCALIZATION NOTE (call_with_contact_title): The title displayed
 ## when calling a contact. Don't translate the part between {{..}} because
 ## this will be replaced by the contact's name.
 ## https://people.mozilla.org/~dhenein/labs/loop-mvp-spec/#call-outgoing
 call_with_contact_title=Conversation with {{contactName}}
 
 # Outgoing conversation
--- a/configure.in
+++ b/configure.in
@@ -5025,16 +5025,23 @@ fi
 
 dnl ========================================================
 dnl = Include share overlay on Android
 dnl ========================================================
 if test -n "$MOZ_ANDROID_SHARE_OVERLAY"; then
     AC_DEFINE(MOZ_ANDROID_SHARE_OVERLAY)
 fi
 
+dnl = Include Tab Queue on Android
+dnl = Temporary build flag to allow development in Nightly
+dnl ========================================================
+if test -n "$MOZ_ANDROID_TAB_QUEUE"; then
+    AC_DEFINE(MOZ_ANDROID_TAB_QUEUE)
+fi
+
 dnl ========================================================
 dnl = Include New Tablet UI on Android
 dnl = Temporary build flag to allow development in Nightly
 dnl ========================================================
 if test -n "$MOZ_ANDROID_NEW_TABLET_UI"; then
     AC_DEFINE(MOZ_ANDROID_NEW_TABLET_UI)
 fi
 
@@ -8530,16 +8537,17 @@ AC_SUBST(MOZ_METRO)
 AC_SUBST(MOZ_ANDROID_HISTORY)
 AC_SUBST(MOZ_WEBSMS_BACKEND)
 AC_SUBST(MOZ_ANDROID_BEAM)
 AC_SUBST(MOZ_LOCALE_SWITCHER)
 AC_SUBST(MOZ_DISABLE_GECKOVIEW)
 AC_SUBST(MOZ_ANDROID_READING_LIST_SERVICE)
 AC_SUBST(MOZ_ANDROID_SEARCH_ACTIVITY)
 AC_SUBST(MOZ_ANDROID_SHARE_OVERLAY)
+AC_SUBST(MOZ_ANDROID_TAB_QUEUE)
 AC_SUBST(MOZ_ANDROID_NEW_TABLET_UI)
 AC_SUBST(MOZ_ANDROID_MLS_STUMBLER)
 AC_SUBST(MOZ_ANDROID_DOWNLOADS_INTEGRATION)
 AC_SUBST(ENABLE_STRIP)
 AC_SUBST(PKG_SKIP_STRIP)
 AC_SUBST(STRIP_FLAGS)
 AC_SUBST(USE_ELF_HACK)
 AC_SUBST(INCREMENTAL_LINKER)
--- a/dom/base/nsFrameLoader.cpp
+++ b/dom/base/nsFrameLoader.cpp
@@ -909,17 +909,18 @@ nsFrameLoader::ShowRemoteFrame(const nsI
                           "remote-browser-shown", nullptr);
     }
   } else {
     nsIntRect dimensions;
     NS_ENSURE_SUCCESS(GetWindowDimensions(dimensions), false);
 
     // Don't show remote iframe if we are waiting for the completion of reflow.
     if (!aFrame || !(aFrame->GetStateBits() & NS_FRAME_FIRST_REFLOW)) {
-      mRemoteBrowser->UpdateDimensions(dimensions, size);
+      nsIntPoint chromeDisp = aFrame->GetChromeDisplacement();
+      mRemoteBrowser->UpdateDimensions(dimensions, size, chromeDisp);
     }
   }
 
   return true;
 }
 
 void
 nsFrameLoader::Hide()
@@ -1954,17 +1955,18 @@ nsFrameLoader::GetWindowDimensions(nsInt
 NS_IMETHODIMP
 nsFrameLoader::UpdatePositionAndSize(nsSubDocumentFrame *aIFrame)
 {
   if (mRemoteFrame) {
     if (mRemoteBrowser) {
       nsIntSize size = aIFrame->GetSubdocumentSize();
       nsIntRect dimensions;
       NS_ENSURE_SUCCESS(GetWindowDimensions(dimensions), NS_ERROR_FAILURE);
-      mRemoteBrowser->UpdateDimensions(dimensions, size);
+      nsIntPoint chromeDisp = aIFrame->GetChromeDisplacement();
+      mRemoteBrowser->UpdateDimensions(dimensions, size, chromeDisp);
     }
     return NS_OK;
   }
   UpdateBaseWindowPositionAndSize(aIFrame);
   return NS_OK;
 }
 
 void
--- a/dom/base/nsFrameLoader.h
+++ b/dom/base/nsFrameLoader.h
@@ -222,19 +222,16 @@ public:
    */
   void ApplySandboxFlags(uint32_t sandboxFlags);
 
   void GetURL(nsString& aURL);
 
   void ActivateUpdateHitRegion();
   void DeactivateUpdateHitRegion();
 
-  // Properly retrieves documentSize of any subdocument type.
-  nsresult GetWindowDimensions(nsIntRect& aRect);
-
 private:
 
   void SetOwnerContent(mozilla::dom::Element* aContent);
 
   bool ShouldUseRemoteProcess();
 
   /**
    * Is this a frameloader for a bona fide <iframe mozbrowser> or
@@ -280,16 +277,19 @@ private:
 
   /**
    * If we are an IPC frame, set mRemoteFrame. Otherwise, create and
    * initialize mDocShell.
    */
   nsresult MaybeCreateDocShell();
   nsresult EnsureMessageManager();
 
+  // Properly retrieves documentSize of any subdocument type.
+  nsresult GetWindowDimensions(nsIntRect& aRect);
+
   // Updates the subdocument position and size. This gets called only
   // when we have our own in-process DocShell.
   void UpdateBaseWindowPositionAndSize(nsSubDocumentFrame *aIFrame);
   nsresult CheckURILoad(nsIURI* aURI);
   void FireErrorEvent();
   nsresult ReallyStartLoadingInternal();
 
   // Return true if remote browser created; nothing else to do
--- a/dom/ipc/TabChild.cpp
+++ b/dom/ipc/TabChild.cpp
@@ -2017,17 +2017,17 @@ TabChild::RecvUpdateDimensions(const nsI
     if (initialSizing) {
       mHasValidInnerSize = true;
     }
 
     mOrientation = orientation;
     ScreenIntSize oldScreenSize = mInnerSize;
     mInnerSize = ScreenIntSize::FromUnknownSize(
       gfx::IntSize(size.width, size.height));
-    mWidget->Resize(rect.x + chromeDisp.x, rect.y + chromeDisp.y, size.width, size.height,
+    mWidget->Resize(0, 0, size.width, size.height,
                     true);
 
     nsCOMPtr<nsIBaseWindow> baseWin = do_QueryInterface(WebNavigation());
     baseWin->SetPositionAndSize(0, 0, size.width, size.height,
                                 true);
 
     if (initialSizing && mContentDocumentIsDisplayed) {
       // If this is the first time we're getting a valid mInnerSize, and the
--- a/dom/ipc/TabParent.cpp
+++ b/dom/ipc/TabParent.cpp
@@ -75,17 +75,16 @@
 #include "LoadContext.h"
 #include "nsNetCID.h"
 #include "nsIAuthInformation.h"
 #include "nsIAuthPromptCallback.h"
 #include "nsAuthInformationHolder.h"
 #include "nsICancelable.h"
 #include "gfxPrefs.h"
 #include "nsILoginManagerPrompter.h"
-#include "nsPIWindowRoot.h"
 #include <algorithm>
 
 using namespace mozilla::dom;
 using namespace mozilla::ipc;
 using namespace mozilla::layers;
 using namespace mozilla::layout;
 using namespace mozilla::services;
 using namespace mozilla::widget;
@@ -317,37 +316,17 @@ TabParent::RemoveTabParentFromTable(uint
     delete sLayerToTabParentTable;
     sLayerToTabParentTable = nullptr;
   }
 }
 
 void
 TabParent::SetOwnerElement(Element* aElement)
 {
-  // If we held previous content then unregister for its events.
-  if (mFrameElement && mFrameElement->OwnerDoc()->GetWindow()) {
-    nsCOMPtr<nsPIDOMWindow> window = mFrameElement->OwnerDoc()->GetWindow();
-    nsCOMPtr<EventTarget> eventTarget = window->GetTopWindowRoot();
-    if (eventTarget) {
-      eventTarget->RemoveEventListener(NS_LITERAL_STRING("MozUpdateWindowPos"),
-                                       this, false);
-    }
-  }
-
-  // Update to the new content, and register to listen for events from it.
   mFrameElement = aElement;
-  if (mFrameElement && mFrameElement->OwnerDoc()->GetWindow()) {
-    nsCOMPtr<nsPIDOMWindow> window = mFrameElement->OwnerDoc()->GetWindow();
-    nsCOMPtr<EventTarget> eventTarget = window->GetTopWindowRoot();
-    if (eventTarget) {
-      eventTarget->AddEventListener(NS_LITERAL_STRING("MozUpdateWindowPos"),
-                                    this, false, false);
-    }
-  }
-
   TryCacheDPIAndScale();
 }
 
 void
 TabParent::GetAppType(nsAString& aOut)
 {
   aOut.Truncate();
   nsCOMPtr<Element> elem = do_QueryInterface(mFrameElement);
@@ -373,18 +352,16 @@ TabParent::IsVisible()
 
 void
 TabParent::Destroy()
 {
   if (mIsDestroyed) {
     return;
   }
 
-  SetOwnerElement(nullptr);
-
   // If this fails, it's most likely due to a content-process crash,
   // and auto-cleanup will kick in.  Otherwise, the child side will
   // destroy itself and send back __delete__().
   unused << SendDestroy();
 
   if (RenderFrameParent* frame = GetRenderFrame()) {
     RemoveTabParentFromTable(frame->GetLayersId());
     frame->Destroy();
@@ -902,41 +879,34 @@ TabParent::RecvSetDimensions(const uint3
     return true;
   }
 
   MOZ_ASSERT(false, "Unknown flags!");
   return false;
 }
 
 void
-TabParent::UpdateDimensions(const nsIntRect& rect, const nsIntSize& size)
+TabParent::UpdateDimensions(const nsIntRect& rect, const nsIntSize& size,
+                            const nsIntPoint& aChromeDisp)
 {
   if (mIsDestroyed) {
     return;
   }
   hal::ScreenConfiguration config;
   hal::GetCurrentScreenConfiguration(&config);
   ScreenOrientation orientation = config.orientation();
 
   if (!mUpdatedDimensions || mOrientation != orientation ||
       mDimensions != size || !mRect.IsEqualEdges(rect)) {
-    nsCOMPtr<nsIWidget> widget = GetWidget();
-    nsIntRect contentRect = rect;
-    if (widget) {
-      contentRect.x += widget->GetClientOffset().x;
-      contentRect.y += widget->GetClientOffset().y;
-    }
-
     mUpdatedDimensions = true;
-    mRect = contentRect;
+    mRect = rect;
     mDimensions = size;
     mOrientation = orientation;
 
-    nsIntPoint chromeOffset = LayoutDevicePixel::ToUntyped(-GetChildProcessOffset());
-    unused << SendUpdateDimensions(mRect, mDimensions, mOrientation, chromeOffset);
+    unused << SendUpdateDimensions(mRect, mDimensions, mOrientation, aChromeDisp);
   }
 }
 
 void
 TabParent::UpdateFrame(const FrameMetrics& aFrameMetrics)
 {
   if (!mIsDestroyed) {
     unused << SendUpdateFrame(aFrameMetrics);
@@ -2656,37 +2626,16 @@ TabParent::AllocPPluginWidgetParent()
 
 bool
 TabParent::DeallocPPluginWidgetParent(mozilla::plugins::PPluginWidgetParent* aActor)
 {
   delete aActor;
   return true;
 }
 
-nsresult
-TabParent::HandleEvent(nsIDOMEvent* aEvent)
-{
-  nsAutoString eventType;
-  aEvent->GetType(eventType);
-
-  if (eventType.EqualsLiteral("MozUpdateWindowPos")) {
-    // This event is sent when the widget moved.  Therefore we only update
-    // the position.
-    nsRefPtr<nsFrameLoader> frameLoader = GetFrameLoader();
-    if (!frameLoader) {
-      return NS_OK;
-    }
-    nsIntRect windowDims;
-    NS_ENSURE_SUCCESS(frameLoader->GetWindowDimensions(windowDims), NS_ERROR_FAILURE);
-    UpdateDimensions(windowDims, mDimensions);
-    return NS_OK;
-  }
-  return NS_OK;
-}
-
 class FakeChannel MOZ_FINAL : public nsIChannel,
                               public nsIAuthPromptCallback,
                               public nsIInterfaceRequestor,
                               public nsILoadContext
 {
 public:
   FakeChannel(const nsCString& aUri, uint64_t aCallbackId, Element* aElement)
     : mCallbackId(aCallbackId)
--- a/dom/ipc/TabParent.h
+++ b/dom/ipc/TabParent.h
@@ -17,17 +17,16 @@
 #include "nsIBrowserDOMWindow.h"
 #include "nsISecureBrowserUI.h"
 #include "nsITabParent.h"
 #include "nsIXULBrowserWindow.h"
 #include "nsWeakReference.h"
 #include "Units.h"
 #include "WritingModes.h"
 #include "js/TypeDecls.h"
-#include "nsIDOMEventListener.h"
 
 class nsFrameLoader;
 class nsIFrameLoader;
 class nsIContent;
 class nsIPrincipal;
 class nsIURI;
 class nsIWidget;
 class nsILoadContext;
@@ -54,18 +53,17 @@ struct IMENotification;
 
 namespace dom {
 
 class ClonedMessageData;
 class nsIContentParent;
 class Element;
 struct StructuredCloneData;
 
-class TabParent : public PBrowserParent
-                , public nsIDOMEventListener
+class TabParent : public PBrowserParent 
                 , public nsITabParent 
                 , public nsIAuthPromptProvider
                 , public nsISecureBrowserUI
                 , public nsSupportsWeakReference
                 , public TabContext
 {
     typedef mozilla::dom::ClonedMessageData ClonedMessageData;
     typedef mozilla::layout::ScrollingBehavior ScrollingBehavior;
@@ -96,19 +94,16 @@ public:
      */
     bool IsVisible();
 
     nsIBrowserDOMWindow *GetBrowserDOMWindow() { return mBrowserDOMWindow; }
     void SetBrowserDOMWindow(nsIBrowserDOMWindow* aBrowserDOMWindow) {
         mBrowserDOMWindow = aBrowserDOMWindow;
     }
 
-    // nsIDOMEventListener interfaces 
-    NS_DECL_NSIDOMEVENTLISTENER
-
     already_AddRefed<nsILoadContext> GetLoadContext();
 
     nsIXULBrowserWindow* GetXULBrowserWindow();
 
     /**
      * Return the TabParent that has decided it wants to capture an
      * event series for fast-path dispatch to its subprocess, if one
      * has.
@@ -237,17 +232,18 @@ public:
     AllocPColorPickerParent(const nsString& aTitle, const nsString& aInitialColor) MOZ_OVERRIDE;
     virtual bool DeallocPColorPickerParent(PColorPickerParent* aColorPicker) MOZ_OVERRIDE;
 
     void LoadURL(nsIURI* aURI);
     // XXX/cjones: it's not clear what we gain by hiding these
     // message-sending functions under a layer of indirection and
     // eating the return values
     void Show(const nsIntSize& size, bool aParentIsActive);
-    void UpdateDimensions(const nsIntRect& rect, const nsIntSize& size);
+    void UpdateDimensions(const nsIntRect& rect, const nsIntSize& size,
+                          const nsIntPoint& chromeDisp);
     void UpdateFrame(const layers::FrameMetrics& aFrameMetrics);
     void UIResolutionChanged();
     void AcknowledgeScrollUpdate(const ViewID& aScrollId, const uint32_t& aScrollGeneration);
     void HandleDoubleTap(const CSSPoint& aPoint,
                          int32_t aModifiers,
                          const ScrollableLayerGuid& aGuid);
     void HandleSingleTap(const CSSPoint& aPoint,
                          int32_t aModifiers,
--- a/dom/mobileconnection/interfaces/nsIMobileConnectionService.idl
+++ b/dom/mobileconnection/interfaces/nsIMobileConnectionService.idl
@@ -103,17 +103,17 @@ interface nsIMobileConnectionListener : 
    */
   void notifyNetworkSelectionModeChanged();
 };
 
 %{C++
 #define NO_ADDITIONAL_INFORMATION 0
 %}
 
-[scriptable, builtinclass, uuid(14d66926-8434-11e4-8c3f-f724194bb5f1)]
+[scriptable, uuid(14d66926-8434-11e4-8c3f-f724194bb5f1)]
 interface nsIMobileConnectionCallback : nsISupports
 {
   /**
    * notify*Success*() will be called, when request is succeed.
    */
   void notifySuccess();
 
   void notifySuccessWithBoolean(in boolean result);
--- a/dom/plugins/base/nsPluginInstanceOwner.cpp
+++ b/dom/plugins/base/nsPluginInstanceOwner.cpp
@@ -784,27 +784,28 @@ NPBool nsPluginInstanceOwner::ConvertPoi
   nsPoint windowPosition = AsNsPoint(rootWidget->GetWindowPosition()) / scaleFactor;
 
   // Window size is tab size + chrome size.
   nsIntRect tabContentBounds;
   NS_ENSURE_SUCCESS(puppetWidget->GetBounds(tabContentBounds), false);
   tabContentBounds.ScaleInverseRoundOut(scaleFactor);
   int32_t windowH = tabContentBounds.height + int(chromeSize.y);
 
+  // This is actually relative to window-chrome.
   nsPoint pluginPosition = AsNsPoint(pluginFrame->GetScreenRect().TopLeft());
 
   // Convert (sourceX, sourceY) to 'real' (not PuppetWidget) screen space.
   // In OSX, the Y-axis increases upward, which is the reverse of ours.
   // We want OSX coordinates for window and screen so those equations are swapped.
   nsPoint sourcePoint(sourceX, sourceY);
   nsPoint screenPoint;
   switch (sourceSpace) {
     case NPCoordinateSpacePlugin:
-      screenPoint = sourcePoint + pluginPosition +
-        pluginFrame->GetContentRectRelativeToSelf().TopLeft() / nsPresContext::AppUnitsPerCSSPixel();
+      screenPoint = sourcePoint + pluginFrame->GetContentRectRelativeToSelf().TopLeft() +
+        chromeSize + pluginPosition + windowPosition;
       break;
     case NPCoordinateSpaceWindow:
       screenPoint = nsPoint(sourcePoint.x, windowH-sourcePoint.y) +
         windowPosition;
       break;
     case NPCoordinateSpaceFlippedWindow:
       screenPoint = sourcePoint + windowPosition;
       break;
@@ -817,18 +818,18 @@ NPBool nsPluginInstanceOwner::ConvertPoi
     default:
       return false;
   }
 
   // Convert from screen to dest space.
   nsPoint destPoint;
   switch (destSpace) {
     case NPCoordinateSpacePlugin:
-      destPoint = screenPoint - pluginPosition -
-        pluginFrame->GetContentRectRelativeToSelf().TopLeft() / nsPresContext::AppUnitsPerCSSPixel();
+      destPoint = screenPoint - pluginFrame->GetContentRectRelativeToSelf().TopLeft() -
+        chromeSize - pluginPosition - windowPosition;
       break;
     case NPCoordinateSpaceWindow:
       destPoint = screenPoint - windowPosition;
       destPoint.y = windowH - destPoint.y;
       break;
     case NPCoordinateSpaceFlippedWindow:
       destPoint = screenPoint - windowPosition;
       break;
--- a/dom/system/gonk/ril_consts.js
+++ b/dom/system/gonk/ril_consts.js
@@ -125,26 +125,41 @@ this.REQUEST_SET_SMSC_ADDRESS = 101;
 this.REQUEST_REPORT_SMS_MEMORY_STATUS = 102;
 this.REQUEST_REPORT_STK_SERVICE_IS_RUNNING = 103;
 this.REQUEST_CDMA_GET_SUBSCRIPTION_SOURCE = 104;
 this.REQUEST_ISIM_AUTHENTICATION = 105;
 this.REQUEST_ACKNOWLEDGE_INCOMING_GSM_SMS_WITH_PDU = 106;
 this.REQUEST_STK_SEND_ENVELOPE_WITH_STATUS = 107;
 this.REQUEST_VOICE_RADIO_TECH = 108;
 this.REQUEST_GET_CELL_INFO_LIST = 109;
+this.REQUEST_SET_UNSOL_CELL_INFO_LIST_RATE = 110;
+this.REQUEST_SET_INITIAL_ATTACH_APN = 111;
+this.REQUEST_IMS_REGISTRATION_STATE = 112;
+this.REQUEST_IMS_SEND_SMS = 113;
+this.REQUEST_SIM_TRANSMIT_APDU_BASIC = 114;
+this.REQUEST_SIM_OPEN_CHANNEL = 115;
+this.REQUEST_SIM_CLOSE_CHANNEL = 116;
+this.REQUEST_SIM_TRANSMIT_APDU_CHANNEL = 117;
+this.REQUEST_NV_READ_ITEM = 118;
+this.REQUEST_NV_WRITE_ITEM = 119;
+this.REQUEST_NV_WRITE_CDMA_PRL = 120;
+this.REQUEST_NV_RESET_CONFIG = 121;
+this.REQUEST_SET_UICC_SUBSCRIPTION = 122;
+this.REQUEST_ALLOW_DATA = 123;
+this.REQUEST_GET_HARDWARE_CONFIG = 124;
+this.REQUEST_SIM_AUTHENTICATION = 125;
+this.REQUEST_GET_DC_RT_INFO = 126;
+this.REQUEST_SET_DC_RT_INFO_RATE = 127;
+this.REQUEST_SET_DATA_PROFILE = 128;
+this.REQUEST_SHUTDOWN = 129;
 
-// CAF specific parcel type. Synced with latest version.
-// Please see https://www.codeaurora.org/cgit/quic/la/platform/hardware/ril/tree/include/telephony/ril.h?h=b2g_kk_3.5
-this.REQUEST_SET_UICC_SUBSCRIPTION = 115;
-this.REQUEST_SET_DATA_SUBSCRIPTION = 116;
-
-// UICC Secure Access.
-this.REQUEST_SIM_OPEN_CHANNEL = 121;
-this.REQUEST_SIM_CLOSE_CHANNEL = 122;
-this.REQUEST_SIM_ACCESS_CHANNEL = 123;
+// CAF specific parcel type. It should be synced with latest version. But CAF
+// doesn't have l version for b2g yet, so we set REQUEST_SET_DATA_SUBSCRIPTION
+// to a value that won't get conflict with known AOSP parcel.
+this.REQUEST_SET_DATA_SUBSCRIPTION = 130;
 
 // Mozilla specific parcel type.
 this.REQUEST_GET_UNLOCK_RETRY_COUNT = 150;
 
 // Fugu specific parcel types.
 this.RIL_REQUEST_GPRS_ATTACH = 5018;
 this.RIL_REQUEST_GPRS_DETACH = 5019;
 
@@ -186,16 +201,22 @@ this.UNSOLICITED_CDMA_INFO_REC = 1027;
 this.UNSOLICITED_OEM_HOOK_RAW = 1028;
 this.UNSOLICITED_RINGBACK_TONE = 1029;
 this.UNSOLICITED_RESEND_INCALL_MUTE = 1030;
 this.UNSOLICITED_CDMA_SUBSCRIPTION_SOURCE_CHANGED = 1031;
 this.UNSOLICITED_CDMA_PRL_CHANGED = 1032;
 this.UNSOLICITED_EXIT_EMERGENCY_CALLBACK_MODE = 1033;
 this.UNSOLICITED_RIL_CONNECTED = 1034;
 this.UNSOLICITED_VOICE_RADIO_TECH_CHANGED = 1035;
+this.UNSOLICITED_CELL_INFO_LIST = 1036;
+this.UNSOLICITED_RESPONSE_IMS_NETWORK_STATE_CHANGED = 1037;
+this.UNSOLICITED_UICC_SUBSCRIPTION_STATUS_CHANGED = 1038;
+this.UNSOLICITED_SRVCC_STATE_NOTIFY = 1039;
+this.UNSOLICITED_HARDWARE_CONFIG_CHANGED = 1040;
+this.UNSOLICITED_DC_RT_INFO_CHANGED = 1041;
 
 this.ERROR_SUCCESS = 0;
 this.ERROR_RADIO_NOT_AVAILABLE = 1;
 this.ERROR_GENERIC_FAILURE = 2;
 this.ERROR_PASSWORD_INCORRECT = 3;
 this.ERROR_SIM_PIN2 = 4;
 this.ERROR_SIM_PUK2 = 5;
 this.ERROR_REQUEST_NOT_SUPPORTED = 6;
--- a/dom/system/gonk/ril_worker.js
+++ b/dom/system/gonk/ril_worker.js
@@ -1317,64 +1317,42 @@ RilObject.prototype = {
   queryVoicePrivacyMode: function(options) {
     this.context.Buf.simpleRequest(REQUEST_CDMA_QUERY_PREFERRED_VOICE_PRIVACY_MODE, options);
   },
 
   /**
    * Open Logical UICC channel (aid) for Secure Element access
    */
   iccOpenChannel: function(options) {
-    if (DEBUG) {
-      this.context.debug("iccOpenChannel: " + JSON.stringify(options));
-    }
-
     let Buf = this.context.Buf;
     Buf.newParcel(REQUEST_SIM_OPEN_CHANNEL, options);
     Buf.writeString(options.aid);
     Buf.sendParcel();
   },
 
   /**
    * Exchange APDU data on an open Logical UICC channel
    */
   iccExchangeAPDU: function(options) {
-    if (DEBUG) this.context.debug("iccExchangeAPDU: " + JSON.stringify(options));
-
-    let cla = options.apdu.cla;
-    let command = options.apdu.command;
-    let channel = options.channel;
-    let path = options.apdu.path || "";
-    let data = options.apdu.data || "";
-    let data2 = options.apdu.data2 || "";
-
-    let p1 = options.apdu.p1;
-    let p2 = options.apdu.p2;
-    let p3 = options.apdu.p3; // Extra
-
-    let Buf = this.context.Buf;
-    Buf.newParcel(REQUEST_SIM_ACCESS_CHANNEL, options);
-    Buf.writeInt32(cla);
-    Buf.writeInt32(command);
-    Buf.writeInt32(channel);
-    Buf.writeString(path); // path
-    Buf.writeInt32(p1);
-    Buf.writeInt32(p2);
-    Buf.writeInt32(p3);
-    Buf.writeString(data); // generic data field.
-    Buf.writeString(data2);
-
+    let Buf = this.context.Buf;
+    Buf.newParcel(REQUEST_SIM_TRANSMIT_APDU_CHANNEL, options);
+    Buf.writeInt32(options.channel);
+    Buf.writeInt32(options.apdu.cla);
+    Buf.writeInt32(options.apdu.command);
+    Buf.writeInt32(options.apdu.p1);
+    Buf.writeInt32(options.apdu.p2);
+    Buf.writeInt32(options.apdu.p3);
+    Buf.writeString(options.apdu.data);
     Buf.sendParcel();
   },
 
   /**
    * Close Logical UICC channel
    */
   iccCloseChannel: function(options) {
-    if (DEBUG) this.context.debug("iccCloseChannel: " + JSON.stringify(options));
-
     let Buf = this.context.Buf;
     Buf.newParcel(REQUEST_SIM_CLOSE_CHANNEL, options);
     Buf.writeInt32(1);
     Buf.writeInt32(options.channel);
     Buf.sendParcel();
   },
 
   /**
@@ -5858,57 +5836,16 @@ RilObject.prototype[REQUEST_CHANGE_BARRI
   if (options.rilMessageType != "sendMMI") {
     this.sendChromeMessage(options);
     return;
   }
 
   options.statusMessage = MMI_SM_KS_PASSWORD_CHANGED;
   this.sendChromeMessage(options);
 };
-RilObject.prototype[REQUEST_SIM_OPEN_CHANNEL] = function REQUEST_SIM_OPEN_CHANNEL(length, options) {
-  if (options.rilRequestError) {
-    options.errorMsg = RIL_ERROR_TO_GECKO_ERROR[options.rilRequestError];
-    this.sendChromeMessage(options);
-    return;
-  }
-
-  options.channel = this.context.Buf.readInt32();
-  if (DEBUG) {
-    this.context.debug("Setting channel number in options: " + options.channel);
-  }
-  this.sendChromeMessage(options);
-};
-RilObject.prototype[REQUEST_SIM_CLOSE_CHANNEL] = function REQUEST_SIM_CLOSE_CHANNEL(length, options) {
-  if (options.rilRequestError) {
-    options.error = RIL_ERROR_TO_GECKO_ERROR[options.rilRequestError];
-    this.sendChromeMessage(options);
-    return;
-  }
-
-  // No return value
-  this.sendChromeMessage(options);
-};
-RilObject.prototype[REQUEST_SIM_ACCESS_CHANNEL] = function REQUEST_SIM_ACCESS_CHANNEL(length, options) {
-  if (options.rilRequestError) {
-    options.error = RIL_ERROR_TO_GECKO_ERROR[options.rilRequestError];
-    this.sendChromeMessage(options);
-  }
-
-  let Buf = this.context.Buf;
-  options.sw1 = Buf.readInt32();
-  options.sw2 = Buf.readInt32();
-  options.simResponse = Buf.readString();
-  if (DEBUG) {
-    this.context.debug("Setting return values for RIL[REQUEST_SIM_ACCESS_CHANNEL]: [" +
-                       options.sw1 + "," +
-                       options.sw2 + ", " +
-                       options.simResponse + "]");
-  }
-  this.sendChromeMessage(options);
-};
 RilObject.prototype[REQUEST_QUERY_NETWORK_SELECTION_MODE] = function REQUEST_QUERY_NETWORK_SELECTION_MODE(length, options) {
   this._receivedNetworkInfo(NETWORK_INFO_NETWORK_SELECTION_MODE);
 
   if (options.rilRequestError) {
     return;
   }
 
   let mode = this.context.Buf.readInt32List();
@@ -6422,22 +6359,74 @@ RilObject.prototype[REQUEST_VOICE_RADIO_
       this.context.debug("Error when getting voice radio tech: " +
                          options.rilRequestError);
     }
     return;
   }
   let radioTech = this.context.Buf.readInt32List();
   this._processRadioTech(radioTech[0]);
 };
+RilObject.prototype[REQUEST_GET_CELL_INFO_LIST] = null;
+RilObject.prototype[REQUEST_SET_UNSOL_CELL_INFO_LIST_RATE] = null;
+RilObject.prototype[REQUEST_SET_INITIAL_ATTACH_APN] = null;
+RilObject.prototype[REQUEST_IMS_REGISTRATION_STATE] = null;
+RilObject.prototype[REQUEST_IMS_SEND_SMS] = null;
+RilObject.prototype[REQUEST_SIM_TRANSMIT_APDU_BASIC] = null;
+RilObject.prototype[REQUEST_SIM_OPEN_CHANNEL] = function REQUEST_SIM_OPEN_CHANNEL(length, options) {
+  if (options.rilRequestError) {
+    options.errorMsg = RIL_ERROR_TO_GECKO_ERROR[options.rilRequestError];
+    this.sendChromeMessage(options);
+    return;
+  }
+
+  options.channel = this.context.Buf.readInt32();
+  if (DEBUG) {
+    this.context.debug("Setting channel number in options: " + options.channel);
+  }
+  this.sendChromeMessage(options);
+};
+RilObject.prototype[REQUEST_SIM_CLOSE_CHANNEL] = function REQUEST_SIM_CLOSE_CHANNEL(length, options) {
+  this.sendDefaultResponse(options);
+};
+RilObject.prototype[REQUEST_SIM_TRANSMIT_APDU_CHANNEL] = function REQUEST_SIM_TRANSMIT_APDU_CHANNEL(length, options) {
+  if (options.rilRequestError) {
+    options.errorMsg = RIL_ERROR_TO_GECKO_ERROR[options.rilRequestError];
+    this.sendChromeMessage(options);
+    return;
+  }
+
+  let Buf = this.context.Buf;
+  options.sw1 = Buf.readInt32();
+  options.sw2 = Buf.readInt32();
+  options.simResponse = Buf.readString();
+  if (DEBUG) {
+    this.context.debug("Setting return values for RIL[REQUEST_SIM_TRANSMIT_APDU_CHANNEL]: [" +
+                       options.sw1 + "," +
+                       options.sw2 + ", " +
+                       options.simResponse + "]");
+  }
+  this.sendChromeMessage(options);
+};
+RilObject.prototype[REQUEST_NV_READ_ITEM] = null;
+RilObject.prototype[REQUEST_NV_WRITE_ITEM] = null;
+RilObject.prototype[REQUEST_NV_WRITE_CDMA_PRL] = null;
+RilObject.prototype[REQUEST_NV_RESET_CONFIG] = null;
 RilObject.prototype[REQUEST_SET_UICC_SUBSCRIPTION] = function REQUEST_SET_UICC_SUBSCRIPTION(length, options) {
   // Resend data subscription after uicc subscription.
   if (this._attachDataRegistration) {
     this.setDataRegistration({attach: true});
   }
 };
+RilObject.prototype[REQUEST_ALLOW_DATA] = null;
+RilObject.prototype[REQUEST_GET_HARDWARE_CONFIG] = null;
+RilObject.prototype[REQUEST_SIM_AUTHENTICATION] = null;
+RilObject.prototype[REQUEST_GET_DC_RT_INFO] = null;
+RilObject.prototype[REQUEST_SET_DC_RT_INFO_RATE] = null;
+RilObject.prototype[REQUEST_SET_DATA_PROFILE] = null;
+RilObject.prototype[REQUEST_SHUTDOWN] = null;
 RilObject.prototype[REQUEST_SET_DATA_SUBSCRIPTION] = function REQUEST_SET_DATA_SUBSCRIPTION(length, options) {
   if (!options.rilMessageType) {
     // The request was made by ril_worker itself. Don't report.
     return;
   }
   options.success = (options.rilRequestError === 0);
   if (!options.success) {
     options.errorMsg = RIL_ERROR_TO_GECKO_ERROR[options.rilRequestError];
@@ -6822,16 +6811,22 @@ RilObject.prototype[UNSOLICITED_RIL_CONN
 RilObject.prototype[UNSOLICITED_VOICE_RADIO_TECH_CHANGED] = function UNSOLICITED_VOICE_RADIO_TECH_CHANGED(length) {
   // This unsolicited response will be sent when the technology of a multi-tech
   // modem is changed, ex. switch between gsm and cdma.
   // TODO: We may need to do more on updating data when switching between gsm
   //       and cdma mode, e.g. IMEI, ESN, iccInfo, iccType ... etc.
   //       See Bug 866038.
   this._processRadioTech(this.context.Buf.readInt32List()[0]);
 };
+RilObject.prototype[UNSOLICITED_CELL_INFO_LIST] = null;
+RilObject.prototype[UNSOLICITED_RESPONSE_IMS_NETWORK_STATE_CHANGED] = null;
+RilObject.prototype[UNSOLICITED_UICC_SUBSCRIPTION_STATUS_CHANGED] = null;
+RilObject.prototype[UNSOLICITED_SRVCC_STATE_NOTIFY] = null;
+RilObject.prototype[UNSOLICITED_HARDWARE_CONFIG_CHANGED] = null;
+RilObject.prototype[UNSOLICITED_DC_RT_INFO_CHANGED] = null;
 
 /**
  * This object exposes the functionality to parse and serialize PDU strings
  *
  * A PDU is a string containing a series of hexadecimally encoded octets
  * or nibble-swapped binary-coded decimals (BCDs). It contains not only the
  * message text but information about the sender, the SMS service center,
  * timestamp, etc.
--- a/dom/system/gonk/ril_worker_buf_object.js
+++ b/dom/system/gonk/ril_worker_buf_object.js
@@ -24,16 +24,36 @@
     this.mToken = 1;
     // Maps tokens we send out with requests to the request type, so that
     // when we get a response parcel back, we know what request it was for.
     this.mTokenRequestMap = new Map();
     // This is because the underlying 'Buf' is still using the 'init' pattern, so
     // this derived one needs to invoke it.
     // Using 'apply' style to mark it's a parent method calling explicitly.
     Buf._init.apply(this);
+
+    // Remapping the request type to different values based on RIL version.
+    // We only have to do this for SUBSCRIPTION right now, so I just make it
+    // simple. A generic logic or structure could be discussed if we have more
+    // use cases, especially the cases from different partners.
+    this._requestMap = {};
+    // RIL version 8.
+    // For the CAF's proprietary parcels. Please see
+    // https://www.codeaurora.org/cgit/quic/la/platform/hardware/ril/tree/include/telephony/ril.h?h=b2g_jb_3.2
+    let map = {};
+    map[REQUEST_SET_UICC_SUBSCRIPTION] = 114;
+    map[REQUEST_SET_DATA_SUBSCRIPTION] = 115;
+    this._requestMap[8] = map;
+    // RIL version 9.
+    // For the CAF's proprietary parcels. Please see
+    // https://www.codeaurora.org/cgit/quic/la/platform/hardware/ril/tree/include/telephony/ril.h?h=b2g_kk_3.5
+    map = {};
+    map[REQUEST_SET_UICC_SUBSCRIPTION] = 115;
+    map[REQUEST_SET_DATA_SUBSCRIPTION] = 116;
+    this._requestMap[9] = map;
   };
 
   /**
    * "inherit" the basic worker buffer.
    */
   BufObject.prototype = Object.create(Buf);
 
   /**
@@ -119,29 +139,28 @@
 
   /**
    * Remapping the request type to different values based on RIL version.
    * We only have to do this for SUBSCRIPTION right now, so I just make it
    * simple. A generic logic or structure could be discussed if we have more
    * use cases, especially the cases from different partners.
    */
   BufObject.prototype._reMapRequestType = function(type) {
-    let newType = type;
-    switch (type) {
-      case REQUEST_SET_UICC_SUBSCRIPTION:
-      case REQUEST_SET_DATA_SUBSCRIPTION:
-        if (this.context.RIL.version < 9) {
-          // Shift the CAF's proprietary parcels. Please see
-          // https://www.codeaurora.org/cgit/quic/la/platform/hardware/ril/tree/include/telephony/ril.h?h=b2g_jb_3.2
-          newType = type - 1;
+    for (let version in this._requestMap) {
+      if (this.context.RIL.version <= version) {
+        let newType = this._requestMap[version][type];
+        if (newType) {
+          if (DEBUG) {
+            this.context.debug("Remap request type to " + newType);
+          }
+          return newType;
         }
-        break;
+      }
     }
-
-    return newType;
+    return type;
   };
 
   // Before we make sure to form it as a module would not add extra
   // overhead of module loading, we need to define it in this way
   // rather than 'module.exports' it as a module component.
   exports.BufObject = BufObject;
 })(self); // in worker self is the global
 
--- a/embedding/browser/nsDocShellTreeOwner.cpp
+++ b/embedding/browser/nsDocShellTreeOwner.cpp
@@ -1448,25 +1448,19 @@ ChromeTooltipListener::sTooltipCallback(
       bool textFound = false;
 
       self->mTooltipTextProvider->GetNodeText(
           self->mPossibleTooltipNode, getter_Copies(tooltipText), &textFound);
 
       if (textFound) {
         nsString tipText(tooltipText);
         LayoutDeviceIntPoint screenDot = widget->WidgetToScreenOffset();
-        double scaleFactor = 1.0;
-        if (shell->GetPresContext()) {
-          scaleFactor = double(nsPresContext::AppUnitsPerCSSPixel())/
-          shell->GetPresContext()->DeviceContext()->AppUnitsPerDevPixelAtUnitFullZoom();
-        }
-        // ShowTooltip expects widget-relative position.
-        self->ShowTooltip(self->mMouseScreenX - screenDot.x / scaleFactor,
-          self->mMouseScreenY - screenDot.y / scaleFactor,
-          tipText);
+        self->ShowTooltip(self->mMouseScreenX - screenDot.x,
+                          self->mMouseScreenY - screenDot.y,
+                          tipText);
       }
     }
 
     // release tooltip target if there is one, NO MATTER WHAT
     self->mPossibleTooltipNode = nullptr;
   } // if "self" data valid
 
 } // sTooltipCallback
--- a/layout/generic/nsSubDocumentFrame.cpp
+++ b/layout/generic/nsSubDocumentFrame.cpp
@@ -1269,8 +1269,29 @@ nsSubDocumentFrame::ObtainIntrinsicSizeF
 
     if (subDocRoot && subDocRoot->GetContent() &&
         subDocRoot->GetContent()->NodeInfo()->Equals(nsGkAtoms::svg, kNameSpaceID_SVG)) {
       return subDocRoot; // SVG documents have an intrinsic size
     }
   }
   return nullptr;
 }
+
+nsIntPoint
+nsSubDocumentFrame::GetChromeDisplacement()
+{
+  nsIFrame* nextFrame = nsLayoutUtils::GetCrossDocParentFrame(this);
+  if (!nextFrame) {
+    NS_WARNING("Couldn't find window chrome to calculate displacement to.");
+    return nsIntPoint();
+  }
+
+  nsIFrame* rootFrame = nextFrame;
+  while (nextFrame) {
+    rootFrame = nextFrame;
+    nextFrame = nsLayoutUtils::GetCrossDocParentFrame(rootFrame);
+  }
+
+  nsPoint offset = GetOffsetToCrossDoc(rootFrame);
+  int32_t appUnitsPerDevPixel = rootFrame->PresContext()->AppUnitsPerDevPixel();
+  return nsIntPoint((int)(offset.x/appUnitsPerDevPixel),
+                    (int)(offset.y/appUnitsPerDevPixel));
+}
--- a/layout/generic/nsSubDocumentFrame.h
+++ b/layout/generic/nsSubDocumentFrame.h
@@ -122,16 +122,18 @@ public:
   }
 
   /**
    * Return true if pointer event hit-testing should be allowed to target
    * content in the subdocument.
    */
   bool PassPointerEventsToChildren();
 
+  nsIntPoint GetChromeDisplacement();
+
 protected:
   friend class AsyncFrameInit;
 
   // Helper method to look up the HTML marginwidth & marginheight attributes
   nsIntSize GetMarginAttributes();
 
   nsFrameLoader* FrameLoader();
 
--- a/mobile/android/base/AppConstants.java.in
+++ b/mobile/android/base/AppConstants.java.in
@@ -168,16 +168,23 @@ public class AppConstants {
 
     public static final boolean MOZ_TELEMETRY_ON_BY_DEFAULT =
 //#ifdef MOZ_TELEMETRY_ON_BY_DEFAULT
     true;
 //#else
     false;
 //#endif
 
+    public static final boolean MOZ_ANDROID_TAB_QUEUE =
+//#ifdef MOZ_ANDROID_TAB_QUEUE
+    true;
+//#else
+    false;
+//#endif
+
     public static final String TELEMETRY_PREF_NAME =
           "toolkit.telemetry.enabled";
 
     public static final boolean MOZ_TELEMETRY_REPORTING =
 //#ifdef MOZ_TELEMETRY_REPORTING
     true;
 //#else
     false;
--- a/mobile/android/base/moz.build
+++ b/mobile/android/base/moz.build
@@ -678,17 +678,17 @@ ANDROID_RES_DIRS += [
 ANDROID_GENERATED_RESFILES += [
     'res/raw/suggestedsites.json',
     'res/values/strings.xml',
 ]
 
 for var in ('MOZ_ANDROID_ANR_REPORTER', 'MOZ_LINKER_EXTRACT', 'MOZILLA_OFFICIAL', 'MOZ_DEBUG',
             'MOZ_ANDROID_SEARCH_ACTIVITY', 'MOZ_NATIVE_DEVICES', 'MOZ_ANDROID_MLS_STUMBLER',
             'MOZ_ANDROID_SHARE_OVERLAY', 'MOZ_ANDROID_DOWNLOADS_INTEGRATION',
-            'MOZ_ANDROID_NEW_TABLET_UI'):
+            'MOZ_ANDROID_NEW_TABLET_UI', 'MOZ_ANDROID_TAB_QUEUE'):
     if CONFIG[var]:
         DEFINES[var] = 1
 
 for var in ('MOZ_UPDATER', 'MOZ_PKG_SPECIAL'):
     if CONFIG[var]:
         DEFINES[var] = CONFIG[var]
 
 for var in ('ANDROID_PACKAGE_NAME', 'ANDROID_CPU_ARCH',
--- a/mobile/android/base/resources/values-large-land-v11/styles.xml
+++ b/mobile/android/base/resources/values-large-land-v11/styles.xml
@@ -1,17 +1,16 @@
 <?xml version="1.0" encoding="utf-8"?>
 <!-- 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/. -->
 
 <resources>
 
     <style name="TabsLayout" parent="TabsLayoutBase">
-         <item name="android:orientation">vertical</item>
          <item name="android:scrollbars">vertical</item>
     </style>
 
     <style name="Widget.BookmarkFolderView" parent="Widget.TwoLinePageRow.Title">
         <item name="android:singleLine">true</item>
         <item name="android:ellipsize">none</item>
         <item name="android:paddingLeft">60dip</item>
         <item name="android:drawablePadding">10dip</item>
--- a/mobile/android/base/resources/values-large-v11/styles.xml
+++ b/mobile/android/base/resources/values-large-v11/styles.xml
@@ -51,18 +51,17 @@
     <style name="UrlBar.Button.Container">
         <item name="android:layout_marginTop">6dp</item>
         <item name="android:layout_marginBottom">6dp</item>
         <!-- Start with forward hidden -->
         <item name="android:orientation">horizontal</item>
     </style>
 
     <style name="TabsLayout" parent="TabsLayoutBase">
-         <item name="android:orientation">horizontal</item>
-         <item name="android:scrollbars">horizontal</item>
+         <item name="android:scrollbars">vertical</item>
     </style>
 
     <style name="TabsItem">
          <item name="android:nextFocusDown">@+id/close</item>
     </style>
 
     <style name="TabsItemClose">
          <item name="android:nextFocusUp">@+id/info</item>
--- a/testing/taskcluster/tasks/branches/try/job_flags.yml
+++ b/testing/taskcluster/tasks/branches/try/job_flags.yml
@@ -39,16 +39,28 @@ builds:
   emulator:
     platfoms:
       - b2g
     types:
       opt:
         task: tasks/builds/b2g_emulator_ics_opt.yml
       debug:
         task: tasks/builds/b2g_emulator_ics_debug.yml
+  flame-kk:
+    platforms:
+      - b2g
+    types:
+      opt:
+        task: tasks/builds/b2g_flame_kk_opt.yml
+  flame-kk-eng:
+    platforms:
+      - b2g
+    types:
+      opt:
+        task: tasks/builds/b2g_flame_kk_eng.yml
 
 tests:
   cppunit:
     allowed_build_tasks:
       tasks/builds/b2g_emulator_ics_opt.yml:
         task: tasks/tests/b2g_emulator_cpp_unit.yml
       tasks/builds/b2g_emulator_ics_debug.yml:
         task: tasks/tests/b2g_emulator_cpp_unit.yml
--- a/testing/taskcluster/tasks/builds/b2g_flame_kk_eng.yml
+++ b/testing/taskcluster/tasks/builds/b2g_flame_kk_eng.yml
@@ -1,12 +1,12 @@
 $inherits:
   from: 'tasks/builds/b2g_phone_base.yml'
 task:
-  workerType: b2gbuild-emulator-kk
+  workerType: flame-kk
   scopes:
     - 'docker-worker:cache:build-flame-kk-eng'
   metadata:
     name: '[TC] B2G Flame KK Eng'
 
   extra:
     treeherder:
       symbol: Be
--- a/testing/taskcluster/tasks/builds/b2g_flame_kk_opt.yml
+++ b/testing/taskcluster/tasks/builds/b2g_flame_kk_opt.yml
@@ -1,12 +1,12 @@
 $inherits:
   from: 'tasks/builds/b2g_phone_base.yml'
 task:
-  workerType: b2gbuild-emulator-kk
+  workerType: flame-kk
   scopes:
     - 'docker-worker:cache:build-flame-kk-opt'
   metadata:
     name: '[TC] B2G Flame KK Opt'
 
   payload:
     cache:
       build-flame-kk-opt: /home/worker/object-folder
--- a/testing/taskcluster/tasks/phone_build.yml
+++ b/testing/taskcluster/tasks/phone_build.yml
@@ -4,21 +4,23 @@
 taskId: {{build_slugid}}
 
 task:
   created: '{{now}}'
   deadline: '{{#from_now}}24 hours{{/from_now}}'
   metadata:
     source: http://todo.com/soon
     owner: mozilla-taskcluster-maintenance@mozilla.com
+
   tags:
     createdForUser: {{owner}}
 
   workerType: b2gbuild
   provisionerId: aws-provisioner
+  schedulerId: task-graph-scheduler
 
   scopes:
     # Nearly all of our build tasks use tc-vcs so just include the scope across
     # the board.
     - 'docker-worker:cache:tc-vcs'
     - 'docker-worker:image:{{#docker_image}}phone-builder{{/docker_image}}'
 
   payload:
@@ -44,9 +46,11 @@ task:
       GECKO_HEAD_REPOSITORY: '{{head_repository}}'
       GECKO_HEAD_REV: '{{head_rev}}'
       GECKO_HEAD_REF: '{{head_ref}}'
       MOZHARNESS_REPOSITORY: '{{mozharness_repository}}'
       MOZHARNESS_REV: '{{mozharness_rev}}'
 
   extra:
     treeherder:
+      groupSymbol: tc
+      groupName: Submitted by taskcluster
       symbol: B
--- a/toolkit/components/satchel/AutoCompleteE10S.jsm
+++ b/toolkit/components/satchel/AutoCompleteE10S.jsm
@@ -76,18 +76,19 @@ this.AutoCompleteE10S = {
   },
 
   _initPopup: function(browserWindow, rect) {
     this.browser = browserWindow.gBrowser.selectedBrowser;
     this.popup = this.browser.autoCompletePopup;
     this.popup.hidden = false;
     this.popup.setAttribute("width", rect.width);
 
-    this.x = rect.left;
-    this.y = rect.top + rect.height;
+    let {x, y} = this.browser.mapScreenCoordinatesFromContent(rect.left, rect.top + rect.height);
+    this.x = x;
+    this.y = y;
   },
 
   _showPopup: function(results) {
     AutoCompleteE10SView.clearResults();
 
     let resultsArray = [];
     let count = results.matchCount;
     for (let i = 0; i < count; i++) {
--- a/toolkit/content/widgets/browser.xml
+++ b/toolkit/content/widgets/browser.xml
@@ -863,17 +863,18 @@
               }
               this.updateBlockedPopups();
               break;
             }
             case "Autoscroll:Start": {
               if (!this.autoscrollEnabled) {
                 return false;
               }
-              this.startScroll(data.scrolldir, data.screenX, data.screenY);
+              let pos = this.mapScreenCoordinatesFromContent(data.screenX, data.screenY);
+              this.startScroll(data.scrolldir, pos.x, pos.y);
               return true;
             }
             case "Autoscroll:Cancel":
               this._autoScrollPopup.hidePopup();
               break;
           }
         ]]></body>
       </method>
@@ -1044,16 +1045,35 @@
                 break;
               }
             }
           }
         ]]>
         </body>
       </method>
 
+      <!--
+        For out-of-process code, event.screen[XY] is relative to the
+        left/top of the content view. For in-process code,
+        event.screen[XY] is relative to the left/top of the screen. We
+        use this method to map screen coordinates received from a
+        (possibly out-of-process) <browser> element to coordinates
+        that are relative to the screen. This code handles the
+        in-process case, where we return the coordinates unchanged.
+      -->
+      <method name="mapScreenCoordinatesFromContent">
+        <parameter name="aScreenX"/>
+        <parameter name="aScreenY"/>
+        <body>
+        <![CDATA[
+          return { x: aScreenX, y: aScreenY };
+        ]]>
+        </body>
+      </method>
+
       <method name="swapDocShells">
         <parameter name="aOtherBrowser"/>
         <body>
         <![CDATA[
           // We need to swap fields that are tied to our docshell or related to
           // the loaded page
           // Fields which are built as a result of notifactions (pageshow/hide,
           // DOMLinkAdded/Removed, onStateChange) should not be swapped here,
--- a/toolkit/content/widgets/remote-browser.xml
+++ b/toolkit/content/widgets/remote-browser.xml
@@ -380,16 +380,37 @@
           if (aTopic == "ask-children-to-exit-fullscreen") {
             if (aSubject == window.document) {
               this.messageManager.sendAsyncMessage("DOMFullscreen:ChildrenMustExit");
             }
           }
         ]]></body>
       </method>
 
+      <!--
+        For out-of-process code, event.screen[XY] is relative to the
+        left/top of the content view. For in-process code,
+        event.screen[XY] is relative to the left/top of the screen. We
+        use this method to map screen coordinates received from a
+        (possibly out-of-process) <browser> element to coordinates
+        that are relative to the screen. This code handles the
+        out-of-process case, where we need to translate by the screen
+        position of the <browser> element.
+      -->
+      <method name="mapScreenCoordinatesFromContent">
+        <parameter name="aScreenX"/>
+        <parameter name="aScreenY"/>
+        <body>
+        <![CDATA[
+          return { x: aScreenX + this.boxObject.screenX,
+                   y: aScreenY + this.boxObject.screenY };
+        ]]>
+        </body>
+      </method>
+
       <method name="enableDisableCommands">
         <parameter name="aAction"/>
         <parameter name="aEnabledLength"/>
         <parameter name="aEnabledCommands"/>
         <parameter name="aDisabledLength"/>
         <parameter name="aDisabledCommands"/>
         <body>
           if (this._controller) {
--- a/toolkit/modules/SelectParentHelper.jsm
+++ b/toolkit/modules/SelectParentHelper.jsm
@@ -23,17 +23,18 @@ this.SelectParentHelper = {
     menulist.selectedIndex = selectedIndex;
   },
 
   open: function(browser, menulist, rect) {
     menulist.hidden = false;
     currentBrowser = browser;
     this._registerListeners(menulist.menupopup);
 
-    menulist.menupopup.openPopupAtScreen(rect.left, rect.top + rect.height);
+    let {x, y} = browser.mapScreenCoordinatesFromContent(rect.left, rect.top + rect.height);
+    menulist.menupopup.openPopupAtScreen(x, y);
     menulist.selectedItem.scrollIntoView();
   },
 
   hide: function(menulist) {
     menulist.menupopup.hidePopup();
   },
 
   handleEvent: function(event) {
--- a/widget/PuppetWidget.cpp
+++ b/widget/PuppetWidget.cpp
@@ -940,23 +940,16 @@ PuppetWidget::GetWindowPosition()
     return nsIntPoint();
   }
 
   int32_t winX, winY, winW, winH;
   NS_ENSURE_SUCCESS(GetOwningTabChild()->GetDimensions(0, &winX, &winY, &winW, &winH), nsIntPoint());
   return nsIntPoint(winX, winY);
 }
 
-NS_METHOD
-PuppetWidget::GetScreenBounds(nsIntRect &aRect) {
-  aRect.MoveTo(LayoutDeviceIntPoint::ToUntyped(WidgetToScreenOffset()));
-  aRect.SizeTo(mBounds.Size());
-  return NS_OK;
-}
-
 PuppetScreen::PuppetScreen(void *nativeScreen)
 {
 }
 
 PuppetScreen::~PuppetScreen()
 {
 }
 
--- a/widget/PuppetWidget.h
+++ b/widget/PuppetWidget.h
@@ -75,36 +75,30 @@ public:
   virtual bool IsVisible() const MOZ_OVERRIDE
   { return mVisible; }
 
   NS_IMETHOD ConstrainPosition(bool     /*ignored aAllowSlop*/,
                                int32_t* aX,
                                int32_t* aY) MOZ_OVERRIDE
   { *aX = kMaxDimension;  *aY = kMaxDimension;  return NS_OK; }
 
-  // Widget position is controlled by the parent process via TabChild.
+  // We're always at <0, 0>, and so ignore move requests.
   NS_IMETHOD Move(double aX, double aY) MOZ_OVERRIDE
   { return NS_OK; }
 
   NS_IMETHOD Resize(double aWidth,
                     double aHeight,
                     bool   aRepaint) MOZ_OVERRIDE;
   NS_IMETHOD Resize(double aX,
                     double aY,
                     double aWidth,
                     double aHeight,
                     bool   aRepaint) MOZ_OVERRIDE
-  {
-    if (mBounds.x != aX || mBounds.y != aY) {
-      NotifyWindowMoved(aX, aY);
-    }
-    mBounds.x = aX;
-    mBounds.y = aY;
-    return Resize(aWidth, aHeight, aRepaint);
-  }
+  // (we're always at <0, 0>)
+  { return Resize(aWidth, aHeight, aRepaint); }
 
   // XXX/cjones: copying gtk behavior here; unclear what disabling a
   // widget is supposed to entail
   NS_IMETHOD Enable(bool aState) MOZ_OVERRIDE
   { mEnabled = aState;  return NS_OK; }
   virtual bool IsEnabled() const MOZ_OVERRIDE
   { return mEnabled; }
 
@@ -124,18 +118,19 @@ public:
   virtual void* GetNativeData(uint32_t aDataType) MOZ_OVERRIDE;
   NS_IMETHOD ReparentNativeWidget(nsIWidget* aNewParent) MOZ_OVERRIDE
   { return NS_ERROR_UNEXPECTED; }
 
   // PuppetWidgets don't have any concept of titles. 
   NS_IMETHOD SetTitle(const nsAString& aTitle) MOZ_OVERRIDE
   { return NS_ERROR_UNEXPECTED; }
   
+  // PuppetWidgets are always at <0, 0>.
   virtual mozilla::LayoutDeviceIntPoint WidgetToScreenOffset() MOZ_OVERRIDE
-  { return LayoutDeviceIntPoint::FromUntyped(GetWindowPosition() + GetChromeDimensions()); }
+  { return mozilla::LayoutDeviceIntPoint(0, 0); }
 
   void InitEvent(WidgetGUIEvent& aEvent, nsIntPoint* aPoint = nullptr);
 
   NS_IMETHOD DispatchEvent(WidgetGUIEvent* aEvent, nsEventStatus& aStatus) MOZ_OVERRIDE;
 
   NS_IMETHOD CaptureRollupEvents(nsIRollupListener* aListener,
                                  bool aDoCapture) MOZ_OVERRIDE
   { return NS_ERROR_UNEXPECTED; }
@@ -198,18 +193,16 @@ public:
   nsIntSize GetScreenDimensions();
 
   // Get the size of the chrome of the window that this tab belongs to.
   nsIntPoint GetChromeDimensions();
 
   // Get the screen position of the application window.
   nsIntPoint GetWindowPosition();
 
-  NS_IMETHOD GetScreenBounds(nsIntRect &aRect) MOZ_OVERRIDE;
-
 protected:
   bool mEnabled;
   bool mVisible;
 
   virtual nsresult NotifyIMEInternal(
                      const IMENotification& aIMENotification) MOZ_OVERRIDE;
 
 private:
--- a/xpfe/appshell/nsWebShellWindow.cpp
+++ b/xpfe/appshell/nsWebShellWindow.cpp
@@ -66,18 +66,16 @@
 
 #include "nsIBaseWindow.h"
 #include "nsIDocShellTreeItem.h"
 
 #include "mozilla/Attributes.h"
 #include "mozilla/DebugOnly.h"
 #include "mozilla/MouseEvents.h"
 
-#include "nsPIWindowRoot.h"
-
 #ifdef XP_MACOSX
 #include "nsINativeMenuService.h"
 #define USE_NATIVE_MENUS
 #endif
 
 using namespace mozilla;
 using namespace mozilla::dom;
 
@@ -254,25 +252,16 @@ nsWebShellWindow::WindowMoved(nsIWidget*
 {
   nsXULPopupManager* pm = nsXULPopupManager::GetInstance();
   if (pm) {
     nsCOMPtr<nsPIDOMWindow> window =
       mDocShell ? mDocShell->GetWindow() : nullptr;
     pm->AdjustPopupsOnWindowChange(window);
   }
 
-  // Notify all tabs that the widget moved.
-  if (mDocShell && mDocShell->GetWindow()) {
-    nsCOMPtr<EventTarget> eventTarget = mDocShell->GetWindow()->GetTopWindowRoot();
-    nsContentUtils::DispatchChromeEvent(mDocShell->GetDocument(),
-                                        eventTarget,
-                                        NS_LITERAL_STRING("MozUpdateWindowPos"),
-                                        false, false, nullptr);
-  }
-
   // Persist position, but not immediately, in case this OS is firing
   // repeated move events as the user drags the window
   SetPersistenceTimer(PAD_POSITION);
   return false;
 }
 
 bool
 nsWebShellWindow::WindowResized(nsIWidget* aWidget, int32_t aWidth, int32_t aHeight)