Merge m-c to inbound. a=merge
authorRyan VanderMeulen <ryanvm@gmail.com>
Mon, 27 Jul 2015 10:41:37 -0400
changeset 286430 e039b166890f312c61b58e6ecc205ba6ad584264
parent 286429 8541ce2730c063e3ed359a8f0212c7e6e1832594 (current diff)
parent 286374 21ca97268bae2d746e09ad6c612f4fbf3df0fe6e (diff)
child 286431 a3fd04625e6b30e69e38b068c5b8b949331ceb03
push id5067
push userraliiev@mozilla.com
push dateMon, 21 Sep 2015 14:04:52 +0000
treeherdermozilla-beta@14221ffe5b2f [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersmerge
milestone42.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/aries/sources.xml
+++ b/b2g/config/aries/sources.xml
@@ -10,25 +10,25 @@
   <!--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="e862ab9177af664f00b4522e2350f4cb13866d73">
     <copyfile dest="Makefile" src="core/root.mk"/>
   </project>
-  <project name="gaia" path="gaia" remote="mozillaorg" revision="ade1ef5249cfab921e00ed67b30f3d03c2e24b5e"/>
+  <project name="gaia" path="gaia" remote="mozillaorg" revision="4e3e21a4ba3f188b45623ee2297f21d0791f8667"/>
   <project name="fake-libdvm" path="dalvik" remote="b2g" revision="d50ae982b19f42f0b66d08b9eb306be81687869f"/>
   <project name="gonk-misc" path="gonk-misc" remote="b2g" revision="9f45c1988fe72749f0659409e6e3320fabf7b79a"/>
   <project name="librecovery" path="librecovery" remote="b2g" revision="1b3591a50ed352fc6ddb77462b7b35d0bfa555a3"/>
   <project name="moztt" path="external/moztt" remote="b2g" revision="657894b4a1dc0a926117f4812e0940229f9f676f"/>
   <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="1b587ca868ee75758959c2470a9c35a21299377f"/>
+  <project name="apitrace" path="external/apitrace" remote="apitrace" revision="5bb657ada461be666c35f419dbe072ed2ce632fc"/>
   <!-- Stock Android things -->
   <project groups="linux" name="platform/prebuilts/gcc/linux-x86/host/i686-linux-glibc2.7-4.6" path="prebuilts/gcc/linux-x86/host/i686-linux-glibc2.7-4.6" revision="95bb5b66b3ec5769c3de8d3f25d681787418e7d2"/>
   <project groups="linux" name="platform/prebuilts/gcc/linux-x86/host/x86_64-linux-glibc2.7-4.6" path="prebuilts/gcc/linux-x86/host/x86_64-linux-glibc2.7-4.6" revision="ebdad82e61c16772f6cd47e9f11936bf6ebe9aa0"/>
   <project groups="linux,arm" name="platform/prebuilts/gcc/linux-x86/arm/arm-eabi-4.7" path="prebuilts/gcc/linux-x86/arm/arm-eabi-4.7" revision="8b880805d454664b3eed11d0f053cdeafa1ff06e"/>
   <project groups="linux,arm" name="platform/prebuilts/gcc/linux-x86/arm/arm-linux-androideabi-4.7" path="prebuilts/gcc/linux-x86/arm/arm-linux-androideabi-4.7" revision="a1e239a0bb5cd1d69680bf1075883aa9a7bf2429"/>
   <project groups="linux,x86" name="platform/prebuilts/gcc/linux-x86/x86/i686-linux-android-4.7" path="prebuilts/gcc/linux-x86/x86/i686-linux-android-4.7" revision="c7931763d41be602407ed9d71e2c0292c6597e00"/>
   <project groups="linux,x86" name="platform/prebuilts/python/linux-x86/2.7.5" path="prebuilts/python/linux-x86/2.7.5" revision="a32003194f707f66a2d8cdb913ed1869f1926c5d"/>
   <project name="device/common" path="device/common" revision="96d4d2006c4fcb2f19a3fa47ab10cb409faa017b"/>
--- a/b2g/config/dolphin/sources.xml
+++ b/b2g/config/dolphin/sources.xml
@@ -10,25 +10,25 @@
   <!--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="e862ab9177af664f00b4522e2350f4cb13866d73">
     <copyfile dest="Makefile" src="core/root.mk"/>
   </project>
-  <project name="gaia" path="gaia" remote="mozillaorg" revision="ade1ef5249cfab921e00ed67b30f3d03c2e24b5e"/>
+  <project name="gaia" path="gaia" remote="mozillaorg" revision="4e3e21a4ba3f188b45623ee2297f21d0791f8667"/>
   <project name="fake-libdvm" path="dalvik" remote="b2g" revision="d50ae982b19f42f0b66d08b9eb306be81687869f"/>
   <project name="gonk-misc" path="gonk-misc" remote="b2g" revision="9f45c1988fe72749f0659409e6e3320fabf7b79a"/>
   <project name="librecovery" path="librecovery" remote="b2g" revision="1b3591a50ed352fc6ddb77462b7b35d0bfa555a3"/>
   <project name="moztt" path="external/moztt" remote="b2g" revision="657894b4a1dc0a926117f4812e0940229f9f676f"/>
   <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="1b587ca868ee75758959c2470a9c35a21299377f"/>
+  <project name="apitrace" path="external/apitrace" remote="apitrace" revision="5bb657ada461be666c35f419dbe072ed2ce632fc"/>
   <!-- Stock Android things -->
   <project groups="linux" name="platform/prebuilts/gcc/linux-x86/host/i686-linux-glibc2.7-4.6" path="prebuilts/gcc/linux-x86/host/i686-linux-glibc2.7-4.6" revision="95bb5b66b3ec5769c3de8d3f25d681787418e7d2"/>
   <project groups="linux" name="platform/prebuilts/gcc/linux-x86/host/x86_64-linux-glibc2.7-4.6" path="prebuilts/gcc/linux-x86/host/x86_64-linux-glibc2.7-4.6" revision="ebdad82e61c16772f6cd47e9f11936bf6ebe9aa0"/>
   <project groups="linux,arm" name="platform/prebuilts/gcc/linux-x86/arm/arm-eabi-4.7" path="prebuilts/gcc/linux-x86/arm/arm-eabi-4.7" revision="8b880805d454664b3eed11d0f053cdeafa1ff06e"/>
   <project groups="linux,arm" name="platform/prebuilts/gcc/linux-x86/arm/arm-linux-androideabi-4.7" path="prebuilts/gcc/linux-x86/arm/arm-linux-androideabi-4.7" revision="a1e239a0bb5cd1d69680bf1075883aa9a7bf2429"/>
   <project groups="linux,x86" name="platform/prebuilts/gcc/linux-x86/x86/i686-linux-android-4.7" path="prebuilts/gcc/linux-x86/x86/i686-linux-android-4.7" revision="c7931763d41be602407ed9d71e2c0292c6597e00"/>
   <project groups="linux,x86" name="platform/prebuilts/python/linux-x86/2.7.5" path="prebuilts/python/linux-x86/2.7.5" revision="83760d213fb3bec7b4117d266fcfbf6fe2ba14ab"/>
   <project name="device/common" path="device/common" revision="6a2995683de147791e516aae2ccb31fdfbe2ad30"/>
--- 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="173b3104bfcbd23fc9dccd4b0035fc49aae3d444">
     <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="ade1ef5249cfab921e00ed67b30f3d03c2e24b5e"/>
+  <project name="gaia.git" path="gaia" remote="mozillaorg" revision="4e3e21a4ba3f188b45623ee2297f21d0791f8667"/>
   <project name="gonk-misc" path="gonk-misc" remote="b2g" revision="9f45c1988fe72749f0659409e6e3320fabf7b79a"/>
   <project name="rilproxy" path="rilproxy" remote="b2g" revision="5ef30994f4778b4052e58a4383dbe7890048c87e"/>
   <project name="platform_hardware_ril" path="hardware/ril" remote="b2g" revision="8bc59310552179f9a8bc6cdd0188e2475df52fb7"/>
   <project name="platform_external_qemu" path="external/qemu" remote="b2g" revision="9d0e5057ee5404a31ec1bf76131cb11336a7c3b6"/>
   <project name="moztt" path="external/moztt" remote="b2g" revision="657894b4a1dc0a926117f4812e0940229f9f676f"/>
   <project name="apitrace" path="external/apitrace" remote="apitrace" revision="34ea6163f9f0e0122fb0bb03607eccdca31ced7a"/>
   <!-- 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,20 +12,20 @@
   <!--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="4efd19d199ae52656604f794c5a77518400220fd">
     <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="ade1ef5249cfab921e00ed67b30f3d03c2e24b5e"/>
+  <project name="gaia" path="gaia" remote="mozillaorg" revision="4e3e21a4ba3f188b45623ee2297f21d0791f8667"/>
   <project name="gonk-misc" path="gonk-misc" remote="b2g" revision="9f45c1988fe72749f0659409e6e3320fabf7b79a"/>
   <project name="moztt" path="external/moztt" remote="b2g" revision="657894b4a1dc0a926117f4812e0940229f9f676f"/>
-  <project name="apitrace" path="external/apitrace" remote="apitrace" revision="1b587ca868ee75758959c2470a9c35a21299377f"/>
+  <project name="apitrace" path="external/apitrace" remote="apitrace" revision="5bb657ada461be666c35f419dbe072ed2ce632fc"/>
   <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"/>
   <project groups="linux" name="platform/prebuilts/gcc/linux-x86/host/i686-linux-glibc2.7-4.6" path="prebuilts/gcc/linux-x86/host/i686-linux-glibc2.7-4.6" revision="9025e50b9d29b3cabbbb21e1dd94d0d13121a17e"/>
   <project groups="linux" name="platform/prebuilts/gcc/linux-x86/host/x86_64-linux-glibc2.7-4.6" path="prebuilts/gcc/linux-x86/host/x86_64-linux-glibc2.7-4.6" revision="b89fda71fcd0fa0cf969310e75be3ea33e048b44"/>
   <project groups="linux,arm" name="platform/prebuilts/gcc/linux-x86/arm/arm-eabi-4.7" path="prebuilts/gcc/linux-x86/arm/arm-eabi-4.7" revision="2e7d5348f35575870b3c7e567a9a9f6d66f8d6c5"/>
--- a/b2g/config/emulator-kk/sources.xml
+++ b/b2g/config/emulator-kk/sources.xml
@@ -10,25 +10,25 @@
   <!--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="e862ab9177af664f00b4522e2350f4cb13866d73">
     <copyfile dest="Makefile" src="core/root.mk"/>
   </project>
-  <project name="gaia" path="gaia" remote="mozillaorg" revision="ade1ef5249cfab921e00ed67b30f3d03c2e24b5e"/>
+  <project name="gaia" path="gaia" remote="mozillaorg" revision="4e3e21a4ba3f188b45623ee2297f21d0791f8667"/>
   <project name="fake-libdvm" path="dalvik" remote="b2g" revision="d50ae982b19f42f0b66d08b9eb306be81687869f"/>
   <project name="gonk-misc" path="gonk-misc" remote="b2g" revision="9f45c1988fe72749f0659409e6e3320fabf7b79a"/>
   <project name="librecovery" path="librecovery" remote="b2g" revision="1b3591a50ed352fc6ddb77462b7b35d0bfa555a3"/>
   <project name="moztt" path="external/moztt" remote="b2g" revision="657894b4a1dc0a926117f4812e0940229f9f676f"/>
   <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="1b587ca868ee75758959c2470a9c35a21299377f"/>
+  <project name="apitrace" path="external/apitrace" remote="apitrace" revision="5bb657ada461be666c35f419dbe072ed2ce632fc"/>
   <!-- Stock Android things -->
   <project groups="linux" name="platform/prebuilts/gcc/linux-x86/host/i686-linux-glibc2.7-4.6" path="prebuilts/gcc/linux-x86/host/i686-linux-glibc2.7-4.6" revision="f92a936f2aa97526d4593386754bdbf02db07a12"/>
   <project groups="linux" name="platform/prebuilts/gcc/linux-x86/host/x86_64-linux-glibc2.7-4.6" path="prebuilts/gcc/linux-x86/host/x86_64-linux-glibc2.7-4.6" revision="6e47ff2790f5656b5b074407829ceecf3e6188c4"/>
   <project groups="linux,arm" name="platform/prebuilts/gcc/linux-x86/arm/arm-eabi-4.7" path="prebuilts/gcc/linux-x86/arm/arm-eabi-4.7" revision="1950e4760fa14688b83cdbb5acaa1af9f82ef434"/>
   <project groups="linux,arm" name="platform/prebuilts/gcc/linux-x86/arm/arm-linux-androideabi-4.7" path="prebuilts/gcc/linux-x86/arm/arm-linux-androideabi-4.7" revision="ac6eb97a37035c09fb5ede0852f0881e9aadf9ad"/>
   <project groups="linux,x86" name="platform/prebuilts/gcc/linux-x86/x86/i686-linux-android-4.7" path="prebuilts/gcc/linux-x86/x86/i686-linux-android-4.7" revision="737f591c5f95477148d26602c7be56cbea0cdeb9"/>
   <project groups="linux,x86" name="platform/prebuilts/python/linux-x86/2.7.5" path="prebuilts/python/linux-x86/2.7.5" revision="51da9b1981be481b92a59a826d4d78dc73d0989a"/>
   <project name="device/common" path="device/common" revision="798a3664597e6041985feab9aef42e98d458bc3d"/>
--- a/b2g/config/emulator-l/sources.xml
+++ b/b2g/config/emulator-l/sources.xml
@@ -10,25 +10,25 @@
   <!--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="07c383a786f188904311a37f6062c2cb84c9b61d">
     <copyfile dest="Makefile" src="core/root.mk"/>
   </project>
-  <project name="gaia" path="gaia" remote="mozillaorg" revision="ade1ef5249cfab921e00ed67b30f3d03c2e24b5e"/>
+  <project name="gaia" path="gaia" remote="mozillaorg" revision="4e3e21a4ba3f188b45623ee2297f21d0791f8667"/>
   <project name="fake-libdvm" path="dalvik" remote="b2g" revision="d50ae982b19f42f0b66d08b9eb306be81687869f"/>
   <project name="gonk-misc" path="gonk-misc" remote="b2g" revision="9f45c1988fe72749f0659409e6e3320fabf7b79a"/>
   <project name="librecovery" path="librecovery" remote="b2g" revision="1b3591a50ed352fc6ddb77462b7b35d0bfa555a3"/>
   <project name="moztt" path="external/moztt" remote="b2g" revision="657894b4a1dc0a926117f4812e0940229f9f676f"/>
   <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="1b587ca868ee75758959c2470a9c35a21299377f"/>
+  <project name="apitrace" path="external/apitrace" remote="apitrace" revision="5bb657ada461be666c35f419dbe072ed2ce632fc"/>
   <!-- Stock Android things -->
   <project groups="pdk,linux" name="platform/prebuilts/clang/linux-x86/host/3.5" path="prebuilts/clang/linux-x86/host/3.5" revision="ffc05a232799fe8fcb3e47b7440b52b1fb4244c0"/>
   <project groups="pdk,linux,arm" name="platform/prebuilts/gcc/linux-x86/aarch64/aarch64-linux-android-4.8" path="prebuilts/gcc/linux-x86/aarch64/aarch64-linux-android-4.8" revision="337e0ef5e40f02a1ae59b90db0548976c70a7226"/>
   <project groups="pdk,linux,arm" name="platform/prebuilts/gcc/linux-x86/arm/arm-eabi-4.8" path="prebuilts/gcc/linux-x86/arm/arm-eabi-4.8" revision="8af5ff6f5dced9eb5a8127459df6c75d24342204"/>
   <project groups="pdk,linux,arm" name="platform/prebuilts/gcc/linux-x86/arm/arm-linux-androideabi-4.8" path="prebuilts/gcc/linux-x86/arm/arm-linux-androideabi-4.8" revision="30915518fa7ea07166efedc191a4f40aef516fe7"/>
   <project groups="pdk,linux" name="platform/prebuilts/gcc/linux-x86/host/x86_64-linux-glibc2.11-4.6" path="prebuilts/gcc/linux-x86/host/x86_64-linux-glibc2.11-4.6" revision="96eee58e3389fb05a835310d6a06a6ba4486097a"/>
   <project groups="pdk,linux" name="platform/prebuilts/gcc/linux-x86/host/x86_64-linux-glibc2.11-4.8" path="prebuilts/gcc/linux-x86/host/x86_64-linux-glibc2.11-4.8" revision="7c8a46698171aa2e0be09edb43d15a6acf832770"/>
   <project groups="pdk,linux,x86" name="platform/prebuilts/gcc/linux-x86/x86/x86_64-linux-android-4.8" path="prebuilts/gcc/linux-x86/x86/x86_64-linux-android-4.8" revision="24b2038be8a636fd4a5d21f0abae1e466b07bcf7"/>
--- 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="173b3104bfcbd23fc9dccd4b0035fc49aae3d444">
     <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="ade1ef5249cfab921e00ed67b30f3d03c2e24b5e"/>
+  <project name="gaia.git" path="gaia" remote="mozillaorg" revision="4e3e21a4ba3f188b45623ee2297f21d0791f8667"/>
   <project name="gonk-misc" path="gonk-misc" remote="b2g" revision="9f45c1988fe72749f0659409e6e3320fabf7b79a"/>
   <project name="rilproxy" path="rilproxy" remote="b2g" revision="5ef30994f4778b4052e58a4383dbe7890048c87e"/>
   <project name="platform_hardware_ril" path="hardware/ril" remote="b2g" revision="8bc59310552179f9a8bc6cdd0188e2475df52fb7"/>
   <project name="platform_external_qemu" path="external/qemu" remote="b2g" revision="9d0e5057ee5404a31ec1bf76131cb11336a7c3b6"/>
   <project name="moztt" path="external/moztt" remote="b2g" revision="657894b4a1dc0a926117f4812e0940229f9f676f"/>
   <project name="apitrace" path="external/apitrace" remote="apitrace" revision="34ea6163f9f0e0122fb0bb03607eccdca31ced7a"/>
   <!-- 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,25 +10,25 @@
   <!--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="e862ab9177af664f00b4522e2350f4cb13866d73">
     <copyfile dest="Makefile" src="core/root.mk"/>
   </project>
-  <project name="gaia" path="gaia" remote="mozillaorg" revision="ade1ef5249cfab921e00ed67b30f3d03c2e24b5e"/>
+  <project name="gaia" path="gaia" remote="mozillaorg" revision="4e3e21a4ba3f188b45623ee2297f21d0791f8667"/>
   <project name="fake-libdvm" path="dalvik" remote="b2g" revision="d50ae982b19f42f0b66d08b9eb306be81687869f"/>
   <project name="gonk-misc" path="gonk-misc" remote="b2g" revision="9f45c1988fe72749f0659409e6e3320fabf7b79a"/>
   <project name="librecovery" path="librecovery" remote="b2g" revision="1b3591a50ed352fc6ddb77462b7b35d0bfa555a3"/>
   <project name="moztt" path="external/moztt" remote="b2g" revision="657894b4a1dc0a926117f4812e0940229f9f676f"/>
   <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="1b587ca868ee75758959c2470a9c35a21299377f"/>
+  <project name="apitrace" path="external/apitrace" remote="apitrace" revision="5bb657ada461be666c35f419dbe072ed2ce632fc"/>
   <!-- Stock Android things -->
   <project groups="linux" name="platform/prebuilts/gcc/linux-x86/host/i686-linux-glibc2.7-4.6" path="prebuilts/gcc/linux-x86/host/i686-linux-glibc2.7-4.6" revision="95bb5b66b3ec5769c3de8d3f25d681787418e7d2"/>
   <project groups="linux" name="platform/prebuilts/gcc/linux-x86/host/x86_64-linux-glibc2.7-4.6" path="prebuilts/gcc/linux-x86/host/x86_64-linux-glibc2.7-4.6" revision="ebdad82e61c16772f6cd47e9f11936bf6ebe9aa0"/>
   <project groups="linux,arm" name="platform/prebuilts/gcc/linux-x86/arm/arm-eabi-4.7" path="prebuilts/gcc/linux-x86/arm/arm-eabi-4.7" revision="8b880805d454664b3eed11d0f053cdeafa1ff06e"/>
   <project groups="linux,arm" name="platform/prebuilts/gcc/linux-x86/arm/arm-linux-androideabi-4.7" path="prebuilts/gcc/linux-x86/arm/arm-linux-androideabi-4.7" revision="a1e239a0bb5cd1d69680bf1075883aa9a7bf2429"/>
   <project groups="linux,x86" name="platform/prebuilts/gcc/linux-x86/x86/i686-linux-android-4.7" path="prebuilts/gcc/linux-x86/x86/i686-linux-android-4.7" revision="c7931763d41be602407ed9d71e2c0292c6597e00"/>
   <project groups="linux,x86" name="platform/prebuilts/python/linux-x86/2.7.5" path="prebuilts/python/linux-x86/2.7.5" revision="a32003194f707f66a2d8cdb913ed1869f1926c5d"/>
   <project name="device/common" path="device/common" revision="96d4d2006c4fcb2f19a3fa47ab10cb409faa017b"/>
--- a/b2g/config/gaia.json
+++ b/b2g/config/gaia.json
@@ -1,9 +1,9 @@
 {
     "git": {
-        "git_revision": "ade1ef5249cfab921e00ed67b30f3d03c2e24b5e", 
+        "git_revision": "4e3e21a4ba3f188b45623ee2297f21d0791f8667", 
         "remote": "https://git.mozilla.org/releases/gaia.git", 
         "branch": ""
     }, 
-    "revision": "e09bb83fd01a4f6ab6d569c3cf2d8b099f30df55", 
+    "revision": "995162ffe23e554b4de57f72e6341916e2844e92", 
     "repo_path": "integration/gaia-central"
 }
--- a/b2g/config/nexus-4/sources.xml
+++ b/b2g/config/nexus-4/sources.xml
@@ -12,20 +12,20 @@
   <!--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="4efd19d199ae52656604f794c5a77518400220fd">
     <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="ade1ef5249cfab921e00ed67b30f3d03c2e24b5e"/>
+  <project name="gaia" path="gaia" remote="mozillaorg" revision="4e3e21a4ba3f188b45623ee2297f21d0791f8667"/>
   <project name="gonk-misc" path="gonk-misc" remote="b2g" revision="9f45c1988fe72749f0659409e6e3320fabf7b79a"/>
   <project name="moztt" path="external/moztt" remote="b2g" revision="657894b4a1dc0a926117f4812e0940229f9f676f"/>
-  <project name="apitrace" path="external/apitrace" remote="apitrace" revision="1b587ca868ee75758959c2470a9c35a21299377f"/>
+  <project name="apitrace" path="external/apitrace" remote="apitrace" revision="5bb657ada461be666c35f419dbe072ed2ce632fc"/>
   <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"/>
   <project groups="linux" name="platform/prebuilts/gcc/linux-x86/host/i686-linux-glibc2.7-4.6" path="prebuilts/gcc/linux-x86/host/i686-linux-glibc2.7-4.6" revision="9025e50b9d29b3cabbbb21e1dd94d0d13121a17e"/>
   <project groups="linux" name="platform/prebuilts/gcc/linux-x86/host/x86_64-linux-glibc2.7-4.6" path="prebuilts/gcc/linux-x86/host/x86_64-linux-glibc2.7-4.6" revision="b89fda71fcd0fa0cf969310e75be3ea33e048b44"/>
   <project groups="linux,arm" name="platform/prebuilts/gcc/linux-x86/arm/arm-eabi-4.7" path="prebuilts/gcc/linux-x86/arm/arm-eabi-4.7" revision="2e7d5348f35575870b3c7e567a9a9f6d66f8d6c5"/>
--- a/b2g/config/nexus-5-l/sources.xml
+++ b/b2g/config/nexus-5-l/sources.xml
@@ -10,25 +10,25 @@
   <!--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="07c383a786f188904311a37f6062c2cb84c9b61d">
     <copyfile dest="Makefile" src="core/root.mk"/>
   </project>
-  <project name="gaia" path="gaia" remote="mozillaorg" revision="ade1ef5249cfab921e00ed67b30f3d03c2e24b5e"/>
+  <project name="gaia" path="gaia" remote="mozillaorg" revision="4e3e21a4ba3f188b45623ee2297f21d0791f8667"/>
   <project name="fake-libdvm" path="dalvik" remote="b2g" revision="d50ae982b19f42f0b66d08b9eb306be81687869f"/>
   <project name="gonk-misc" path="gonk-misc" remote="b2g" revision="9f45c1988fe72749f0659409e6e3320fabf7b79a"/>
   <project name="librecovery" path="librecovery" remote="b2g" revision="1b3591a50ed352fc6ddb77462b7b35d0bfa555a3"/>
   <project name="moztt" path="external/moztt" remote="b2g" revision="657894b4a1dc0a926117f4812e0940229f9f676f"/>
   <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="1b587ca868ee75758959c2470a9c35a21299377f"/>
+  <project name="apitrace" path="external/apitrace" remote="apitrace" revision="5bb657ada461be666c35f419dbe072ed2ce632fc"/>
   <!-- Stock Android things -->
   <project groups="pdk,linux" name="platform/prebuilts/clang/linux-x86/host/3.5" path="prebuilts/clang/linux-x86/host/3.5" revision="ffc05a232799fe8fcb3e47b7440b52b1fb4244c0"/>
   <project groups="pdk,linux,arm" name="platform/prebuilts/gcc/linux-x86/aarch64/aarch64-linux-android-4.8" path="prebuilts/gcc/linux-x86/aarch64/aarch64-linux-android-4.8" revision="337e0ef5e40f02a1ae59b90db0548976c70a7226"/>
   <project groups="pdk,linux,arm" name="platform/prebuilts/gcc/linux-x86/arm/arm-eabi-4.8" path="prebuilts/gcc/linux-x86/arm/arm-eabi-4.8" revision="8af5ff6f5dced9eb5a8127459df6c75d24342204"/>
   <project groups="pdk,linux,arm" name="platform/prebuilts/gcc/linux-x86/arm/arm-linux-androideabi-4.8" path="prebuilts/gcc/linux-x86/arm/arm-linux-androideabi-4.8" revision="30915518fa7ea07166efedc191a4f40aef516fe7"/>
   <project groups="pdk,linux" name="platform/prebuilts/gcc/linux-x86/host/x86_64-linux-glibc2.11-4.6" path="prebuilts/gcc/linux-x86/host/x86_64-linux-glibc2.11-4.6" revision="96eee58e3389fb05a835310d6a06a6ba4486097a"/>
   <project groups="pdk,linux" name="platform/prebuilts/gcc/linux-x86/host/x86_64-linux-glibc2.11-4.8" path="prebuilts/gcc/linux-x86/host/x86_64-linux-glibc2.11-4.8" revision="7c8a46698171aa2e0be09edb43d15a6acf832770"/>
   <project groups="pdk,linux,x86" name="platform/prebuilts/gcc/linux-x86/x86/x86_64-linux-android-4.8" path="prebuilts/gcc/linux-x86/x86/x86_64-linux-android-4.8" revision="24b2038be8a636fd4a5d21f0abae1e466b07bcf7"/>
--- a/b2g/installer/package-manifest.in
+++ b/b2g/installer/package-manifest.in
@@ -706,16 +706,17 @@
 ; Modules
 @RESPATH@/modules/*
 
 ; Safe Browsing
 @RESPATH@/components/nsURLClassifier.manifest
 @RESPATH@/components/nsUrlClassifierHashCompleter.js
 @RESPATH@/components/nsUrlClassifierListManager.js
 @RESPATH@/components/nsUrlClassifierLib.js
+@RESPATH@/components/PrivateBrowsingTrackingProtectionWhitelist.js
 @RESPATH@/components/url-classifier.xpt
 
 ; GNOME hooks
 #ifdef MOZ_ENABLE_GNOME_COMPONENT
 @RESPATH@/components/@DLL_PREFIX@mozgnome@DLL_SUFFIX@
 #endif
 
 ; ANGLE on Win32
--- a/browser/base/content/aboutaccounts/aboutaccounts.js
+++ b/browser/base/content/aboutaccounts/aboutaccounts.js
@@ -143,18 +143,23 @@ let wrapper = {
    *
    * @param accountData the user's account data and credentials
    */
   onLogin: function (accountData) {
     log("Received: 'login'. Data:" + JSON.stringify(accountData));
 
     if (accountData.customizeSync) {
       Services.prefs.setBoolPref(PREF_SYNC_SHOW_CUSTOMIZATION, true);
-      delete accountData.customizeSync;
     }
+    delete accountData.customizeSync;
+    // sessionTokenContext is erroneously sent by the content server.
+    // https://github.com/mozilla/fxa-content-server/issues/2766
+    // To avoid having the FxA storage manager not knowing what to do with
+    // it we delete it here.
+    delete accountData.sessionTokenContext;
 
     // We need to confirm a relink - see shouldAllowRelink for more
     let newAccountEmail = accountData.email;
     // The hosted code may have already checked for the relink situation
     // by sending the can_link_account command. If it did, then
     // it will indicate we don't need to ask twice.
     if (!accountData.verifiedCanLinkAccount && !shouldAllowRelink(newAccountEmail)) {
       // we need to tell the page we successfully received the message, but
--- a/browser/base/content/browser-trackingprotection.js
+++ b/browser/base/content/browser-trackingprotection.js
@@ -111,35 +111,42 @@ let TrackingProtection = {
     // Any scheme turned into https is correct.
     let normalizedUrl = Services.io.newURI(
       "https://" + gBrowser.selectedBrowser.currentURI.hostPort,
       null, null);
 
     // Add the current host in the 'trackingprotection' consumer of
     // the permission manager using a normalized URI. This effectively
     // places this host on the tracking protection allowlist.
-    Services.perms.add(normalizedUrl,
-      "trackingprotection", Services.perms.ALLOW_ACTION);
+    if (PrivateBrowsingUtils.isBrowserPrivate(gBrowser.selectedBrowser)) {
+      PrivateBrowsingUtils.addToTrackingAllowlist(normalizedUrl);
+    } else {
+      Services.perms.add(normalizedUrl,
+        "trackingprotection", Services.perms.ALLOW_ACTION);
+    }
 
     // Telemetry for disable protection.
     this.eventsHistogram.add(1);
 
     BrowserReload();
   },
 
   enableForCurrentPage() {
     // Remove the current host from the 'trackingprotection' consumer
     // of the permission manager. This effectively removes this host
     // from the tracking protection allowlist.
     let normalizedUrl = Services.io.newURI(
       "https://" + gBrowser.selectedBrowser.currentURI.hostPort,
       null, null);
 
-    Services.perms.remove(normalizedUrl,
-      "trackingprotection");
+    if (PrivateBrowsingUtils.isBrowserPrivate(gBrowser.selectedBrowser)) {
+      PrivateBrowsingUtils.removeFromTrackingAllowlist(normalizedUrl);
+    } else {
+      Services.perms.remove(normalizedUrl, "trackingprotection");
+    }
 
     // Telemetry for enable protection.
     this.eventsHistogram.add(2);
 
     BrowserReload();
   },
 
   showIntroPanel: Task.async(function*() {
--- a/browser/base/content/test/general/browser.ini
+++ b/browser/base/content/test/general/browser.ini
@@ -426,16 +426,20 @@ support-files =
   benignPage.html
 [browser_trackingUI_3.js]
 tags = trackingprotection
 [browser_trackingUI_4.js]
 tags = trackingprotection
 support-files =
   trackingPage.html
   benignPage.html
+[browser_trackingUI_5.js]
+tags = trackingprotection
+support-files =
+  trackingPage.html
 [browser_typeAheadFind.js]
 skip-if = buildapp == 'mulet'
 [browser_unknownContentType_title.js]
 [browser_unloaddialogs.js]
 skip-if = e10s # Bug 1100700 - test relies on unload event firing on closed tabs, which it doesn't
 [browser_urlHighlight.js]
 [browser_urlbarAutoFillTrimURLs.js]
 [browser_urlbarCopying.js]
new file mode 100644
--- /dev/null
+++ b/browser/base/content/test/general/browser_trackingUI_5.js
@@ -0,0 +1,122 @@
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// Test that sites added to the Tracking Protection whitelist in private
+// browsing mode don't persist once the private browsing window closes.
+
+const PB_PREF = "privacy.trackingprotection.pbmode.enabled";
+const TRACKING_PAGE = "http://tracking.example.org/browser/browser/base/content/test/general/trackingPage.html";
+let TrackingProtection = null;
+let browser = null;
+let {UrlClassifierTestUtils} = Cu.import("resource://testing-common/UrlClassifierTestUtils.jsm", {});
+
+registerCleanupFunction(function() {
+  TrackingProtection = browser = null;
+  UrlClassifierTestUtils.cleanupTestTrackers();
+});
+
+function hidden(sel) {
+  let win = browser.ownerGlobal;
+  let el = win.document.querySelector(sel);
+  let display = win.getComputedStyle(el).getPropertyValue("display", null);
+  return display === "none";
+}
+
+function clickButton(sel) {
+  let win = browser.ownerGlobal;
+  let el = win.document.querySelector(sel);
+  el.doCommand();
+}
+
+function testTrackingPage(window) {
+  info("Tracking content must be blocked");
+  ok(!TrackingProtection.container.hidden, "The container is visible");
+  is(TrackingProtection.content.getAttribute("state"), "blocked-tracking-content",
+     'content: state="blocked-tracking-content"');
+  is(TrackingProtection.icon.getAttribute("state"), "blocked-tracking-content",
+     'icon: state="blocked-tracking-content"');
+
+  ok(!hidden("#tracking-protection-icon"), "icon is visible");
+  ok(hidden("#tracking-action-block"), "blockButton is hidden");
+
+  ok(hidden("#tracking-action-unblock"), "unblockButton is hidden");
+  ok(!hidden("#tracking-action-unblock-private"), "unblockButtonPrivate is visible");
+
+  // Make sure that the blocked tracking elements message appears
+  ok(hidden("#tracking-not-detected"), "labelNoTracking is hidden");
+  ok(hidden("#tracking-loaded"), "labelTrackingLoaded is hidden");
+  ok(!hidden("#tracking-blocked"), "labelTrackingBlocked is visible");
+}
+
+function testTrackingPageUnblocked() {
+  info("Tracking content must be white-listed and not blocked");
+  ok(!TrackingProtection.container.hidden, "The container is visible");
+  is(TrackingProtection.content.getAttribute("state"), "loaded-tracking-content",
+     'content: state="loaded-tracking-content"');
+  is(TrackingProtection.icon.getAttribute("state"), "loaded-tracking-content",
+     'icon: state="loaded-tracking-content"');
+
+  ok(!hidden("#tracking-protection-icon"), "icon is visible");
+  ok(!hidden("#tracking-action-block"), "blockButton is visible");
+  ok(hidden("#tracking-action-unblock"), "unblockButton is hidden");
+
+  // Make sure that the blocked tracking elements message appears
+  ok(hidden("#tracking-not-detected"), "labelNoTracking is hidden");
+  ok(!hidden("#tracking-loaded"), "labelTrackingLoaded is visible");
+  ok(hidden("#tracking-blocked"), "labelTrackingBlocked is hidden");
+}
+
+add_task(function* testExceptionAddition() {
+  yield UrlClassifierTestUtils.addTestTrackers();
+  let privateWin = yield promiseOpenAndLoadWindow({private: true}, true);
+  browser = privateWin.gBrowser;
+  let tab = browser.selectedTab = browser.addTab();
+
+  TrackingProtection = browser.ownerGlobal.TrackingProtection;
+  yield pushPrefs([PB_PREF, true]);
+
+  ok(TrackingProtection.enabled, "TP is enabled after setting the pref");
+
+  info("Load a test page containing tracking elements");
+  yield promiseTabLoadEvent(tab, TRACKING_PAGE);
+
+  testTrackingPage(tab.ownerDocument.defaultView);
+
+  info("Disable TP for the page (which reloads the page)");
+  let tabReloadPromise = promiseTabLoadEvent(tab);
+  clickButton("#tracking-action-unblock");
+  yield tabReloadPromise;
+  testTrackingPageUnblocked();
+
+  info("Test that the exception is remembered across tabs in the same private window");
+  tab = browser.selectedTab = browser.addTab();
+
+  info("Load a test page containing tracking elements");
+  yield promiseTabLoadEvent(tab, TRACKING_PAGE);
+  testTrackingPageUnblocked();
+
+  yield promiseWindowClosed(privateWin);
+});
+
+add_task(function* testExceptionPersistence() {
+  info("Open another private browsing window");
+  let privateWin = yield promiseOpenAndLoadWindow({private: true}, true);
+  browser = privateWin.gBrowser;
+  let tab = browser.selectedTab = browser.addTab();
+
+  TrackingProtection = browser.ownerGlobal.TrackingProtection;
+  ok(TrackingProtection.enabled, "TP is still enabled");
+
+  info("Load a test page containing tracking elements");
+  yield promiseTabLoadEvent(tab, TRACKING_PAGE);
+
+  testTrackingPage(tab.ownerDocument.defaultView);
+
+  info("Disable TP for the page (which reloads the page)");
+  let tabReloadPromise = promiseTabLoadEvent(tab);
+  clickButton("#tracking-action-unblock");
+  yield tabReloadPromise;
+  testTrackingPageUnblocked();
+
+  privateWin.close();
+});
--- a/browser/components/customizableui/CustomizableWidgets.jsm
+++ b/browser/components/customizableui/CustomizableWidgets.jsm
@@ -952,33 +952,40 @@ const CustomizableWidgets = [
       let win = aEvent.view;
       win.MailIntegration.sendLinkForBrowser(win.gBrowser.selectedBrowser)
     }
   }, {
     id: "loop-button",
     type: "custom",
     label: "loop-call-button3.label",
     tooltiptext: "loop-call-button3.tooltiptext",
+    privateBrowsingTooltiptext: "loop-call-button3-pb.tooltiptext",
     defaultArea: CustomizableUI.AREA_NAVBAR,
-    // Not in private browsing, see bug 1108187.
-    showInPrivateBrowsing: false,
     introducedInVersion: 4,
     onBuild: function(aDocument) {
       // If we're not supposed to see the button, return zip.
       if (!Services.prefs.getBoolPref("loop.enabled")) {
         return null;
       }
 
+      let isWindowPrivate = PrivateBrowsingUtils.isWindowPrivate(aDocument.defaultView);
+
       let node = aDocument.createElementNS(kNSXUL, "toolbarbutton");
       node.setAttribute("id", this.id);
       node.classList.add("toolbarbutton-1");
       node.classList.add("chromeclass-toolbar-additional");
       node.classList.add("badged-button");
       node.setAttribute("label", CustomizableUI.getLocalizedProperty(this, "label"));
-      node.setAttribute("tooltiptext", CustomizableUI.getLocalizedProperty(this, "tooltiptext"));
+      if (isWindowPrivate)
+        node.setAttribute("disabled", "true");
+      let tooltiptext = isWindowPrivate ?
+        CustomizableUI.getLocalizedProperty(this, "privateBrowsingTooltiptext",
+          [CustomizableUI.getLocalizedProperty(this, "label")]) :
+        CustomizableUI.getLocalizedProperty(this, "tooltiptext");
+      node.setAttribute("tooltiptext", tooltiptext);
       node.setAttribute("removable", "true");
       node.addEventListener("command", function(event) {
         aDocument.defaultView.LoopUI.togglePanel(event);
       });
 
       return node;
     }
   }, {
--- a/browser/components/customizableui/test/browser_946320_tabs_from_other_computers.js
+++ b/browser/components/customizableui/test/browser_946320_tabs_from_other_computers.js
@@ -114,17 +114,17 @@ function configureFxAccountIdentity() {
   };
 
   let MockInternal = {
     newAccountState(credentials) {
       isnot(credentials, "not expecting credentials");
       let storageManager = new MockFxaStorageManager();
       // and init storage with our user.
       storageManager.initialize(user);
-      return new AccountState(this, storageManager);
+      return new AccountState(storageManager);
     },
     getCertificate(data, keyPair, mustBeValidUntil) {
       this.cert = {
         validUntil: this.now() + 10000,
         cert: "certificate",
       };
       return Promise.resolve(this.cert.cert);
     },
--- a/browser/components/loop/content/js/conversationViews.js
+++ b/browser/components/loop/content/js/conversationViews.js
@@ -565,23 +565,25 @@ loop.conversationViews = (function(mozL1
           )
         )
       );
     }
   });
 
   var OngoingConversationView = React.createClass({displayName: "OngoingConversationView",
     mixins: [
-      loop.store.StoreMixin("conversationStore"),
       sharedMixins.MediaSetupMixin
     ],
 
     propTypes: {
       // local
       audio: React.PropTypes.object,
+      // We pass conversationStore here rather than use the mixin, to allow
+      // easy configurability for the ui-showcase.
+      conversationStore: React.PropTypes.instanceOf(loop.store.ConversationStore).isRequired,
       dispatcher: React.PropTypes.instanceOf(loop.Dispatcher).isRequired,
       // The poster URLs are for UI-showcase testing and development.
       localPosterUrl: React.PropTypes.string,
       // This is used from the props rather than the state to make it easier for
       // the ui-showcase.
       mediaConnected: React.PropTypes.bool,
       remotePosterUrl: React.PropTypes.string,
       remoteVideoEnabled: React.PropTypes.bool,
@@ -592,17 +594,27 @@ loop.conversationViews = (function(mozL1
     getDefaultProps: function() {
       return {
         video: {enabled: true, visible: true},
         audio: {enabled: true, visible: true}
       };
     },
 
     getInitialState: function() {
-      return this.getStoreState();
+      return this.props.conversationStore.getStoreState();
+    },
+
+    componentWillMount: function() {
+      this.props.conversationStore.on("change", function() {
+        this.setState(this.props.conversationStore.getStoreState());
+      }, this);
+    },
+
+    componentWillUnmount: function() {
+      this.props.conversationStore.off("change", null, this);
     },
 
     componentDidMount: function() {
       // The SDK needs to know about the configuration and the elements to use
       // for display. So the best way seems to pass the information here - ideally
       // the sdk wouldn't need to know this, but we can't change that.
       this.props.dispatcher.dispatch(new sharedActions.SetupStreamElements({
         publisherConfig: this.getDefaultPublisherConfig({
@@ -628,64 +640,79 @@ loop.conversationViews = (function(mozL1
     publishStream: function(type, enabled) {
       this.props.dispatcher.dispatch(
         new sharedActions.SetMute({
           type: type,
           enabled: enabled
         }));
     },
 
+    /**
+     * Should we render a visual cue to the user (e.g. a spinner) that a local
+     * stream is on its way from the camera?
+     *
+     * @returns {boolean}
+     * @private
+     */
+    _isLocalLoading: function () {
+      return !this.state.localSrcVideoObject && !this.props.localPosterUrl;
+    },
+
+    /**
+     * Should we render a visual cue to the user (e.g. a spinner) that a remote
+     * stream is on its way from the other user?
+     *
+     * @returns {boolean}
+     * @private
+     */
+    _isRemoteLoading: function() {
+      return !!(!this.state.remoteSrcVideoObject &&
+                !this.props.remotePosterUrl &&
+                !this.state.mediaConnected);
+    },
+
     shouldRenderRemoteVideo: function() {
       if (this.props.mediaConnected) {
         // If remote video is not enabled, we're muted, so we'll show an avatar
         // instead.
         return this.props.remoteVideoEnabled;
       }
 
       // We're not yet connected, but we don't want to show the avatar, and in
       // the common case, we'll just transition to the video.
       return true;
     },
 
     render: function() {
-      var localStreamClasses = React.addons.classSet({
-        local: true,
-        "local-stream": true,
-        "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 focus-stream"}, 
-                  React.createElement(sharedViews.MediaView, {displayAvatar: !this.shouldRenderRemoteVideo(), 
-                    isLoading: false, 
-                    mediaType: "remote", 
-                    posterUrl: this.props.remotePosterUrl, 
-                    srcVideoObject: this.state.remoteSrcVideoObject})
-                )
-              ), 
-              React.createElement("div", {className: localStreamClasses}, 
-                React.createElement(sharedViews.MediaView, {displayAvatar: !this.props.video.enabled, 
-                  isLoading: false, 
-                  mediaType: "local", 
-                  posterUrl: this.props.localPosterUrl, 
-                  srcVideoObject: this.state.localSrcVideoObject})
-              )
-            ), 
-            React.createElement(loop.shared.views.ConversationToolbar, {
-              audio: this.props.audio, 
-              dispatcher: this.props.dispatcher, 
-              edit: { visible: false, enabled: false}, 
-              hangup: this.hangup, 
-              publishStream: this.publishStream, 
-              video: this.props.video})
-          )
+        React.createElement("div", {className: "desktop-call-wrapper"}, 
+          React.createElement(sharedViews.MediaLayoutView, {
+            dispatcher: this.props.dispatcher, 
+            displayScreenShare: false, 
+            isLocalLoading: this._isLocalLoading(), 
+            isRemoteLoading: this._isRemoteLoading(), 
+            isScreenShareLoading: false, 
+            localPosterUrl: this.props.localPosterUrl, 
+            localSrcVideoObject: this.state.localSrcVideoObject, 
+            localVideoMuted: !this.props.video.enabled, 
+            matchMedia: this.state.matchMedia || window.matchMedia.bind(window), 
+            remotePosterUrl: this.props.remotePosterUrl, 
+            remoteSrcVideoObject: this.state.remoteSrcVideoObject, 
+            renderRemoteVideo: this.shouldRenderRemoteVideo(), 
+            screenSharePosterUrl: null, 
+            screenShareVideoObject: this.state.screenShareVideoObject, 
+            showContextRoomName: false, 
+            useDesktopPaths: true}), 
+          React.createElement(loop.shared.views.ConversationToolbar, {
+            audio: this.props.audio, 
+            dispatcher: this.props.dispatcher, 
+            edit: { visible: false, enabled: false}, 
+            hangup: this.hangup, 
+            publishStream: this.publishStream, 
+            video: this.props.video})
         )
       );
     }
   });
 
   /**
    * Master View Controller for outgoing calls. This manages
    * the different views that need displaying.
@@ -773,16 +800,17 @@ loop.conversationViews = (function(mozL1
           return (React.createElement(CallFailedView, {
             contact: this.state.contact, 
             dispatcher: this.props.dispatcher, 
             outgoing: this.state.outgoing}));
         }
         case CALL_STATES.ONGOING: {
           return (React.createElement(OngoingConversationView, {
             audio: {enabled: !this.state.audioMuted}, 
+            conversationStore: this.getStore(), 
             dispatcher: this.props.dispatcher, 
             mediaConnected: this.state.mediaConnected, 
             remoteSrcVideoObject: this.state.remoteSrcVideoObject, 
             remoteVideoEnabled: this.state.remoteVideoEnabled, 
             video: {enabled: !this.state.videoMuted}})
           );
         }
         case CALL_STATES.FINISHED: {
--- a/browser/components/loop/content/js/conversationViews.jsx
+++ b/browser/components/loop/content/js/conversationViews.jsx
@@ -565,23 +565,25 @@ loop.conversationViews = (function(mozL1
           </div>
         </div>
       );
     }
   });
 
   var OngoingConversationView = React.createClass({
     mixins: [
-      loop.store.StoreMixin("conversationStore"),
       sharedMixins.MediaSetupMixin
     ],
 
     propTypes: {
       // local
       audio: React.PropTypes.object,
+      // We pass conversationStore here rather than use the mixin, to allow
+      // easy configurability for the ui-showcase.
+      conversationStore: React.PropTypes.instanceOf(loop.store.ConversationStore).isRequired,
       dispatcher: React.PropTypes.instanceOf(loop.Dispatcher).isRequired,
       // The poster URLs are for UI-showcase testing and development.
       localPosterUrl: React.PropTypes.string,
       // This is used from the props rather than the state to make it easier for
       // the ui-showcase.
       mediaConnected: React.PropTypes.bool,
       remotePosterUrl: React.PropTypes.string,
       remoteVideoEnabled: React.PropTypes.bool,
@@ -592,17 +594,27 @@ loop.conversationViews = (function(mozL1
     getDefaultProps: function() {
       return {
         video: {enabled: true, visible: true},
         audio: {enabled: true, visible: true}
       };
     },
 
     getInitialState: function() {
-      return this.getStoreState();
+      return this.props.conversationStore.getStoreState();
+    },
+
+    componentWillMount: function() {
+      this.props.conversationStore.on("change", function() {
+        this.setState(this.props.conversationStore.getStoreState());
+      }, this);
+    },
+
+    componentWillUnmount: function() {
+      this.props.conversationStore.off("change", null, this);
     },
 
     componentDidMount: function() {
       // The SDK needs to know about the configuration and the elements to use
       // for display. So the best way seems to pass the information here - ideally
       // the sdk wouldn't need to know this, but we can't change that.
       this.props.dispatcher.dispatch(new sharedActions.SetupStreamElements({
         publisherConfig: this.getDefaultPublisherConfig({
@@ -628,64 +640,79 @@ loop.conversationViews = (function(mozL1
     publishStream: function(type, enabled) {
       this.props.dispatcher.dispatch(
         new sharedActions.SetMute({
           type: type,
           enabled: enabled
         }));
     },
 
+    /**
+     * Should we render a visual cue to the user (e.g. a spinner) that a local
+     * stream is on its way from the camera?
+     *
+     * @returns {boolean}
+     * @private
+     */
+    _isLocalLoading: function () {
+      return !this.state.localSrcVideoObject && !this.props.localPosterUrl;
+    },
+
+    /**
+     * Should we render a visual cue to the user (e.g. a spinner) that a remote
+     * stream is on its way from the other user?
+     *
+     * @returns {boolean}
+     * @private
+     */
+    _isRemoteLoading: function() {
+      return !!(!this.state.remoteSrcVideoObject &&
+                !this.props.remotePosterUrl &&
+                !this.state.mediaConnected);
+    },
+
     shouldRenderRemoteVideo: function() {
       if (this.props.mediaConnected) {
         // If remote video is not enabled, we're muted, so we'll show an avatar
         // instead.
         return this.props.remoteVideoEnabled;
       }
 
       // We're not yet connected, but we don't want to show the avatar, and in
       // the common case, we'll just transition to the video.
       return true;
     },
 
     render: function() {
-      var localStreamClasses = React.addons.classSet({
-        local: true,
-        "local-stream": true,
-        "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 focus-stream">
-                  <sharedViews.MediaView displayAvatar={!this.shouldRenderRemoteVideo()}
-                    isLoading={false}
-                    mediaType="remote"
-                    posterUrl={this.props.remotePosterUrl}
-                    srcVideoObject={this.state.remoteSrcVideoObject} />
-                </div>
-              </div>
-              <div className={localStreamClasses}>
-                <sharedViews.MediaView displayAvatar={!this.props.video.enabled}
-                  isLoading={false}
-                  mediaType="local"
-                  posterUrl={this.props.localPosterUrl}
-                  srcVideoObject={this.state.localSrcVideoObject} />
-              </div>
-            </div>
-            <loop.shared.views.ConversationToolbar
-              audio={this.props.audio}
-              dispatcher={this.props.dispatcher}
-              edit={{ visible: false, enabled: false }}
-              hangup={this.hangup}
-              publishStream={this.publishStream}
-              video={this.props.video} />
-          </div>
+        <div className="desktop-call-wrapper">
+          <sharedViews.MediaLayoutView
+            dispatcher={this.props.dispatcher}
+            displayScreenShare={false}
+            isLocalLoading={this._isLocalLoading()}
+            isRemoteLoading={this._isRemoteLoading()}
+            isScreenShareLoading={false}
+            localPosterUrl={this.props.localPosterUrl}
+            localSrcVideoObject={this.state.localSrcVideoObject}
+            localVideoMuted={!this.props.video.enabled}
+            matchMedia={this.state.matchMedia || window.matchMedia.bind(window)}
+            remotePosterUrl={this.props.remotePosterUrl}
+            remoteSrcVideoObject={this.state.remoteSrcVideoObject}
+            renderRemoteVideo={this.shouldRenderRemoteVideo()}
+            screenSharePosterUrl={null}
+            screenShareVideoObject={this.state.screenShareVideoObject}
+            showContextRoomName={false}
+            useDesktopPaths={true} />
+          <loop.shared.views.ConversationToolbar
+            audio={this.props.audio}
+            dispatcher={this.props.dispatcher}
+            edit={{ visible: false, enabled: false }}
+            hangup={this.hangup}
+            publishStream={this.publishStream}
+            video={this.props.video} />
         </div>
       );
     }
   });
 
   /**
    * Master View Controller for outgoing calls. This manages
    * the different views that need displaying.
@@ -773,16 +800,17 @@ loop.conversationViews = (function(mozL1
           return (<CallFailedView
             contact={this.state.contact}
             dispatcher={this.props.dispatcher}
             outgoing={this.state.outgoing} />);
         }
         case CALL_STATES.ONGOING: {
           return (<OngoingConversationView
             audio={{enabled: !this.state.audioMuted}}
+            conversationStore={this.getStore()}
             dispatcher={this.props.dispatcher}
             mediaConnected={this.state.mediaConnected}
             remoteSrcVideoObject={this.state.remoteSrcVideoObject}
             remoteVideoEnabled={this.state.remoteVideoEnabled}
             video={{enabled: !this.state.videoMuted}} />
           );
         }
         case CALL_STATES.FINISHED: {
--- a/browser/components/loop/content/js/roomViews.js
+++ b/browser/components/loop/content/js/roomViews.js
@@ -660,29 +660,29 @@ loop.roomViews = (function(mozL10n) {
 
     /**
      * Should we render a visual cue to the user (e.g. a spinner) that a local
      * stream is on its way from the camera?
      *
      * @returns {boolean}
      * @private
      */
-    _shouldRenderLocalLoading: function () {
+    _isLocalLoading: function () {
       return this.state.roomState === ROOM_STATES.MEDIA_WAIT &&
              !this.state.localSrcVideoObject;
     },
 
     /**
      * Should we render a visual cue to the user (e.g. a spinner) that a remote
      * stream is on its way from the other user?
      *
      * @returns {boolean}
      * @private
      */
-    _shouldRenderRemoteLoading: function() {
+    _isRemoteLoading: function() {
       return !!(this.state.roomState === ROOM_STATES.HAS_PARTICIPANTS &&
                 !this.state.remoteSrcVideoObject &&
                 !this.state.mediaConnected);
     },
 
     handleAddContextClick: function() {
       this.setState({ showEditContext: true });
     },
@@ -736,73 +736,64 @@ loop.roomViews = (function(mozL10n) {
           );
         }
         case ROOM_STATES.ENDED: {
           // When conversation ended we either display a feedback form or
           // close the window. This is decided in the AppControllerView.
           return null;
         }
         default: {
-
           return (
-            React.createElement("div", {className: "room-conversation-wrapper"}, 
-              React.createElement("div", {className: "video-layout-wrapper"}, 
-                React.createElement("div", {className: "conversation room-conversation"}, 
-                  React.createElement("div", {className: "media nested"}, 
-                    React.createElement(DesktopRoomInvitationView, {
-                      dispatcher: this.props.dispatcher, 
-                      error: this.state.error, 
-                      mozLoop: this.props.mozLoop, 
-                      onAddContextClick: this.handleAddContextClick, 
-                      onEditContextClose: this.handleEditContextClose, 
-                      roomData: roomData, 
-                      savingContext: this.state.savingContext, 
-                      show: shouldRenderInvitationOverlay, 
-                      showEditContext: shouldRenderInvitationOverlay && shouldRenderEditContextView, 
-                      socialShareProviders: this.state.socialShareProviders}), 
-                    React.createElement("div", {className: "video_wrapper remote_wrapper"}, 
-                      React.createElement("div", {className: "video_inner remote focus-stream"}, 
-                        React.createElement(sharedViews.MediaView, {displayAvatar: !this.shouldRenderRemoteVideo(), 
-                          isLoading: this._shouldRenderRemoteLoading(), 
-                          mediaType: "remote", 
-                          posterUrl: this.props.remotePosterUrl, 
-                          srcVideoObject: this.state.remoteSrcVideoObject})
-                      )
-                    ), 
-                    React.createElement("div", {className: localStreamClasses}, 
-                      React.createElement(sharedViews.MediaView, {displayAvatar: this.state.videoMuted, 
-                        isLoading: this._shouldRenderLocalLoading(), 
-                        mediaType: "local", 
-                        posterUrl: this.props.localPosterUrl, 
-                        srcVideoObject: this.state.localSrcVideoObject})
-                    ), 
-                    React.createElement(DesktopRoomEditContextView, {
-                      dispatcher: this.props.dispatcher, 
-                      error: this.state.error, 
-                      mozLoop: this.props.mozLoop, 
-                      onClose: this.handleEditContextClose, 
-                      roomData: roomData, 
-                      savingContext: this.state.savingContext, 
-                      show: !shouldRenderInvitationOverlay && shouldRenderEditContextView})
-                  ), 
-                  React.createElement(sharedViews.ConversationToolbar, {
-                    audio: {enabled: !this.state.audioMuted, visible: true}, 
-                    dispatcher: this.props.dispatcher, 
-                    edit: { visible: this.state.contextEnabled, enabled: !this.state.showEditContext}, 
-                    hangup: this.leaveRoom, 
-                    onEditClick: this.handleEditContextClick, 
-                    publishStream: this.publishStream, 
-                    screenShare: screenShareData, 
-                    video: {enabled: !this.state.videoMuted, visible: true}})
-                )
+            React.createElement("div", {className: "room-conversation-wrapper desktop-room-wrapper"}, 
+              React.createElement(sharedViews.MediaLayoutView, {
+                dispatcher: this.props.dispatcher, 
+                displayScreenShare: false, 
+                isLocalLoading: this._isLocalLoading(), 
+                isRemoteLoading: this._isRemoteLoading(), 
+                isScreenShareLoading: false, 
+                localPosterUrl: this.props.localPosterUrl, 
+                localSrcVideoObject: this.state.localSrcVideoObject, 
+                localVideoMuted: this.state.videoMuted, 
+                matchMedia: this.state.matchMedia || window.matchMedia.bind(window), 
+                remotePosterUrl: this.props.remotePosterUrl, 
+                remoteSrcVideoObject: this.state.remoteSrcVideoObject, 
+                renderRemoteVideo: this.shouldRenderRemoteVideo(), 
+                screenSharePosterUrl: null, 
+                screenShareVideoObject: this.state.screenShareVideoObject, 
+                showContextRoomName: false, 
+                useDesktopPaths: true}, 
+                React.createElement(DesktopRoomInvitationView, {
+                  dispatcher: this.props.dispatcher, 
+                  error: this.state.error, 
+                  mozLoop: this.props.mozLoop, 
+                  onAddContextClick: this.handleAddContextClick, 
+                  onEditContextClose: this.handleEditContextClose, 
+                  roomData: roomData, 
+                  savingContext: this.state.savingContext, 
+                  show: shouldRenderInvitationOverlay, 
+                  showEditContext: shouldRenderInvitationOverlay && shouldRenderEditContextView, 
+                  socialShareProviders: this.state.socialShareProviders}), 
+                React.createElement(DesktopRoomEditContextView, {
+                  dispatcher: this.props.dispatcher, 
+                  error: this.state.error, 
+                  mozLoop: this.props.mozLoop, 
+                  onClose: this.handleEditContextClose, 
+                  roomData: roomData, 
+                  savingContext: this.state.savingContext, 
+                  show: !shouldRenderInvitationOverlay && shouldRenderEditContextView})
               ), 
-              React.createElement(sharedViews.chat.TextChatView, {
+              React.createElement(sharedViews.ConversationToolbar, {
+                audio: {enabled: !this.state.audioMuted, visible: true}, 
                 dispatcher: this.props.dispatcher, 
-                showRoomName: false, 
-                useDesktopPaths: true})
+                edit: { visible: this.state.contextEnabled, enabled: !this.state.showEditContext}, 
+                hangup: this.leaveRoom, 
+                onEditClick: this.handleEditContextClick, 
+                publishStream: this.publishStream, 
+                screenShare: screenShareData, 
+                video: {enabled: !this.state.videoMuted, visible: true}})
             )
           );
         }
       }
     }
   });
 
   return {
--- a/browser/components/loop/content/js/roomViews.jsx
+++ b/browser/components/loop/content/js/roomViews.jsx
@@ -660,29 +660,29 @@ loop.roomViews = (function(mozL10n) {
 
     /**
      * Should we render a visual cue to the user (e.g. a spinner) that a local
      * stream is on its way from the camera?
      *
      * @returns {boolean}
      * @private
      */
-    _shouldRenderLocalLoading: function () {
+    _isLocalLoading: function () {
       return this.state.roomState === ROOM_STATES.MEDIA_WAIT &&
              !this.state.localSrcVideoObject;
     },
 
     /**
      * Should we render a visual cue to the user (e.g. a spinner) that a remote
      * stream is on its way from the other user?
      *
      * @returns {boolean}
      * @private
      */
-    _shouldRenderRemoteLoading: function() {
+    _isRemoteLoading: function() {
       return !!(this.state.roomState === ROOM_STATES.HAS_PARTICIPANTS &&
                 !this.state.remoteSrcVideoObject &&
                 !this.state.mediaConnected);
     },
 
     handleAddContextClick: function() {
       this.setState({ showEditContext: true });
     },
@@ -736,73 +736,64 @@ loop.roomViews = (function(mozL10n) {
           );
         }
         case ROOM_STATES.ENDED: {
           // When conversation ended we either display a feedback form or
           // close the window. This is decided in the AppControllerView.
           return null;
         }
         default: {
-
           return (
-            <div className="room-conversation-wrapper">
-              <div className="video-layout-wrapper">
-                <div className="conversation room-conversation">
-                  <div className="media nested">
-                    <DesktopRoomInvitationView
-                      dispatcher={this.props.dispatcher}
-                      error={this.state.error}
-                      mozLoop={this.props.mozLoop}
-                      onAddContextClick={this.handleAddContextClick}
-                      onEditContextClose={this.handleEditContextClose}
-                      roomData={roomData}
-                      savingContext={this.state.savingContext}
-                      show={shouldRenderInvitationOverlay}
-                      showEditContext={shouldRenderInvitationOverlay && shouldRenderEditContextView}
-                      socialShareProviders={this.state.socialShareProviders} />
-                    <div className="video_wrapper remote_wrapper">
-                      <div className="video_inner remote focus-stream">
-                        <sharedViews.MediaView displayAvatar={!this.shouldRenderRemoteVideo()}
-                          isLoading={this._shouldRenderRemoteLoading()}
-                          mediaType="remote"
-                          posterUrl={this.props.remotePosterUrl}
-                          srcVideoObject={this.state.remoteSrcVideoObject} />
-                      </div>
-                    </div>
-                    <div className={localStreamClasses}>
-                      <sharedViews.MediaView displayAvatar={this.state.videoMuted}
-                        isLoading={this._shouldRenderLocalLoading()}
-                        mediaType="local"
-                        posterUrl={this.props.localPosterUrl}
-                        srcVideoObject={this.state.localSrcVideoObject} />
-                    </div>
-                    <DesktopRoomEditContextView
-                      dispatcher={this.props.dispatcher}
-                      error={this.state.error}
-                      mozLoop={this.props.mozLoop}
-                      onClose={this.handleEditContextClose}
-                      roomData={roomData}
-                      savingContext={this.state.savingContext}
-                      show={!shouldRenderInvitationOverlay && shouldRenderEditContextView} />
-                  </div>
-                  <sharedViews.ConversationToolbar
-                    audio={{enabled: !this.state.audioMuted, visible: true}}
-                    dispatcher={this.props.dispatcher}
-                    edit={{ visible: this.state.contextEnabled, enabled: !this.state.showEditContext }}
-                    hangup={this.leaveRoom}
-                    onEditClick={this.handleEditContextClick}
-                    publishStream={this.publishStream}
-                    screenShare={screenShareData}
-                    video={{enabled: !this.state.videoMuted, visible: true}} />
-                </div>
-              </div>
-              <sharedViews.chat.TextChatView
+            <div className="room-conversation-wrapper desktop-room-wrapper">
+              <sharedViews.MediaLayoutView
                 dispatcher={this.props.dispatcher}
-                showRoomName={false}
-                useDesktopPaths={true} />
+                displayScreenShare={false}
+                isLocalLoading={this._isLocalLoading()}
+                isRemoteLoading={this._isRemoteLoading()}
+                isScreenShareLoading={false}
+                localPosterUrl={this.props.localPosterUrl}
+                localSrcVideoObject={this.state.localSrcVideoObject}
+                localVideoMuted={this.state.videoMuted}
+                matchMedia={this.state.matchMedia || window.matchMedia.bind(window)}
+                remotePosterUrl={this.props.remotePosterUrl}
+                remoteSrcVideoObject={this.state.remoteSrcVideoObject}
+                renderRemoteVideo={this.shouldRenderRemoteVideo()}
+                screenSharePosterUrl={null}
+                screenShareVideoObject={this.state.screenShareVideoObject}
+                showContextRoomName={false}
+                useDesktopPaths={true}>
+                <DesktopRoomInvitationView
+                  dispatcher={this.props.dispatcher}
+                  error={this.state.error}
+                  mozLoop={this.props.mozLoop}
+                  onAddContextClick={this.handleAddContextClick}
+                  onEditContextClose={this.handleEditContextClose}
+                  roomData={roomData}
+                  savingContext={this.state.savingContext}
+                  show={shouldRenderInvitationOverlay}
+                  showEditContext={shouldRenderInvitationOverlay && shouldRenderEditContextView}
+                  socialShareProviders={this.state.socialShareProviders} />
+                <DesktopRoomEditContextView
+                  dispatcher={this.props.dispatcher}
+                  error={this.state.error}
+                  mozLoop={this.props.mozLoop}
+                  onClose={this.handleEditContextClose}
+                  roomData={roomData}
+                  savingContext={this.state.savingContext}
+                  show={!shouldRenderInvitationOverlay && shouldRenderEditContextView} />
+              </sharedViews.MediaLayoutView>
+              <sharedViews.ConversationToolbar
+                audio={{enabled: !this.state.audioMuted, visible: true}}
+                dispatcher={this.props.dispatcher}
+                edit={{ visible: this.state.contextEnabled, enabled: !this.state.showEditContext }}
+                hangup={this.leaveRoom}
+                onEditClick={this.handleEditContextClick}
+                publishStream={this.publishStream}
+                screenShare={screenShareData}
+                video={{enabled: !this.state.videoMuted, visible: true}} />
             </div>
           );
         }
       }
     }
   });
 
   return {
--- a/browser/components/loop/content/shared/css/conversation.css
+++ b/browser/components/loop/content/shared/css/conversation.css
@@ -499,38 +499,16 @@
 
 .feedback .info {
   display: block;
   font-size: 10px;
   color: #CCC;
   text-align: center;
 }
 
-.fx-embedded .local-stream {
-  position: absolute;
-  right: 3px;
-  bottom: 5px;
-  /* next two lines are workaround for lack of object-fit; see bug 1020445 */
-  max-width: 140px;
-  width: 30%;
-  height: 28%;
-  max-height: 105px;
-}
-
-.fx-embedded .local-stream.room-preview {
-  top: 0px;
-  left: 0px;
-  right: 0px;
-  bottom: 0px;
-  height: 100%;
-  width: 100%;
-  max-width: none;
-  max-height: none;
-}
-
 .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;
@@ -587,25 +565,21 @@
   }
 }
 
 .conversation .local .avatar {
   position: absolute;
   z-index: 1;
 }
 
-.remote .avatar {
+.remote > .avatar {
   /* make visually distinct from local avatar */
   opacity: 0.25;
 }
 
-.fx-embedded .media.nested {
-  min-height: 200px;
-}
-
 .fx-embedded-call-identifier {
   display: inline;
   width: 100%;
   padding: 1.2em;
 }
 
 .fx-embedded-call-identifier-item {
   height: 50px;
@@ -670,17 +644,19 @@
   }
 
 /* Force full height on all parents up to the video elements
  * this way we can ensure the aspect ratio and use height 100%
  * on the video element
  * */
 html, .fx-embedded, #main,
 .video-layout-wrapper,
-.conversation {
+.conversation,
+.desktop-call-wrapper,
+.desktop-room-wrapper {
   height: 100%;
 }
 
 /* We use 641px rather than 640, as min-width and max-width are inclusive */
 @media screen and (min-width: 641px) {
   .standalone .conversation .conversation-toolbar {
     position: absolute;
     bottom: 0;
@@ -930,33 +906,32 @@ body[platform="win"] .share-service-drop
   background-color: #E8F6FE;
 }
 
 .room-context {
   background: rgba(0,0,0,.8);
   border-top: 2px solid #444;
   border-bottom: 2px solid #444;
   padding: .5rem;
-  max-height: 400px;
   position: absolute;
   left: 0;
   bottom: 0;
   width: 100%;
   /* Stretch to the maximum available space whilst not covering the conversation
      toolbar (26px). */
   height: calc(100% - 26px);
   font-size: .9em;
   display: flex;
   flex-flow: column nowrap;
   align-content: flex-start;
   align-items: flex-start;
   overflow-x: hidden;
   overflow-y: auto;
   /* Make the context view float atop the video elements. */
-  z-index: 2;
+  z-index: 3;
 }
 
 .room-invitation-overlay .room-context {
   position: relative;
   left: auto;
   bottom: auto;
   flex: 0 1 auto;
   height: 100%;
@@ -1082,22 +1057,22 @@ html[dir="rtl"] .room-context-btn-close 
 
 .media-layout {
   height: 100%;
 }
 
 .standalone-room-wrapper > .media-layout {
   /* 50px is the header, 64px for toolbar, 3em is the footer. */
   height: calc(100% - 50px - 64px - 3em);
+  margin: 0 10px;
 }
 
 .media-layout > .media-wrapper {
   display: flex;
   flex-flow: column wrap;
-  margin: 0 10px;
   height: 100%;
 }
 
 .media-wrapper > .focus-stream {
   /* We want this to be the width, minus 200px which is for the right-side text
      chat and video displays. */
   width: calc(100% - 200px);
   /* 100% height to fill up media-layout, thus forcing other elements into the
@@ -1134,16 +1109,24 @@ html[dir="rtl"] .room-context-btn-close 
 }
 
 .media-wrapper.showing-local-streams.receiving-screen-share > .text-chat-view {
   /* When we're displaying the local streams, then we need to make the text
      chat view a bit shorter to give room. */
   height: calc(100% - 300px);
 }
 
+.desktop-call-wrapper > .media-layout > .media-wrapper > .text-chat-view,
+.desktop-room-wrapper > .media-layout > .media-wrapper > .text-chat-view {
+  /* Account for height of .conversation-toolbar on desktop */
+  /* When we change the toolbar in bug 1184559 we can remove this. */
+  margin-top: 26px;
+  height: calc(100% - 150px - 26px);
+}
+
 /* Temporarily slaved from .media-wrapper until we use it in more places
    to avoid affecting the conversation window on desktop. */
 .media-wrapper > .text-chat-view > .text-chat-entries {
   /* 40px is the height of .text-chat-box. */
   height: calc(100% - 40px);
 }
 
 .media-wrapper > .text-chat-disabled > .text-chat-entries {
@@ -1199,41 +1182,47 @@ html[dir="rtl"] .room-context-btn-close 
   }
 
   .media-wrapper.receiving-screen-share > .focus-stream {
     height: 50%;
   }
 
   /* Temporarily slaved from .media-wrapper until we use it in more places
      to avoid affecting the conversation window on desktop. */
-  .media-wrapper > .text-chat-view > .text-chat-entries {
+  .text-chat-view > .text-chat-entries {
     /* 40px is the height of .text-chat-box. */
     height: calc(100% - 40px);
     width: 100%;
   }
 
   .media-wrapper > .text-chat-disabled > .text-chat-entries {
     /* When text chat is disabled, the entries box should be 100% height. */
     height: 100%;
   }
 
-  .media-wrapper > .local {
+  .media-wrapper > .focus-stream > .local {
     /* Position over the remote video */
     position: absolute;
     /* Make sure its on top */
-    z-index: 1001;
+    z-index: 2;
     margin: 3px;
     right: 0;
     /* 29px is (30% of 50px high header) + (height toolbar (38px) +
        height footer (25px) - height header (50px)) */
-    bottom: calc(30% + 29px);
+    bottom: 0;
     width: 120px;
     height: 120px;
   }
 
+  .standalone-room-wrapper > .media-layout > .media-wrapper > .local {
+    /* Add 10px for the margin on standalone */
+    right: 10px;
+  }
+
+
   html[dir="rtl"] .media-wrapper > .local {
     right: auto;
     left: 0;
   }
 
   .media-wrapper > .text-chat-view {
     order: 3;
     flex: 1 1 auto;
@@ -1242,16 +1231,25 @@ html[dir="rtl"] .room-context-btn-close 
 
   .media-wrapper > .text-chat-view,
   .media-wrapper.showing-local-streams > .text-chat-view,
   .media-wrapper.showing-local-streams.receiving-screen-share > .text-chat-view {
     /* The remaining 30% that the .focus-stream doesn't use. */
     height: 30%;
   }
 
+  .desktop-call-wrapper > .media-layout > .media-wrapper > .text-chat-view,
+  .desktop-room-wrapper > .media-layout > .media-wrapper > .text-chat-view {
+    /* When we change the toolbar in bug 1184559 we can remove this. */
+    /* Reset back to 0 for .conversation-toolbar override on desktop */
+    margin-top: 0;
+    /* This is temp, to echo the .media-wrapper > .text-chat-view above */
+    height: 30%;
+  }
+
   .media-wrapper.receiving-screen-share > .screen {
     order: 1;
   }
 
   .media-wrapper.receiving-screen-share > .remote {
     /* Screen shares have remote & local video side-by-side on narrow screens */
     order: 2;
     flex: 1 1 auto;
@@ -1283,16 +1281,57 @@ html[dir="rtl"] .room-context-btn-close 
     margin: 0;
   }
 
   .media-wrapper.receiving-screen-share > .text-chat-view {
     order: 4;
   }
 }
 
+/* e.g. very narrow widths similar to conversation window */
+@media screen and (max-width:300px) {
+  .media-layout > .media-wrapper {
+    flex-flow: column nowrap;
+  }
+
+  .media-wrapper > .focus-stream > .local {
+    position: absolute;
+    right: 0;
+    /* 30% is the height of the text chat. As we have a margin,
+       we don't need to worry about any offset for a border */
+    bottom: 0;
+    margin: 3px;
+    object-fit: contain;
+    /* These make the avatar look reasonable and the local
+       video not too big */
+    width: 25%;
+    height: 25%;
+  }
+
+  .media-wrapper:not(.showing-remote-streams) > .focus-stream > .no-video {
+    display: none;
+  }
+
+  .media-wrapper:not(.showing-remote-streams) > .focus-stream > .local {
+    position: relative;
+    margin: 0;
+    right: auto;
+    left: auto;
+    bottom: auto;
+    width: 100%;
+    height: 100%;
+    background-color: black;
+  }
+
+  .media-wrapper > .focus-stream {
+    flex: 1 1 auto;
+    height: auto;
+  }
+}
+
 .standalone > #main > .room-conversation-wrapper > .media-layout > .conversation-toolbar {
   border: none;
 }
 
 /* Standalone rooms */
 
 .standalone .room-conversation-wrapper {
   position: relative;
@@ -1410,47 +1449,22 @@ html[dir="rtl"] .standalone .room-conver
   display: block;
 }
 
 .standalone .room-conversation-wrapper .ended-conversation {
   position: relative;
   height: auto;
 }
 
-/* Text chat in rooms styles */
-
-.fx-embedded .room-conversation-wrapper {
-  display: flex;
-  flex-flow: column nowrap;
-}
-
-.fx-embedded .video-layout-wrapper {
-  flex: 1 1 auto;
-}
+/* Text chat in styles */
 
 .text-chat-view {
   background: white;
 }
 
-.fx-embedded .text-chat-view {
-  flex: 1 0 auto;
-  display: flex;
-  flex-flow: column nowrap;
-}
-
-.fx-embedded .text-chat-entries {
-  flex: 1 1 auto;
-  max-height: 120px;
-  min-height: 60px;
-}
-
-.fx-embedded .text-chat-view > .text-chat-entries-empty {
-  display: none;
-}
-
 .text-chat-box {
   flex: 0 0 auto;
   max-height: 40px;
   min-height: 40px;
   width: 100%;
 }
 
 .text-chat-entries {
@@ -1735,16 +1749,57 @@ html[dir="rtl"] .text-chat-entry.receive
   }
 
   .standalone .media.nested {
     /* This forces the remote video stream to fit within wrapper's height */
     min-height: 0px;
   }
 }
 
+/* e.g. very narrow widths similar to conversation window */
+@media screen and (max-width:300px) {
+  .text-chat-view {
+    flex: 0 0 auto;
+    display: flex;
+    flex-flow: column nowrap;
+    /* 120px max-height of .text-chat-entries plus 40px of .text-chat-box */
+    max-height: 160px;
+    /* 60px min-height of .text-chat-entries plus 40px of .text-chat-box */
+    min-height: 100px;
+    /* The !important is to override the values defined above which have more
+       specificity when we fix bug 1184559, we should be able to remove it,
+       but this should be tests first. */
+    height: auto !important;
+  }
+
+  .text-chat-entries {
+    /* The !important is to override the values defined above which have more
+       specificity when we fix bug 1184559, we should be able to remove it,
+       but this should be tests first. */
+    flex: 1 1 auto !important;
+    max-height: 120px;
+    min-height: 60px;
+  }
+
+  .text-chat-entries-empty.text-chat-disabled {
+    display: none;
+  }
+
+  /* When the text chat entries are not present, then hide the entries view
+     and just show the chat box. */
+  .text-chat-entries-empty {
+    max-height: 40px;
+    min-height: 40px;
+  }
+
+  .text-chat-entries-empty > .text-chat-entries {
+    display: none;
+  }
+}
+
 .self-view-hidden-message {
   /* Not displayed by default; display is turned on elsewhere when the
    * self-view is actually hidden.
    */
   display: none;
 }
 
 /* Avoid the privacy problem where a user can size the window so small that
--- a/browser/components/loop/content/shared/js/activeRoomStore.js
+++ b/browser/components/loop/content/shared/js/activeRoomStore.js
@@ -575,31 +575,16 @@ loop.store.ActiveRoomStore = (function()
     },
 
     /**
      * Handles disconnection of this local client from the sdk servers.
      *
      * @param {sharedActions.ConnectionFailure} actionData
      */
     connectionFailure: function(actionData) {
-      /**
-       * XXX This is a workaround for desktop machines that do not have a
-       * camera installed. As we don't yet have device enumeration, when
-       * we do, this can be removed (bug 1138851), and the sdk should handle it.
-       */
-      if (this._isDesktop &&
-          actionData.reason === FAILURE_DETAILS.UNABLE_TO_PUBLISH_MEDIA &&
-          this.getStoreState().videoMuted === false) {
-        // We failed to publish with media, so due to the bug, we try again without
-        // video.
-        this.setStoreState({videoMuted: true});
-        this._sdkDriver.retryPublishWithoutVideo();
-        return;
-      }
-
       var exitState = this._storeState.roomState === ROOM_STATES.FAILED ?
         this._storeState.failureExitState : this._storeState.roomState;
 
       // Treat all reasons as something failed. In theory, clientDisconnected
       // could be a success case, but there's no way we should be intentionally
       // sending that and still have the window open.
       this.setStoreState({
         failureReason: actionData.reason,
--- a/browser/components/loop/content/shared/js/conversationStore.js
+++ b/browser/components/loop/content/shared/js/conversationStore.js
@@ -141,31 +141,16 @@ loop.store = loop.store || {};
 
     /**
      * Handles the connection failure action, setting the state to
      * terminated.
      *
      * @param {sharedActions.ConnectionFailure} actionData The action data.
      */
     connectionFailure: function(actionData) {
-      /**
-       * XXX This is a workaround for desktop machines that do not have a
-       * camera installed. As we don't yet have device enumeration, when
-       * we do, this can be removed (bug 1138851), and the sdk should handle it.
-       */
-      if (this._isDesktop &&
-          actionData.reason === FAILURE_DETAILS.UNABLE_TO_PUBLISH_MEDIA &&
-          this.getStoreState().videoMuted === false) {
-        // We failed to publish with media, so due to the bug, we try again without
-        // video.
-        this.setStoreState({videoMuted: true});
-        this.sdkDriver.retryPublishWithoutVideo();
-        return;
-      }
-
       this._endSession();
       this.setStoreState({
         callState: CALL_STATES.TERMINATED,
         callStateReason: actionData.reason
       });
     },
 
     /**
--- a/browser/components/loop/content/shared/js/otSdkDriver.js
+++ b/browser/components/loop/content/shared/js/otSdkDriver.js
@@ -56,25 +56,36 @@ loop.OTSdkDriver = (function() {
     // about:config, or use
     //
     // localStorage.setItem("debug.twoWayMediaTelemetry", true);
     this._debugTwoWayMediaTelemetry =
       loop.shared.utils.getBoolPreference("debug.twoWayMediaTelemetry");
 
     /**
      * XXX This is a workaround for desktop machines that do not have a
-     * camera installed. As we don't yet have device enumeration, when
-     * we do, this can be removed (bug 1138851), and the sdk should handle it.
+     * camera installed. The SDK doesn't currently do use the new device
+     * enumeration apis, when it does (bug 1138851), we can drop this part.
      */
-    if (this._isDesktop && !window.MediaStreamTrack.getSources) {
+    if (this._isDesktop) {
       // If there's no getSources function, the sdk defines its own and caches
-      // the result. So here we define the "normal" one which doesn't get cached, so
-      // we can change it later.
+      // the result. So here we define our own one which wraps around the
+      // real device enumeration api.
       window.MediaStreamTrack.getSources = function(callback) {
-        callback([{kind: "audio"}, {kind: "video"}]);
+        navigator.mediaDevices.enumerateDevices().then(function(devices) {
+          var result = [];
+          devices.forEach(function(device) {
+            if (device.kind === "audioinput") {
+              result.push({kind: "audio"});
+            }
+            if (device.kind === "videoinput") {
+              result.push({kind: "video"});
+            }
+          });
+          callback(result);
+        });
       };
     }
   };
 
   OTSdkDriver.prototype = {
     /**
      * Clones the publisher config into a new object, as the sdk modifies the
      * properties object.
@@ -104,54 +115,35 @@ loop.OTSdkDriver = (function() {
      * @param {sharedActions.SetupStreamElements} actionData The data associated
      *   with the action. See action.js.
      */
     setupStreamElements: function(actionData) {
       this.publisherConfig = actionData.publisherConfig;
 
       this.sdk.on("exception", this._onOTException.bind(this));
 
-      // At this state we init the publisher, even though we might be waiting for
-      // the initial connect of the session. This saves time when setting up
-      // the media.
-      this._publishLocalStreams();
-    },
-
-    /**
-     * Internal function to publish a local stream.
-     * XXX This can be simplified when bug 1138851 is actioned.
-     */
-    _publishLocalStreams: function() {
       // We expect the local video to be muted automatically by the SDK. Hence
       // we don't mute it manually here.
       this._mockPublisherEl = document.createElement("div");
 
+      // At this state we init the publisher, even though we might be waiting for
+      // the initial connect of the session. This saves time when setting up
+      // the media.
       this.publisher = this.sdk.initPublisher(this._mockPublisherEl,
         _.extend(this._getDataChannelSettings, this._getCopyPublisherConfig));
 
       this.publisher.on("streamCreated", this._onLocalStreamCreated.bind(this));
       this.publisher.on("streamDestroyed", this._onLocalStreamDestroyed.bind(this));
       this.publisher.on("accessAllowed", this._onPublishComplete.bind(this));
       this.publisher.on("accessDenied", this._onPublishDenied.bind(this));
       this.publisher.on("accessDialogOpened",
         this._onAccessDialogOpened.bind(this));
     },
 
     /**
-     * Forces the sdk into not using video, and starts publishing again.
-     * XXX This is part of the work around that will be removed by bug 1138851.
-     */
-    retryPublishWithoutVideo: function() {
-      window.MediaStreamTrack.getSources = function(callback) {
-        callback([{kind: "audio"}]);
-      };
-      this._publishLocalStreams();
-    },
-
-    /**
      * Handles the setMute action. Informs the published stream to mute
      * or unmute audio as appropriate.
      *
      * @param {sharedActions.SetMute} actionData The data associated with the
      *                                           action. See action.js.
      */
     setMute: function(actionData) {
       if (actionData.type === "audio") {
--- a/browser/components/loop/content/shared/js/textChatView.js
+++ b/browser/components/loop/content/shared/js/textChatView.js
@@ -145,18 +145,17 @@ loop.shared.views.chat = (function(mozL1
       }
     },
 
     render: function() {
       /* Keep track of the last printed timestamp. */
       var lastTimestamp = 0;
 
       var entriesClasses = React.addons.classSet({
-        "text-chat-entries": true,
-        "text-chat-entries-empty": !this.props.messageList.length
+        "text-chat-entries": true
       });
 
       return (
         React.createElement("div", {className: entriesClasses}, 
           React.createElement("div", {className: "text-chat-scroller"}, 
             
               this.props.messageList.map(function(entry, i) {
                 if (entry.type === CHAT_MESSAGE_TYPES.SPECIAL) {
@@ -377,17 +376,18 @@ loop.shared.views.chat = (function(mozL1
           return item.type !== CHAT_MESSAGE_TYPES.SPECIAL ||
             item.contentType !== CHAT_CONTENT_TYPES.ROOM_NAME;
         });
         hasNonSpecialMessages = !!messageList.length;
       }
 
       var textChatViewClasses = React.addons.classSet({
         "text-chat-view": true,
-        "text-chat-disabled": !this.state.textChatEnabled
+        "text-chat-disabled": !this.state.textChatEnabled,
+        "text-chat-entries-empty": !messageList.length
       });
 
       return (
         React.createElement("div", {className: textChatViewClasses}, 
           React.createElement(TextChatEntriesView, {
             dispatcher: this.props.dispatcher, 
             messageList: messageList, 
             useDesktopPaths: this.props.useDesktopPaths}), 
--- a/browser/components/loop/content/shared/js/textChatView.jsx
+++ b/browser/components/loop/content/shared/js/textChatView.jsx
@@ -145,18 +145,17 @@ loop.shared.views.chat = (function(mozL1
       }
     },
 
     render: function() {
       /* Keep track of the last printed timestamp. */
       var lastTimestamp = 0;
 
       var entriesClasses = React.addons.classSet({
-        "text-chat-entries": true,
-        "text-chat-entries-empty": !this.props.messageList.length
+        "text-chat-entries": true
       });
 
       return (
         <div className={entriesClasses}>
           <div className="text-chat-scroller">
             {
               this.props.messageList.map(function(entry, i) {
                 if (entry.type === CHAT_MESSAGE_TYPES.SPECIAL) {
@@ -377,17 +376,18 @@ loop.shared.views.chat = (function(mozL1
           return item.type !== CHAT_MESSAGE_TYPES.SPECIAL ||
             item.contentType !== CHAT_CONTENT_TYPES.ROOM_NAME;
         });
         hasNonSpecialMessages = !!messageList.length;
       }
 
       var textChatViewClasses = React.addons.classSet({
         "text-chat-view": true,
-        "text-chat-disabled": !this.state.textChatEnabled
+        "text-chat-disabled": !this.state.textChatEnabled,
+        "text-chat-entries-empty": !messageList.length
       });
 
       return (
         <div className={textChatViewClasses}>
           <TextChatEntriesView
             dispatcher={this.props.dispatcher}
             messageList={messageList}
             useDesktopPaths={this.props.useDesktopPaths} />
--- a/browser/components/loop/content/shared/js/views.js
+++ b/browser/components/loop/content/shared/js/views.js
@@ -941,83 +941,141 @@ loop.shared.views = (function(_, mozL10n
                {className: this.props.mediaType + "-video", 
                muted: true}))
       );
     }
   });
 
   var MediaLayoutView = React.createClass({displayName: "MediaLayoutView",
     propTypes: {
+      children: React.PropTypes.node,
       dispatcher: React.PropTypes.instanceOf(loop.Dispatcher).isRequired,
       displayScreenShare: React.PropTypes.bool.isRequired,
       isLocalLoading: React.PropTypes.bool.isRequired,
       isRemoteLoading: React.PropTypes.bool.isRequired,
       isScreenShareLoading: React.PropTypes.bool.isRequired,
       // The poster URLs are for UI-showcase testing and development.
       localPosterUrl: React.PropTypes.string,
       localSrcVideoObject: React.PropTypes.object,
       localVideoMuted: React.PropTypes.bool.isRequired,
+      // Passing in matchMedia, allows it to be overriden for ui-showcase's
+      // benefit. We expect either the override or window.matchMedia.
+      matchMedia: React.PropTypes.func.isRequired,
       remotePosterUrl: React.PropTypes.string,
       remoteSrcVideoObject: React.PropTypes.object,
       renderRemoteVideo: React.PropTypes.bool.isRequired,
       screenSharePosterUrl: React.PropTypes.string,
       screenShareVideoObject: React.PropTypes.object,
       showContextRoomName: React.PropTypes.bool.isRequired,
       useDesktopPaths: React.PropTypes.bool.isRequired
     },
 
+    isLocalMediaAbsolutelyPositioned: function(matchMedia) {
+      if (!matchMedia) {
+        matchMedia = this.props.matchMedia;
+      }
+      return matchMedia &&
+        // The screen width is less than 640px and we are not screen sharing.
+        ((matchMedia("screen and (max-width:640px)").matches &&
+         !this.props.displayScreenShare) ||
+         // or the screen width is less than 300px.
+         (matchMedia("screen and (max-width:300px)").matches));
+    },
+
+    getInitialState: function() {
+      return {
+        localMediaAboslutelyPositioned: this.isLocalMediaAbsolutelyPositioned()
+      };
+    },
+
+    componentWillReceiveProps: function(nextProps) {
+      // This is all for the ui-showcase's benefit.
+      if (this.props.matchMedia != nextProps.matchMedia) {
+        this.updateLocalMediaState(null, nextProps.matchMedia);
+      }
+    },
+
+    componentDidMount: function() {
+      window.addEventListener("resize", this.updateLocalMediaState);
+    },
+
+    componentWillUnmount: function() {
+      window.removeEventListener("resize", this.updateLocalMediaState);
+    },
+
+    updateLocalMediaState: function(event, matchMedia) {
+      var newState = this.isLocalMediaAbsolutelyPositioned(matchMedia);
+      if (this.state.localMediaAboslutelyPositioned != newState) {
+        this.setState({
+          localMediaAboslutelyPositioned: newState
+        });
+      }
+    },
+
+    renderLocalVideo: function() {
+      return (
+        React.createElement("div", {className: "local"}, 
+          React.createElement(MediaView, {displayAvatar: this.props.localVideoMuted, 
+            isLoading: this.props.isLocalLoading, 
+            mediaType: "local", 
+            posterUrl: this.props.localPosterUrl, 
+            srcVideoObject: this.props.localSrcVideoObject})
+        )
+      );
+    },
+
     render: function() {
       var remoteStreamClasses = React.addons.classSet({
         "remote": true,
         "focus-stream": !this.props.displayScreenShare
       });
 
       var screenShareStreamClasses = React.addons.classSet({
         "screen": true,
         "focus-stream": this.props.displayScreenShare
       });
 
       var mediaWrapperClasses = React.addons.classSet({
         "media-wrapper": true,
         "receiving-screen-share": this.props.displayScreenShare,
         "showing-local-streams": this.props.localSrcVideoObject ||
-          this.props.localPosterUrl
+          this.props.localPosterUrl,
+        "showing-remote-streams": this.props.remoteSrcVideoObject ||
+          this.props.remotePosterUrl || this.props.isRemoteLoading
       });
 
       return (
         React.createElement("div", {className: "media-layout"}, 
           React.createElement("div", {className: mediaWrapperClasses}, 
             React.createElement("span", {className: "self-view-hidden-message"}, 
               mozL10n.get("self_view_hidden_message")
             ), 
             React.createElement("div", {className: remoteStreamClasses}, 
               React.createElement(MediaView, {displayAvatar: !this.props.renderRemoteVideo, 
                 isLoading: this.props.isRemoteLoading, 
                 mediaType: "remote", 
                 posterUrl: this.props.remotePosterUrl, 
-                srcVideoObject: this.props.remoteSrcVideoObject})
+                srcVideoObject: this.props.remoteSrcVideoObject}), 
+               this.state.localMediaAboslutelyPositioned ?
+                this.renderLocalVideo() : null, 
+               this.props.children
             ), 
             React.createElement("div", {className: screenShareStreamClasses}, 
               React.createElement(MediaView, {displayAvatar: false, 
                 isLoading: this.props.isScreenShareLoading, 
                 mediaType: "screen-share", 
                 posterUrl: this.props.screenSharePosterUrl, 
                 srcVideoObject: this.props.screenShareVideoObject})
             ), 
             React.createElement(loop.shared.views.chat.TextChatView, {
               dispatcher: this.props.dispatcher, 
               showRoomName: this.props.showContextRoomName, 
               useDesktopPaths: false}), 
-            React.createElement("div", {className: "local"}, 
-              React.createElement(MediaView, {displayAvatar: this.props.localVideoMuted, 
-                isLoading: this.props.isLocalLoading, 
-                mediaType: "local", 
-                posterUrl: this.props.localPosterUrl, 
-                srcVideoObject: this.props.localSrcVideoObject})
-            )
+             this.state.localMediaAboslutelyPositioned ?
+              null : this.renderLocalVideo()
           )
         )
       );
     }
   });
 
   return {
     AvatarView: AvatarView,
--- a/browser/components/loop/content/shared/js/views.jsx
+++ b/browser/components/loop/content/shared/js/views.jsx
@@ -941,83 +941,141 @@ loop.shared.views = (function(_, mozL10n
                className={this.props.mediaType + "-video"}
                muted />
       );
     }
   });
 
   var MediaLayoutView = React.createClass({
     propTypes: {
+      children: React.PropTypes.node,
       dispatcher: React.PropTypes.instanceOf(loop.Dispatcher).isRequired,
       displayScreenShare: React.PropTypes.bool.isRequired,
       isLocalLoading: React.PropTypes.bool.isRequired,
       isRemoteLoading: React.PropTypes.bool.isRequired,
       isScreenShareLoading: React.PropTypes.bool.isRequired,
       // The poster URLs are for UI-showcase testing and development.
       localPosterUrl: React.PropTypes.string,
       localSrcVideoObject: React.PropTypes.object,
       localVideoMuted: React.PropTypes.bool.isRequired,
+      // Passing in matchMedia, allows it to be overriden for ui-showcase's
+      // benefit. We expect either the override or window.matchMedia.
+      matchMedia: React.PropTypes.func.isRequired,
       remotePosterUrl: React.PropTypes.string,
       remoteSrcVideoObject: React.PropTypes.object,
       renderRemoteVideo: React.PropTypes.bool.isRequired,
       screenSharePosterUrl: React.PropTypes.string,
       screenShareVideoObject: React.PropTypes.object,
       showContextRoomName: React.PropTypes.bool.isRequired,
       useDesktopPaths: React.PropTypes.bool.isRequired
     },
 
+    isLocalMediaAbsolutelyPositioned: function(matchMedia) {
+      if (!matchMedia) {
+        matchMedia = this.props.matchMedia;
+      }
+      return matchMedia &&
+        // The screen width is less than 640px and we are not screen sharing.
+        ((matchMedia("screen and (max-width:640px)").matches &&
+         !this.props.displayScreenShare) ||
+         // or the screen width is less than 300px.
+         (matchMedia("screen and (max-width:300px)").matches));
+    },
+
+    getInitialState: function() {
+      return {
+        localMediaAboslutelyPositioned: this.isLocalMediaAbsolutelyPositioned()
+      };
+    },
+
+    componentWillReceiveProps: function(nextProps) {
+      // This is all for the ui-showcase's benefit.
+      if (this.props.matchMedia != nextProps.matchMedia) {
+        this.updateLocalMediaState(null, nextProps.matchMedia);
+      }
+    },
+
+    componentDidMount: function() {
+      window.addEventListener("resize", this.updateLocalMediaState);
+    },
+
+    componentWillUnmount: function() {
+      window.removeEventListener("resize", this.updateLocalMediaState);
+    },
+
+    updateLocalMediaState: function(event, matchMedia) {
+      var newState = this.isLocalMediaAbsolutelyPositioned(matchMedia);
+      if (this.state.localMediaAboslutelyPositioned != newState) {
+        this.setState({
+          localMediaAboslutelyPositioned: newState
+        });
+      }
+    },
+
+    renderLocalVideo: function() {
+      return (
+        <div className="local">
+          <MediaView displayAvatar={this.props.localVideoMuted}
+            isLoading={this.props.isLocalLoading}
+            mediaType="local"
+            posterUrl={this.props.localPosterUrl}
+            srcVideoObject={this.props.localSrcVideoObject} />
+        </div>
+      );
+    },
+
     render: function() {
       var remoteStreamClasses = React.addons.classSet({
         "remote": true,
         "focus-stream": !this.props.displayScreenShare
       });
 
       var screenShareStreamClasses = React.addons.classSet({
         "screen": true,
         "focus-stream": this.props.displayScreenShare
       });
 
       var mediaWrapperClasses = React.addons.classSet({
         "media-wrapper": true,
         "receiving-screen-share": this.props.displayScreenShare,
         "showing-local-streams": this.props.localSrcVideoObject ||
-          this.props.localPosterUrl
+          this.props.localPosterUrl,
+        "showing-remote-streams": this.props.remoteSrcVideoObject ||
+          this.props.remotePosterUrl || this.props.isRemoteLoading
       });
 
       return (
         <div className="media-layout">
           <div className={mediaWrapperClasses}>
             <span className="self-view-hidden-message">
               {mozL10n.get("self_view_hidden_message")}
             </span>
             <div className={remoteStreamClasses}>
               <MediaView displayAvatar={!this.props.renderRemoteVideo}
                 isLoading={this.props.isRemoteLoading}
                 mediaType="remote"
                 posterUrl={this.props.remotePosterUrl}
                 srcVideoObject={this.props.remoteSrcVideoObject} />
+              { this.state.localMediaAboslutelyPositioned ?
+                this.renderLocalVideo() : null }
+              { this.props.children }
             </div>
             <div className={screenShareStreamClasses}>
               <MediaView displayAvatar={false}
                 isLoading={this.props.isScreenShareLoading}
                 mediaType="screen-share"
                 posterUrl={this.props.screenSharePosterUrl}
                 srcVideoObject={this.props.screenShareVideoObject} />
             </div>
             <loop.shared.views.chat.TextChatView
               dispatcher={this.props.dispatcher}
               showRoomName={this.props.showContextRoomName}
               useDesktopPaths={false} />
-            <div className="local">
-              <MediaView displayAvatar={this.props.localVideoMuted}
-                isLoading={this.props.isLocalLoading}
-                mediaType="local"
-                posterUrl={this.props.localPosterUrl}
-                srcVideoObject={this.props.localSrcVideoObject} />
-            </div>
+            { this.state.localMediaAboslutelyPositioned ?
+              null : this.renderLocalVideo() }
           </div>
         </div>
       );
     }
   });
 
   return {
     AvatarView: AvatarView,
--- a/browser/components/loop/standalone/content/js/standaloneRoomViews.js
+++ b/browser/components/loop/standalone/content/js/standaloneRoomViews.js
@@ -251,21 +251,22 @@ loop.standaloneRoomViews = (function(moz
       );
     }
   });
 
   var StandaloneRoomView = React.createClass({displayName: "StandaloneRoomView",
     mixins: [
       Backbone.Events,
       sharedMixins.MediaSetupMixin,
-      sharedMixins.RoomsAudioMixin,
-      loop.store.StoreMixin("activeRoomStore")
+      sharedMixins.RoomsAudioMixin
     ],
 
     propTypes: {
+      // We pass conversationStore here rather than use the mixin, to allow
+      // easy configurability for the ui-showcase.
       activeRoomStore: React.PropTypes.oneOfType([
         React.PropTypes.instanceOf(loop.store.ActiveRoomStore),
         React.PropTypes.instanceOf(loop.store.FxOSActiveRoomStore)
       ]).isRequired,
       dispatcher: React.PropTypes.instanceOf(loop.Dispatcher).isRequired,
       isFirefox: React.PropTypes.bool.isRequired,
       // The poster URLs are for UI-showcase testing and development
       localPosterUrl: React.PropTypes.string,
@@ -277,16 +278,26 @@ loop.standaloneRoomViews = (function(moz
     getInitialState: function() {
       var storeState = this.props.activeRoomStore.getStoreState();
       return _.extend({}, storeState, {
         // Used by the UI showcase.
         roomState: this.props.roomState || storeState.roomState
       });
     },
 
+    componentWillMount: function() {
+      this.props.activeRoomStore.on("change", function() {
+        this.setState(this.props.activeRoomStore.getStoreState());
+      }, this);
+    },
+
+    componentWillUnmount: function() {
+      this.props.activeRoomStore.off("change", null, this);
+    },
+
     componentDidMount: function() {
       // Adding a class to the document body element from here to ease styling it.
       document.body.classList.add("is-standalone-room");
     },
 
     /**
      * Watches for when we transition to MEDIA_WAIT room state, so we can request
      * user media access.
@@ -424,17 +435,18 @@ loop.standaloneRoomViews = (function(moz
      * Should we render a visual cue to the user (e.g. a spinner) that a remote
      * screen-share is on its way from the other user?
      *
      * @returns {boolean}
      * @private
      */
     _isScreenShareLoading: function() {
       return this.state.receivingScreenShare &&
-             !this.state.screenShareVideoObject;
+             !this.state.screenShareVideoObject &&
+             !this.props.screenSharePosterUrl;
     },
 
     render: function() {
       var displayScreenShare = !!(this.state.receivingScreenShare ||
         this.props.screenSharePosterUrl);
 
       return (
         React.createElement("div", {className: "room-conversation-wrapper standalone-room-wrapper"}, 
@@ -451,16 +463,17 @@ loop.standaloneRoomViews = (function(moz
             dispatcher: this.props.dispatcher, 
             displayScreenShare: displayScreenShare, 
             isLocalLoading: this._isLocalLoading(), 
             isRemoteLoading: this._isRemoteLoading(), 
             isScreenShareLoading: this._isScreenShareLoading(), 
             localPosterUrl: this.props.localPosterUrl, 
             localSrcVideoObject: this.state.localSrcVideoObject, 
             localVideoMuted: this.state.videoMuted, 
+            matchMedia: this.state.matchMedia || window.matchMedia.bind(window), 
             remotePosterUrl: this.props.remotePosterUrl, 
             remoteSrcVideoObject: this.state.remoteSrcVideoObject, 
             renderRemoteVideo: this.shouldRenderRemoteVideo(), 
             screenSharePosterUrl: this.props.screenSharePosterUrl, 
             screenShareVideoObject: this.state.screenShareVideoObject, 
             showContextRoomName: true, 
             useDesktopPaths: false}), 
           React.createElement(sharedViews.ConversationToolbar, {
--- a/browser/components/loop/standalone/content/js/standaloneRoomViews.jsx
+++ b/browser/components/loop/standalone/content/js/standaloneRoomViews.jsx
@@ -251,21 +251,22 @@ loop.standaloneRoomViews = (function(moz
       );
     }
   });
 
   var StandaloneRoomView = React.createClass({
     mixins: [
       Backbone.Events,
       sharedMixins.MediaSetupMixin,
-      sharedMixins.RoomsAudioMixin,
-      loop.store.StoreMixin("activeRoomStore")
+      sharedMixins.RoomsAudioMixin
     ],
 
     propTypes: {
+      // We pass conversationStore here rather than use the mixin, to allow
+      // easy configurability for the ui-showcase.
       activeRoomStore: React.PropTypes.oneOfType([
         React.PropTypes.instanceOf(loop.store.ActiveRoomStore),
         React.PropTypes.instanceOf(loop.store.FxOSActiveRoomStore)
       ]).isRequired,
       dispatcher: React.PropTypes.instanceOf(loop.Dispatcher).isRequired,
       isFirefox: React.PropTypes.bool.isRequired,
       // The poster URLs are for UI-showcase testing and development
       localPosterUrl: React.PropTypes.string,
@@ -277,16 +278,26 @@ loop.standaloneRoomViews = (function(moz
     getInitialState: function() {
       var storeState = this.props.activeRoomStore.getStoreState();
       return _.extend({}, storeState, {
         // Used by the UI showcase.
         roomState: this.props.roomState || storeState.roomState
       });
     },
 
+    componentWillMount: function() {
+      this.props.activeRoomStore.on("change", function() {
+        this.setState(this.props.activeRoomStore.getStoreState());
+      }, this);
+    },
+
+    componentWillUnmount: function() {
+      this.props.activeRoomStore.off("change", null, this);
+    },
+
     componentDidMount: function() {
       // Adding a class to the document body element from here to ease styling it.
       document.body.classList.add("is-standalone-room");
     },
 
     /**
      * Watches for when we transition to MEDIA_WAIT room state, so we can request
      * user media access.
@@ -424,17 +435,18 @@ loop.standaloneRoomViews = (function(moz
      * Should we render a visual cue to the user (e.g. a spinner) that a remote
      * screen-share is on its way from the other user?
      *
      * @returns {boolean}
      * @private
      */
     _isScreenShareLoading: function() {
       return this.state.receivingScreenShare &&
-             !this.state.screenShareVideoObject;
+             !this.state.screenShareVideoObject &&
+             !this.props.screenSharePosterUrl;
     },
 
     render: function() {
       var displayScreenShare = !!(this.state.receivingScreenShare ||
         this.props.screenSharePosterUrl);
 
       return (
         <div className="room-conversation-wrapper standalone-room-wrapper">
@@ -451,16 +463,17 @@ loop.standaloneRoomViews = (function(moz
             dispatcher={this.props.dispatcher}
             displayScreenShare={displayScreenShare}
             isLocalLoading={this._isLocalLoading()}
             isRemoteLoading={this._isRemoteLoading()}
             isScreenShareLoading={this._isScreenShareLoading()}
             localPosterUrl={this.props.localPosterUrl}
             localSrcVideoObject={this.state.localSrcVideoObject}
             localVideoMuted={this.state.videoMuted}
+            matchMedia={this.state.matchMedia || window.matchMedia.bind(window)}
             remotePosterUrl={this.props.remotePosterUrl}
             remoteSrcVideoObject={this.state.remoteSrcVideoObject}
             renderRemoteVideo={this.shouldRenderRemoteVideo()}
             screenSharePosterUrl={this.props.screenSharePosterUrl}
             screenShareVideoObject={this.state.screenShareVideoObject}
             showContextRoomName={true}
             useDesktopPaths={false} />
           <sharedViews.ConversationToolbar
--- a/browser/components/loop/test/desktop-local/conversationViews_test.js
+++ b/browser/components/loop/test/desktop-local/conversationViews_test.js
@@ -469,63 +469,46 @@ describe("loop.conversationViews", funct
         sinon.assert.calledWith(document.mozL10n.get,
           "generic_contact_unavailable_title");
     });
   });
 
   describe("OngoingConversationView", function() {
     function mountTestComponent(extraProps) {
       var props = _.extend({
-        dispatcher: dispatcher
+        conversationStore: conversationStore,
+        dispatcher: dispatcher,
+        matchMedia: window.matchMedia
       }, extraProps);
       return TestUtils.renderIntoDocument(
         React.createElement(loop.conversationViews.OngoingConversationView, props));
     }
 
     it("should dispatch a setupStreamElements action when the view is created",
       function() {
         view = mountTestComponent();
 
         sinon.assert.calledOnce(dispatcher.dispatch);
         sinon.assert.calledWithMatch(dispatcher.dispatch,
           sinon.match.hasOwn("name", "setupStreamElements"));
       });
 
-    it("should display an avatar for remote video when the stream is not enabled", function() {
-      view = mountTestComponent({
-        mediaConnected: true,
-        remoteVideoEnabled: false
-      });
-
-      TestUtils.findRenderedComponentWithType(view, sharedViews.AvatarView);
-    });
-
     it("should display the remote video when the stream is enabled", function() {
       conversationStore.setStoreState({
         remoteSrcVideoObject: { fake: 1 }
       });
 
       view = mountTestComponent({
         mediaConnected: true,
         remoteVideoEnabled: true
       });
 
       expect(view.getDOMNode().querySelector(".remote video")).not.eql(null);
     });
 
-    it("should display an avatar for local video when the stream is not enabled", function() {
-      view = mountTestComponent({
-        video: {
-          enabled: false
-        }
-      });
-
-      TestUtils.findRenderedComponentWithType(view, sharedViews.AvatarView);
-    });
-
     it("should display the local video when the stream is enabled", function() {
       conversationStore.setStoreState({
         localSrcVideoObject: { fake: 1 }
       });
 
       view = mountTestComponent({
         video: {
           enabled: true
--- a/browser/components/loop/test/desktop-local/index.html
+++ b/browser/components/loop/test/desktop-local/index.html
@@ -90,17 +90,17 @@
     describe("Uncaught Error Check", function() {
       it("should load the tests without errors", function() {
         chai.expect(uncaughtError && uncaughtError.message).to.be.undefined;
       });
     });
 
     describe("Unexpected Warnings Check", function() {
       it("should long only the warnings we expect", function() {
-        chai.expect(caughtWarnings.length).to.eql(27);
+        chai.expect(caughtWarnings.length).to.eql(28);
       });
     });
 
     mocha.run(function () {
       var completeNode = document.createElement("p");
       completeNode.setAttribute("id", "complete");
       completeNode.appendChild(document.createTextNode("Complete"));
       document.getElementById("mocha").appendChild(completeNode);
--- a/browser/components/loop/test/desktop-local/roomViews_test.js
+++ b/browser/components/loop/test/desktop-local/roomViews_test.js
@@ -593,31 +593,16 @@ describe("loop.roomViews", function () {
 
         view = mountTestComponent();
 
         expect(view.getDOMNode().querySelector(".local video")).not.eql(null);
       });
 
     });
 
-    describe("Mute", function() {
-      it("should render local media as audio-only if video is muted",
-        function() {
-          activeRoomStore.setStoreState({
-            roomState: ROOM_STATES.SESSION_CONNECTED,
-            videoMuted: true
-          });
-
-          view = mountTestComponent();
-
-          expect(view.getDOMNode().querySelector(".local-stream-audio"))
-            .not.eql(null);
-        });
-    });
-
     describe("Edit Context", function() {
       it("should show the form when the edit button is clicked", function() {
         view = mountTestComponent();
         var node = view.getDOMNode();
 
         expect(node.querySelector(".room-context")).to.eql(null);
 
         var editButton = node.querySelector(".btn-mute-edit");
--- a/browser/components/loop/test/functional/test_1_browser_call.py
+++ b/browser/components/loop/test/functional/test_1_browser_call.py
@@ -94,17 +94,17 @@ class Test1BrowserCall(MarionetteTestCas
         self.wait_for_element_enabled(button, 120)
 
         button.click()
 
     def local_check_room_self_video(self):
         self.switch_to_chatbox()
 
         # expect a video container on desktop side
-        media_container = self.wait_for_element_displayed(By.CLASS_NAME, "media")
+        media_container = self.wait_for_element_displayed(By.CLASS_NAME, "media-layout")
         self.assertEqual(media_container.tag_name, "div", "expect a video container")
 
         self.check_video(".local-video")
 
     def local_get_and_verify_room_url(self):
         self.switch_to_chatbox()
         button = self.wait_for_element_displayed(By.CLASS_NAME, "btn-copy")
 
--- a/browser/components/loop/test/mochitest/.eslintrc
+++ b/browser/components/loop/test/mochitest/.eslintrc
@@ -16,16 +16,17 @@
     "HAWK_TOKEN_LENGTH": true,
     "checkLoggedOutState": false,
     "checkFxAOAuthTokenData": false,
     "loadLoopPanel": false,
     "getLoopString": false,
     "gMozLoopAPI": true,
     "mockDb": true,
     "mockPushHandler": true,
+    "OpenBrowserWindow": true,
     "promiseDeletedOAuthParams": false,
     "promiseOAuthGetRegistration": false,
     "promiseOAuthParamsSetup": false,
     "promiseObserverNotified": false,
     "promiseWaitForCondition": false,
     "resetFxA": true,
     // Loop specific items
     "MozLoopServiceInternal": true,
--- a/browser/components/loop/test/mochitest/browser_toolbarbutton.js
+++ b/browser/components/loop/test/mochitest/browser_toolbarbutton.js
@@ -162,8 +162,24 @@ add_task(function* test_panelToggle_on_c
 
 add_task(function* test_screen_share() {
   Assert.strictEqual(LoopUI.toolbarButton.node.getAttribute("state"), "", "Check button is in default state");
   MozLoopService.setScreenShareState("1", true);
   Assert.strictEqual(LoopUI.toolbarButton.node.getAttribute("state"), "action", "Check button is in action state");
   MozLoopService.setScreenShareState("1", false);
   Assert.strictEqual(LoopUI.toolbarButton.node.getAttribute("state"), "", "Check button is in default state");
 });
+
+add_task(function* test_private_browsing_window() {
+  let win = OpenBrowserWindow({ private: true });
+  yield new Promise(resolve => {
+    win.addEventListener("load", function listener() {
+      win.removeEventListener("load", listener);
+      resolve();
+    });
+  });
+
+  let button = win.LoopUI.toolbarButton.node;
+  Assert.ok(button, "Loop button should be present");
+  Assert.ok(button.getAttribute("disabled"), "Disabled attribute should be set");
+
+  win.close();
+});
--- a/browser/components/loop/test/shared/activeRoomStore_test.js
+++ b/browser/components/loop/test/shared/activeRoomStore_test.js
@@ -911,36 +911,16 @@ describe("loop.store.ActiveRoomStore", f
         sessionToken: "1627384950"
       });
 
       connectionFailureAction = new sharedActions.ConnectionFailure({
         reason: "FAIL"
       });
     });
 
-    it("should retry publishing if on desktop, and in the videoMuted state", function() {
-      store._isDesktop = true;
-
-      store.connectionFailure(new sharedActions.ConnectionFailure({
-        reason: FAILURE_DETAILS.UNABLE_TO_PUBLISH_MEDIA
-      }));
-
-      sinon.assert.calledOnce(fakeSdkDriver.retryPublishWithoutVideo);
-    });
-
-    it("should set videoMuted to try when retrying publishing", function() {
-      store._isDesktop = true;
-
-      store.connectionFailure(new sharedActions.ConnectionFailure({
-        reason: FAILURE_DETAILS.UNABLE_TO_PUBLISH_MEDIA
-      }));
-
-      expect(store.getStoreState().videoMuted).eql(true);
-    });
-
     it("should store the failure reason", function() {
       store.connectionFailure(connectionFailureAction);
 
       expect(store.getStoreState().failureReason).eql("FAIL");
     });
 
     it("should reset the multiplexGum", function() {
       store.connectionFailure(connectionFailureAction);
--- a/browser/components/loop/test/shared/conversationStore_test.js
+++ b/browser/components/loop/test/shared/conversationStore_test.js
@@ -142,36 +142,16 @@ describe("loop.store.ConversationStore",
   });
 
   describe("#connectionFailure", function() {
     beforeEach(function() {
       store._websocket = fakeWebsocket;
       store.setStoreState({windowId: "42"});
     });
 
-    it("should retry publishing if on desktop, and in the videoMuted state", function() {
-      store._isDesktop = true;
-
-      store.connectionFailure(new sharedActions.ConnectionFailure({
-        reason: FAILURE_DETAILS.UNABLE_TO_PUBLISH_MEDIA
-      }));
-
-      sinon.assert.calledOnce(sdkDriver.retryPublishWithoutVideo);
-    });
-
-    it("should set videoMuted to try when retrying publishing", function() {
-      store._isDesktop = true;
-
-      store.connectionFailure(new sharedActions.ConnectionFailure({
-        reason: FAILURE_DETAILS.UNABLE_TO_PUBLISH_MEDIA
-      }));
-
-      expect(store.getStoreState().videoMuted).eql(true);
-    });
-
     it("should disconnect the session", function() {
       store.connectionFailure(
         new sharedActions.ConnectionFailure({reason: "fake"}));
 
       sinon.assert.calledOnce(sdkDriver.disconnectSession);
     });
 
     it("should ensure the websocket is closed", function() {
--- a/browser/components/loop/test/shared/otSdkDriver_test.js
+++ b/browser/components/loop/test/shared/otSdkDriver_test.js
@@ -128,53 +128,16 @@ describe("loop.OTSdkDriver", function ()
 
       sinon.assert.calledOnce(sdk.initPublisher);
       sinon.assert.calledWith(sdk.initPublisher,
         sinon.match.instanceOf(HTMLDivElement),
         expectedConfig);
     });
   });
 
-  describe("#retryPublishWithoutVideo", function() {
-    beforeEach(function() {
-      sdk.initPublisher.returns(publisher);
-
-      driver.setupStreamElements(new sharedActions.SetupStreamElements({
-        publisherConfig: publisherConfig
-      }));
-    });
-
-    it("should make MediaStreamTrack.getSources return without a video source", function(done) {
-      driver.retryPublishWithoutVideo();
-
-      window.MediaStreamTrack.getSources(function(sources) {
-        expect(sources.some(function(src) {
-          return src.kind === "video";
-        })).eql(false);
-
-        done();
-      });
-    });
-
-    it("should call initPublisher", function() {
-      driver.retryPublishWithoutVideo();
-
-      var expectedConfig = _.extend({
-        channels: {
-          text: {}
-        }
-      }, publisherConfig);
-
-      sinon.assert.calledTwice(sdk.initPublisher);
-      sinon.assert.calledWith(sdk.initPublisher,
-        sinon.match.instanceOf(HTMLDivElement),
-        expectedConfig);
-    });
-  });
-
   describe("#setMute", function() {
     beforeEach(function() {
       sdk.initPublisher.returns(publisher);
 
       driver.setupStreamElements(new sharedActions.SetupStreamElements({
         publisherConfig: publisherConfig
       }));
     });
--- a/browser/components/loop/test/shared/textChatView_test.js
+++ b/browser/components/loop/test/shared/textChatView_test.js
@@ -51,37 +51,16 @@ describe("loop.shared.views.TextChatView
         React.createElement(loop.shared.views.chat.TextChatEntriesView,
           _.extend(basicProps, extraProps)));
     }
 
     beforeEach(function() {
       store.setStoreState({ textChatEnabled: true });
     });
 
-    it("should add an empty class when the list is empty", function() {
-      view = mountTestComponent({
-        messageList: []
-      });
-
-      expect(view.getDOMNode().classList.contains("text-chat-entries-empty")).eql(true);
-    });
-
-    it("should not add an empty class when the list is has items", function() {
-      view = mountTestComponent({
-        messageList: [{
-          type: CHAT_MESSAGE_TYPES.RECEIVED,
-          contentType: CHAT_CONTENT_TYPES.TEXT,
-          message: "Hello!",
-          receivedTimestamp: "2015-06-25T17:53:55.357Z"
-        }]
-      });
-
-      expect(view.getDOMNode().classList.contains("text-chat-entries-empty")).eql(false);
-    });
-
     it("should render message entries when message were sent/ received", function() {
       view = mountTestComponent({
         messageList: [{
           type: CHAT_MESSAGE_TYPES.RECEIVED,
           contentType: CHAT_CONTENT_TYPES.TEXT,
           message: "Hello!",
           receivedTimestamp: "2015-06-25T17:53:55.357Z"
         }, {
@@ -292,16 +271,51 @@ describe("loop.shared.views.TextChatView
       fakeServer = sinon.fakeServer.create();
       store.setStoreState({ textChatEnabled: true });
     });
 
     afterEach(function() {
       fakeServer.restore();
     });
 
+    it("should add a disabled class when text chat is disabled", function() {
+      view = mountTestComponent();
+
+      store.setStoreState({ textChatEnabled: false });
+
+      expect(view.getDOMNode().classList.contains("text-chat-disabled")).eql(true);
+    });
+
+    it("should not a disabled class when text chat is enabled", function() {
+      view = mountTestComponent();
+
+      store.setStoreState({ textChatEnabled: true });
+
+      expect(view.getDOMNode().classList.contains("text-chat-disabled")).eql(false);
+    });
+
+    it("should add an empty class when the entries list is empty", function() {
+      view = mountTestComponent();
+
+      expect(view.getDOMNode().classList.contains("text-chat-entries-empty")).eql(true);
+    });
+
+    it("should not add an empty class when the entries list is has items", function() {
+      view = mountTestComponent();
+
+      store.sendTextChatMessage({
+        contentType: CHAT_CONTENT_TYPES.TEXT,
+        message: "Hello!",
+        sentTimestamp: "1970-01-01T00:02:00.000Z",
+        receivedTimestamp: "1970-01-01T00:02:00.000Z"
+      });
+
+      expect(view.getDOMNode().classList.contains("text-chat-entries-empty")).eql(false);
+    });
+
     it("should show timestamps from msgs sent more than 1 min apart", function() {
       view = mountTestComponent();
 
       store.sendTextChatMessage({
         contentType: CHAT_CONTENT_TYPES.TEXT,
         message: "Hello!",
         sentTimestamp: "1970-01-01T00:02:00.000Z",
         receivedTimestamp: "1970-01-01T00:02:00.000Z"
@@ -321,22 +335,16 @@ describe("loop.shared.views.TextChatView
       });
 
       var node = view.getDOMNode();
 
       expect(node.querySelectorAll(".text-chat-entry-timestamp").length)
           .to.eql(2);
     });
 
-    it("should display the view if no messages and text chat is enabled", function() {
-      view = mountTestComponent();
-
-      expect(view.getDOMNode()).not.eql(null);
-    });
-
     it("should render message entries when message were sent/ received", function() {
       view = mountTestComponent();
 
       store.receivedTextChatMessage({
         contentType: CHAT_CONTENT_TYPES.TEXT,
         message: "Hello!",
         sentTimestamp: "1970-01-01T00:03:00.000Z",
         receivedTimestamp: "1970-01-01T00:03:00.000Z"
--- a/browser/components/loop/test/shared/views_test.js
+++ b/browser/components/loop/test/shared/views_test.js
@@ -1052,16 +1052,17 @@ describe("loop.shared.views", function()
     function mountTestComponent(extraProps) {
       var defaultProps = {
         dispatcher: dispatcher,
         displayScreenShare: false,
         isLocalLoading: false,
         isRemoteLoading: false,
         isScreenShareLoading: false,
         localVideoMuted: false,
+        matchMedia: window.matchMedia,
         renderRemoteVideo: false,
         showContextRoomName: false,
         useDesktopPaths: false
       };
 
       return TestUtils.renderIntoDocument(
         React.createElement(sharedViews.MediaLayoutView,
           _.extend(defaultProps, extraProps)));
@@ -1139,10 +1140,40 @@ describe("loop.shared.views", function()
       view = mountTestComponent({
         localSrcVideoObject: {},
         localPosterUrl: "fake/url"
       });
 
       expect(view.getDOMNode().querySelector(".media-wrapper")
         .classList.contains("showing-local-streams")).eql(true);
     });
+
+    it("should not mark the wrapper as showing remote streams when not displaying a stream", function() {
+      view = mountTestComponent({
+        remoteSrcVideoObject: null,
+        remotePosterUrl: null
+      });
+
+      expect(view.getDOMNode().querySelector(".media-wrapper")
+        .classList.contains("showing-remote-streams")).eql(false);
+    });
+
+    it("should mark the wrapper as showing remote streams when displaying a stream", function() {
+      view = mountTestComponent({
+        remoteSrcVideoObject: {},
+        remotePosterUrl: null
+      });
+
+      expect(view.getDOMNode().querySelector(".media-wrapper")
+        .classList.contains("showing-remote-streams")).eql(true);
+    });
+
+    it("should mark the wrapper as showing remote streams when displaying a poster url", function() {
+      view = mountTestComponent({
+        remoteSrcVideoObject: {},
+        remotePosterUrl: "fake/url"
+      });
+
+      expect(view.getDOMNode().querySelector(".media-wrapper")
+        .classList.contains("showing-remote-streams")).eql(true);
+    });
   });
 });
--- a/browser/components/loop/ui/ui-showcase.js
+++ b/browser/components/loop/ui/ui-showcase.js
@@ -70,23 +70,35 @@
     }
     window.removeEventListener(eventName, func);
   };
 
   loop.shared.mixins.setRootObject(rootObject);
 
   var dispatcher = new loop.Dispatcher();
 
-  var mockSDK = _.extend({
+  var MockSDK = function() {
+    dispatcher.register(this, [
+      "setupStreamElements"
+    ]);
+  };
+
+  MockSDK.prototype = {
+    setupStreamElements: function() {
+      // Dummy function to stop warnings.
+    },
+
     sendTextChatMessage: function(message) {
       dispatcher.dispatch(new loop.shared.actions.ReceivedTextChatMessage({
         message: message.message
       }));
     }
-  }, Backbone.Events);
+  };
+
+  var mockSDK = new MockSDK();
 
   /**
    * Every view that uses an activeRoomStore needs its own; if they shared
    * an active store, they'd interfere with each other.
    *
    * @param options
    * @returns {loop.store.ActiveRoomStore}
    */
@@ -111,17 +123,16 @@
       remoteVideoEnabled: options.remoteVideoEnabled,
       roomName: "A Very Long Conversation Name",
       roomState: options.roomState,
       used: !!options.roomUsed,
       videoMuted: !!options.videoMuted
     });
 
     store.forcedUpdate = function forcedUpdate(contentWindow) {
-
       // Since this is called by setTimeout, we don't want to lose any
       // exceptions if there's a problem and we need to debug, so...
       try {
         // the dimensions here are taken from the poster images that we're
         // using, since they give the <video> elements their initial intrinsic
         // size.  This ensures that the right aspect ratios are calculated.
         // These are forced to 640x480, because it makes it visually easy to
         // validate that the showcase looks like the real app on a chine
@@ -131,16 +142,27 @@
             camera: {height: 480, orientation: 0, width: 640}
           },
           mediaConnected: options.mediaConnected,
           receivingScreenShare: !!options.receivingScreenShare,
           remoteVideoDimensions: {
             camera: {height: 480, orientation: 0, width: 640}
           },
           remoteVideoEnabled: options.remoteVideoEnabled,
+          // Override the matchMedia, this is so that the correct version is
+          // used for the frame.
+          //
+          // Currently, we use an icky hack, and the showcase conspires with
+          // react-frame-component to set iframe.contentWindow.matchMedia onto
+          // the store. Once React context matures a bit (somewhere between
+          // 0.14 and 1.0, apparently):
+          //
+          // https://facebook.github.io/react/blog/2015/02/24/streamlining-react-elements.html#solution-make-context-parent-based-instead-of-owner-based
+          //
+          // we should be able to use those to clean this up.
           matchMedia: contentWindow.matchMedia.bind(contentWindow),
           roomState: options.roomState,
           videoMuted: !!options.videoMuted
         };
 
         if (options.receivingScreenShare) {
           // Note that the image we're using had to be scaled a bit, and
           // it still ended up a bit narrower than the live thing that
@@ -180,38 +202,49 @@
     mediaConnected: false,
     roomState: ROOM_STATES.READY
   });
 
   var updatingActiveRoomStore = makeActiveRoomStore({
     roomState: ROOM_STATES.HAS_PARTICIPANTS
   });
 
+  var updatingMobileActiveRoomStore = makeActiveRoomStore({
+    roomState: ROOM_STATES.HAS_PARTICIPANTS
+  });
+
   var localFaceMuteRoomStore = makeActiveRoomStore({
     roomState: ROOM_STATES.HAS_PARTICIPANTS,
     videoMuted: true
   });
 
   var remoteFaceMuteRoomStore = makeActiveRoomStore({
     roomState: ROOM_STATES.HAS_PARTICIPANTS,
     remoteVideoEnabled: false,
     mediaConnected: true
   });
 
   var updatingSharingRoomStore = makeActiveRoomStore({
     roomState: ROOM_STATES.HAS_PARTICIPANTS,
     receivingScreenShare: true
   });
 
+  var updatingSharingRoomMobileStore = makeActiveRoomStore({
+    roomState: ROOM_STATES.HAS_PARTICIPANTS,
+    receivingScreenShare: true
+  });
+
   var loadingRemoteLoadingScreenStore = makeActiveRoomStore({
     mediaConnected: false,
+    receivingScreenShare: true,
     roomState: ROOM_STATES.HAS_PARTICIPANTS,
     remoteSrcVideoObject: false
   });
   var loadingScreenSharingRoomStore = makeActiveRoomStore({
+    receivingScreenShare: true,
     roomState: ROOM_STATES.HAS_PARTICIPANTS
   });
 
   /* Set up the stores for pending screen sharing */
   loadingScreenSharingRoomStore.receivingScreenShare({
     receiving: true,
     srcVideoObject: false
   });
@@ -229,17 +262,20 @@
   });
 
   var endedRoomStore = makeActiveRoomStore({
     roomState: ROOM_STATES.ENDED,
     roomUsed: true
   });
 
   var invitationRoomStore = new loop.store.RoomStore(dispatcher, {
-    mozLoop: navigator.mozLoop
+    mozLoop: navigator.mozLoop,
+    activeRoomStore: makeActiveRoomStore({
+      roomState: ROOM_STATES.INIT
+    })
   });
 
   var roomStore = new loop.store.RoomStore(dispatcher, {
     mozLoop: navigator.mozLoop,
     activeRoomStore: makeActiveRoomStore({
       roomState: ROOM_STATES.HAS_PARTICIPANTS
     })
   });
@@ -248,16 +284,30 @@
     mozLoop: navigator.mozLoop,
     activeRoomStore: makeActiveRoomStore({
       roomState: ROOM_STATES.HAS_PARTICIPANTS,
       mediaConnected: false,
       remoteSrcVideoObject: false
     })
   });
 
+  var desktopRoomStoreMedium = new loop.store.RoomStore(dispatcher, {
+    mozLoop: navigator.mozLoop,
+    activeRoomStore: makeActiveRoomStore({
+      roomState: ROOM_STATES.HAS_PARTICIPANTS
+    })
+  });
+
+  var desktopRoomStoreLarge = new loop.store.RoomStore(dispatcher, {
+    mozLoop: navigator.mozLoop,
+    activeRoomStore: makeActiveRoomStore({
+      roomState: ROOM_STATES.HAS_PARTICIPANTS
+    })
+  });
+
   var desktopLocalFaceMuteActiveRoomStore = makeActiveRoomStore({
     roomState: ROOM_STATES.HAS_PARTICIPANTS,
     videoMuted: true
   });
   var desktopLocalFaceMuteRoomStore = new loop.store.RoomStore(dispatcher, {
     mozLoop: navigator.mozLoop,
     activeRoomStore: desktopLocalFaceMuteActiveRoomStore
   });
@@ -267,25 +317,69 @@
     remoteVideoEnabled: false,
     mediaConnected: true
   });
   var desktopRemoteFaceMuteRoomStore = new loop.store.RoomStore(dispatcher, {
     mozLoop: navigator.mozLoop,
     activeRoomStore: desktopRemoteFaceMuteActiveRoomStore
   });
 
-  var conversationStore = new loop.store.ConversationStore(dispatcher, {
-    client: {},
-    mozLoop: navigator.mozLoop,
-    sdkDriver: mockSDK
-  });
   var textChatStore = new loop.store.TextChatStore(dispatcher, {
     sdkDriver: mockSDK
   });
 
+  /**
+   * Every view that uses an conversationStore needs its own; if they shared
+   * a conversation store, they'd interfere with each other.
+   *
+   * @param options
+   * @returns {loop.store.ConversationStore}
+   */
+  function makeConversationStore() {
+    var roomDispatcher = new loop.Dispatcher();
+
+    var store = new loop.store.ConversationStore(dispatcher, {
+      client: {},
+      mozLoop: navigator.mozLoop,
+      sdkDriver: mockSDK
+    });
+
+    store.forcedUpdate = function forcedUpdate(contentWindow) {
+      // Since this is called by setTimeout, we don't want to lose any
+      // exceptions if there's a problem and we need to debug, so...
+      try {
+        var newStoreState = {
+          // Override the matchMedia, this is so that the correct version is
+          // used for the frame.
+          //
+          // Currently, we use an icky hack, and the showcase conspires with
+          // react-frame-component to set iframe.contentWindow.matchMedia onto
+          // the store. Once React context matures a bit (somewhere between
+          // 0.14 and 1.0, apparently):
+          //
+          // https://facebook.github.io/react/blog/2015/02/24/streamlining-react-elements.html#solution-make-context-parent-based-instead-of-owner-based
+          //
+          // we should be able to use those to clean this up.
+          matchMedia: contentWindow.matchMedia.bind(contentWindow)
+        };
+
+        store.setStoreState(newStoreState);
+      } catch (ex) {
+        console.error("exception in forcedUpdate:", ex);
+      }
+    };
+
+    return store;
+  }
+
+  var conversationStores = [];
+  for (var index = 0; index < 5; index++) {
+    conversationStores[index] = makeConversationStore();
+  }
+
   // Update the text chat store with the room info.
   textChatStore.updateRoomInfo(new sharedActions.UpdateRoomInfo({
     roomName: "A Very Long Conversation Name",
     roomOwner: "fake",
     roomUrl: "http://showcase",
     urls: [{
       description: "A wonderful page!",
       location: "http://wonderful.invalid"
@@ -336,17 +430,17 @@
   dispatcher.dispatch(new sharedActions.SendTextChatMessage({
     contentType: loop.store.CHAT_CONTENT_TYPES.TEXT,
     message: "Cool",
     sentTimestamp: "2015-06-23T22:27:45.590Z"
   }));
 
   loop.store.StoreMixin.register({
     activeRoomStore: activeRoomStore,
-    conversationStore: conversationStore,
+    conversationStore: conversationStores[0],
     textChatStore: textChatStore
   });
 
   // Local mocks
 
   var mockMozLoopRooms = _.extend({}, navigator.mozLoop);
 
   var mockContact = {
@@ -355,24 +449,16 @@
       value: "smith@invalid.com"
     }]
   };
 
   var mockClient = {
     requestCallUrlInfo: noop
   };
 
-  var mockConversationModel = new loop.shared.models.ConversationModel({
-    callerId: "Mrs Jones",
-    urlCreationDate: (new Date() / 1000).toString()
-  }, {
-    sdk: mockSDK
-  });
-  mockConversationModel.startSession = noop;
-
   var mockWebSocket = new loop.CallConnectionWebSocket({
     url: "fake",
     callId: "fakeId",
     websocketToken: "fakeToken"
   });
 
   var notifications = new loop.shared.models.NotificationCollection();
   var errNotifications = new loop.shared.models.NotificationCollection();
@@ -758,93 +844,128 @@
 
           React.createElement(Section, {name: "CallFailedView"}, 
             React.createElement(Example, {dashed: true, 
                      style: {width: "300px", height: "272px"}, 
                      summary: "Call Failed - Incoming"}, 
               React.createElement("div", {className: "fx-embedded"}, 
                 React.createElement(CallFailedView, {dispatcher: dispatcher, 
                                 outgoing: false, 
-                                store: conversationStore})
+                                store: conversationStores[0]})
               )
             ), 
             React.createElement(Example, {dashed: true, 
                      style: {width: "300px", height: "272px"}, 
                      summary: "Call Failed - Outgoing"}, 
               React.createElement("div", {className: "fx-embedded"}, 
                 React.createElement(CallFailedView, {dispatcher: dispatcher, 
                                 outgoing: true, 
-                                store: conversationStore})
+                                store: conversationStores[1]})
               )
             ), 
             React.createElement(Example, {dashed: true, 
                      style: {width: "300px", height: "272px"}, 
                      summary: "Call Failed — with call URL error"}, 
               React.createElement("div", {className: "fx-embedded"}, 
                 React.createElement(CallFailedView, {dispatcher: dispatcher, emailLinkError: true, 
                                 outgoing: true, 
-                                store: conversationStore})
+                                store: conversationStores[0]})
               )
             )
           ), 
 
           React.createElement(Section, {name: "OngoingConversationView"}, 
-            React.createElement(FramedExample, {height: 254, 
-                           summary: "Desktop ongoing conversation window", 
-                           width: 298}, 
+            React.createElement(FramedExample, {
+              dashed: true, 
+              height: 394, 
+              onContentsRendered: conversationStores[0].forcedUpdate, 
+              summary: "Desktop ongoing conversation window", 
+              width: 298}, 
               React.createElement("div", {className: "fx-embedded"}, 
                 React.createElement(OngoingConversationView, {
                   audio: {enabled: true}, 
+                  conversationStore: conversationStores[0], 
                   dispatcher: dispatcher, 
                   localPosterUrl: "sample-img/video-screen-local.png", 
                   mediaConnected: true, 
                   remotePosterUrl: "sample-img/video-screen-remote.png", 
                   remoteVideoEnabled: true, 
                   video: {enabled: true}})
               )
             ), 
 
-            React.createElement(FramedExample, {height: 600, 
-                           summary: "Desktop ongoing conversation window large", 
-                           width: 800}, 
-                React.createElement("div", {className: "fx-embedded"}, 
-                  React.createElement(OngoingConversationView, {
-                    audio: {enabled: true}, 
-                    dispatcher: dispatcher, 
-                    localPosterUrl: "sample-img/video-screen-local.png", 
-                    mediaConnected: true, 
-                    remotePosterUrl: "sample-img/video-screen-remote.png", 
-                    remoteVideoEnabled: true, 
-                    video: {enabled: true}})
-                )
+            React.createElement(FramedExample, {
+              dashed: true, 
+              height: 400, 
+              onContentsRendered: conversationStores[1].forcedUpdate, 
+              summary: "Desktop ongoing conversation window (medium)", 
+              width: 600}, 
+              React.createElement("div", {className: "fx-embedded"}, 
+                React.createElement(OngoingConversationView, {
+                  audio: {enabled: true}, 
+                  conversationStore: conversationStores[1], 
+                  dispatcher: dispatcher, 
+                  localPosterUrl: "sample-img/video-screen-local.png", 
+                  mediaConnected: true, 
+                  remotePosterUrl: "sample-img/video-screen-remote.png", 
+                  remoteVideoEnabled: true, 
+                  video: {enabled: true}})
+              )
             ), 
 
-            React.createElement(FramedExample, {height: 254, 
+            React.createElement(FramedExample, {
+              height: 600, 
+              onContentsRendered: conversationStores[2].forcedUpdate, 
+              summary: "Desktop ongoing conversation window (large)", 
+              width: 800}, 
+              React.createElement("div", {className: "fx-embedded"}, 
+                React.createElement(OngoingConversationView, {
+                  audio: {enabled: true}, 
+                  conversationStore: conversationStores[2], 
+                  dispatcher: dispatcher, 
+                  localPosterUrl: "sample-img/video-screen-local.png", 
+                  mediaConnected: true, 
+                  remotePosterUrl: "sample-img/video-screen-remote.png", 
+                  remoteVideoEnabled: true, 
+                  video: {enabled: true}})
+              )
+            ), 
+
+            React.createElement(FramedExample, {
+              dashed: true, 
+              height: 394, 
+              onContentsRendered: conversationStores[3].forcedUpdate, 
               summary: "Desktop ongoing conversation window - local face mute", 
               width: 298}, 
               React.createElement("div", {className: "fx-embedded"}, 
                 React.createElement(OngoingConversationView, {
                   audio: {enabled: true}, 
+                  conversationStore: conversationStores[3], 
                   dispatcher: dispatcher, 
+                  localPosterUrl: "sample-img/video-screen-local.png", 
                   mediaConnected: true, 
                   remotePosterUrl: "sample-img/video-screen-remote.png", 
                   remoteVideoEnabled: true, 
                   video: {enabled: false}})
               )
             ), 
 
-            React.createElement(FramedExample, {height: 254, 
+            React.createElement(FramedExample, {
+              dashed: true, height: 394, 
+              onContentsRendered: conversationStores[4].forcedUpdate, 
               summary: "Desktop ongoing conversation window - remote face mute", 
               width: 298}, 
               React.createElement("div", {className: "fx-embedded"}, 
                 React.createElement(OngoingConversationView, {
                   audio: {enabled: true}, 
+                  conversationStore: conversationStores[4], 
                   dispatcher: dispatcher, 
                   localPosterUrl: "sample-img/video-screen-local.png", 
                   mediaConnected: true, 
+                  remotePosterUrl: "sample-img/video-screen-remote.png", 
                   remoteVideoEnabled: false, 
                   video: {enabled: true}})
               )
             )
 
           ), 
 
           React.createElement(Section, {name: "FeedbackView"}, 
@@ -889,86 +1010,133 @@
               React.createElement("div", {className: "standalone"}, 
                 React.createElement(UnsupportedDeviceView, {platform: "ios"})
               )
             )
           ), 
 
           React.createElement(Section, {name: "DesktopRoomConversationView"}, 
             React.createElement(FramedExample, {
-              height: 254, 
+              height: 398, 
+              onContentsRendered: invitationRoomStore.activeRoomStore.forcedUpdate, 
               summary: "Desktop room conversation (invitation, text-chat inclusion/scrollbars don't happen in real client)", 
               width: 298}, 
               React.createElement("div", {className: "fx-embedded"}, 
                 React.createElement(DesktopRoomConversationView, {
                   dispatcher: dispatcher, 
                   localPosterUrl: "sample-img/video-screen-local.png", 
                   mozLoop: navigator.mozLoop, 
                   onCallTerminated: function(){}, 
                   roomState: ROOM_STATES.INIT, 
                   roomStore: invitationRoomStore})
               )
             ), 
 
             React.createElement(FramedExample, {
               dashed: true, 
               height: 394, 
+              onContentsRendered: desktopRoomStoreLoading.activeRoomStore.forcedUpdate, 
               summary: "Desktop room conversation (loading)", 
               width: 298}, 
               /* Hide scrollbars here. Rotating loading div overflows and causes
                scrollbars to appear */
               React.createElement("div", {className: "fx-embedded overflow-hidden"}, 
                 React.createElement(DesktopRoomConversationView, {
                   dispatcher: dispatcher, 
                   localPosterUrl: "sample-img/video-screen-local.png", 
                   mozLoop: navigator.mozLoop, 
                   onCallTerminated: function(){}, 
                   remotePosterUrl: "sample-img/video-screen-remote.png", 
                   roomState: ROOM_STATES.HAS_PARTICIPANTS, 
                   roomStore: desktopRoomStoreLoading})
               )
             ), 
 
-            React.createElement(FramedExample, {height: 254, 
-                           summary: "Desktop room conversation"}, 
+            React.createElement(FramedExample, {
+              dashed: true, 
+              height: 394, 
+              onContentsRendered: roomStore.activeRoomStore.forcedUpdate, 
+              summary: "Desktop room conversation", 
+              width: 298}, 
               React.createElement("div", {className: "fx-embedded"}, 
                 React.createElement(DesktopRoomConversationView, {
                   dispatcher: dispatcher, 
                   localPosterUrl: "sample-img/video-screen-local.png", 
                   mozLoop: navigator.mozLoop, 
                   onCallTerminated: function(){}, 
                   remotePosterUrl: "sample-img/video-screen-remote.png", 
                   roomState: ROOM_STATES.HAS_PARTICIPANTS, 
                   roomStore: roomStore})
               )
             ), 
 
-            React.createElement(FramedExample, {dashed: true, 
-                           height: 394, 
-                           summary: "Desktop room conversation local face-mute", 
-                           width: 298}, 
+            React.createElement(FramedExample, {
+              dashed: true, 
+              height: 482, 
+              onContentsRendered: desktopRoomStoreMedium.activeRoomStore.forcedUpdate, 
+              summary: "Desktop room conversation (medium)", 
+              width: 602}, 
+              React.createElement("div", {className: "fx-embedded"}, 
+                React.createElement(DesktopRoomConversationView, {
+                  dispatcher: dispatcher, 
+                  localPosterUrl: "sample-img/video-screen-local.png", 
+                  mozLoop: navigator.mozLoop, 
+                  onCallTerminated: function(){}, 
+                  remotePosterUrl: "sample-img/video-screen-remote.png", 
+                  roomState: ROOM_STATES.HAS_PARTICIPANTS, 
+                  roomStore: desktopRoomStoreMedium})
+              )
+            ), 
+
+            React.createElement(FramedExample, {
+              dashed: true, 
+              height: 485, 
+              onContentsRendered: desktopRoomStoreLarge.activeRoomStore.forcedUpdate, 
+              summary: "Desktop room conversation (large)", 
+              width: 646}, 
+              React.createElement("div", {className: "fx-embedded"}, 
+                React.createElement(DesktopRoomConversationView, {
+                  dispatcher: dispatcher, 
+                  localPosterUrl: "sample-img/video-screen-local.png", 
+                  mozLoop: navigator.mozLoop, 
+                  onCallTerminated: function(){}, 
+                  remotePosterUrl: "sample-img/video-screen-remote.png", 
+                  roomState: ROOM_STATES.HAS_PARTICIPANTS, 
+                  roomStore: desktopRoomStoreLarge})
+              )
+            ), 
+
+            React.createElement(FramedExample, {
+              dashed: true, 
+              height: 394, 
+              onContentsRendered: desktopLocalFaceMuteRoomStore.activeRoomStore.forcedUpdate, 
+              summary: "Desktop room conversation local face-mute", 
+              width: 298}, 
               React.createElement("div", {className: "fx-embedded"}, 
                 React.createElement(DesktopRoomConversationView, {
                   dispatcher: dispatcher, 
                   mozLoop: navigator.mozLoop, 
                   onCallTerminated: function(){}, 
                   remotePosterUrl: "sample-img/video-screen-remote.png", 
                   roomStore: desktopLocalFaceMuteRoomStore})
               )
             ), 
 
-            React.createElement(FramedExample, {dashed: true, height: 394, 
+            React.createElement(FramedExample, {dashed: true, 
+                           height: 394, 
+                           onContentsRendered: desktopRemoteFaceMuteRoomStore.activeRoomStore.forcedUpdate, 
                            summary: "Desktop room conversation remote face-mute", 
                            width: 298}, 
               React.createElement("div", {className: "fx-embedded"}, 
                 React.createElement(DesktopRoomConversationView, {
                   dispatcher: dispatcher, 
                   localPosterUrl: "sample-img/video-screen-local.png", 
                   mozLoop: navigator.mozLoop, 
                   onCallTerminated: function(){}, 
+                  remotePosterUrl: "sample-img/video-screen-remote.png", 
                   roomStore: desktopRemoteFaceMuteRoomStore})
               )
             )
           ), 
 
           React.createElement(Section, {name: "StandaloneRoomView"}, 
             React.createElement(FramedExample, {cssClass: "standalone", 
                            dashed: true, 
@@ -1076,18 +1244,17 @@
                scrollbars to appear */
                React.createElement("div", {className: "standalone overflow-hidden"}, 
                   React.createElement(StandaloneRoomView, {
                     activeRoomStore: loadingRemoteLoadingScreenStore, 
                     dispatcher: dispatcher, 
                     isFirefox: true, 
                     localPosterUrl: "sample-img/video-screen-local.png", 
                     remotePosterUrl: "sample-img/video-screen-remote.png", 
-                    roomState: ROOM_STATES.HAS_PARTICIPANTS, 
-                    screenSharePosterUrl: "sample-img/video-screen-baz.png"})
+                    roomState: ROOM_STATES.HAS_PARTICIPANTS})
                 )
             ), 
 
             React.createElement(FramedExample, {
               cssClass: "standalone", 
               dashed: true, 
               height: 660, 
               onContentsRendered: loadingScreenSharingRoomStore.forcedUpdate, 
@@ -1097,18 +1264,17 @@
                scrollbars to appear */
                React.createElement("div", {className: "standalone overflow-hidden"}, 
                   React.createElement(StandaloneRoomView, {
                     activeRoomStore: loadingScreenSharingRoomStore, 
                     dispatcher: dispatcher, 
                     isFirefox: true, 
                     localPosterUrl: "sample-img/video-screen-local.png", 
                     remotePosterUrl: "sample-img/video-screen-remote.png", 
-                    roomState: ROOM_STATES.HAS_PARTICIPANTS, 
-                    screenSharePosterUrl: "sample-img/video-screen-baz.png"})
+                    roomState: ROOM_STATES.HAS_PARTICIPANTS})
                 )
             ), 
 
             React.createElement(FramedExample, {
               cssClass: "standalone", 
               dashed: true, 
               height: 660, 
               onContentsRendered: updatingSharingRoomStore.forcedUpdate, 
@@ -1166,40 +1332,40 @@
             )
           ), 
 
           React.createElement(Section, {name: "StandaloneRoomView (Mobile)"}, 
             React.createElement(FramedExample, {
               cssClass: "standalone", 
               dashed: true, 
               height: 480, 
-              onContentsRendered: updatingActiveRoomStore.forcedUpdate, 
+              onContentsRendered: updatingMobileActiveRoomStore.forcedUpdate, 
               summary: "Standalone room conversation (has-participants, 600x480)", 
               width: 600}, 
                 React.createElement("div", {className: "standalone"}, 
                   React.createElement(StandaloneRoomView, {
-                    activeRoomStore: updatingActiveRoomStore, 
+                    activeRoomStore: updatingMobileActiveRoomStore, 
                     dispatcher: dispatcher, 
                     isFirefox: true, 
                     localPosterUrl: "sample-img/video-screen-local.png", 
                     remotePosterUrl: "sample-img/video-screen-remote.png", 
                     roomState: ROOM_STATES.HAS_PARTICIPANTS})
                 )
             ), 
 
             React.createElement(FramedExample, {
               cssClass: "standalone", 
               dashed: true, 
               height: 480, 
-              onContentsRendered: updatingSharingRoomStore.forcedUpdate, 
+              onContentsRendered: updatingSharingRoomMobileStore.forcedUpdate, 
               summary: "Standalone room convo (has-participants, receivingScreenShare, 600x480)", 
               width: 600}, 
                 React.createElement("div", {className: "standalone", cssClass: "standalone"}, 
                   React.createElement(StandaloneRoomView, {
-                    activeRoomStore: updatingSharingRoomStore, 
+                    activeRoomStore: updatingSharingRoomMobileStore, 
                     dispatcher: dispatcher, 
                     isFirefox: true, 
                     localPosterUrl: "sample-img/video-screen-local.png", 
                     remotePosterUrl: "sample-img/video-screen-remote.png", 
                     roomState: ROOM_STATES.HAS_PARTICIPANTS, 
                     screenSharePosterUrl: "sample-img/video-screen-terminal.png"})
                 )
             )
@@ -1277,17 +1443,17 @@
         setTimeout(waitForQueuedFrames, 500);
         return;
       }
       // Put the title back, in case views changed it.
       document.title = "Loop UI Components Showcase";
 
       // This simulates the mocha layout for errors which means we can run
       // this alongside our other unit tests but use the same harness.
-      var expectedWarningsCount = 23;
+      var expectedWarningsCount = 18;
       var warningsMismatch = caughtWarnings.length !== expectedWarningsCount;
       if (uncaughtError || warningsMismatch) {
         $("#results").append("<div class='failures'><em>" +
           ((uncaughtError && warningsMismatch) ? 2 : 1) + "</em></div>");
         if (warningsMismatch) {
           $("#results").append("<li class='test fail'>" +
             "<h2>Unexpected number of warnings detected in UI-Showcase</h2>" +
             "<pre class='error'>Got: " + caughtWarnings.length + "\n" +
--- a/browser/components/loop/ui/ui-showcase.jsx
+++ b/browser/components/loop/ui/ui-showcase.jsx
@@ -70,23 +70,35 @@
     }
     window.removeEventListener(eventName, func);
   };
 
   loop.shared.mixins.setRootObject(rootObject);
 
   var dispatcher = new loop.Dispatcher();
 
-  var mockSDK = _.extend({
+  var MockSDK = function() {
+    dispatcher.register(this, [
+      "setupStreamElements"
+    ]);
+  };
+
+  MockSDK.prototype = {
+    setupStreamElements: function() {
+      // Dummy function to stop warnings.
+    },
+
     sendTextChatMessage: function(message) {
       dispatcher.dispatch(new loop.shared.actions.ReceivedTextChatMessage({
         message: message.message
       }));
     }
-  }, Backbone.Events);
+  };
+
+  var mockSDK = new MockSDK();
 
   /**
    * Every view that uses an activeRoomStore needs its own; if they shared
    * an active store, they'd interfere with each other.
    *
    * @param options
    * @returns {loop.store.ActiveRoomStore}
    */
@@ -111,17 +123,16 @@
       remoteVideoEnabled: options.remoteVideoEnabled,
       roomName: "A Very Long Conversation Name",
       roomState: options.roomState,
       used: !!options.roomUsed,
       videoMuted: !!options.videoMuted
     });
 
     store.forcedUpdate = function forcedUpdate(contentWindow) {
-
       // Since this is called by setTimeout, we don't want to lose any
       // exceptions if there's a problem and we need to debug, so...
       try {
         // the dimensions here are taken from the poster images that we're
         // using, since they give the <video> elements their initial intrinsic
         // size.  This ensures that the right aspect ratios are calculated.
         // These are forced to 640x480, because it makes it visually easy to
         // validate that the showcase looks like the real app on a chine
@@ -131,16 +142,27 @@
             camera: {height: 480, orientation: 0, width: 640}
           },
           mediaConnected: options.mediaConnected,
           receivingScreenShare: !!options.receivingScreenShare,
           remoteVideoDimensions: {
             camera: {height: 480, orientation: 0, width: 640}
           },
           remoteVideoEnabled: options.remoteVideoEnabled,
+          // Override the matchMedia, this is so that the correct version is
+          // used for the frame.
+          //
+          // Currently, we use an icky hack, and the showcase conspires with
+          // react-frame-component to set iframe.contentWindow.matchMedia onto
+          // the store. Once React context matures a bit (somewhere between
+          // 0.14 and 1.0, apparently):
+          //
+          // https://facebook.github.io/react/blog/2015/02/24/streamlining-react-elements.html#solution-make-context-parent-based-instead-of-owner-based
+          //
+          // we should be able to use those to clean this up.
           matchMedia: contentWindow.matchMedia.bind(contentWindow),
           roomState: options.roomState,
           videoMuted: !!options.videoMuted
         };
 
         if (options.receivingScreenShare) {
           // Note that the image we're using had to be scaled a bit, and
           // it still ended up a bit narrower than the live thing that
@@ -180,38 +202,49 @@
     mediaConnected: false,
     roomState: ROOM_STATES.READY
   });
 
   var updatingActiveRoomStore = makeActiveRoomStore({
     roomState: ROOM_STATES.HAS_PARTICIPANTS
   });
 
+  var updatingMobileActiveRoomStore = makeActiveRoomStore({
+    roomState: ROOM_STATES.HAS_PARTICIPANTS
+  });
+
   var localFaceMuteRoomStore = makeActiveRoomStore({
     roomState: ROOM_STATES.HAS_PARTICIPANTS,
     videoMuted: true
   });
 
   var remoteFaceMuteRoomStore = makeActiveRoomStore({
     roomState: ROOM_STATES.HAS_PARTICIPANTS,
     remoteVideoEnabled: false,
     mediaConnected: true
   });
 
   var updatingSharingRoomStore = makeActiveRoomStore({
     roomState: ROOM_STATES.HAS_PARTICIPANTS,
     receivingScreenShare: true
   });
 
+  var updatingSharingRoomMobileStore = makeActiveRoomStore({
+    roomState: ROOM_STATES.HAS_PARTICIPANTS,
+    receivingScreenShare: true
+  });
+
   var loadingRemoteLoadingScreenStore = makeActiveRoomStore({
     mediaConnected: false,
+    receivingScreenShare: true,
     roomState: ROOM_STATES.HAS_PARTICIPANTS,
     remoteSrcVideoObject: false
   });
   var loadingScreenSharingRoomStore = makeActiveRoomStore({
+    receivingScreenShare: true,
     roomState: ROOM_STATES.HAS_PARTICIPANTS
   });
 
   /* Set up the stores for pending screen sharing */
   loadingScreenSharingRoomStore.receivingScreenShare({
     receiving: true,
     srcVideoObject: false
   });
@@ -229,17 +262,20 @@
   });
 
   var endedRoomStore = makeActiveRoomStore({
     roomState: ROOM_STATES.ENDED,
     roomUsed: true
   });
 
   var invitationRoomStore = new loop.store.RoomStore(dispatcher, {
-    mozLoop: navigator.mozLoop
+    mozLoop: navigator.mozLoop,
+    activeRoomStore: makeActiveRoomStore({
+      roomState: ROOM_STATES.INIT
+    })
   });
 
   var roomStore = new loop.store.RoomStore(dispatcher, {
     mozLoop: navigator.mozLoop,
     activeRoomStore: makeActiveRoomStore({
       roomState: ROOM_STATES.HAS_PARTICIPANTS
     })
   });
@@ -248,16 +284,30 @@
     mozLoop: navigator.mozLoop,
     activeRoomStore: makeActiveRoomStore({
       roomState: ROOM_STATES.HAS_PARTICIPANTS,
       mediaConnected: false,
       remoteSrcVideoObject: false
     })
   });
 
+  var desktopRoomStoreMedium = new loop.store.RoomStore(dispatcher, {
+    mozLoop: navigator.mozLoop,
+    activeRoomStore: makeActiveRoomStore({
+      roomState: ROOM_STATES.HAS_PARTICIPANTS
+    })
+  });
+
+  var desktopRoomStoreLarge = new loop.store.RoomStore(dispatcher, {
+    mozLoop: navigator.mozLoop,
+    activeRoomStore: makeActiveRoomStore({
+      roomState: ROOM_STATES.HAS_PARTICIPANTS
+    })
+  });
+
   var desktopLocalFaceMuteActiveRoomStore = makeActiveRoomStore({
     roomState: ROOM_STATES.HAS_PARTICIPANTS,
     videoMuted: true
   });
   var desktopLocalFaceMuteRoomStore = new loop.store.RoomStore(dispatcher, {
     mozLoop: navigator.mozLoop,
     activeRoomStore: desktopLocalFaceMuteActiveRoomStore
   });
@@ -267,25 +317,69 @@
     remoteVideoEnabled: false,
     mediaConnected: true
   });
   var desktopRemoteFaceMuteRoomStore = new loop.store.RoomStore(dispatcher, {
     mozLoop: navigator.mozLoop,
     activeRoomStore: desktopRemoteFaceMuteActiveRoomStore
   });
 
-  var conversationStore = new loop.store.ConversationStore(dispatcher, {
-    client: {},
-    mozLoop: navigator.mozLoop,
-    sdkDriver: mockSDK
-  });
   var textChatStore = new loop.store.TextChatStore(dispatcher, {
     sdkDriver: mockSDK
   });
 
+  /**
+   * Every view that uses an conversationStore needs its own; if they shared
+   * a conversation store, they'd interfere with each other.
+   *
+   * @param options
+   * @returns {loop.store.ConversationStore}
+   */
+  function makeConversationStore() {
+    var roomDispatcher = new loop.Dispatcher();
+
+    var store = new loop.store.ConversationStore(dispatcher, {
+      client: {},
+      mozLoop: navigator.mozLoop,
+      sdkDriver: mockSDK
+    });
+
+    store.forcedUpdate = function forcedUpdate(contentWindow) {
+      // Since this is called by setTimeout, we don't want to lose any
+      // exceptions if there's a problem and we need to debug, so...
+      try {
+        var newStoreState = {
+          // Override the matchMedia, this is so that the correct version is
+          // used for the frame.
+          //
+          // Currently, we use an icky hack, and the showcase conspires with
+          // react-frame-component to set iframe.contentWindow.matchMedia onto
+          // the store. Once React context matures a bit (somewhere between
+          // 0.14 and 1.0, apparently):
+          //
+          // https://facebook.github.io/react/blog/2015/02/24/streamlining-react-elements.html#solution-make-context-parent-based-instead-of-owner-based
+          //
+          // we should be able to use those to clean this up.
+          matchMedia: contentWindow.matchMedia.bind(contentWindow)
+        };
+
+        store.setStoreState(newStoreState);
+      } catch (ex) {
+        console.error("exception in forcedUpdate:", ex);
+      }
+    };
+
+    return store;
+  }
+
+  var conversationStores = [];
+  for (var index = 0; index < 5; index++) {
+    conversationStores[index] = makeConversationStore();
+  }
+
   // Update the text chat store with the room info.
   textChatStore.updateRoomInfo(new sharedActions.UpdateRoomInfo({
     roomName: "A Very Long Conversation Name",
     roomOwner: "fake",
     roomUrl: "http://showcase",
     urls: [{
       description: "A wonderful page!",
       location: "http://wonderful.invalid"
@@ -336,17 +430,17 @@
   dispatcher.dispatch(new sharedActions.SendTextChatMessage({
     contentType: loop.store.CHAT_CONTENT_TYPES.TEXT,
     message: "Cool",
     sentTimestamp: "2015-06-23T22:27:45.590Z"
   }));
 
   loop.store.StoreMixin.register({
     activeRoomStore: activeRoomStore,
-    conversationStore: conversationStore,
+    conversationStore: conversationStores[0],
     textChatStore: textChatStore
   });
 
   // Local mocks
 
   var mockMozLoopRooms = _.extend({}, navigator.mozLoop);
 
   var mockContact = {
@@ -355,24 +449,16 @@
       value: "smith@invalid.com"
     }]
   };
 
   var mockClient = {
     requestCallUrlInfo: noop
   };
 
-  var mockConversationModel = new loop.shared.models.ConversationModel({
-    callerId: "Mrs Jones",
-    urlCreationDate: (new Date() / 1000).toString()
-  }, {
-    sdk: mockSDK
-  });
-  mockConversationModel.startSession = noop;
-
   var mockWebSocket = new loop.CallConnectionWebSocket({
     url: "fake",
     callId: "fakeId",
     websocketToken: "fakeToken"
   });
 
   var notifications = new loop.shared.models.NotificationCollection();
   var errNotifications = new loop.shared.models.NotificationCollection();
@@ -758,93 +844,128 @@
 
           <Section name="CallFailedView">
             <Example dashed={true}
                      style={{width: "300px", height: "272px"}}
                      summary="Call Failed - Incoming">
               <div className="fx-embedded">
                 <CallFailedView dispatcher={dispatcher}
                                 outgoing={false}
-                                store={conversationStore} />
+                                store={conversationStores[0]} />
               </div>
             </Example>
             <Example dashed={true}
                      style={{width: "300px", height: "272px"}}
                      summary="Call Failed - Outgoing">
               <div className="fx-embedded">
                 <CallFailedView dispatcher={dispatcher}
                                 outgoing={true}
-                                store={conversationStore} />
+                                store={conversationStores[1]} />
               </div>
             </Example>
             <Example dashed={true}
                      style={{width: "300px", height: "272px"}}
                      summary="Call Failed — with call URL error">
               <div className="fx-embedded">
                 <CallFailedView dispatcher={dispatcher} emailLinkError={true}
                                 outgoing={true}
-                                store={conversationStore} />
+                                store={conversationStores[0]} />
               </div>
             </Example>
           </Section>
 
           <Section name="OngoingConversationView">
-            <FramedExample height={254}
-                           summary="Desktop ongoing conversation window"
-                           width={298}>
+            <FramedExample
+              dashed={true}
+              height={394}
+              onContentsRendered={conversationStores[0].forcedUpdate}
+              summary="Desktop ongoing conversation window"
+              width={298}>
               <div className="fx-embedded">
                 <OngoingConversationView
                   audio={{enabled: true}}
+                  conversationStore={conversationStores[0]}
                   dispatcher={dispatcher}
                   localPosterUrl="sample-img/video-screen-local.png"
                   mediaConnected={true}
                   remotePosterUrl="sample-img/video-screen-remote.png"
                   remoteVideoEnabled={true}
                   video={{enabled: true}} />
               </div>
             </FramedExample>
 
-            <FramedExample height={600}
-                           summary="Desktop ongoing conversation window large"
-                           width={800}>
-                <div className="fx-embedded">
-                  <OngoingConversationView
-                    audio={{enabled: true}}
-                    dispatcher={dispatcher}
-                    localPosterUrl="sample-img/video-screen-local.png"
-                    mediaConnected={true}
-                    remotePosterUrl="sample-img/video-screen-remote.png"
-                    remoteVideoEnabled={true}
-                    video={{enabled: true}} />
-                </div>
+            <FramedExample
+              dashed={true}
+              height={400}
+              onContentsRendered={conversationStores[1].forcedUpdate}
+              summary="Desktop ongoing conversation window (medium)"
+              width={600}>
+              <div className="fx-embedded">
+                <OngoingConversationView
+                  audio={{enabled: true}}
+                  conversationStore={conversationStores[1]}
+                  dispatcher={dispatcher}
+                  localPosterUrl="sample-img/video-screen-local.png"
+                  mediaConnected={true}
+                  remotePosterUrl="sample-img/video-screen-remote.png"
+                  remoteVideoEnabled={true}
+                  video={{enabled: true}} />
+              </div>
             </FramedExample>
 
-            <FramedExample height={254}
+            <FramedExample
+              height={600}
+              onContentsRendered={conversationStores[2].forcedUpdate}
+              summary="Desktop ongoing conversation window (large)"
+              width={800}>
+              <div className="fx-embedded">
+                <OngoingConversationView
+                  audio={{enabled: true}}
+                  conversationStore={conversationStores[2]}
+                  dispatcher={dispatcher}
+                  localPosterUrl="sample-img/video-screen-local.png"
+                  mediaConnected={true}
+                  remotePosterUrl="sample-img/video-screen-remote.png"
+                  remoteVideoEnabled={true}
+                  video={{enabled: true}} />
+              </div>
+            </FramedExample>
+
+            <FramedExample
+              dashed={true}
+              height={394}
+              onContentsRendered={conversationStores[3].forcedUpdate}
               summary="Desktop ongoing conversation window - local face mute"
               width={298} >
               <div className="fx-embedded">
                 <OngoingConversationView
                   audio={{enabled: true}}
+                  conversationStore={conversationStores[3]}
                   dispatcher={dispatcher}
+                  localPosterUrl="sample-img/video-screen-local.png"
                   mediaConnected={true}
                   remotePosterUrl="sample-img/video-screen-remote.png"
                   remoteVideoEnabled={true}
                   video={{enabled: false}} />
               </div>
             </FramedExample>
 
-            <FramedExample height={254}
+            <FramedExample
+              dashed={true} height={394}
+              onContentsRendered={conversationStores[4].forcedUpdate}
               summary="Desktop ongoing conversation window - remote face mute"
               width={298} >
               <div className="fx-embedded">
                 <OngoingConversationView
                   audio={{enabled: true}}
+                  conversationStore={conversationStores[4]}
                   dispatcher={dispatcher}
                   localPosterUrl="sample-img/video-screen-local.png"
                   mediaConnected={true}
+                  remotePosterUrl="sample-img/video-screen-remote.png"
                   remoteVideoEnabled={false}
                   video={{enabled: true}} />
               </div>
             </FramedExample>
 
           </Section>
 
           <Section name="FeedbackView">
@@ -889,86 +1010,133 @@
               <div className="standalone">
                 <UnsupportedDeviceView platform="ios"/>
               </div>
             </Example>
           </Section>
 
           <Section name="DesktopRoomConversationView">
             <FramedExample
-              height={254}
+              height={398}
+              onContentsRendered={invitationRoomStore.activeRoomStore.forcedUpdate}
               summary="Desktop room conversation (invitation, text-chat inclusion/scrollbars don't happen in real client)"
               width={298}>
               <div className="fx-embedded">
                 <DesktopRoomConversationView
                   dispatcher={dispatcher}
                   localPosterUrl="sample-img/video-screen-local.png"
                   mozLoop={navigator.mozLoop}
                   onCallTerminated={function(){}}
                   roomState={ROOM_STATES.INIT}
                   roomStore={invitationRoomStore} />
               </div>
             </FramedExample>
 
             <FramedExample
               dashed={true}
               height={394}
+              onContentsRendered={desktopRoomStoreLoading.activeRoomStore.forcedUpdate}
               summary="Desktop room conversation (loading)"
               width={298}>
               {/* Hide scrollbars here. Rotating loading div overflows and causes
                scrollbars to appear */}
               <div className="fx-embedded overflow-hidden">
                 <DesktopRoomConversationView
                   dispatcher={dispatcher}
                   localPosterUrl="sample-img/video-screen-local.png"
                   mozLoop={navigator.mozLoop}
                   onCallTerminated={function(){}}
                   remotePosterUrl="sample-img/video-screen-remote.png"
                   roomState={ROOM_STATES.HAS_PARTICIPANTS}
                   roomStore={desktopRoomStoreLoading} />
               </div>
             </FramedExample>
 
-            <FramedExample height={254}
-                           summary="Desktop room conversation">
+            <FramedExample
+              dashed={true}
+              height={394}
+              onContentsRendered={roomStore.activeRoomStore.forcedUpdate}
+              summary="Desktop room conversation"
+              width={298}>
               <div className="fx-embedded">
                 <DesktopRoomConversationView
                   dispatcher={dispatcher}
                   localPosterUrl="sample-img/video-screen-local.png"
                   mozLoop={navigator.mozLoop}
                   onCallTerminated={function(){}}
                   remotePosterUrl="sample-img/video-screen-remote.png"
                   roomState={ROOM_STATES.HAS_PARTICIPANTS}
                   roomStore={roomStore} />
               </div>
             </FramedExample>
 
-            <FramedExample dashed={true}
-                           height={394}
-                           summary="Desktop room conversation local face-mute"
-                           width={298}>
+            <FramedExample
+              dashed={true}
+              height={482}
+              onContentsRendered={desktopRoomStoreMedium.activeRoomStore.forcedUpdate}
+              summary="Desktop room conversation (medium)"
+              width={602}>
+              <div className="fx-embedded">
+                <DesktopRoomConversationView
+                  dispatcher={dispatcher}
+                  localPosterUrl="sample-img/video-screen-local.png"
+                  mozLoop={navigator.mozLoop}
+                  onCallTerminated={function(){}}
+                  remotePosterUrl="sample-img/video-screen-remote.png"
+                  roomState={ROOM_STATES.HAS_PARTICIPANTS}
+                  roomStore={desktopRoomStoreMedium} />
+              </div>
+            </FramedExample>
+
+            <FramedExample
+              dashed={true}
+              height={485}
+              onContentsRendered={desktopRoomStoreLarge.activeRoomStore.forcedUpdate}
+              summary="Desktop room conversation (large)"
+              width={646}>
+              <div className="fx-embedded">
+                <DesktopRoomConversationView
+                  dispatcher={dispatcher}
+                  localPosterUrl="sample-img/video-screen-local.png"
+                  mozLoop={navigator.mozLoop}
+                  onCallTerminated={function(){}}
+                  remotePosterUrl="sample-img/video-screen-remote.png"
+                  roomState={ROOM_STATES.HAS_PARTICIPANTS}
+                  roomStore={desktopRoomStoreLarge} />
+              </div>
+            </FramedExample>
+
+            <FramedExample
+              dashed={true}
+              height={394}
+              onContentsRendered={desktopLocalFaceMuteRoomStore.activeRoomStore.forcedUpdate}
+              summary="Desktop room conversation local face-mute"
+              width={298}>
               <div className="fx-embedded">
                 <DesktopRoomConversationView
                   dispatcher={dispatcher}
                   mozLoop={navigator.mozLoop}
                   onCallTerminated={function(){}}
                   remotePosterUrl="sample-img/video-screen-remote.png"
                   roomStore={desktopLocalFaceMuteRoomStore} />
               </div>
             </FramedExample>
 
-            <FramedExample dashed={true} height={394}
+            <FramedExample dashed={true}
+                           height={394}
+                           onContentsRendered={desktopRemoteFaceMuteRoomStore.activeRoomStore.forcedUpdate}
                            summary="Desktop room conversation remote face-mute"
                            width={298} >
               <div className="fx-embedded">
                 <DesktopRoomConversationView
                   dispatcher={dispatcher}
                   localPosterUrl="sample-img/video-screen-local.png"
                   mozLoop={navigator.mozLoop}
                   onCallTerminated={function(){}}
+                  remotePosterUrl="sample-img/video-screen-remote.png"
                   roomStore={desktopRemoteFaceMuteRoomStore} />
               </div>
             </FramedExample>
           </Section>
 
           <Section name="StandaloneRoomView">
             <FramedExample cssClass="standalone"
                            dashed={true}
@@ -1076,18 +1244,17 @@
                scrollbars to appear */}
                <div className="standalone overflow-hidden">
                   <StandaloneRoomView
                     activeRoomStore={loadingRemoteLoadingScreenStore}
                     dispatcher={dispatcher}
                     isFirefox={true}
                     localPosterUrl="sample-img/video-screen-local.png"
                     remotePosterUrl="sample-img/video-screen-remote.png"
-                    roomState={ROOM_STATES.HAS_PARTICIPANTS}
-                    screenSharePosterUrl="sample-img/video-screen-baz.png" />
+                    roomState={ROOM_STATES.HAS_PARTICIPANTS} />
                 </div>
             </FramedExample>
 
             <FramedExample
               cssClass="standalone"
               dashed={true}
               height={660}
               onContentsRendered={loadingScreenSharingRoomStore.forcedUpdate}
@@ -1097,18 +1264,17 @@
                scrollbars to appear */}
                <div className="standalone overflow-hidden">
                   <StandaloneRoomView
                     activeRoomStore={loadingScreenSharingRoomStore}
                     dispatcher={dispatcher}
                     isFirefox={true}
                     localPosterUrl="sample-img/video-screen-local.png"
                     remotePosterUrl="sample-img/video-screen-remote.png"
-                    roomState={ROOM_STATES.HAS_PARTICIPANTS}
-                    screenSharePosterUrl="sample-img/video-screen-baz.png" />
+                    roomState={ROOM_STATES.HAS_PARTICIPANTS} />
                 </div>
             </FramedExample>
 
             <FramedExample
               cssClass="standalone"
               dashed={true}
               height={660}
               onContentsRendered={updatingSharingRoomStore.forcedUpdate}
@@ -1166,40 +1332,40 @@
             </FramedExample>
           </Section>
 
           <Section name="StandaloneRoomView (Mobile)">
             <FramedExample
               cssClass="standalone"
               dashed={true}
               height={480}
-              onContentsRendered={updatingActiveRoomStore.forcedUpdate}
+              onContentsRendered={updatingMobileActiveRoomStore.forcedUpdate}
               summary="Standalone room conversation (has-participants, 600x480)"
               width={600}>
                 <div className="standalone">
                   <StandaloneRoomView
-                    activeRoomStore={updatingActiveRoomStore}
+                    activeRoomStore={updatingMobileActiveRoomStore}
                     dispatcher={dispatcher}
                     isFirefox={true}
                     localPosterUrl="sample-img/video-screen-local.png"
                     remotePosterUrl="sample-img/video-screen-remote.png"
                     roomState={ROOM_STATES.HAS_PARTICIPANTS} />
                 </div>
             </FramedExample>
 
             <FramedExample
               cssClass="standalone"
               dashed={true}
               height={480}
-              onContentsRendered={updatingSharingRoomStore.forcedUpdate}
+              onContentsRendered={updatingSharingRoomMobileStore.forcedUpdate}
               summary="Standalone room convo (has-participants, receivingScreenShare, 600x480)"
               width={600} >
                 <div className="standalone" cssClass="standalone">
                   <StandaloneRoomView
-                    activeRoomStore={updatingSharingRoomStore}
+                    activeRoomStore={updatingSharingRoomMobileStore}
                     dispatcher={dispatcher}
                     isFirefox={true}
                     localPosterUrl="sample-img/video-screen-local.png"
                     remotePosterUrl="sample-img/video-screen-remote.png"
                     roomState={ROOM_STATES.HAS_PARTICIPANTS}
                     screenSharePosterUrl="sample-img/video-screen-terminal.png" />
                 </div>
             </FramedExample>
@@ -1277,17 +1443,17 @@
         setTimeout(waitForQueuedFrames, 500);
         return;
       }
       // Put the title back, in case views changed it.
       document.title = "Loop UI Components Showcase";
 
       // This simulates the mocha layout for errors which means we can run
       // this alongside our other unit tests but use the same harness.
-      var expectedWarningsCount = 23;
+      var expectedWarningsCount = 18;
       var warningsMismatch = caughtWarnings.length !== expectedWarningsCount;
       if (uncaughtError || warningsMismatch) {
         $("#results").append("<div class='failures'><em>" +
           ((uncaughtError && warningsMismatch) ? 2 : 1) + "</em></div>");
         if (warningsMismatch) {
           $("#results").append("<li class='test fail'>" +
             "<h2>Unexpected number of warnings detected in UI-Showcase</h2>" +
             "<pre class='error'>Got: " + caughtWarnings.length + "\n" +
--- a/browser/devtools/styleinspector/computed-view.js
+++ b/browser/devtools/styleinspector/computed-view.js
@@ -1,15 +1,16 @@
 /* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
 /* vim: set ts=2 et sw=2 tw=80: */
 /* 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/. */
 
-/* globals overlays, StyleInspectorMenu */
+/* globals overlays, StyleInspectorMenu, loader, clipboardHelper,
+  _Iterator, StopIteration */
 
 "use strict";
 
 const {Cc, Ci, Cu} = require("chrome");
 
 const ToolDefinitions = require("main").Tools;
 const {CssLogic} = require("devtools/styleinspector/css-logic");
 const {ELEMENT_STYLE} = require("devtools/server/actors/styles");
@@ -44,90 +45,86 @@ const HTML_NS = "http://www.w3.org/1999/
  *          onBatch {function} Will be called after each batch of iterations,
  *            before yielding to the main loop.
  *          onDone {function} Will be called when iteration is complete.
  *          onCancel {function} Will be called if the process is canceled.
  *          threshold {int} How long to process before yielding, in ms.
  *
  * @constructor
  */
-function UpdateProcess(aWin, aGenerator, aOptions)
-{
+function UpdateProcess(aWin, aGenerator, aOptions) {
   this.win = aWin;
   this.iter = _Iterator(aGenerator);
   this.onItem = aOptions.onItem || function() {};
   this.onBatch = aOptions.onBatch || function() {};
   this.onDone = aOptions.onDone || function() {};
   this.onCancel = aOptions.onCancel || function() {};
   this.threshold = aOptions.threshold || 45;
 
   this.canceled = false;
 }
 
 UpdateProcess.prototype = {
   /**
    * Schedule a new batch on the main loop.
    */
-  schedule: function UP_schedule()
-  {
+  schedule: function() {
     if (this.canceled) {
       return;
     }
     this._timeout = this.win.setTimeout(this._timeoutHandler.bind(this), 0);
   },
 
   /**
    * Cancel the running process.  onItem will not be called again,
    * and onCancel will be called.
    */
-  cancel: function UP_cancel()
-  {
+  cancel: function() {
     if (this._timeout) {
       this.win.clearTimeout(this._timeout);
       this._timeout = 0;
     }
     this.canceled = true;
     this.onCancel();
   },
 
-  _timeoutHandler: function UP_timeoutHandler() {
+  _timeoutHandler: function() {
     this._timeout = null;
     try {
       this._runBatch();
       this.schedule();
     } catch(e) {
       if (e instanceof StopIteration) {
         this.onBatch();
         this.onDone();
         return;
       }
       console.error(e);
       throw e;
     }
   },
 
-  _runBatch: function Y_runBatch()
-  {
+  _runBatch: function() {
     let time = Date.now();
-    while(!this.canceled) {
+    while (!this.canceled) {
       // Continue until iter.next() throws...
       let next = this.iter.next();
       this.onItem(next[1]);
       if ((Date.now() - time) > this.threshold) {
         this.onBatch();
         return;
       }
     }
   }
 };
 
 /**
- * CssComputedView is a panel that manages the display of a table sorted by style.
- * There should be one instance of CssComputedView per style display (of which there
- * will generally only be one).
+ * CssComputedView is a panel that manages the display of a table
+ * sorted by style. There should be one instance of CssComputedView
+ * per style display (of which there will generally only be one).
  *
  * @param {Inspector} inspector toolbox panel
  * @param {Document} document The document that will contain the computed view.
  * @param {PageStyleFront} pageStyle
  *        Front for the page style actor that will be providing
  *        the style information.
  *
  * @constructor
@@ -137,18 +134,18 @@ function CssComputedView(inspector, docu
   this.styleDocument = document;
   this.styleWindow = this.styleDocument.defaultView;
   this.pageStyle = pageStyle;
 
   this.propertyViews = [];
 
   this._outputParser = new OutputParser();
 
-  let chromeReg = Cc["@mozilla.org/chrome/chrome-registry;1"].
-    getService(Ci.nsIXULChromeRegistry);
+  let chromeReg = Cc["@mozilla.org/chrome/chrome-registry;1"]
+    .getService(Ci.nsIXULChromeRegistry);
   this.getRTLAttr = chromeReg.isLocaleRTL("global") ? "rtl" : "ltr";
 
   // Create bound methods.
   this.focusWindow = this.focusWindow.bind(this);
   this._onKeypress = this._onKeypress.bind(this);
   this._onContextMenu = this._onContextMenu.bind(this);
   this._onClick = this._onClick.bind(this);
   this._onCopy = this._onCopy.bind(this);
@@ -206,18 +203,17 @@ function CssComputedView(inspector, docu
   this.highlighters.addToView();
 }
 
 /**
  * Memoized lookup of a l10n string from a string bundle.
  * @param {string} aName The key to lookup.
  * @returns A localized version of the given key.
  */
-CssComputedView.l10n = function CssComputedView_l10n(aName)
-{
+CssComputedView.l10n = function(aName) {
   try {
     return CssComputedView._strings.GetStringFromName(aName);
   } catch (ex) {
     Services.console.logStringMessage("Error reading '" + aName + "'");
     throw new Error("l10n error with " + aName);
   }
 };
 
@@ -246,31 +242,31 @@ CssComputedView.prototype = {
 
   // Number of visible properties
   numVisibleProperties: 0,
 
   setPageStyle: function(pageStyle) {
     this.pageStyle = pageStyle;
   },
 
-  get includeBrowserStyles()
-  {
+  get includeBrowserStyles() {
     return this.includeBrowserStylesCheckbox.checked;
   },
 
   _handlePrefChange: function(event, data) {
     if (this._computed && (data.pref == "devtools.defaultColorUnit" ||
         data.pref == PREF_ORIG_SOURCES)) {
       this.refreshPanel();
     }
   },
 
   /**
-   * Update the view with a new selected element.
-   * The CssComputedView panel will show the style information for the given element.
+   * Update the view with a new selected element. The CssComputedView panel
+   * will show the style information for the given element.
+   *
    * @param {NodeFront} aElement The highlighted node to get styles for.
    * @returns a promise that will be resolved when highlighting is complete.
    */
   selectElement: function(aElement) {
     if (!aElement) {
       this.viewedElement = null;
       this.noResults.hidden = false;
 
@@ -378,30 +374,30 @@ CssComputedView.prototype = {
       value.url = node.href;
     } else {
       return null;
     }
 
     return {type, value};
   },
 
-  _createPropertyViews: function()
-  {
+  _createPropertyViews: function() {
     if (this._createViewsPromise) {
       return this._createViewsPromise;
     }
 
     let deferred = promise.defer();
     this._createViewsPromise = deferred.promise;
 
     this.refreshSourceFilter();
     this.numVisibleProperties = 0;
     let fragment = this.styleDocument.createDocumentFragment();
 
-    this._createViewsProcess = new UpdateProcess(this.styleWindow, CssComputedView.propertyNames, {
+    this._createViewsProcess = new UpdateProcess(
+      this.styleWindow, CssComputedView.propertyNames, {
       onItem: (aPropertyName) => {
         // Per-item callback.
         let propView = new PropertyView(this, aPropertyName);
         fragment.appendChild(propView.buildMain());
         fragment.appendChild(propView.buildSelectorContainer());
 
         if (propView.visible) {
           this.numVisibleProperties++;
@@ -421,39 +417,38 @@ CssComputedView.prototype = {
 
     this._createViewsProcess.schedule();
     return deferred.promise;
   },
 
   /**
    * Refresh the panel content.
    */
-  refreshPanel: function CssComputedView_refreshPanel()
-  {
+  refreshPanel: function() {
     if (!this.viewedElement) {
       return promise.resolve();
     }
 
     // Capture the current viewed element to return from the promise handler
     // early if it changed
     let viewedElement = this.viewedElement;
 
     return promise.all([
       this._createPropertyViews(),
       this.pageStyle.getComputed(this.viewedElement, {
         filter: this._sourceFilter,
         onlyMatched: !this.includeBrowserStyles,
         markMatched: true
       })
-    ]).then(([createViews, computed]) => {
+    ]).then(([, computed]) => {
       if (viewedElement !== this.viewedElement) {
-        return;
+        return promise.resolve();
       }
 
-      this._matchedProperties = new Set;
+      this._matchedProperties = new Set();
       for (let name in computed) {
         if (computed[name].matched) {
           this._matchedProperties.add(name);
         }
       }
       this._computed = computed;
 
       if (this._refreshProcess) {
@@ -464,17 +459,18 @@ CssComputedView.prototype = {
 
       // Reset visible property count
       this.numVisibleProperties = 0;
 
       // Reset zebra striping.
       this._darkStripe = true;
 
       let deferred = promise.defer();
-      this._refreshProcess = new UpdateProcess(this.styleWindow, this.propertyViews, {
+      this._refreshProcess = new UpdateProcess(
+        this.styleWindow, this.propertyViews, {
         onItem: (aPropView) => {
           aPropView.refresh();
         },
         onDone: () => {
           this._refreshProcess = null;
           this.noResults.hidden = this.numVisibleProperties > 0;
 
           if (this.searchField.value.length > 0 && !this.numVisibleProperties) {
@@ -506,18 +502,17 @@ CssComputedView.prototype = {
     }
   },
 
   /**
    * Called when the user enters a search term in the filter style search box.
    *
    * @param {Event} aEvent the DOM Event object.
    */
-  _onFilterStyles: function(aEvent)
-  {
+  _onFilterStyles: function(aEvent) {
     let win = this.styleWindow;
 
     if (this._filterChangedTimeout) {
       win.clearTimeout(this._filterChangedTimeout);
     }
 
     let filterTimeout = (this.searchField.value.length > 0)
       ? FILTER_CHANGED_TIMEOUT : 0;
@@ -575,49 +570,45 @@ CssComputedView.prototype = {
     return false;
   },
 
   /**
    * The change event handler for the includeBrowserStyles checkbox.
    *
    * @param {Event} aEvent the DOM Event object.
    */
-  _onIncludeBrowserStyles: function(aEvent)
-  {
+  _onIncludeBrowserStyles: function(aEvent) {
     this.refreshSourceFilter();
     this.refreshPanel();
   },
 
   /**
    * When includeBrowserStylesCheckbox.checked is false we only display
    * properties that have matched selectors and have been included by the
    * document or one of thedocument's stylesheets. If .checked is false we
    * display all properties including those that come from UA stylesheets.
    */
-  refreshSourceFilter: function CssComputedView_setSourceFilter()
-  {
+  refreshSourceFilter: function() {
     this._matchedProperties = null;
     this._sourceFilter = this.includeBrowserStyles ?
                                  CssLogic.FILTER.UA :
                                  CssLogic.FILTER.USER;
   },
 
-  _onSourcePrefChanged: function CssComputedView__onSourcePrefChanged()
-  {
+  _onSourcePrefChanged: function() {
     for (let propView of this.propertyViews) {
       propView.updateSourceLinks();
     }
     this.inspector.emit("computed-view-sourcelinks-updated");
   },
 
   /**
    * The CSS as displayed by the UI.
    */
-  createStyleViews: function CssComputedView_createStyleViews()
-  {
+  createStyleViews: function() {
     if (CssComputedView.propertyNames) {
       return;
     }
 
     CssComputedView.propertyNames = [];
 
     // Here we build and cache a list of css properties supported by the browser
     // We could use any element but let's use the main document's root element
@@ -636,41 +627,39 @@ CssComputedView.prototype = {
     }
 
     CssComputedView.propertyNames.sort();
     CssComputedView.propertyNames.push.apply(CssComputedView.propertyNames,
       mozProps.sort());
 
     this._createPropertyViews().then(null, e => {
       if (!this._isDestroyed) {
-        console.warn("The creation of property views was cancelled because the " +
-          "computed-view was destroyed before it was done creating views");
+        console.warn("The creation of property views was cancelled because " +
+          "the computed-view was destroyed before it was done creating views");
       } else {
         console.error(e);
       }
     });
   },
 
   /**
    * Get a set of properties that have matched selectors.
    *
    * @return {Set} If a property name is in the set, it has matching selectors.
    */
-  get matchedProperties()
-  {
-    return this._matchedProperties || new Set;
+  get matchedProperties() {
+    return this._matchedProperties || new Set();
   },
 
   /**
    * Focus the window on mousedown.
    *
-   * @param aEvent The event object
+   * @param event The event object
    */
-  focusWindow: function(aEvent)
-  {
+  focusWindow: function(event) {
     let win = this.styleDocument.defaultView;
     win.focus();
   },
 
   /**
    * Context menu handler.
    */
   _onContextMenu: function(event) {
@@ -702,18 +691,18 @@ CssComputedView.prototype = {
   /**
    * Copy the current selection to the clipboard
    */
   copySelection: function() {
     try {
       let win = this.styleDocument.defaultView;
       let text = win.getSelection().toString().trim();
 
-      // Tidy up block headings by moving CSS property names and their values onto
-      // the same line and inserting a colon between them.
+      // Tidy up block headings by moving CSS property names and their
+      // values onto the same line and inserting a colon between them.
       let textArray = text.split(/[\r\n]+/);
       let result = "";
 
       // Parse text array to output string.
       if (textArray.length > 1) {
         for (let prop of textArray) {
           if (CssComputedView.propertyNames.indexOf(prop) !== -1) {
             // Property name
@@ -732,18 +721,17 @@ CssComputedView.prototype = {
     } catch(e) {
       console.error(e);
     }
   },
 
   /**
    * Destructor for CssComputedView.
    */
-  destroy: function CssComputedView_destroy()
-  {
+  destroy: function() {
     this.viewedElement = null;
     this._outputParser = null;
 
     gDevTools.off("pref-changed", this._handlePrefChange);
 
     this._prefObserver.off(PREF_ORIG_SOURCES, this._onSourcePrefChanged);
     this._prefObserver.destroy();
 
@@ -814,18 +802,17 @@ PropertyInfo.prototype = {
 /**
  * A container to give easy access to property data from the template engine.
  *
  * @constructor
  * @param {CssComputedView} aTree the CssComputedView instance we are working with.
  * @param {string} aName the CSS property name for which this PropertyView
  * instance will render the rules.
  */
-function PropertyView(aTree, aName)
-{
+function PropertyView(aTree, aName) {
   this.tree = aTree;
   this.name = aName;
   this.getRTLAttr = aTree.getRTLAttr;
 
   this.link = "https://developer.mozilla.org/CSS/" + aName;
 
   this._propertyInfo = new PropertyInfo(aTree, aName);
 }
@@ -859,42 +846,38 @@ PropertyView.prototype = {
   prevViewedElement: null,
 
   /**
    * Get the computed style for the current property.
    *
    * @return {string} the computed style for the current property of the
    * currently highlighted element.
    */
-  get value()
-  {
+  get value() {
     return this.propertyInfo.value;
   },
 
   /**
    * An easy way to access the CssPropertyInfo behind this PropertyView.
    */
-  get propertyInfo()
-  {
+  get propertyInfo() {
     return this._propertyInfo;
   },
 
   /**
    * Does the property have any matched selectors?
    */
-  get hasMatchedSelectors()
-  {
+  get hasMatchedSelectors() {
     return this.tree.matchedProperties.has(this.name);
   },
 
   /**
    * Should this property be visible?
    */
-  get visible()
-  {
+  get visible() {
     if (!this.tree.viewedElement) {
       return false;
     }
 
     if (!this.tree.includeBrowserStyles && !this.hasMatchedSelectors) {
       return false;
     }
 
@@ -908,45 +891,42 @@ PropertyView.prototype = {
 
     return true;
   },
 
   /**
    * Returns the className that should be assigned to the propertyView.
    * @return string
    */
-  get propertyHeaderClassName()
-  {
+  get propertyHeaderClassName() {
     if (this.visible) {
       let isDark = this.tree._darkStripe = !this.tree._darkStripe;
       return isDark ? "property-view row-striped" : "property-view";
     }
     return "property-view-hidden";
   },
 
   /**
    * Returns the className that should be assigned to the propertyView content
    * container.
    * @return string
    */
-  get propertyContentClassName()
-  {
+  get propertyContentClassName() {
     if (this.visible) {
       let isDark = this.tree._darkStripe;
       return isDark ? "property-content row-striped" : "property-content";
     }
     return "property-content-hidden";
   },
 
   /**
    * Build the markup for on computed style
    * @return Element
    */
-  buildMain: function PropertyView_buildMain()
-  {
+  buildMain: function() {
     let doc = this.tree.styleDocument;
 
     // Build the container element
     this.onMatchedToggle = this.onMatchedToggle.bind(this);
     this.element = doc.createElementNS(HTML_NS, "div");
     this.element.setAttribute("class", this.propertyHeaderClassName);
     this.element.addEventListener("dblclick", this.onMatchedToggle, false);
 
@@ -993,33 +973,31 @@ PropertyView.prototype = {
     this.valueNode.setAttribute("dir", "ltr");
     // Make it hand over the focus to the container
     this.valueNode.addEventListener("click", this.onFocus, false);
     this.element.appendChild(this.valueNode);
 
     return this.element;
   },
 
-  buildSelectorContainer: function PropertyView_buildSelectorContainer()
-  {
+  buildSelectorContainer: function() {
     let doc = this.tree.styleDocument;
     let element = doc.createElementNS(HTML_NS, "div");
     element.setAttribute("class", this.propertyContentClassName);
     this.matchedSelectorsContainer = doc.createElementNS(HTML_NS, "div");
     this.matchedSelectorsContainer.setAttribute("class", "matchedselectors");
     element.appendChild(this.matchedSelectorsContainer);
 
     return element;
   },
 
   /**
    * Refresh the panel's CSS property value.
    */
-  refresh: function PropertyView_refresh()
-  {
+  refresh: function() {
     this.element.className = this.propertyHeaderClassName;
     this.element.nextElementSibling.className = this.propertyContentClassName;
 
     if (this.prevViewedElement != this.tree.viewedElement) {
       this._matchedSelectorViews = null;
       this.prevViewedElement = this.tree.viewedElement;
     }
 
@@ -1046,50 +1024,50 @@ PropertyView.prototype = {
     this.valueNode.appendChild(frag);
 
     this.refreshMatchedSelectors();
   },
 
   /**
    * Refresh the panel matched rules.
    */
-  refreshMatchedSelectors: function PropertyView_refreshMatchedSelectors()
-  {
+  refreshMatchedSelectors: function() {
     let hasMatchedSelectors = this.hasMatchedSelectors;
     this.matchedSelectorsContainer.parentNode.hidden = !hasMatchedSelectors;
 
     if (hasMatchedSelectors) {
       this.matchedExpander.classList.add("expandable");
     } else {
       this.matchedExpander.classList.remove("expandable");
     }
 
     if (this.matchedExpanded && hasMatchedSelectors) {
-      return this.tree.pageStyle.getMatchedSelectors(this.tree.viewedElement, this.name).then(matched => {
-        if (!this.matchedExpanded) {
-          return;
-        }
+      return this.tree.pageStyle
+        .getMatchedSelectors(this.tree.viewedElement, this.name)
+        .then(matched => {
+          if (!this.matchedExpanded) {
+            return promise.resolve(undefined);
+          }
 
-        this._matchedSelectorResponse = matched;
+          this._matchedSelectorResponse = matched;
 
-        return this._buildMatchedSelectors().then(() => {
-          this.matchedExpander.setAttribute("open", "");
-          this.tree.inspector.emit("computed-view-property-expanded");
-        });
-      }).then(null, console.error);
-    } else {
-      this.matchedSelectorsContainer.innerHTML = "";
-      this.matchedExpander.removeAttribute("open");
-      this.tree.inspector.emit("computed-view-property-collapsed");
-      return promise.resolve(undefined);
+          return this._buildMatchedSelectors().then(() => {
+            this.matchedExpander.setAttribute("open", "");
+            this.tree.inspector.emit("computed-view-property-expanded");
+          });
+        }).then(null, console.error);
     }
+
+    this.matchedSelectorsContainer.innerHTML = "";
+    this.matchedExpander.removeAttribute("open");
+    this.tree.inspector.emit("computed-view-property-collapsed");
+    return promise.resolve(undefined);
   },
 
-  get matchedSelectors()
-  {
+  get matchedSelectors() {
     return this._matchedSelectorResponse;
   },
 
   _buildMatchedSelectors: function() {
     let promises = [];
     let frag = this.element.ownerDocument.createDocumentFragment();
 
     for (let selector of this.matchedSelectorViews) {
@@ -1125,76 +1103,73 @@ PropertyView.prototype = {
     this.matchedSelectorsContainer.appendChild(frag);
     return promise.all(promises);
   },
 
   /**
    * Provide access to the matched SelectorViews that we are currently
    * displaying.
    */
-  get matchedSelectorViews()
-  {
+  get matchedSelectorViews() {
     if (!this._matchedSelectorViews) {
       this._matchedSelectorViews = [];
       this._matchedSelectorResponse.forEach(
-        function matchedSelectorViews_convert(aSelectorInfo) {
-          this._matchedSelectorViews.push(new SelectorView(this.tree, aSelectorInfo));
+        function(aSelectorInfo) {
+          let selectorView = new SelectorView(this.tree, aSelectorInfo);
+          this._matchedSelectorViews.push(selectorView);
         }, this);
     }
     return this._matchedSelectorViews;
   },
 
   /**
    * Update all the selector source links to reflect whether we're linking to
    * original sources (e.g. Sass files).
    */
-  updateSourceLinks: function PropertyView_updateSourceLinks()
-  {
+  updateSourceLinks: function() {
     if (!this._matchedSelectorViews) {
       return;
     }
     for (let view of this._matchedSelectorViews) {
       view.updateSourceLink();
     }
   },
 
   /**
    * The action when a user expands matched selectors.
    *
    * @param {Event} aEvent Used to determine the class name of the targets click
    * event.
    */
-  onMatchedToggle: function PropertyView_onMatchedToggle(aEvent)
-  {
+  onMatchedToggle: function(aEvent) {
     if (aEvent.shiftKey) {
       return;
     }
     this.matchedExpanded = !this.matchedExpanded;
     this.refreshMatchedSelectors();
     aEvent.preventDefault();
   },
 
   /**
    * The action when a user clicks on the MDN help link for a property.
    */
-  mdnLinkClick: function PropertyView_mdnLinkClick(aEvent)
-  {
+  mdnLinkClick: function(aEvent) {
     let inspector = this.tree.inspector;
 
     if (inspector.target.tab) {
       let browserWin = inspector.target.tab.ownerDocument.defaultView;
       browserWin.openUILinkIn(this.link, "tab");
     }
     aEvent.preventDefault();
   },
 
   /**
    * Destroy this property view, removing event listeners
    */
-  destroy: function PropertyView_destroy() {
+  destroy: function() {
     this.element.removeEventListener("dblclick", this.onMatchedToggle, false);
     this.element.removeEventListener("keydown", this.onKeyDown, false);
     this.element = null;
 
     this.matchedExpander.removeEventListener("click", this.onMatchedToggle, false);
     this.matchedExpander = null;
 
     this.nameNode.removeEventListener("click", this.onFocus, false);
@@ -1205,18 +1180,17 @@ PropertyView.prototype = {
   }
 };
 
 /**
  * A container to give us easy access to display data from a CssRule
  * @param CssComputedView aTree, the owning CssComputedView
  * @param aSelectorInfo
  */
-function SelectorView(aTree, aSelectorInfo)
-{
+function SelectorView(aTree, aSelectorInfo) {
   this.tree = aTree;
   this.selectorInfo = aSelectorInfo;
   this._cacheStatusNames();
 
   this.openStyleEditor = this.openStyleEditor.bind(this);
   this.maybeOpenStyleEditor = this.maybeOpenStyleEditor.bind(this);
 
   this.ready = this.updateSourceLink();
@@ -1240,71 +1214,63 @@ SelectorView.prototype = {
    * Cache localized status names.
    *
    * These statuses are localized inside the styleinspector.properties string
    * bundle.
    * @see css-logic.js - the CssLogic.STATUS array.
    *
    * @return {void}
    */
-  _cacheStatusNames: function SelectorView_cacheStatusNames()
-  {
+  _cacheStatusNames: function() {
     if (SelectorView.STATUS_NAMES.length) {
       return;
     }
 
     for (let status in CssLogic.STATUS) {
       let i = CssLogic.STATUS[status];
       if (i > CssLogic.STATUS.UNMATCHED) {
         let value = CssComputedView.l10n("rule.status." + status);
         // Replace normal spaces with non-breaking spaces
-        SelectorView.STATUS_NAMES[i] = value.replace(/ /g, '\u00A0');
+        SelectorView.STATUS_NAMES[i] = value.replace(/ /g, "\u00A0");
       }
     }
   },
 
   /**
    * A localized version of cssRule.status
    */
-  get statusText()
-  {
+  get statusText() {
     return SelectorView.STATUS_NAMES[this.selectorInfo.status];
   },
 
   /**
    * Get class name for selector depending on status
    */
-  get statusClass()
-  {
+  get statusClass() {
     return SelectorView.CLASS_NAMES[this.selectorInfo.status - 1];
   },
 
-  get href()
-  {
+  get href() {
     if (this._href) {
       return this._href;
     }
     let sheet = this.selectorInfo.rule.parentStyleSheet;
     this._href = sheet ? sheet.href : "#";
     return this._href;
   },
 
-  get sourceText()
-  {
+  get sourceText() {
     return this.selectorInfo.sourceText;
   },
 
-
-  get value()
-  {
+  get value() {
     return this.selectorInfo.value;
   },
 
-  get outputFragment()
-  {
+  get outputFragment() {
     // Sadly, because this fragment is added to the template by DOM Templater
     // we lose any events that are attached. This means that URLs will open in a
     // new window. At some point we should fix this by stopping using the
     // templater.
     let outputParser = this.tree._outputParser;
     let frag = outputParser.parseCssProperty(
       this.selectorInfo.name,
       this.selectorInfo.value, {
@@ -1315,35 +1281,33 @@ SelectorView.prototype = {
     });
     return frag;
   },
 
   /**
    * Update the text of the source link to reflect whether we're showing
    * original sources or not.
    */
-  updateSourceLink: function()
-  {
+  updateSourceLink: function() {
     return this.updateSource().then((oldSource) => {
       if (oldSource != this.source && this.tree.element) {
         let selector = '[sourcelocation="' + oldSource + '"]';
         let link = this.tree.element.querySelector(selector);
         if (link) {
           link.textContent = this.source;
           link.setAttribute("sourcelocation", this.source);
         }
       }
     });
   },
 
   /**
    * Update the 'source' store based on our original sources preference.
    */
-  updateSource: function()
-  {
+  updateSource: function() {
     let rule = this.selectorInfo.rule;
     this.sheet = rule.parentStyleSheet;
 
     if (!rule || !this.sheet) {
       let oldSource = this.source;
       this.source = CssLogic.l10n("rule.sourceElement");
       return promise.resolve(oldSource);
     }
@@ -1368,53 +1332,44 @@ SelectorView.prototype = {
     let oldSource = this.source;
     this.source = CssLogic.shortSource(this.sheet) + ":" + rule.line;
     return promise.resolve(oldSource);
   },
 
   /**
    * Open the style editor if the RETURN key was pressed.
    */
-  maybeOpenStyleEditor: function(aEvent)
-  {
+  maybeOpenStyleEditor: function(aEvent) {
     let keyEvent = Ci.nsIDOMKeyEvent;
     if (aEvent.keyCode == keyEvent.DOM_VK_RETURN) {
       this.openStyleEditor();
     }
   },
 
   /**
    * When a css link is clicked this method is called in order to either:
    *   1. Open the link in view source (for chrome stylesheets).
    *   2. Open the link in the style editor.
    *
    *   We can only view stylesheets contained in document.styleSheets inside the
    *   style editor.
    *
    * @param aEvent The click event
    */
-  openStyleEditor: function(aEvent)
-  {
+  openStyleEditor: function(aEvent) {
     let inspector = this.tree.inspector;
     let rule = this.selectorInfo.rule;
 
     // The style editor can only display stylesheets coming from content because
     // chrome stylesheets are not listed in the editor's stylesheet selector.
     //
     // If the stylesheet is a content stylesheet we send it to the style
     // editor else we display it in the view source window.
-    let sheet = rule.parentStyleSheet;
-    if (!sheet || sheet.isSystem) {
-      let contentDoc = null;
-      if (this.tree.viewedElement.isLocal_toBeDeprecated()) {
-        let rawNode = this.tree.viewedElement.rawNode();
-        if (rawNode) {
-          contentDoc = rawNode.ownerDocument;
-        }
-      }
+    let parentStyleSheet = rule.parentStyleSheet;
+    if (!parentStyleSheet || parentStyleSheet.isSystem) {
       let toolbox = gDevTools.getToolbox(inspector.target);
       toolbox.viewSource(rule.href, rule.line);
       return;
     }
 
     let location = promise.resolve(rule.location);
     if (Services.prefs.getBoolPref(PREF_ORIG_SOURCES)) {
       location = rule.getOriginalLocation();
@@ -1442,17 +1397,17 @@ SelectorView.prototype = {
  *        A set of attributes to set on the node.
  */
 function createChild(aParent, aTag, aAttributes={}) {
   let elt = aParent.ownerDocument.createElementNS(HTML_NS, aTag);
   for (let attr in aAttributes) {
     if (aAttributes.hasOwnProperty(attr)) {
       if (attr === "textContent") {
         elt.textContent = aAttributes[attr];
-      } else if(attr === "child") {
+      } else if (attr === "child") {
         elt.appendChild(aAttributes[attr]);
       } else {
         elt.setAttribute(attr, aAttributes[attr]);
       }
     }
   }
   aParent.appendChild(elt);
   return elt;
--- a/browser/devtools/styleinspector/rule-view.js
+++ b/browser/devtools/styleinspector/rule-view.js
@@ -1,16 +1,17 @@
 /* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
 /* vim: set ts=2 et sw=2 tw=80: */
 /* 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/. */
 
 /* globals overlays, Services, EventEmitter, StyleInspectorMenu,
-   clipboardHelper, _strings, domUtils, AutocompletePopup */
+   clipboardHelper, _strings, domUtils, AutocompletePopup, loader,
+   osString */
 
 "use strict";
 
 const {Cc, Ci, Cu} = require("chrome");
 const {Promise: promise} = Cu.import("resource://gre/modules/Promise.jsm", {});
 const {CssLogic} = require("devtools/styleinspector/css-logic");
 const {InplaceEditor, editableField, editableItem} =
       require("devtools/shared/inplace-editor");
@@ -204,17 +205,17 @@ ElementStyle.prototype = {
    */
   populate: function() {
     let populated = this.pageStyle.getApplied(this.element, {
       inherited: true,
       matchedSelectors: true,
       filter: this.showUserAgentStyles ? "ua" : undefined,
     }).then(entries => {
       if (this.destroyed) {
-        return;
+        return promise.resolve(undefined);
       }
 
       // Make sure the dummy element has been created before continuing...
       return this.dummyElementPromise.then(() => {
         if (this.populated != populated) {
           // Don't care anymore.
           return;
         }
@@ -231,24 +232,22 @@ ElementStyle.prototype = {
 
         // Mark overridden computed styles.
         this.markOverriddenAll();
 
         this._sortRulesForPseudoElement();
 
         // We're done with the previous list of rules.
         delete this._refreshRules;
-
-        return null;
       });
     }).then(null, e => {
       // populate is often called after a setTimeout,
       // the connection may already be closed.
       if (this.destroyed) {
-        return;
+        return promise.resolve(undefined);
       }
       return promiseWarn(e);
     });
     this.populated = populated;
     return this.populated;
   },
 
   /**
@@ -631,17 +630,17 @@ Rule.prototype = {
     // Store disabled properties in the disabled store.
     let disabled = this.elementStyle.store.disabled;
     if (disabledProps.length > 0) {
       disabled.set(this.style, disabledProps);
     } else {
       disabled.delete(this.style);
     }
 
-    let promise = aModifications.apply().then(() => {
+    let modificationsPromise = aModifications.apply().then(() => {
       let cssProps = {};
       for (let cssProp of parseDeclarations(this.style.cssText)) {
         cssProps[cssProp.name] = cssProp;
       }
 
       for (let textProp of this.textProps) {
         if (!textProp.enabled) {
           continue;
@@ -663,18 +662,18 @@ Rule.prototype = {
 
       if (promise === this._applyingModifications) {
         this._applyingModifications = null;
       }
 
       this.elementStyle._changed();
     }).then(null, promiseWarn);
 
-    this._applyingModifications = promise;
-    return promise;
+    this._applyingModifications = modificationsPromise;
+    return modificationsPromise;
   },
 
   /**
    * Renames a property.
    *
    * @param {TextProperty} aProperty
    *        The property to rename.
    * @param {string} aName
@@ -1106,17 +1105,18 @@ TextProperty.prototype = {
     this.rule.removeProperty(this);
   },
 
   /**
    * Return a string representation of the rule property.
    */
   stringifyProperty: function() {
     // Get the displayed property value
-    let declaration = this.name + ": " + this.editor.committed.value + ";";
+    let declaration = this.name + ": " + this.editor.valueSpan.textContent +
+      ";";
 
     // Comment out property declarations that are not enabled
     if (!this.enabled) {
       declaration = "/* " + declaration + " */";
     }
 
     return declaration;
   }
@@ -1736,17 +1736,17 @@ CssRuleView.prototype = {
   },
 
   /**
    * Update the rules for the currently highlighted element.
    */
   refreshPanel: function() {
     // Ignore refreshes during editing or when no element is selected.
     if (this.isEditing || !this._elementStyle) {
-      return;
+      return promise.resolve(undefined);
     }
 
     // Repopulate the element style once the current modifications are done.
     let promises = [];
     for (let rule of this._elementStyle.rules) {
       if (rule._applyingModifications) {
         promises.push(rule._applyingModifications);
       }
@@ -1888,19 +1888,20 @@ CssRuleView.prototype = {
       this._showPseudoElements =
         Services.prefs.getBoolPref("devtools.inspector.show_pseudo_elements");
     }
     return this._showPseudoElements;
   },
 
   /**
    * Creates an expandable container in the rule view
-   * @param  {String}  aLabel The label for the container header
-   * @param  {Boolean} isPseudo Whether or not the container will hold
-   *                            pseudo element rules
+   * @param  {String} aLabel
+   *         The label for the container header
+   * @param  {Boolean} isPseudo
+   *         Whether or not the container will hold pseudo element rules
    * @return {DOMNode} The container element
    */
   createExpandableContainer: function(aLabel, isPseudo = false) {
     let header = this.styleDocument.createElementNS(HTML_NS, "div");
     header.className = this._getRuleViewHeaderClassName(true);
     header.classList.add("show-expandable-container");
     header.textContent = aLabel;
 
@@ -1910,54 +1911,69 @@ CssRuleView.prototype = {
 
     header.insertBefore(twisty, header.firstChild);
     this.element.appendChild(header);
 
     let container = this.styleDocument.createElementNS(HTML_NS, "div");
     container.classList.add("ruleview-expandable-container");
     this.element.appendChild(container);
 
-    let toggleContainerVisibility = (isPseudo, showPseudo) => {
-      let isOpen = twisty.getAttribute("open");
-
-      if (isPseudo) {
-        this._showPseudoElements = !!showPseudo;
-
-        Services.prefs.setBoolPref("devtools.inspector.show_pseudo_elements",
-          this.showPseudoElements);
-
-        header.classList.toggle("show-expandable-container",
-          this.showPseudoElements);
-
-        isOpen = !this.showPseudoElements;
-      } else {
-        header.classList.toggle("show-expandable-container");
-      }
-
-      if (isOpen) {
-        twisty.removeAttribute("open");
-      } else {
-        twisty.setAttribute("open", "true");
-      }
-    };
-
     header.addEventListener("dblclick", () => {
-      toggleContainerVisibility(isPseudo, !this.showPseudoElements);
+      this._toggleContainerVisibility(twisty, header, isPseudo,
+        !this.showPseudoElements);
     }, false);
+
     twisty.addEventListener("click", () => {
-      toggleContainerVisibility(isPseudo, !this.showPseudoElements);
+      this._toggleContainerVisibility(twisty, header, isPseudo,
+        !this.showPseudoElements);
     }, false);
 
     if (isPseudo) {
-      toggleContainerVisibility(isPseudo, this.showPseudoElements);
+      this._toggleContainerVisibility(twisty, header, isPseudo,
+        this.showPseudoElements);
     }
 
     return container;
   },
 
+  /**
+   * Toggle the visibility of an expandable container
+   * @param  {DOMNode}  twisty
+   *         clickable toggle DOM Node
+   * @param  {DOMNode}  header
+   *         expandable container header DOM Node
+   * @param  {Boolean}  isPseudo
+   *         whether or not the container will hold pseudo element rules
+   * @param  {Boolean}  showPseudo
+   *         whether or not pseudo element rules should be displayed
+   */
+  _toggleContainerVisibility: function(twisty, header, isPseudo, showPseudo) {
+    let isOpen = twisty.getAttribute("open");
+
+    if (isPseudo) {
+      this._showPseudoElements = !!showPseudo;
+
+      Services.prefs.setBoolPref("devtools.inspector.show_pseudo_elements",
+        this.showPseudoElements);
+
+      header.classList.toggle("show-expandable-container",
+        this.showPseudoElements);
+
+      isOpen = !this.showPseudoElements;
+    } else {
+      header.classList.toggle("show-expandable-container");
+    }
+
+    if (isOpen) {
+      twisty.removeAttribute("open");
+    } else {
+      twisty.setAttribute("open", "true");
+    }
+  },
+
   _getRuleViewHeaderClassName: function(isPseudo) {
     let baseClassName = "theme-gutter ruleview-header";
     return isPseudo ? baseClassName + " ruleview-expandable-header" :
       baseClassName;
   },
 
   /**
    * Creates editor UI for each of the rules in _elementStyle.
--- a/browser/devtools/styleinspector/style-inspector-menu.js
+++ b/browser/devtools/styleinspector/style-inspector-menu.js
@@ -226,16 +226,17 @@ StyleInspectorMenu.prototype = {
   /**
    * Display the necessary copy context menu items depending on the clicked
    * node and selection in the rule view.
    */
   _updateCopyMenuItems: function() {
     this.menuitemCopy.hidden = !this._hasTextSelected();
     this.menuitemCopyColor.hidden = !this._isColorPopup();
     this.menuitemCopyImageDataUrl.hidden = !this._isImageUrl();
+    this.menuitemCopyUrl.hidden = !this._isImageUrl();
 
     this.menuitemCopyRule.hidden = true;
     this.menuitemCopyLocation.hidden = true;
     this.menuitemCopyPropertyDeclaration.hidden = true;
     this.menuitemCopyPropertyName.hidden = true;
     this.menuitemCopyPropertyValue.hidden = true;
     this.menuitemCopySelector.hidden = true;
 
@@ -373,16 +374,20 @@ StyleInspectorMenu.prototype = {
   _onCopyColor: function() {
     clipboardHelper.copyString(this._colorToCopy);
   },
 
   /*
    * Retrieve the url for the selected image and copy it to the clipboard
    */
   _onCopyUrl: function() {
+    if (!this._clickedNodeInfo) {
+      return;
+    }
+
     clipboardHelper.copyString(this._clickedNodeInfo.value.url);
   },
 
   /**
    * Retrieve the image data for the selected image url and copy it to the clipboard
    */
   _onCopyImageDataUrl: Task.async(function*() {
     if (!this._clickedNodeInfo) {
--- a/browser/devtools/styleinspector/test/browser_ruleview_copy_styles.js
+++ b/browser/devtools/styleinspector/test/browser_ruleview_copy_styles.js
@@ -48,37 +48,66 @@ add_task(function*() {
         copyPropertyDeclaration: false,
         copyPropertyName: true,
         copyPropertyValue: false,
         copySelector: true,
         copyRule: false
       }
     },
     {
+      desc: "Test Copy Property Value with Priority",
+      node: ruleEditor.rule.textProps[3].editor.valueSpan,
+      menuItem: contextmenu.menuitemCopyPropertyValue,
+      expectedPattern: "#00F !important",
+      hidden: {
+        copyLocation: true,
+        copyPropertyDeclaration: false,
+        copyPropertyName: true,
+        copyPropertyValue: false,
+        copySelector: true,
+        copyRule: false
+      }
+    },
+    {
       desc: "Test Copy Property Declaration",
       node: ruleEditor.rule.textProps[2].editor.nameSpan,
       menuItem: contextmenu.menuitemCopyPropertyDeclaration,
       expectedPattern: "font-size: 12px;",
       hidden: {
         copyLocation: true,
         copyPropertyDeclaration: false,
         copyPropertyName: false,
         copyPropertyValue: true,
         copySelector: true,
         copyRule: false
       }
     },
     {
+      desc: "Test Copy Property Declaration with Priority",
+      node: ruleEditor.rule.textProps[3].editor.nameSpan,
+      menuItem: contextmenu.menuitemCopyPropertyDeclaration,
+      expectedPattern: "border-color: #00F !important;",
+      hidden: {
+        copyLocation: true,
+        copyPropertyDeclaration: false,
+        copyPropertyName: false,
+        copyPropertyValue: true,
+        copySelector: true,
+        copyRule: false
+      }
+    },
+    {
       desc: "Test Copy Rule",
       node: ruleEditor.rule.textProps[2].editor.nameSpan,
       menuItem: contextmenu.menuitemCopyRule,
       expectedPattern: "#testid {[\\r\\n]+" +
                        "\tcolor: #F00;[\\r\\n]+" +
                        "\tbackground-color: #00F;[\\r\\n]+" +
                        "\tfont-size: 12px;[\\r\\n]+" +
+                       "\tborder-color: #00F !important;[\\r\\n]+" +
                        "}",
       hidden: {
         copyLocation: true,
         copyPropertyDeclaration: false,
         copyPropertyName: false,
         copyPropertyValue: true,
         copySelector: true,
         copyRule: false
@@ -119,16 +148,17 @@ add_task(function*() {
       },
       desc: "Test Copy Rule with Disabled Property",
       node: ruleEditor.rule.textProps[2].editor.nameSpan,
       menuItem: contextmenu.menuitemCopyRule,
       expectedPattern: "#testid {[\\r\\n]+" +
                        "\t\/\\* color: #F00; \\*\/[\\r\\n]+" +
                        "\tbackground-color: #00F;[\\r\\n]+" +
                        "\tfont-size: 12px;[\\r\\n]+" +
+                       "\tborder-color: #00F !important;[\\r\\n]+" +
                        "}",
       hidden: {
         copyLocation: true,
         copyPropertyDeclaration: false,
         copyPropertyName: false,
         copyPropertyValue: true,
         copySelector: true,
         copyRule: false
--- a/browser/devtools/styleinspector/test/browser_styleinspector_context-menu-copy-urls.js
+++ b/browser/devtools/styleinspector/test/browser_styleinspector_context-menu-copy-urls.js
@@ -75,16 +75,17 @@ function* testCopyUrlToClipboard({view, 
   let rect = imageLink.getClientRects()[0];
   let x = rect.left + 2;
   let y = rect.top + 2;
 
   EventUtils.synthesizeMouseAtPoint(x, y, {button: 2, type: "contextmenu"}, getViewWindow(view));
   yield popup;
 
   info("Context menu is displayed");
+  ok(!view._contextmenu.menuitemCopyUrl.hidden, "\"Copy URL\" menu entry is displayed");
   ok(!view._contextmenu.menuitemCopyImageDataUrl.hidden, "\"Copy Image Data-URL\" menu entry is displayed");
 
   if (type == "data-uri") {
     info("Click Copy Data URI and wait for clipboard");
     yield waitForClipboard(() => view._contextmenu.menuitemCopyImageDataUrl.click(), expected);
   } else {
     info("Click Copy URL and wait for clipboard");
     yield waitForClipboard(() => view._contextmenu.menuitemCopyUrl.click(), expected);
--- a/browser/devtools/styleinspector/test/doc_copystyles.css
+++ b/browser/devtools/styleinspector/test/doc_copystyles.css
@@ -1,9 +1,10 @@
 /* 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/. */
 
 html, body, #testid {
   color: #F00;
   background-color: #00F;
   font-size: 12px;
+  border-color: #00F !important;
 }
--- a/browser/installer/package-manifest.in
+++ b/browser/installer/package-manifest.in
@@ -633,16 +633,17 @@
 @RESPATH@/modules/*
 
 ; Safe Browsing
 #ifdef MOZ_URL_CLASSIFIER
 @RESPATH@/components/nsURLClassifier.manifest
 @RESPATH@/components/nsUrlClassifierHashCompleter.js
 @RESPATH@/components/nsUrlClassifierListManager.js
 @RESPATH@/components/nsUrlClassifierLib.js
+@RESPATH@/components/PrivateBrowsingTrackingProtectionWhitelist.js
 @RESPATH@/components/url-classifier.xpt
 #endif
 
 ; ANGLE GLES-on-D3D rendering library
 #ifdef MOZ_ANGLE_RENDERER
 @BINPATH@/libEGL.dll
 @BINPATH@/libGLESv2.dll
 
--- a/browser/locales/en-US/chrome/browser/customizableui/customizableWidgets.properties
+++ b/browser/locales/en-US/chrome/browser/customizableui/customizableWidgets.properties
@@ -92,16 +92,19 @@ quit-button.tooltiptext.linux2 = Quit %1
 # LOCALIZATION NOTE(quit-button.tooltiptext.mac): %1$S is the brand name (e.g. Firefox),
 # %2$S is the keyboard shortcut
 quit-button.tooltiptext.mac = Quit %1$S (%2$S)
 
 # LOCALIZATION NOTE(loop-call-button3.label): This is a brand name, request
 # approval before you change it.
 loop-call-button3.label = Hello
 loop-call-button3.tooltiptext = Start a conversation
+# LOCALIZATION NOTE(loop-call-button3-pb.tooltiptext): Shown when the button is
+# placed inside a Private Browsing window. %S is the value of loop-call-button3.label.
+loop-call-button3-pb.tooltiptext = %S is not available in Private Browsing
 
 social-share-button.label = Share This Page
 social-share-button.tooltiptext = Share this page
 
 panic-button.label = Forget
 panic-button.tooltiptext = Forget about some browsing history
 
 web-apps-button.label = Apps
--- a/browser/locales/en-US/chrome/browser/loop/loop.properties
+++ b/browser/locales/en-US/chrome/browser/loop/loop.properties
@@ -201,16 +201,18 @@ 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_tabs_button_title2=Share your Tabs
 share_windows_button_title=Share other Windows
+self_view_hidden_message=Self-view hidden but still being sent; resize window to show
+
 
 ## 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.
 call_with_contact_title=Conversation with {{contactName}}
 
 # Outgoing conversation
 
--- a/browser/themes/windows/browser-aero.css
+++ b/browser/themes/windows/browser-aero.css
@@ -110,17 +110,17 @@
         #titlebar-buttonbox,
         .titlebar-button {
           -moz-appearance: none !important;
         }
 
         .titlebar-button {
           border: none;
           margin: 0 !important;
-          padding: 12px 17px;
+          padding: 10px 17px;
         }
 
         #main-window[sizemode=maximized] .titlebar-button {
           padding-top: 8px;
           padding-bottom: 8px;
         }
 
         .titlebar-button > .toolbarbutton-icon {
@@ -142,16 +142,30 @@
 
         #titlebar-close {
           list-style-image: url(chrome://browser/skin/caption-buttons.svg#close);
         }
         #titlebar-close:hover {
           list-style-image: url(chrome://browser/skin/caption-buttons.svg#close-white);
         }
 
+        #titlebar-min:-moz-lwtheme {
+          list-style-image: url(chrome://browser/skin/caption-buttons.svg#minimize-themes);
+        }
+        #titlebar-max:-moz-lwtheme {
+          list-style-image: url(chrome://browser/skin/caption-buttons.svg#maximize-themes);
+        }
+        #main-window[sizemode="maximized"] #titlebar-max:-moz-lwtheme {
+          list-style-image: url(chrome://browser/skin/caption-buttons.svg#restore-themes);
+        }
+        #titlebar-close:-moz-lwtheme {
+          list-style-image: url(chrome://browser/skin/caption-buttons.svg#close-themes);
+        }
+
+
         /* the 12px image renders a 10px icon, and the 10px upscaled gets rounded to 12.5, which
          * rounds up to 13px, which makes the icon one pixel too big on 1.25dppx. Fix: */
         @media (min-resolution: 1.20dppx) and (max-resolution: 1.45dppx) {
           .titlebar-button > .toolbarbutton-icon {
             width: 11.5px;
             height: 11.5px;
           }
         }
@@ -217,30 +231,42 @@
         @media not all and (-moz-windows-default-theme) {
           .titlebar-button {
             background-color: -moz-field;
           }
           .titlebar-button:hover {
             background-color: Highlight;
           }
 
+          #titlebar-min {
+            list-style-image: url(chrome://browser/skin/caption-buttons.svg#minimize-highcontrast);
+          }
           #titlebar-min:hover {
-            list-style-image: url(chrome://browser/skin/caption-buttons.svg#minimize-highlight);
+            list-style-image: url(chrome://browser/skin/caption-buttons.svg#minimize-highcontrast-hover);
           }
 
+          #titlebar-max {
+            list-style-image: url(chrome://browser/skin/caption-buttons.svg#maximize-highcontrast);
+          }
           #titlebar-max:hover {
-            list-style-image: url(chrome://browser/skin/caption-buttons.svg#maximize-highlight);
+            list-style-image: url(chrome://browser/skin/caption-buttons.svg#maximize-highcontrast-hover);
           }
 
+          #main-window[sizemode="maximized"] #titlebar-max {
+            list-style-image: url(chrome://browser/skin/caption-buttons.svg#restore-highcontrast);
+          }
           #main-window[sizemode="maximized"] #titlebar-max:hover {
-            list-style-image: url(chrome://browser/skin/caption-buttons.svg#restore-highlight);
+            list-style-image: url(chrome://browser/skin/caption-buttons.svg#restore-highcontrast-hover);
           }
 
+          #titlebar-close {
+            list-style-image: url(chrome://browser/skin/caption-buttons.svg#close-highcontrast);
+          }
           #titlebar-close:hover {
-            list-style-image: url(chrome://browser/skin/caption-buttons.svg#close-highlight);
+            list-style-image: url(chrome://browser/skin/caption-buttons.svg#close-highcontrast-hover);
           }
         }
       }
     }
   }
 
   @media (-moz-os-version: windows-vista),
          (-moz-os-version: windows-win7),
--- a/browser/themes/windows/browser.css
+++ b/browser/themes/windows/browser.css
@@ -654,17 +654,17 @@ toolbar[brighttext] .toolbarbutton-1 > .
   -moz-box-pack: center;
 }
 
 #nav-bar #PanelUI-menu-button {
   -moz-padding-start: 5px;
   -moz-padding-end: 5px;
 }
 
-#nav-bar .toolbarbutton-1[type=panel]:not(#back-button):not(#forward-button):not(#feed-button):not(#PanelUI-menu-button),
+#nav-bar .toolbarbutton-1[type=panel],
 #nav-bar .toolbarbutton-1[type=menu]:not(#back-button):not(#forward-button):not(#feed-button):not(#PanelUI-menu-button) {
   padding-left: 5px;
   padding-right: 5px;
 }
 
 #nav-bar .toolbarbutton-1 > menupopup {
   margin-top: -3px;
 }
@@ -766,18 +766,18 @@ toolbarbutton[constrain-size="true"][cui
   width: 16px;
 }
 
 #nav-bar toolbarbutton[constrain-size="true"][cui-areatype="toolbar"] > .toolbarbutton-icon {
   /* XXXgijs box models strike again: this is 16px + 2 * 7px padding + 2 * 1px border (from the rules above) */
   width: 32px;
 }
 
-#nav-bar .toolbarbutton-1[type=panel]:not(#back-button):not(#forward-button):not(#feed-button):not(#PanelUI-menu-button) > .toolbarbutton-icon,
-#nav-bar .toolbarbutton-1[type=panel]:not(#back-button):not(#forward-button):not(#feed-button):not(#PanelUI-menu-button) > .toolbarbutton-badge-container,
+#nav-bar .toolbarbutton-1[type=panel] > .toolbarbutton-icon,
+#nav-bar .toolbarbutton-1[type=panel] > .toolbarbutton-badge-container,
 #nav-bar .toolbarbutton-1[type=menu]:not(#back-button):not(#forward-button):not(#feed-button):not(#PanelUI-menu-button) > .toolbarbutton-icon,
 #nav-bar .toolbarbutton-1[type=menu]:not(#back-button):not(#forward-button):not(#feed-button):not(#PanelUI-menu-button) > .toolbarbutton-badge-container,
 #nav-bar .toolbarbutton-1[type=menu] > .toolbarbutton-text /* hack for add-ons that forcefully display the label */ {
   -moz-padding-end: 17px;
 }
 
 #nav-bar .toolbarbutton-1 > .toolbarbutton-menu-dropmarker {
   -moz-margin-start: -15px;
@@ -1198,16 +1198,21 @@ toolbarbutton[constrain-size="true"][cui
   background-clip: padding-box;
   border: 1px solid ThreeDShadow;
 }
 
 #urlbar {
   -moz-padding-end: 2px;
 }
 
+/* overlap the urlbar's border */
+#PopupAutoCompleteRichResult {
+  margin-top: -1px;
+}
+
 @media (-moz-os-version: windows-xp),
        (-moz-os-version: windows-vista),
        (-moz-os-version: windows-win7) {
   #urlbar,
   .searchbar-textbox {
     border-radius: 2px;
   }
 }
@@ -1241,16 +1246,21 @@ toolbarbutton[constrain-size="true"][cui
     .searchbar-textbox:not(:-moz-lwtheme):hover {
       border-color: hsl(0,0%,80%);
     }
 
     #urlbar:not(:-moz-lwtheme)[focused],
     .searchbar-textbox:not(:-moz-lwtheme)[focused] {
       box-shadow: 0 0 0 1px Highlight inset;
     }
+
+    /* overlap the urlbar's border and inset box-shadow */
+    #PopupAutoCompleteRichResult:not(:-moz-lwtheme) {
+      margin-top: -2px;
+    }
   }
 
   @media not all and (-moz-os-version: windows-xp) {
     #urlbar:not(:-moz-lwtheme)[focused],
     .searchbar-textbox:not(:-moz-lwtheme)[focused] {
       border-color: Highlight;
     }
   }
--- a/browser/themes/windows/caption-buttons.svg
+++ b/browser/themes/windows/caption-buttons.svg
@@ -4,52 +4,104 @@
 <svg width="12" height="12" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
   <style>
     g {
       stroke: ButtonText;
       stroke-width: 0.9px;
       fill: none;
     }
 
-    g:not(#close) {
+    g:not([id|="close"]) {
       shape-rendering: crispEdges;
     }
 
     g:not(:target) {
       display: none;
     }
 
     use:target > g {
       display: initial;
     }
 
-    [id$="-highlight"] > g {
+    g.highlight {
+      stroke-width: 1.9px;
+    }
+
+    g.themes {
+      stroke: #fff;
+      stroke-width: 1.9px;
+    }
+
+    .outer-stroke {
+      stroke: #000;
+      stroke-width: 3.6;
+      opacity: .75;
+    }
+
+    .restore-background-window {
+      stroke-width: .9;
+    }
+
+    [id$="-highcontrast-hover"] > g {
       stroke: HighlightText;
     }
 
     [id$="-white"] > g {
       stroke: #fff;
     }
+
   </style>
   <g id="close">
-    <line x1="1" y1="1" x2="11" y2="11"/>
-    <line x1="11" y1="1" x2="1" y2="11"/>
+    <path d="M1,1 l 10,10 M1,11 l 10,-10"/>
   </g>
   <g id="maximize">
     <rect x="1.5" y="1.5" width="9" height="9"/>
   </g>
   <g id="minimize">
     <line x1="1" y1="5.5" x2="11" y2="5.5"/>
   </g>
   <g id="restore">
     <rect x="1.5" y="3.5" width="7" height="7"/>
     <polyline points="3.5,3.5 3.5,1.5 10.5,1.5 10.5,8.5 8.5,8.5"/>
   </g>
-  <use id="close-highlight" xlink:href="#close"/>
-  <use id="maximize-highlight" xlink:href="#maximize"/>
-  <use id="minimize-highlight" xlink:href="#minimize"/>
-  <use id="restore-highlight" xlink:href="#restore"/>
 
   <use id="close-white" xlink:href="#close"/>
   <use id="maximize-white" xlink:href="#maximize"/>
   <use id="minimize-white" xlink:href="#minimize"/>
   <use id="restore-white" xlink:href="#restore"/>
+
+  <g id="close-highcontrast" class="highlight">
+    <path d="M1,1 l 10,10 M1,11 l 10,-10"/>
+  </g>
+  <g id="maximize-highcontrast" class="highlight">
+    <rect x="2" y="2" width="8" height="8"/>
+  </g>
+  <g id="minimize-highcontrast" class="highlight">
+    <line x1="1" y1="6" x2="11" y2="6"/>
+  </g>
+  <g id="restore-highcontrast" class="highlight">
+    <rect x="2" y="4" width="6" height="6"/>
+    <polyline points="3.5,1.5 10.5,1.5 10.5,8.5" class="restore-background-window"/>
+  </g>
+
+  <use id="close-highcontrast-hover" xlink:href="#close-highcontrast"/>
+  <use id="maximize-highcontrast-hover" xlink:href="#maximize-highcontrast"/>
+  <use id="minimize-highcontrast-hover" xlink:href="#minimize-highcontrast"/>
+  <use id="restore-highcontrast-hover" xlink:href="#restore-highcontrast"/>
+
+  <g id="close-themes" class="themes">
+    <path d="M1,1 l 10,10 M1,11 l 10,-10" class="outer-stroke" />
+    <path d="M1.75,1.75 l 8.5,8.5 M1.75,10.25 l 8.5,-8.5"/>
+  </g>
+  <g id="maximize-themes" class="themes">
+    <rect x="2" y="2" width="8" height="8" class="outer-stroke"/>
+    <rect x="2" y="2" width="8" height="8"/>
+  </g>
+  <g id="minimize-themes" class="themes">
+    <line x1="0" y1="6" x2="12" y2="6" class="outer-stroke"/>
+    <line x1="1" y1="6" x2="11" y2="6"/>
+  </g>
+  <g id="restore-themes" class="themes">
+    <path d="M2,4 l 6,0 l 0,6 l -6,0z M2.5,1.5 l 8,0 l 0,8" class="outer-stroke"/>
+    <rect x="2" y="4" width="6" height="6"/>
+    <polyline points="3.5,1.5 10.5,1.5 10.5,8.5" class="restore-background-window"/>
+  </g>
 </svg>
--- a/docshell/base/nsDocShell.cpp
+++ b/docshell/base/nsDocShell.cpp
@@ -2950,139 +2950,26 @@ nsDocShell::GetRecordProfileTimelineMark
 {
   *aValue = IsObserved();
   return NS_OK;
 }
 
 nsresult
 nsDocShell::PopProfileTimelineMarkers(
     JSContext* aCx,
-    JS::MutableHandle<JS::Value> aProfileTimelineMarkers)
-{
-  // Looping over all markers gathered so far at the docShell level, whenever a
-  // START marker is found, look for the corresponding END marker and build a
-  // {name,start,end} JS object.
-  // Paint markers are different because paint is handled at root docShell level
-  // in the information that a paint was done is then stored at each sub
-  // docShell level but we can only be sure that a paint did happen in a
-  // docShell if an Layer marker type was recorded too.
-
-  nsTArray<mozilla::dom::ProfileTimelineMarker> profileTimelineMarkers;
-  SequenceRooter<mozilla::dom::ProfileTimelineMarker> rooter(
-    aCx, &profileTimelineMarkers);
-
-  if (!IsObserved()) {
-    if (!ToJSValue(aCx, profileTimelineMarkers, aProfileTimelineMarkers)) {
-      JS_ClearPendingException(aCx);
-      return NS_ERROR_UNEXPECTED;
-    }
-    return NS_OK;
-  }
-
-  nsTArray<UniquePtr<TimelineMarker>>& markersStore = mObserved.get()->mTimelineMarkers;
-
-  // If we see an unpaired START, we keep it around for the next call
-  // to PopProfileTimelineMarkers.  We store the kept START objects in
-  // this array.
-  nsTArray<UniquePtr<TimelineMarker>> keptMarkers;
-
-  for (uint32_t i = 0; i < markersStore.Length(); ++i) {
-    UniquePtr<TimelineMarker>& startPayload = markersStore[i];
-    const char* startMarkerName = startPayload->GetName();
-
-    bool hasSeenPaintedLayer = false;
-    bool isPaint = strcmp(startMarkerName, "Paint") == 0;
-
-    // If we are processing a Paint marker, we append information from
-    // all the embedded Layer markers to this array.
-    dom::Sequence<dom::ProfileTimelineLayerRect> layerRectangles;
-
-    // If this is a TRACING_TIMESTAMP marker, there's no corresponding "end"
-    // marker, as it's a single unit of time, not a duration, create the final
-    // marker here.
-    if (startPayload->GetMetaData() == TRACING_TIMESTAMP) {
-      mozilla::dom::ProfileTimelineMarker* marker =
-        profileTimelineMarkers.AppendElement();
-
-      marker->mName = NS_ConvertUTF8toUTF16(startPayload->GetName());
-      marker->mStart = startPayload->GetTime();
-      marker->mEnd = startPayload->GetTime();
-      marker->mStack = startPayload->GetStack();
-      startPayload->AddDetails(aCx, *marker);
-      continue;
-    }
-
-    if (startPayload->GetMetaData() == TRACING_INTERVAL_START) {
-      bool hasSeenEnd = false;
-
-      // DOM events can be nested, so we must take care when searching
-      // for the matching end.  It doesn't hurt to apply this logic to
-      // all event types.
-      uint32_t markerDepth = 0;
-
-      // The assumption is that the devtools timeline flushes markers frequently
-      // enough for the amount of markers to always be small enough that the
-      // nested for loop isn't going to be a performance problem.
-      for (uint32_t j = i + 1; j < markersStore.Length(); ++j) {
-        UniquePtr<TimelineMarker>& endPayload = markersStore[j];
-        const char* endMarkerName = endPayload->GetName();
-
-        // Look for Layer markers to stream out paint markers.
-        if (isPaint && strcmp(endMarkerName, "Layer") == 0) {
-          hasSeenPaintedLayer = true;
-          endPayload->AddLayerRectangles(layerRectangles);
-        }
-
-        if (!startPayload->Equals(*endPayload)) {
-          continue;
-        }
-
-        // Pair start and end markers.
-        if (endPayload->GetMetaData() == TRACING_INTERVAL_START) {
-          ++markerDepth;
-        } else if (endPayload->GetMetaData() == TRACING_INTERVAL_END) {
-          if (markerDepth > 0) {
-            --markerDepth;
-          } else {
-            // But ignore paint start/end if no layer has been painted.
-            if (!isPaint || (isPaint && hasSeenPaintedLayer)) {
-              mozilla::dom::ProfileTimelineMarker* marker =
-                profileTimelineMarkers.AppendElement();
-
-              marker->mName = NS_ConvertUTF8toUTF16(startPayload->GetName());
-              marker->mStart = startPayload->GetTime();
-              marker->mEnd = endPayload->GetTime();
-              marker->mStack = startPayload->GetStack();
-              if (isPaint) {
-                marker->mRectangles.Construct(layerRectangles);
-              }
-              startPayload->AddDetails(aCx, *marker);
-              endPayload->AddDetails(aCx, *marker);
-            }
-
-            // We want the start to be dropped either way.
-            hasSeenEnd = true;
-
-            break;
-          }
-        }
-      }
-
-      // If we did not see the corresponding END, keep the START.
-      if (!hasSeenEnd) {
-        keptMarkers.AppendElement(Move(markersStore[i]));
-        markersStore.RemoveElementAt(i);
-        --i;
-      }
-    }
-  }
-
-  markersStore.SwapElements(keptMarkers);
-
-  if (!ToJSValue(aCx, profileTimelineMarkers, aProfileTimelineMarkers)) {
+    JS::MutableHandle<JS::Value> aOut)
+{
+  nsTArray<dom::ProfileTimelineMarker> store;
+  SequenceRooter<dom::ProfileTimelineMarker> rooter(aCx, &store);
+
+  if (IsObserved()) {
+    mObserved->PopMarkers(aCx, store);
+  }
+
+  if (!ToJSValue(aCx, store, aOut)) {
     JS_ClearPendingException(aCx);
     return NS_ERROR_UNEXPECTED;
   }
 
   return NS_OK;
 }
 
 nsresult
--- a/docshell/base/timeline/ObservedDocShell.cpp
+++ b/docshell/base/timeline/ObservedDocShell.cpp
@@ -29,9 +29,106 @@ ObservedDocShell::AddMarker(UniquePtr<Ti
 }
 
 void
 ObservedDocShell::ClearMarkers()
 {
   mTimelineMarkers.Clear();
 }
 
+void
+ObservedDocShell::PopMarkers(JSContext* aCx,
+                             nsTArray<dom::ProfileTimelineMarker>& aStore)
+{
+  // If we see an unpaired START, we keep it around for the next call
+  // to ObservedDocShell::PopMarkers. We store the kept START objects here.
+  nsTArray<UniquePtr<TimelineMarker>> keptStartMarkers;
+
+  for (uint32_t i = 0; i < mTimelineMarkers.Length(); ++i) {
+    UniquePtr<TimelineMarker>& startPayload = mTimelineMarkers[i];
+
+    // If this is a TRACING_TIMESTAMP marker, there's no corresponding END
+    // as it's a single unit of time, not a duration.
+    if (startPayload->GetMetaData() == TRACING_TIMESTAMP) {
+      dom::ProfileTimelineMarker* marker = aStore.AppendElement();
+      marker->mName = NS_ConvertUTF8toUTF16(startPayload->GetName());
+      marker->mStart = startPayload->GetTime();
+      marker->mEnd = startPayload->GetTime();
+      marker->mStack = startPayload->GetStack();
+      startPayload->AddDetails(aCx, *marker);
+      continue;
+    }
+
+    // Whenever a START marker is found, look for the corresponding END
+    // and build a {name,start,end} JS object.
+    if (startPayload->GetMetaData() == TRACING_INTERVAL_START) {
+      bool hasSeenEnd = false;
+
+      // "Paint" markers are different because painting is handled at root
+      // docshell level. The information that a paint was done is stored at
+      // sub-docshell level, but we can only be sure that a paint did actually
+      // happen in if a "Layer" marker was recorded too.
+      bool startIsPaintType = strcmp(startPayload->GetName(), "Paint") == 0;
+      bool hasSeenLayerType = false;
+
+      // If we are processing a "Paint" marker, we append information from
+      // all the embedded "Layer" markers to this array.
+      dom::Sequence<dom::ProfileTimelineLayerRect> layerRectangles;
+
+      // DOM events can be nested, so we must take care when searching
+      // for the matching end. It doesn't hurt to apply this logic to
+      // all event types.
+      uint32_t markerDepth = 0;
+
+      // The assumption is that the devtools timeline flushes markers frequently
+      // enough for the amount of markers to always be small enough that the
+      // nested for loop isn't going to be a performance problem.
+      for (uint32_t j = i + 1; j < mTimelineMarkers.Length(); ++j) {
+        UniquePtr<TimelineMarker>& endPayload = mTimelineMarkers[j];
+        bool endIsLayerType = strcmp(endPayload->GetName(), "Layer") == 0;
+
+        // Look for "Layer" markers to stream out "Paint" markers.
+        if (startIsPaintType && endIsLayerType) {
+          hasSeenLayerType = true;
+          endPayload->AddLayerRectangles(layerRectangles);
+        }
+        if (!startPayload->Equals(*endPayload)) {
+          continue;
+        }
+        if (endPayload->GetMetaData() == TRACING_INTERVAL_START) {
+          ++markerDepth;
+          continue;
+        }
+        if (endPayload->GetMetaData() == TRACING_INTERVAL_END) {
+          if (markerDepth > 0) {
+            --markerDepth;
+            continue;
+          }
+          if (!startIsPaintType || (startIsPaintType && hasSeenLayerType)) {
+            dom::ProfileTimelineMarker* marker = aStore.AppendElement();
+            marker->mName = NS_ConvertUTF8toUTF16(startPayload->GetName());
+            marker->mStart = startPayload->GetTime();
+            marker->mEnd = endPayload->GetTime();
+            marker->mStack = startPayload->GetStack();
+            if (hasSeenLayerType) {
+              marker->mRectangles.Construct(layerRectangles);
+            }
+            startPayload->AddDetails(aCx, *marker);
+            endPayload->AddDetails(aCx, *marker);
+          }
+          hasSeenEnd = true;
+          break;
+        }
+      }
+
+      // If we did not see the corresponding END, keep the START.
+      if (!hasSeenEnd) {
+        keptStartMarkers.AppendElement(Move(mTimelineMarkers[i]));
+        mTimelineMarkers.RemoveElementAt(i);
+        --i;
+      }
+    }
+  }
+
+  mTimelineMarkers.SwapElements(keptStartMarkers);
+}
+
 } // namespace mozilla
--- a/docshell/base/timeline/ObservedDocShell.h
+++ b/docshell/base/timeline/ObservedDocShell.h
@@ -10,34 +10,35 @@
 #include "GeckoProfiler.h"
 #include "nsTArray.h"
 #include "nsRefPtr.h"
 
 class nsDocShell;
 class TimelineMarker;
 
 namespace mozilla {
+namespace dom {
+struct ProfileTimelineMarker;
+}
 
 // # ObservedDocShell
 //
 // A wrapper around a docshell for which docshell-specific markers are
 // allowed to exist. See TimelineConsumers for register/unregister logic.
 class ObservedDocShell : public LinkedListElement<ObservedDocShell>
 {
 private:
   nsRefPtr<nsDocShell> mDocShell;
+  nsTArray<UniquePtr<TimelineMarker>> mTimelineMarkers;
 
 public:
-  // FIXME: make this private once all marker-specific logic has been
-  // moved out of nsDocShell.
-  nsTArray<UniquePtr<TimelineMarker>> mTimelineMarkers;
-
   explicit ObservedDocShell(nsDocShell* aDocShell);
   nsDocShell* operator*() const { return mDocShell.get(); }
 
   void AddMarker(const char* aName, TracingMetadata aMetaData);
   void AddMarker(UniquePtr<TimelineMarker>&& aMarker);
   void ClearMarkers();
+  void PopMarkers(JSContext* aCx, nsTArray<dom::ProfileTimelineMarker>& aStore);
 };
 
 } // namespace mozilla
 
 #endif /* ObservedDocShell_h_ */
--- a/dom/bluetooth/bluedroid/BluetoothServiceBluedroid.cpp
+++ b/dom/bluetooth/bluedroid/BluetoothServiceBluedroid.cpp
@@ -110,21 +110,18 @@ static nsTArray<nsRefPtr<BluetoothReplyR
 static nsTArray<nsRefPtr<BluetoothReplyRunnable> > sBondingRunnableArray;
 static nsTArray<nsRefPtr<BluetoothReplyRunnable> > sUnbondingRunnableArray;
 static bool sIsRestart(false);
 static bool sIsFirstTimeToggleOffBt(false);
 
 #ifndef MOZ_B2G_BT_API_V1
 static bool sAdapterEnabled(false);
 
-// Use a static hash table to keep the name of remote device during the pairing
-// procedure. In this manner, BT service and adapter can get the name of paired
-// device name when bond state changed.
-// The hash Key is BD address, the Value is remote BD name.
-static nsDataHashtable<nsStringHashKey, nsString> sPairingNameTable;
+// Static hash table to map device name from address
+static nsDataHashtable<nsStringHashKey, nsString> sDeviceNameMap;
 
 static nsTArray<nsRefPtr<BluetoothReplyRunnable> > sChangeAdapterStateRunnableArray;
 static nsTArray<nsRefPtr<BluetoothReplyRunnable> > sChangeDiscoveryRunnableArray;
 static nsTArray<nsRefPtr<BluetoothReplyRunnable> > sFetchUuidsRunnableArray;
 #else
 // Missing in Bluetooth v1
 #endif
 
@@ -1891,17 +1888,17 @@ BluetoothServiceBluedroid::AdapterStateC
     // Bluetooth just enabled, clear profile controllers and runnable arrays.
     sControllerArray.Clear();
     sChangeDiscoveryRunnableArray.Clear();
     sSetPropertyRunnableArray.Clear();
     sGetDeviceRunnableArray.Clear();
     sFetchUuidsRunnableArray.Clear();
     sBondingRunnableArray.Clear();
     sUnbondingRunnableArray.Clear();
-    sPairingNameTable.Clear();
+    sDeviceNameMap.Clear();
 
     // Bluetooth scan mode is SCAN_MODE_CONNECTABLE by default, i.e., it should
     // be connectable and non-discoverable.
     NS_ENSURE_TRUE_VOID(sBtInterface);
     sBtInterface->SetAdapterProperty(
       BluetoothNamedValue(NS_ConvertUTF8toUTF16("Discoverable"), false),
       new SetAdapterPropertyDiscoverableResultHandler());
 
@@ -2173,25 +2170,29 @@ BluetoothServiceBluedroid::RemoteDeviceP
   BluetoothStatus aStatus, const nsAString& aBdAddr,
   int aNumProperties, const BluetoothProperty* aProperties)
 {
 #ifndef MOZ_B2G_BT_API_V1
   MOZ_ASSERT(NS_IsMainThread());
 
   InfallibleTArray<BluetoothNamedValue> propertiesArray;
 
-  BT_APPEND_NAMED_VALUE(propertiesArray, "Address", nsString(aBdAddr));
+  nsString bdAddr(aBdAddr);
+  BT_APPEND_NAMED_VALUE(propertiesArray, "Address", bdAddr);
 
   for (int i = 0; i < aNumProperties; ++i) {
 
     const BluetoothProperty& p = aProperties[i];
 
     if (p.mType == PROPERTY_BDNAME) {
       BT_APPEND_NAMED_VALUE(propertiesArray, "Name", p.mString);
 
+      // Update <address, name> mapping
+      sDeviceNameMap.Remove(bdAddr);
+      sDeviceNameMap.Put(bdAddr, p.mString);
     } else if (p.mType == PROPERTY_CLASS_OF_DEVICE) {
       uint32_t cod = p.mUint32;
       BT_APPEND_NAMED_VALUE(propertiesArray, "Cod", cod);
 
     } else if (p.mType == PROPERTY_UUIDS) {
       nsTArray<nsString> uuids;
 
       // Construct a sorted uuid set
@@ -2377,29 +2378,29 @@ BluetoothServiceBluedroid::RemoteDeviceP
 
 void
 BluetoothServiceBluedroid::DeviceFoundNotification(
   int aNumProperties, const BluetoothProperty* aProperties)
 {
 #ifndef MOZ_B2G_BT_API_V1
   MOZ_ASSERT(NS_IsMainThread());
 
-  BluetoothValue propertyValue;
   InfallibleTArray<BluetoothNamedValue> propertiesArray;
 
+  nsString bdAddr, bdName;
   for (int i = 0; i < aNumProperties; i++) {
 
     const BluetoothProperty& p = aProperties[i];
 
     if (p.mType == PROPERTY_BDADDR) {
       BT_APPEND_NAMED_VALUE(propertiesArray, "Address", p.mString);
-
+      bdAddr = p.mString;
     } else if (p.mType == PROPERTY_BDNAME) {
       BT_APPEND_NAMED_VALUE(propertiesArray, "Name", p.mString);
-
+      bdName = p.mString;
     } else if (p.mType == PROPERTY_CLASS_OF_DEVICE) {
       BT_APPEND_NAMED_VALUE(propertiesArray, "Cod", p.mUint32);
 
     } else if (p.mType == PROPERTY_UUIDS) {
       nsTArray<nsString> uuids;
 
       // Construct a sorted uuid set
       for (uint32_t index = 0; index < p.mUuidArray.Length(); ++index) {
@@ -2418,16 +2419,20 @@ BluetoothServiceBluedroid::DeviceFoundNo
 
     } else if (p.mType == PROPERTY_UNKNOWN) {
       /* Bug 1065999: working around unknown properties */
     } else {
       BT_LOGD("Not handled remote device property: %d", p.mType);
     }
   }
 
+  // Update <address, name> mapping
+  sDeviceNameMap.Remove(bdAddr);
+  sDeviceNameMap.Put(bdAddr, bdName);
+
   DistributeSignal(NS_LITERAL_STRING("DeviceFound"),
                    NS_LITERAL_STRING(KEY_ADAPTER),
                    BluetoothValue(propertiesArray));
 #else
   MOZ_ASSERT(NS_IsMainThread());
 
   BluetoothValue propertyValue;
   InfallibleTArray<BluetoothNamedValue> propertiesArray;
@@ -2510,24 +2515,33 @@ BluetoothServiceBluedroid::PinRequestNot
                                                   const nsAString& aBdName,
                                                   uint32_t aCod)
 {
 #ifndef MOZ_B2G_BT_API_V1
   MOZ_ASSERT(NS_IsMainThread());
 
   InfallibleTArray<BluetoothNamedValue> propertiesArray;
 
-  BT_APPEND_NAMED_VALUE(propertiesArray, "address", nsString(aRemoteBdAddr));
-  BT_APPEND_NAMED_VALUE(propertiesArray, "name", nsString(aBdName));
+  // If |aBdName| is empty, get device name from |sDeviceNameMap|;
+  // Otherwise update <address, name> mapping with |aBdName|
+  nsString bdAddr(aRemoteBdAddr);
+  nsString bdName(aBdName);
+  if (bdName.IsEmpty()) {
+    sDeviceNameMap.Get(bdAddr, &bdName);
+  } else {
+    sDeviceNameMap.Remove(bdAddr);
+    sDeviceNameMap.Put(bdAddr, bdName);
+  }
+
+  BT_APPEND_NAMED_VALUE(propertiesArray, "address", bdAddr);
+  BT_APPEND_NAMED_VALUE(propertiesArray, "name", bdName);
   BT_APPEND_NAMED_VALUE(propertiesArray, "passkey", EmptyString());
   BT_APPEND_NAMED_VALUE(propertiesArray, "type",
                         NS_LITERAL_STRING(PAIRING_REQ_TYPE_ENTERPINCODE));
 
-  sPairingNameTable.Put(nsString(aRemoteBdAddr), nsString(aBdName));
-
   DistributeSignal(NS_LITERAL_STRING("PairingRequest"),
                    NS_LITERAL_STRING(KEY_PAIRING_LISTENER),
                    BluetoothValue(propertiesArray));
 #else
   MOZ_ASSERT(NS_IsMainThread());
 
   InfallibleTArray<BluetoothNamedValue> propertiesArray;
 
@@ -2546,27 +2560,38 @@ void
 BluetoothServiceBluedroid::SspRequestNotification(
   const nsAString& aRemoteBdAddr, const nsAString& aBdName, uint32_t aCod,
   BluetoothSspVariant aPairingVariant, uint32_t aPassKey)
 {
   MOZ_ASSERT(NS_IsMainThread());
 
 #ifndef MOZ_B2G_BT_API_V1
   InfallibleTArray<BluetoothNamedValue> propertiesArray;
-  nsAutoString passkey;
-  nsAutoString pairingType;
+
+  // If |aBdName| is empty, get device name from |sDeviceNameMap|;
+  // Otherwise update <address, name> mapping with |aBdName|
+  nsString bdAddr(aRemoteBdAddr);
+  nsString bdName(aBdName);
+  if (bdName.IsEmpty()) {
+    sDeviceNameMap.Get(bdAddr, &bdName);
+  } else {
+    sDeviceNameMap.Remove(bdAddr);
+    sDeviceNameMap.Put(bdAddr, bdName);
+  }
 
   /**
    * Assign pairing request type and passkey based on the pairing variant.
    *
    * passkey value based on pairing request type:
    * 1) aPasskey: PAIRING_REQ_TYPE_CONFIRMATION and
    *              PAIRING_REQ_TYPE_DISPLAYPASSKEY
    * 2) empty string: PAIRING_REQ_TYPE_CONSENT
    */
+  nsAutoString passkey;
+  nsAutoString pairingType;
   switch (aPairingVariant) {
     case SSP_VARIANT_PASSKEY_CONFIRMATION:
       pairingType.AssignLiteral(PAIRING_REQ_TYPE_CONFIRMATION);
       passkey.AppendInt(aPassKey);
       break;
     case SSP_VARIANT_PASSKEY_NOTIFICATION:
       pairingType.AssignLiteral(PAIRING_REQ_TYPE_DISPLAYPASSKEY);
       passkey.AppendInt(aPassKey);
@@ -2574,23 +2599,21 @@ BluetoothServiceBluedroid::SspRequestNot
     case SSP_VARIANT_CONSENT:
       pairingType.AssignLiteral(PAIRING_REQ_TYPE_CONSENT);
       break;
     default:
       BT_WARNING("Unhandled SSP Bonding Variant: %d", aPairingVariant);
       return;
   }
 
-  BT_APPEND_NAMED_VALUE(propertiesArray, "address", nsString(aRemoteBdAddr));
-  BT_APPEND_NAMED_VALUE(propertiesArray, "name", nsString(aBdName));
+  BT_APPEND_NAMED_VALUE(propertiesArray, "address", bdAddr);
+  BT_APPEND_NAMED_VALUE(propertiesArray, "name", bdName);
   BT_APPEND_NAMED_VALUE(propertiesArray, "passkey", passkey);
   BT_APPEND_NAMED_VALUE(propertiesArray, "type", pairingType);
 
-  sPairingNameTable.Put(nsString(aRemoteBdAddr), nsString(aBdName));
-
   DistributeSignal(NS_LITERAL_STRING("PairingRequest"),
                    NS_LITERAL_STRING(KEY_PAIRING_LISTENER),
                    BluetoothValue(propertiesArray));
 #else
   InfallibleTArray<BluetoothNamedValue> propertiesArray;
 
   BT_APPEND_NAMED_VALUE(propertiesArray, "address", nsString(aRemoteBdAddr));
   BT_APPEND_NAMED_VALUE(propertiesArray, "method",
@@ -2637,48 +2660,41 @@ BluetoothServiceBluedroid::BondStateChan
       // Reject unpair promise
       DispatchReplyError(sUnbondingRunnableArray[0], aStatus);
       sUnbondingRunnableArray.RemoveElementAt(0);
     }
 
     return;
   }
 
-  // Retrieve and remove pairing device name from hash table
-  nsString deviceName;
-  bool nameExists = sPairingNameTable.Get(aRemoteBdAddr, &deviceName);
-  if (nameExists) {
-    sPairingNameTable.Remove(aRemoteBdAddr);
-  }
+  // Query pairing device name from hash table
+  nsString remoteBdAddr(aRemoteBdAddr);
+  nsString remotebdName;
+  sDeviceNameMap.Get(remoteBdAddr, &remotebdName);
 
   // Update bonded address array and append pairing device name
   InfallibleTArray<BluetoothNamedValue> propertiesArray;
-  nsString remoteBdAddr = nsString(aRemoteBdAddr);
   if (!bonded) {
     sAdapterBondedAddressArray.RemoveElement(remoteBdAddr);
   } else {
     if (!sAdapterBondedAddressArray.Contains(remoteBdAddr)) {
       sAdapterBondedAddressArray.AppendElement(remoteBdAddr);
     }
 
-    // We don't assert |!deviceName.IsEmpty()| here since empty string is
-    // also a valid name. According to Bluetooth Core Spec. v3.0 - Sec. 6.22,
+    // We don't assert |!remotebdName.IsEmpty()| since empty string is also
+    // valid, according to Bluetooth Core Spec. v3.0 - Sec. 6.22:
     // "a valid Bluetooth name is a UTF-8 encoding string which is up to 248
     // bytes in length."
-    // Furthermore, we don't assert |nameExists| here since it's expected to be
-    // 'false' if remote device is using "SSP just works without user
-    // interaction" or "legacy pairing with auto-pairing".
-
-    BT_APPEND_NAMED_VALUE(propertiesArray, "Name", deviceName);
+    BT_APPEND_NAMED_VALUE(propertiesArray, "Name", remotebdName);
   }
 
   // Notify device of attribute changed
   BT_APPEND_NAMED_VALUE(propertiesArray, "Paired", bonded);
   DistributeSignal(NS_LITERAL_STRING("PropertyChanged"),
-                   aRemoteBdAddr,
+                   remoteBdAddr,
                    BluetoothValue(propertiesArray));
 
   // Notify adapter of device paired/unpaired
   BT_INSERT_NAMED_VALUE(propertiesArray, 0, "Address", remoteBdAddr);
   DistributeSignal(bonded ? NS_LITERAL_STRING(DEVICE_PAIRED_ID)
                           : NS_LITERAL_STRING(DEVICE_UNPAIRED_ID),
                    NS_LITERAL_STRING(KEY_ADAPTER),
                    BluetoothValue(propertiesArray));
--- a/media/webrtc/signaling/src/peerconnection/MediaPipelineFactory.cpp
+++ b/media/webrtc/signaling/src/peerconnection/MediaPipelineFactory.cpp
@@ -893,17 +893,17 @@ MediaPipelineFactory::EnsureExternalCode
        bool enabled = mozilla::Preferences::GetBool("media.navigator.hardware.vp8_encode.acceleration_enabled", false);
 #else
        bool enabled = false;
 #endif
        if (enabled) {
          nsCOMPtr<nsIGfxInfo> gfxInfo = do_GetService("@mozilla.org/gfx/info;1");
          if (gfxInfo) {
            int32_t status;
-           if (NS_SUCCEEDED(gfxInfo->GetFeatureStatus(nsIGfxInfo::FEATURE_WEBRTC_HW_ACCELERATION, &status))) {
+           if (NS_SUCCEEDED(gfxInfo->GetFeatureStatus(nsIGfxInfo::FEATURE_WEBRTC_HW_ACCELERATION_ENCODE, &status))) {
              if (status != nsIGfxInfo::FEATURE_STATUS_OK) {
                NS_WARNING("VP8 encoder hardware is not whitelisted: disabling.\n");
              } else {
                VideoEncoder* encoder = nullptr;
                encoder = MediaCodecVideoCodec::CreateEncoder(MediaCodecVideoCodec::CodecType::CODEC_VP8);
                if (encoder) {
                  return aConduit.SetExternalSendCodec(aConfig, encoder);
                } else {
@@ -918,21 +918,20 @@ MediaPipelineFactory::EnsureExternalCode
        bool enabled = mozilla::Preferences::GetBool("media.navigator.hardware.vp8_decode.acceleration_enabled", false);
 #else
        bool enabled = false;
 #endif
        if (enabled) {
          nsCOMPtr<nsIGfxInfo> gfxInfo = do_GetService("@mozilla.org/gfx/info;1");
          if (gfxInfo) {
            int32_t status;
-           if (NS_SUCCEEDED(gfxInfo->GetFeatureStatus(nsIGfxInfo::FEATURE_WEBRTC_HW_ACCELERATION, &status))) {
+           if (NS_SUCCEEDED(gfxInfo->GetFeatureStatus(nsIGfxInfo::FEATURE_WEBRTC_HW_ACCELERATION_DECODE, &status))) {
              if (status != nsIGfxInfo::FEATURE_STATUS_OK) {
                NS_WARNING("VP8 decoder hardware is not whitelisted: disabling.\n");
              } else {
-
                VideoDecoder* decoder;
                decoder = MediaCodecVideoCodec::CreateDecoder(MediaCodecVideoCodec::CodecType::CODEC_VP8);
                if (decoder) {
                  return aConduit.SetExternalRecvCodec(aConfig, decoder);
                } else {
                  return kMediaConduitNoError;
                }
              }
--- a/mobile/android/base/AppConstants.java.in
+++ b/mobile/android/base/AppConstants.java.in
@@ -59,16 +59,18 @@ public class AppConstants {
         public static final boolean feature20Plus = MIN_SDK_VERSION >= 20 || (MAX_SDK_VERSION >= 20 && Build.VERSION.SDK_INT >= 20);
         public static final boolean feature21Plus = MIN_SDK_VERSION >= 21 || (MAX_SDK_VERSION >= 21 && Build.VERSION.SDK_INT >= 21);
 
         /*
          * If our MIN_SDK_VERSION is 14 or higher, we must be an ICS device.
          * If our MAX_SDK_VERSION is lower than ICS, we must not be an ICS device.
          * Otherwise, we need a range check.
          */
+	public static final boolean preM = MAX_SDK_VERSION < 23 ||
+                (MIN_SDK_VERSION < 23 && Build.VERSION.SDK_INT < 23 && !Build.VERSION.RELEASE.equals("M"));
         public static final boolean preLollipop = MAX_SDK_VERSION < 21 || (MIN_SDK_VERSION < 21 && Build.VERSION.SDK_INT < 21);
         public static final boolean preJBMR2 = MAX_SDK_VERSION < 18 || (MIN_SDK_VERSION < 18 && Build.VERSION.SDK_INT < 18);
         public static final boolean preJBMR1 = MAX_SDK_VERSION < 17 || (MIN_SDK_VERSION < 17 && Build.VERSION.SDK_INT < 17);
         public static final boolean preJB = MAX_SDK_VERSION < 16 || (MIN_SDK_VERSION < 16 && Build.VERSION.SDK_INT < 16);
         public static final boolean preICS = MAX_SDK_VERSION < 14 || (MIN_SDK_VERSION < 14 && Build.VERSION.SDK_INT < 14);
         public static final boolean preHCMR2 = MAX_SDK_VERSION < 13 || (MIN_SDK_VERSION < 13 && Build.VERSION.SDK_INT < 13);
         public static final boolean preHCMR1 = MAX_SDK_VERSION < 12 || (MIN_SDK_VERSION < 12 && Build.VERSION.SDK_INT < 12);
         public static final boolean preHC = MAX_SDK_VERSION < 11 || (MIN_SDK_VERSION < 11 && Build.VERSION.SDK_INT < 11);
--- a/mobile/android/base/GeckoAppShell.java
+++ b/mobile/android/base/GeckoAppShell.java
@@ -42,16 +42,17 @@ import org.mozilla.gecko.mozglue.GeckoLo
 import org.mozilla.gecko.mozglue.JNITarget;
 import org.mozilla.gecko.mozglue.RobocopTarget;
 import org.mozilla.gecko.mozglue.generatorannotations.OptionalGeneratedParameter;
 import org.mozilla.gecko.mozglue.generatorannotations.WrapElementForJNI;
 import org.mozilla.gecko.overlays.ui.ShareDialog;
 import org.mozilla.gecko.prompts.PromptService;
 import org.mozilla.gecko.util.EventCallback;
 import org.mozilla.gecko.util.GeckoRequest;
+import org.mozilla.gecko.util.HardwareCodecCapabilityUtils;
 import org.mozilla.gecko.util.HardwareUtils;
 import org.mozilla.gecko.util.NativeEventListener;
 import org.mozilla.gecko.util.NativeJSContainer;
 import org.mozilla.gecko.util.NativeJSObject;
 import org.mozilla.gecko.util.ProxySelector;
 import org.mozilla.gecko.util.ThreadUtils;
 
 import android.annotation.TargetApi;
@@ -87,16 +88,17 @@ import android.hardware.SensorEventListe
 import android.hardware.SensorManager;
 import android.location.Criteria;
 import android.location.Location;
 import android.location.LocationListener;
 import android.location.LocationManager;
 import android.net.ConnectivityManager;
 import android.net.NetworkInfo;
 import android.net.Uri;
+import android.os.Build;
 import android.os.Bundle;
 import android.os.Environment;
 import android.os.Handler;
 import android.os.Looper;
 import android.os.Message;
 import android.os.MessageQueue;
 import android.os.SystemClock;
 import android.os.Vibrator;
@@ -956,16 +958,26 @@ public class GeckoAppShell
         Uri uri = aURL.indexOf(':') >= 0 ? Uri.parse(aURL) : new Uri.Builder().scheme(aURL).build();
 
         Intent intent = getOpenURIIntent(getContext(), uri.toString(), "",
             TextUtils.isEmpty(aAction) ? Intent.ACTION_VIEW : aAction, "");
 
         return getHandlersForIntent(intent);
     }
 
+    @WrapElementForJNI(stubName = "GetHWEncoderCapability")
+    static boolean getHWEncoderCapability() {
+      return HardwareCodecCapabilityUtils.getHWEncoderCapability();
+    }
+
+    @WrapElementForJNI(stubName = "GetHWDecoderCapability")
+    static boolean getHWDecoderCapability() {
+      return HardwareCodecCapabilityUtils.getHWDecoderCapability();
+    }
+
     static List<ResolveInfo> queryIntentActivities(Intent intent) {
         final PackageManager pm = getContext().getPackageManager();
 
         // Exclude any non-exported activities: we can't open them even if we want to!
         // Bug 1031569 has some details.
         final ArrayList<ResolveInfo> list = new ArrayList<>();
         for (ResolveInfo ri: pm.queryIntentActivities(intent, 0)) {
             if (ri.activityInfo.exported) {
--- a/mobile/android/base/home/BrowserSearch.java
+++ b/mobile/android/base/home/BrowserSearch.java
@@ -232,16 +232,18 @@ public class BrowserSearch extends HomeF
 
     @Override
     public void onResume() {
         super.onResume();
 
         // Fetch engines if we need to.
         if (mSearchEngines.isEmpty() || !Locale.getDefault().equals(mLastLocale)) {
             GeckoAppShell.sendEventToGecko(GeckoEvent.createBroadcastEvent("SearchEngines:GetVisible", null));
+        } else {
+            updateSearchEngineBar();
         }
 
         Telemetry.startUISession(TelemetryContract.Session.FRECENCY);
     }
 
     @Override
     public void onPause() {
         super.onPause();
@@ -337,20 +339,16 @@ public class BrowserSearch extends HomeF
                 return false;
             }
         });
 
         registerForContextMenu(mList);
         EventDispatcher.getInstance().registerGeckoThreadListener(this,
             "SearchEngines:Data");
 
-        // If the view backed by this Fragment is being recreated, we will not receive
-        // a new search engine data event so refresh the new search engine bar's data
-        // & Views with the data we have.
-        updateSearchEngineBar();
         mSearchEngineBar.setOnSearchBarClickListener(this);
     }
 
     @Override
     public void onActivityCreated(Bundle savedInstanceState) {
         super.onActivityCreated(savedInstanceState);
 
         // Initialize the search adapter
--- a/mobile/android/base/moz.build
+++ b/mobile/android/base/moz.build
@@ -78,16 +78,17 @@ gujar.sources += [
     'util/EventCallback.java',
     'util/FileUtils.java',
     'util/FloatUtils.java',
     'util/GamepadUtils.java',
     'util/GeckoBackgroundThread.java',
     'util/GeckoEventListener.java',
     'util/GeckoJarReader.java',
     'util/GeckoRequest.java',
+    'util/HardwareCodecCapabilityUtils.java',
     'util/HardwareUtils.java',
     'util/INIParser.java',
     'util/INISection.java',
     'util/InputOptionsUtils.java',
     'util/IOUtils.java',
     'util/JSONUtils.java',
     'util/MenuUtils.java',
     'util/NativeEventListener.java',
--- a/mobile/android/base/preferences/AndroidImportPreference.java
+++ b/mobile/android/base/preferences/AndroidImportPreference.java
@@ -1,15 +1,16 @@
 /* -*- Mode: Java; c-basic-offset: 4; tab-width: 4; indent-tabs-mode: nil; -*-
  * 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/. */
 
 package org.mozilla.gecko.preferences;
 
+import org.mozilla.gecko.AppConstants.Versions;
 import org.mozilla.gecko.R;
 import org.mozilla.gecko.util.ThreadUtils;
 import org.mozilla.gecko.RestrictedProfiles;
 import org.mozilla.gecko.RestrictedProfiles.Restriction;
 
 import java.util.Set;
 
 import android.app.ProgressDialog;
@@ -21,17 +22,18 @@ import android.util.Log;
 class AndroidImportPreference extends MultiPrefMultiChoicePreference {
     private static final String LOGTAG = "AndroidImport";
     public static final String PREF_KEY = "android.not_a_preference.import_android";
     private static final String PREF_KEY_PREFIX = "import_android.data.";
     private final Context mContext;
 
     public static class Handler implements GeckoPreferences.PrefHandler {
         public boolean setupPref(Context context, Preference pref) {
-            return RestrictedProfiles.isAllowed(context, Restriction.DISALLOW_IMPORT_SETTINGS);
+            // Feature disabled on devices running Android M+ (Bug 1183559)
+            return Versions.preM && RestrictedProfiles.isAllowed(context, Restriction.DISALLOW_IMPORT_SETTINGS);
         }
 
         public void onChange(Context context, Preference pref, Object newValue) { }
     }
 
     public AndroidImportPreference(Context context, AttributeSet attrs) {
         super(context, attrs);
         mContext = context;
--- a/mobile/android/base/preferences/GeckoPreferences.java
+++ b/mobile/android/base/preferences/GeckoPreferences.java
@@ -826,17 +826,17 @@ OnSharedPreferenceChangeListener
                 } else if (PREFS_VOICE_INPUT_ENABLED.equals(key)) {
                     if (!InputOptionsUtils.supportsVoiceRecognizer(getApplicationContext(), getResources().getString(R.string.voicesearch_prompt))) {
                         // Remove UI for voice input on non nightly builds.
                         preferences.removePreference(pref);
                         i--;
                         continue;
                     }
                 } else if (PREFS_QRCODE_ENABLED.equals(key)) {
-                    if (!AppConstants.NIGHTLY_BUILD || !InputOptionsUtils.supportsQrCodeReader(getApplicationContext())) {
+                    if (!InputOptionsUtils.supportsQrCodeReader(getApplicationContext())) {
                         // Remove UI for qr code input on non nightly builds
                         preferences.removePreference(pref);
                         i--;
                         continue;
                     }
                 }
 
                 // Some Preference UI elements are not actually preferences,
--- a/mobile/android/base/toolbar/ToolbarEditLayout.java
+++ b/mobile/android/base/toolbar/ToolbarEditLayout.java
@@ -248,20 +248,16 @@ public class ToolbarEditLayout extends T
                 final InputMethodManager imm =
                         (InputMethodManager) getContext().getSystemService(Context.INPUT_METHOD_SERVICE);
                 imm.showSoftInput(mEditText, InputMethodManager.SHOW_IMPLICIT);
             }
         });
     }
 
     private boolean qrCodeIsEnabled(Context context) {
-        // QR code is enabled for nightly only
-        if(!AppConstants.NIGHTLY_BUILD) {
-            return false;
-        }
         final boolean qrCodeIsSupported = InputOptionsUtils.supportsQrCodeReader(context);
         if (!qrCodeIsSupported) {
             return false;
         }
         return GeckoSharedPrefs.forApp(context)
                 .getBoolean(GeckoPreferences.PREFS_QRCODE_ENABLED, true);
     }
 
new file mode 100644
--- /dev/null
+++ b/mobile/android/base/util/HardwareCodecCapabilityUtils.java
@@ -0,0 +1,143 @@
+/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
+ *  * 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/. */
+
+
+package org.mozilla.gecko.util;
+
+import org.mozilla.gecko.AppConstants.Versions;
+
+import android.media.MediaCodecInfo;
+import android.media.MediaCodecInfo.CodecCapabilities;
+import android.media.MediaCodecList;
+import android.util.Log;
+
+public final class HardwareCodecCapabilityUtils {
+  private static final String LOGTAG = "GeckoHardwareCodecCapabilityUtils";
+
+  // List of supported HW VP8 encoders.
+  private static final String[] supportedVp8HwEncCodecPrefixes =
+  {"OMX.qcom.", "OMX.Intel." };
+  // List of supported HW VP8 decoders.
+  private static final String[] supportedVp8HwDecCodecPrefixes =
+  {"OMX.qcom.", "OMX.Nvidia.", "OMX.Exynos.", "OMX.Intel." };
+  private static final String VP8_MIME_TYPE = "video/x-vnd.on2.vp8";
+  // NV12 color format supported by QCOM codec, but not declared in MediaCodec -
+  // see /hardware/qcom/media/mm-core/inc/OMX_QCOMExtns.h
+  private static final int
+    COLOR_QCOM_FORMATYUV420PackedSemiPlanar32m = 0x7FA30C04;
+  // Allowable color formats supported by codec - in order of preference.
+  private static final int[] supportedColorList = {
+    CodecCapabilities.COLOR_FormatYUV420Planar,
+    CodecCapabilities.COLOR_FormatYUV420SemiPlanar,
+    CodecCapabilities.COLOR_QCOM_FormatYUV420SemiPlanar,
+    COLOR_QCOM_FORMATYUV420PackedSemiPlanar32m
+  };
+
+
+  public static boolean getHWEncoderCapability() {
+    if (Versions.feature20Plus) {
+      for (int i = 0; i < MediaCodecList.getCodecCount(); ++i) {
+        MediaCodecInfo info = MediaCodecList.getCodecInfoAt(i);
+        if (!info.isEncoder()) {
+          continue;
+        }
+        String name = null;
+        for (String mimeType : info.getSupportedTypes()) {
+          if (mimeType.equals(VP8_MIME_TYPE)) {
+            name = info.getName();
+            break;
+          }
+        }
+        if (name == null) {
+          continue;  // No HW support in this codec; try the next one.
+        }
+        Log.e(LOGTAG, "Found candidate encoder " + name);
+
+        // Check if this is supported encoder.
+        boolean supportedCodec = false;
+        for (String codecPrefix : supportedVp8HwEncCodecPrefixes) {
+          if (name.startsWith(codecPrefix)) {
+            supportedCodec = true;
+            break;
+          }
+        }
+        if (!supportedCodec) {
+          continue;
+        }
+
+        // Check if codec supports either yuv420 or nv12.
+        CodecCapabilities capabilities =
+          info.getCapabilitiesForType(VP8_MIME_TYPE);
+        for (int colorFormat : capabilities.colorFormats) {
+          Log.v(LOGTAG, "   Color: 0x" + Integer.toHexString(colorFormat));
+        }
+        for (int supportedColorFormat : supportedColorList) {
+          for (int codecColorFormat : capabilities.colorFormats) {
+            if (codecColorFormat == supportedColorFormat) {
+              // Found supported HW Encoder.
+              Log.e(LOGTAG, "Found target encoder " + name +
+                  ". Color: 0x" + Integer.toHexString(codecColorFormat));
+              return true;
+            }
+          }
+        }
+      }
+    }
+    // No HW encoder.
+    return false;
+  }
+
+  public static boolean getHWDecoderCapability() {
+    if (Versions.feature20Plus) { 
+      for (int i = 0; i < MediaCodecList.getCodecCount(); ++i) {
+        MediaCodecInfo info = MediaCodecList.getCodecInfoAt(i);
+        if (info.isEncoder()) {
+          continue;
+        }
+        String name = null;
+        for (String mimeType : info.getSupportedTypes()) {
+          if (mimeType.equals(VP8_MIME_TYPE)) {
+            name = info.getName();
+            break;
+          }
+        }
+        if (name == null) {
+          continue;  // No HW support in this codec; try the next one.
+        }
+        Log.e(LOGTAG, "Found candidate decoder " + name);
+
+        // Check if this is supported decoder.
+        boolean supportedCodec = false;
+        for (String codecPrefix : supportedVp8HwDecCodecPrefixes) {
+          if (name.startsWith(codecPrefix)) {
+            supportedCodec = true;
+            break;
+          }
+        }
+        if (!supportedCodec) {
+          continue;
+        }
+
+        // Check if codec supports either yuv420 or nv12.
+        CodecCapabilities capabilities =
+          info.getCapabilitiesForType(VP8_MIME_TYPE);
+        for (int colorFormat : capabilities.colorFormats) {
+          Log.v(LOGTAG, "   Color: 0x" + Integer.toHexString(colorFormat));
+        }
+        for (int supportedColorFormat : supportedColorList) {
+          for (int codecColorFormat : capabilities.colorFormats) {
+            if (codecColorFormat == supportedColorFormat) {
+              // Found supported HW decoder.
+              Log.e(LOGTAG, "Found target decoder " + name +
+                  ". Color: 0x" + Integer.toHexString(codecColorFormat));
+              return true;
+            }
+          }
+        }
+      }
+    }
+    return false;  // No HW decoder.
+  }
+}
--- a/mobile/android/base/util/InputOptionsUtils.java
+++ b/mobile/android/base/util/InputOptionsUtils.java
@@ -33,14 +33,17 @@ public class InputOptionsUtils {
 
     public static boolean supportsQrCodeReader(Context context) {
         final Intent intent = createQRCodeReaderIntent();
         return supportsIntent(intent, context);
     }
 
     public static Intent createQRCodeReaderIntent() {
         // Bug 602818 enables QR code input if you have the particular app below installed in your device
-        Intent intent = new Intent("com.google.zxing.client.android.SCAN");
+        final String appPackage = "com.google.zxing.client.android";
+
+        Intent intent = new Intent(appPackage + ".SCAN");
+        intent.setPackage(appPackage);
         intent.putExtra("SCAN_MODE", "QR_CODE_MODE");
         intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
         return intent;
     }
 }
--- a/mobile/android/chrome/content/aboutLogins.js
+++ b/mobile/android/chrome/content/aboutLogins.js
@@ -213,17 +213,17 @@ let Logins = {
     let origUsername = this._selectedLogin.username;
     let origPassword = this._selectedLogin.password;
     let origDomain = this._selectedLogin.hostname;
 
     try {
       if ((newUsername === origUsername) &&
           (newPassword === origPassword) &&
           (newDomain === origDomain) ) {
-        gChromeWin.NativeWindow.toast.show(gStringBundle.GetStringFromName("editLogin.saved"), "short");
+        gChromeWin.NativeWindow.toast.show(gStringBundle.GetStringFromName("editLogin.saved1"), "short");
         this._showList();
         return;
       }
 
       let logins = Services.logins.findLogins({}, origDomain, origDomain, null);
 
       for (let i = 0; i < logins.length; i++) {
         if (logins[i].username == origUsername) {
@@ -235,17 +235,17 @@ let Logins = {
           Services.logins.addLogin(clone);
           break;
         }
       }
     } catch (e) {
       gChromeWin.NativeWindow.toast.show(gStringBundle.GetStringFromName("editLogin.couldNotSave"), "short");
       return;
     }
-    gChromeWin.NativeWindow.toast.show(gStringBundle.GetStringFromName("editLogin.saved"), "short");
+    gChromeWin.NativeWindow.toast.show(gStringBundle.GetStringFromName("editLogin.saved1"), "short");
     this._showList();
   },
 
   _onPasswordBtn: function () {
     this._updatePasswordBtn(this._isPasswordBtnInHideMode());
   },
 
   _updatePasswordBtn: function (aShouldShow) {
--- a/mobile/android/chrome/content/browser.js
+++ b/mobile/android/chrome/content/browser.js
@@ -1754,23 +1754,31 @@ var BrowserApp = {
             // Convert document URI into the format used by
             // nsChannelClassifier::ShouldEnableTrackingProtection
             // (any scheme turned into https is correct)
             let normalizedUrl = Services.io.newURI("https://" + browser.currentURI.hostPort, null, null);
             if (data.allowContent) {
               // Add the current host in the 'trackingprotection' consumer of
               // the permission manager using a normalized URI. This effectively
               // places this host on the tracking protection white list.
-              Services.perms.add(normalizedUrl, "trackingprotection", Services.perms.ALLOW_ACTION);
+              if (PrivateBrowsingUtils.isBrowserPrivate(browser)) {
+                PrivateBrowsingUtils.addToTrackingAllowlist(normalizedUrl);
+              } else {
+                Services.perms.add(normalizedUrl, "trackingprotection", Services.perms.ALLOW_ACTION);
+              }
               Telemetry.addData("TRACKING_PROTECTION_EVENTS", 1);
             } else {
               // Remove the current host from the 'trackingprotection' consumer
               // of the permission manager. This effectively removes this host
               // from the tracking protection white list (any list actually).
-              Services.perms.remove(normalizedUrl, "trackingprotection");
+              if (PrivateBrowsingUtils.isBrowserPrivate(browser)) {
+                PrivateBrowsingUtils.removeFromTrackingAllowlist(normalizedUrl);
+              } else {
+                Services.perms.remove(normalizedUrl, "trackingprotection");
+              }
               Telemetry.addData("TRACKING_PROTECTION_EVENTS", 2);
             }
           }
         }
 
         // Try to use the session history to reload so that framesets are
         // handled properly. If the window has no session history, fall back
         // to using the web navigation's reload method.
--- a/mobile/android/installer/package-manifest.in
+++ b/mobile/android/installer/package-manifest.in
@@ -475,16 +475,17 @@
 @BINPATH@/modules/*
 
 #ifdef MOZ_SAFE_BROWSING
 ; Safe Browsing
 @BINPATH@/components/nsURLClassifier.manifest
 @BINPATH@/components/nsUrlClassifierHashCompleter.js
 @BINPATH@/components/nsUrlClassifierListManager.js
 @BINPATH@/components/nsUrlClassifierLib.js
+@BINPATH@/components/PrivateBrowsingTrackingProtectionWhitelist.js
 @BINPATH@/components/url-classifier.xpt
 #endif
 
 ; GNOME hooks
 #ifdef MOZ_ENABLE_GNOME_COMPONENT
 @BINPATH@/components/@DLL_PREFIX@mozgnome@DLL_SUFFIX@
 #endif
 
--- a/mobile/android/locales/en-US/chrome/aboutLogins.properties
+++ b/mobile/android/locales/en-US/chrome/aboutLogins.properties
@@ -9,17 +9,17 @@ loginsMenu.editLogin=Edit login
 loginsMenu.delete=Delete
 
 loginsDialog.confirmDelete=Delete this login?
 loginsDialog.copy=Copy
 loginsDialog.confirm=OK
 loginsDialog.cancel=Cancel
 
 editLogin.fallbackTitle=Edit Login
-editLogin.saved=Saved login
+editLogin.saved1=Saved login
 editLogin.couldNotSave=Changes could not be saved
 
 loginsDetails.age=Age: %S days
 
 loginsDetails.copyFailed=Copy failed
 loginsDetails.passwordCopied=Password copied
 loginsDetails.usernameCopied=Username copied
 
--- a/mobile/android/tests/browser/robocop/StringHelper.java
+++ b/mobile/android/tests/browser/robocop/StringHelper.java
@@ -129,16 +129,18 @@ public class StringHelper {
     public final String TABS_LABEL;
 
     // Display
     public final String TEXT_SIZE_LABEL;
     public final String TITLE_BAR_LABEL = "Title bar";
     public final String SCROLL_TITLE_BAR_LABEL;
     public final String VOICE_INPUT_TITLE_LABEL;
     public final String VOICE_INPUT_SUMMARY_LABEL;
+    public final String QRCODE_INPUT_TITLE_LABEL;
+    public final String QRCODE_INPUT_SUMMARY_LABEL;
     public final String TEXT_REFLOW_LABEL;
     public final String CHARACTER_ENCODING_LABEL;
     public final String PLUGINS_LABEL;
 
     // Title bar
     public final String SHOW_PAGE_TITLE_LABEL = "Show page title";
     public final String SHOW_PAGE_ADDRESS_LABEL = "Show page address";
 
@@ -318,16 +320,18 @@ public class StringHelper {
         IMPORT_FROM_ANDROID_LABEL = res.getString(R.string.pref_import_android);
         TABS_LABEL = res.getString(R.string.pref_restore);
 
         // Display
         TEXT_SIZE_LABEL = res.getString(R.string.pref_text_size);
         SCROLL_TITLE_BAR_LABEL = res.getString(R.string.pref_scroll_title_bar2);
         VOICE_INPUT_TITLE_LABEL = res.getString(R.string.pref_voice_input);
         VOICE_INPUT_SUMMARY_LABEL = res.getString(R.string.pref_voice_input_summary);
+        QRCODE_INPUT_TITLE_LABEL = res.getString(R.string.pref_qrcode_enabled);
+        QRCODE_INPUT_SUMMARY_LABEL = res.getString(R.string.pref_qrcode_enabled_summary);
         TEXT_REFLOW_LABEL = res.getString(R.string.pref_reflow_on_zoom);
         CHARACTER_ENCODING_LABEL = res.getString(R.string.pref_char_encoding);
         PLUGINS_LABEL = res.getString(R.string.pref_plugins);
 
         // Privacy
         TRACKING_PROTECTION_LABEL = res.getString(R.string.pref_tracking_protection_title);
         TRACKING_PROTECTION_PROMPT_TITLE = res.getString(R.string.tracking_protection_prompt_title);
         TRACKING_PROTECTION_PROMPT_BUTTON = res.getString(R.string.tracking_protection_prompt_action_button);
--- a/mobile/android/tests/browser/robocop/testSettingsMenuItems.java
+++ b/mobile/android/tests/browser/robocop/testSettingsMenuItems.java
@@ -228,16 +228,22 @@ public class testSettingsMenuItems exten
             settingsMap.get(PATH_DISPLAY).remove(TITLE_BAR_LABEL_ARR);
         }
 
         // Voice input
         if (InputOptionsUtils.supportsVoiceRecognizer(this.getActivity().getApplicationContext(), this.getActivity().getResources().getString(R.string.voicesearch_prompt))) {
             String[] voiceInputUi = { mStringHelper.VOICE_INPUT_TITLE_LABEL, mStringHelper.VOICE_INPUT_SUMMARY_LABEL };
             settingsMap.get(PATH_DISPLAY).add(voiceInputUi);
         }
+
+        // QR Code input
+        if (InputOptionsUtils.supportsQrCodeReader(this.getActivity().getApplicationContext())) {
+            String[] qrCodeInputUi = { mStringHelper.QRCODE_INPUT_TITLE_LABEL, mStringHelper.QRCODE_INPUT_SUMMARY_LABEL };
+            settingsMap.get(PATH_DISPLAY).add(qrCodeInputUi);
+        }
     }
 
     public void checkMenuHierarchy(Map<String[], List<String[]>> settingsMap) {
         // Check the items within each category.
         String section = null;
         for (Entry<String[], List<String[]>> e : settingsMap.entrySet()) {
             final String[] menuPath = e.getKey();
 
--- a/netwerk/base/nsChannelClassifier.cpp
+++ b/netwerk/base/nsChannelClassifier.cpp
@@ -14,16 +14,17 @@
 #include "nsIDocShell.h"
 #include "nsIDocument.h"
 #include "nsIDOMDocument.h"
 #include "nsIDOMWindow.h"
 #include "nsIHttpChannelInternal.h"
 #include "nsIIOService.h"
 #include "nsIParentChannel.h"
 #include "nsIPermissionManager.h"
+#include "nsIPrivateBrowsingTrackingProtectionWhitelist.h"
 #include "nsIProtocolHandler.h"
 #include "nsIScriptError.h"
 #include "nsIScriptSecurityManager.h"
 #include "nsISecureBrowserUI.h"
 #include "nsISecurityEventSink.h"
 #include "nsIURL.h"
 #include "nsIWebProgressListener.h"
 #include "nsPIDOMWindow.h"
@@ -158,16 +159,35 @@ nsChannelClassifier::ShouldEnableTrackin
 
     if (permissions == nsIPermissionManager::ALLOW_ACTION) {
       mIsAllowListed = true;
       *result = false;
     } else {
       *result = true;
     }
 
+    // In Private Browsing Mode we also check against an in-memory list.
+    if (NS_UsePrivateBrowsing(aChannel)) {
+      nsCOMPtr<nsIPrivateBrowsingTrackingProtectionWhitelist> pbmtpWhitelist =
+          do_GetService(NS_PBTRACKINGPROTECTIONWHITELIST_CONTRACTID, &rv);
+      NS_ENSURE_SUCCESS(rv, rv);
+
+      bool exists = false;
+      rv = pbmtpWhitelist->ExistsInAllowList(topWinURI, &exists);
+      NS_ENSURE_SUCCESS(rv, rv);
+
+      if (exists) {
+        mIsAllowListed = true;
+        LOG(("nsChannelClassifier[%p]: Allowlisting channel[%p] in PBM for %s",
+             this, aChannel, escaped.get()));
+      }
+
+      *result = !exists;
+    }
+
     // Tracking protection will be enabled so return without updating
     // the security state. If any channels are subsequently cancelled
     // (page elements blocked) the state will be then updated.
     if (*result) {
 #ifdef DEBUG
       nsCString topspec;
       nsCString spec;
       topWinURI->GetSpec(topspec);
--- a/services/fxaccounts/FxAccounts.jsm
+++ b/services/fxaccounts/FxAccounts.jsm
@@ -67,35 +67,33 @@ let publicProperties = [
 // somePromiseBasedFunction: function() {
 //   let currentState = this.currentAccountState;
 //   return someOtherPromiseFunction().then(
 //     data => currentState.resolve(data)
 //   );
 // }
 // If the state has changed between the function being called and the promise
 // being resolved, the .resolve() call will actually be rejected.
-let AccountState = this.AccountState = function(fxaInternal, storageManager) {
-  this.fxaInternal = fxaInternal;
+let AccountState = this.AccountState = function(storageManager) {
   this.storageManager = storageManager;
   this.promiseInitialized = this.storageManager.getAccountData().then(data => {
     this.oauthTokens = data && data.oauthTokens ? data.oauthTokens : {};
   }).catch(err => {
     log.error("Failed to initialize the storage manager", err);
     // Things are going to fall apart, but not much we can do about it here.
   });
 };
 
 AccountState.prototype = {
-  cert: null,
-  keyPair: null,
   oauthTokens: null,
   whenVerifiedDeferred: null,
   whenKeysReadyDeferred: null,
 
-  get isCurrent() this.fxaInternal && this.fxaInternal.currentAccountState === this,
+  // If the storage manager has been nuked then we are no longer current.
+  get isCurrent() this.storageManager != null,
 
   abort() {
     if (this.whenVerifiedDeferred) {
       this.whenVerifiedDeferred.reject(
         new Error("Verification aborted; Another user signing in"));
       this.whenVerifiedDeferred = null;
     }
 
@@ -103,17 +101,16 @@ AccountState.prototype = {
       this.whenKeysReadyDeferred.reject(
         new Error("Verification aborted; Another user signing in"));
       this.whenKeysReadyDeferred = null;
     }
 
     this.cert = null;
     this.keyPair = null;
     this.oauthTokens = null;
-    this.fxaInternal = null;
     // Avoid finalizing the storageManager multiple times (ie, .signOut()
     // followed by .abort())
     if (!this.storageManager) {
       return Promise.resolve();
     }
     let storageManager = this.storageManager;
     this.storageManager = null;
     return storageManager.finalize();
@@ -126,92 +123,35 @@ AccountState.prototype = {
     this.oauthTokens = null;
     let storageManager = this.storageManager;
     this.storageManager = null;
     return storageManager.deleteAccountData().then(() => {
       return storageManager.finalize();
     });
   },
 
-  getUserAccountData() {
+  // Get user account data. Optionally specify explcit field names to fetch
+  // (and note that if you require an in-memory field you *must* specify the
+  // field name(s).)
+  getUserAccountData(fieldNames = null) {
     if (!this.isCurrent) {
       return Promise.reject(new Error("Another user has signed in"));
     }
-    return this.storageManager.getAccountData().then(result => {
+    return this.storageManager.getAccountData(fieldNames).then(result => {
       return this.resolve(result);
     });
   },
 
   updateUserAccountData(updatedFields) {
     if (!this.isCurrent) {
       return Promise.reject(new Error("Another user has signed in"));
     }
     return this.storageManager.updateAccountData(updatedFields);
   },
 
-  getCertificate: function(data, keyPair, mustBeValidUntil) {
-    // TODO: get the lifetime from the cert's .exp field
-    if (this.cert && this.cert.validUntil > mustBeValidUntil) {
-      log.debug(" getCertificate already had one");
-      return this.resolve(this.cert.cert);
-    }
-
-    if (Services.io.offline) {
-      return this.reject(new Error(ERROR_OFFLINE));
-    }
-
-    let willBeValidUntil = this.fxaInternal.now() + CERT_LIFETIME;
-    return this.fxaInternal.getCertificateSigned(data.sessionToken,
-                                                 keyPair.serializedPublicKey,
-                                                 CERT_LIFETIME).then(
-      cert => {
-        log.debug("getCertificate got a new one: " + !!cert);
-        this.cert = {
-          cert: cert,
-          validUntil: willBeValidUntil
-        };
-        return cert;
-      }
-    ).then(result => this.resolve(result));
-  },
-
-  getKeyPair: function(mustBeValidUntil) {
-    // If the debugging pref to ignore cached authentication credentials is set for Sync,
-    // then don't use any cached key pair, i.e., generate a new one and get it signed.
-    // The purpose of this pref is to expedite any auth errors as the result of a
-    // expired or revoked FxA session token, e.g., from resetting or changing the FxA
-    // password.
-    let ignoreCachedAuthCredentials = false;
-    try {
-      ignoreCachedAuthCredentials = Services.prefs.getBoolPref("services.sync.debug.ignoreCachedAuthCredentials");
-    } catch(e) {
-      // Pref doesn't exist
-    }
-    if (!ignoreCachedAuthCredentials && this.keyPair && (this.keyPair.validUntil > mustBeValidUntil)) {
-      log.debug("getKeyPair: already have a keyPair");
-      return this.resolve(this.keyPair.keyPair);
-    }
-    // Otherwse, create a keypair and set validity limit.
-    let willBeValidUntil = this.fxaInternal.now() + KEY_LIFETIME;
-    let d = Promise.defer();
-    jwcrypto.generateKeyPair("DS160", (err, kp) => {
-      if (err) {
-        return this.reject(err);
-      }
-      this.keyPair = {
-        keyPair: kp,
-        validUntil: willBeValidUntil
-      };
-      log.debug("got keyPair");
-      delete this.cert;
-      d.resolve(this.keyPair.keyPair);
-    });
-    return d.promise.then(result => this.resolve(result));
-  },
-
   resolve: function(result) {
     if (!this.isCurrent) {
       log.info("An accountState promise was resolved, but was actually rejected" +
                " due to a different user being signed in. Originally resolved" +
                " with", result);
       return Promise.reject(new Error("A different user signed in"));
     }
     return Promise.resolve(result);
@@ -422,17 +362,17 @@ FxAccountsInternal.prototype = {
     }
     return this._profile;
   },
 
   // A hook-point for tests who may want a mocked AccountState or mocked storage.
   newAccountState(credentials) {
     let storage = new FxAccountsStorageManager();
     storage.initialize(credentials);
-    return new AccountState(this, storage);
+    return new AccountState(storage);
   },
 
   /**
    * Return the current time in milliseconds as an integer.  Allows tests to
    * manipulate the date to simulate certificate expiration.
    */
   now: function() {
     return this.fxAccountsClient.now();
@@ -555,34 +495,80 @@ FxAccountsInternal.prototype = {
         }
       }).then(() => {
         return currentAccountState.resolve();
       });
     })
   },
 
   /**
+   * returns a promise that fires with the keypair.
+   */
+  getKeyPair: Task.async(function* (mustBeValidUntil) {
+    // If the debugging pref to ignore cached authentication credentials is set for Sync,
+    // then don't use any cached key pair, i.e., generate a new one and get it signed.
+    // The purpose of this pref is to expedite any auth errors as the result of a
+    // expired or revoked FxA session token, e.g., from resetting or changing the FxA
+    // password.
+    let ignoreCachedAuthCredentials = false;
+    try {
+      ignoreCachedAuthCredentials = Services.prefs.getBoolPref("services.sync.debug.ignoreCachedAuthCredentials");
+    } catch(e) {
+      // Pref doesn't exist
+    }
+    let currentState = this.currentAccountState;
+    let accountData = yield currentState.getUserAccountData("keyPair");
+    if (!ignoreCachedAuthCredentials && accountData.keyPair && (accountData.keyPair.validUntil > mustBeValidUntil)) {
+      log.debug("getKeyPair: already have a keyPair");
+      return accountData.keyPair.keyPair;
+    }
+    // Otherwse, create a keypair and set validity limit.
+    let willBeValidUntil = this.now() + KEY_LIFETIME;
+    let kp = yield new Promise((resolve, reject) => {
+      jwcrypto.generateKeyPair("DS160", (err, kp) => {
+        if (err) {
+          return reject(err);
+        }
+        log.debug("got keyPair");
+        let toUpdate = {
+          keyPair: {
+            keyPair: kp,
+            validUntil: willBeValidUntil
+          },
+          cert: null
+        };
+        currentState.updateUserAccountData(toUpdate).then(() => {
+          resolve(kp);
+        }).catch(err => {
+          log.error("Failed to update account data with keypair and cert");
+        });
+      });
+    });
+    return kp;
+  }),
+
+  /**
    * returns a promise that fires with the assertion.  If there is no verified
    * signed-in user, fires with null.
    */
   getAssertion: function getAssertion(audience) {
     log.debug("enter getAssertion()");
     let currentState = this.currentAccountState;
     let mustBeValidUntil = this.now() + ASSERTION_USE_PERIOD;
     return currentState.getUserAccountData().then(data => {
       if (!data) {
         // No signed-in user
         return null;
       }
       if (!this.isUserEmailVerified(data)) {
         // Signed-in user has not verified email
         return null;
       }
-      return currentState.getKeyPair(mustBeValidUntil).then(keyPair => {
-        return currentState.getCertificate(data, keyPair, mustBeValidUntil)
+      return this.getKeyPair(mustBeValidUntil).then(keyPair => {
+        return this.getCertificate(data, keyPair, mustBeValidUntil)
           .then(cert => {
             return this.getAssertionFromCert(data, keyPair, cert, audience);
           });
       });
     }).then(result => currentState.resolve(result));
   },
 
   /**
@@ -840,16 +826,47 @@ FxAccountsInternal.prototype = {
     }
     return this.fxAccountsClient.signCertificate(
       sessionToken,
       JSON.parse(serializedPublicKey),
       lifetime
     );
   },
 
+  /**
+   * returns a promise that fires with a certificate.
+   */
+  getCertificate: Task.async(function* (data, keyPair, mustBeValidUntil) {
+    // TODO: get the lifetime from the cert's .exp field
+    let currentState = this.currentAccountState;
+    let accountData = yield currentState.getUserAccountData("cert");
+    if (accountData.cert && accountData.cert.validUntil > mustBeValidUntil) {
+      log.debug(" getCertificate already had one");
+      return accountData.cert.cert;
+    }
+    if (Services.io.offline) {
+      throw new Error(ERROR_OFFLINE);
+    }
+    let willBeValidUntil = this.now() + CERT_LIFETIME;
+    let cert = yield this.getCertificateSigned(data.sessionToken,
+                                               keyPair.serializedPublicKey,
+                                               CERT_LIFETIME);
+    log.debug("getCertificate got a new one: " + !!cert);
+    if (cert) {
+      let toUpdate = {
+        cert: {
+          cert: cert,
+          validUntil: willBeValidUntil
+        }
+      };
+      yield currentState.updateUserAccountData(toUpdate);
+    }
+    return cert;
+  }),
+
   getUserAccountData: function() {
     return this.currentAccountState.getUserAccountData();
   },
 
   isUserEmailVerified: function isUserEmailVerified(data) {
     return !!(data && data.verified);
   },
 
--- a/services/fxaccounts/FxAccountsCommon.js
+++ b/services/fxaccounts/FxAccountsCommon.js
@@ -207,23 +207,32 @@ exports.ERROR_AUTH_ERROR                
 exports.ERROR_INVALID_PARAMETER              = "INVALID_PARAMETER";
 
 // Status code errors
 exports.ERROR_CODE_METHOD_NOT_ALLOWED        = 405;
 exports.ERROR_MSG_METHOD_NOT_ALLOWED         = "METHOD_NOT_ALLOWED";
 
 // FxAccounts has the ability to "split" the credentials between a plain-text
 // JSON file in the profile dir and in the login manager.
-// These constants relate to that.
+// In order to prevent new fields accidentally ending up in the "wrong" place,
+// all fields stored are listed here.
 
 // The fields we save in the plaintext JSON.
 // See bug 1013064 comments 23-25 for why the sessionToken is "safe"
-exports.FXA_PWDMGR_PLAINTEXT_FIELDS = ["email", "verified", "authAt",
-                                       "sessionToken", "uid", "oauthTokens",
-                                       "profile"];
+exports.FXA_PWDMGR_PLAINTEXT_FIELDS = new Set(
+  ["email", "verified", "authAt", "sessionToken", "uid", "oauthTokens", "profile"]);
+
+// Fields we store in secure storage if it exists.
+exports.FXA_PWDMGR_SECURE_FIELDS = new Set(
+  ["kA", "kB", "keyFetchToken", "unwrapBKey", "assertion"]);
+
+// Fields we keep in memory and don't persist anywhere.
+exports.FXA_PWDMGR_MEMORY_FIELDS = new Set(
+  ["cert", "keyPair"]);
+
 // The pseudo-host we use in the login manager
 exports.FXA_PWDMGR_HOST = "chrome://FirefoxAccounts";
 // The realm we use in the login manager.
 exports.FXA_PWDMGR_REALM = "Firefox Accounts credentials";
 
 // Error matching.
 exports.SERVER_ERRNO_TO_ERROR = {};
 
--- a/services/fxaccounts/FxAccountsStorage.jsm
+++ b/services/fxaccounts/FxAccountsStorage.jsm
@@ -57,20 +57,28 @@ this.FxAccountsStorageManager.prototype 
   _initialize: Task.async(function* (accountData) {
     log.trace("initializing new storage manager");
     try {
       if (accountData) {
         // If accountData is passed we don't need to read any storage.
         this._needToReadSecure = false;
         // split it into the 2 parts, write it and we are done.
         for (let [name, val] of Iterator(accountData)) {
-          if (FXA_PWDMGR_PLAINTEXT_FIELDS.indexOf(name) >= 0) {
+          if (FXA_PWDMGR_PLAINTEXT_FIELDS.has(name)) {
             this.cachedPlain[name] = val;
+          } else if (FXA_PWDMGR_SECURE_FIELDS.has(name)) {
+            this.cachedSecure[name] = val;
           } else {
-            this.cachedSecure[name] = val;
+            // Hopefully it's an "in memory" field. If it's not we log a warning
+            // but still treat it as such (so it will still be available in this
+            // session but isn't persisted anywhere.)
+            if (!FXA_PWDMGR_MEMORY_FIELDS.has(name)) {
+              log.warn("Unknown FxA field name in user data, treating as in-memory", name);
+            }
+            this.cachedMemory[name] = val;
           }
         }
         // write it out and we are done.
         yield this._write();
         return;
       }
       // So we were initialized without account data - that means we need to
       // read the state from storage. We try and read plain storage first and
@@ -116,40 +124,77 @@ this.FxAccountsStorageManager.prototype 
     // be in a rejected state)
     this._promiseStorageComplete = result.catch(err => {
       log.error("${func} failed: ${err}", {func, err});
     });
     return result;
   },
 
   // Get the account data by combining the plain and secure storage.
-  getAccountData: Task.async(function* () {
+  // If fieldNames is specified, it may be a string or an array of strings,
+  // and only those fields are returned. If not specified the entire account
+  // data is returned except for "in memory" fields. Note that not specifying
+  // field names will soon be deprecated/removed - we want all callers to
+  // specify the fields they care about.
+  getAccountData: Task.async(function* (fieldNames = null) {
     yield this._promiseInitialized;
     // We know we are initialized - this means our .cachedPlain is accurate
     // and doesn't need to be read (it was read if necessary by initialize).
     // So if there's no uid, there's no user signed in.
     if (!('uid' in this.cachedPlain)) {
       return null;
     }
     let result = {};
-    for (let [name, value] of Iterator(this.cachedPlain)) {
-      result[name] = value;
+    if (fieldNames === null) {
+      // The "old" deprecated way of fetching a logged in user.
+      for (let [name, value] of Iterator(this.cachedPlain)) {
+        result[name] = value;
+      }
+      // But the secure data may not have been read, so try that now.
+      yield this._maybeReadAndUpdateSecure();
+      // .cachedSecure now has as much as it possibly can (which is possibly
+      // nothing if (a) secure storage remains locked and (b) we've never updated
+      // a field to be stored in secure storage.)
+      for (let [name, value] of Iterator(this.cachedSecure)) {
+        result[name] = value;
+      }
+      // Note we don't return cachedMemory fields here - they must be explicitly
+      // requested.
+      return result;
+    }
+    // The new explicit way of getting attributes.
+    if (!Array.isArray(fieldNames)) {
+      fieldNames = [fieldNames];
     }
-    // But the secure data may not have been read, so try that now.
-    yield this._maybeReadAndUpdateSecure();
-    // .cachedSecure now has as much as it possibly can (which is possibly
-    // nothing if (a) secure storage remains locked and (b) we've never updated
-    // a field to be stored in secure storage.)
-    for (let [name, value] of Iterator(this.cachedSecure)) {
-      result[name] = value;
+    let checkedSecure = false;
+    for (let fieldName of fieldNames) {
+      if (FXA_PWDMGR_MEMORY_FIELDS.has(fieldName)) {
+        if (this.cachedMemory[fieldName] !== undefined) {
+          result[fieldName] = this.cachedMemory[fieldName];
+        }
+      } else if (FXA_PWDMGR_PLAINTEXT_FIELDS.has(fieldName)) {
+        if (this.cachedPlain[fieldName] !== undefined) {
+          result[fieldName] = this.cachedPlain[fieldName];
+        }
+      } else if (FXA_PWDMGR_SECURE_FIELDS.has(fieldName)) {
+        // We may not have read secure storage yet.
+        if (!checkedSecure) {
+          yield this._maybeReadAndUpdateSecure();
+          checkedSecure = true;
+        }
+        if (this.cachedSecure[fieldName] !== undefined) {
+          result[fieldName] = this.cachedSecure[fieldName];
+        }
+      } else {
+        throw new Error("unexpected field '" + name + "'");
+      }
     }
     return result;
   }),
 
-
   // Update just the specified fields. This DOES NOT allow you to change to
   // a different user, nor to set the user as signed-out.
   updateAccountData: Task.async(function* (newFields) {
     yield this._promiseInitialized;
     if (!('uid' in this.cachedPlain)) {
       // If this storage instance shows no logged in user, then you can't
       // update fields.
       throw new Error("No user is logged in");
@@ -158,38 +203,50 @@ this.FxAccountsStorageManager.prototype 
       // Once we support
       // user changing email address this may need to change, but it's not
       // clear how we would be told of such a change anyway...
       throw new Error("Can't change uid or email address");
     }
     log.debug("_updateAccountData with items", Object.keys(newFields));
     // work out what bucket.
     for (let [name, value] of Iterator(newFields)) {
-      if (FXA_PWDMGR_PLAINTEXT_FIELDS.indexOf(name) >= 0) {
+      if (FXA_PWDMGR_MEMORY_FIELDS.has(name)) {
+        if (value == null) {
+          delete this.cachedMemory[name];
+        } else {
+          this.cachedMemory[name] = value;
+        }
+      } else if (FXA_PWDMGR_PLAINTEXT_FIELDS.has(name)) {
         if (value == null) {
           delete this.cachedPlain[name];
         } else {
           this.cachedPlain[name] = value;
         }
-      } else {
+      } else if (FXA_PWDMGR_SECURE_FIELDS.has(name)) {
         // don't do the "delete on null" thing here - we need to keep it until
         // we have managed to read so we can nuke it on write.
         this.cachedSecure[name] = value;
+      } else {
+        // Throwing seems reasonable here as some client code has explicitly
+        // specified the field name, so it's either confused or needs to update
+        // how this field is to be treated.
+        throw new Error("unexpected field '" + name + "'");
       }
     }
     // If we haven't yet read the secure data, do so now, else we may write
     // out partial data.
     yield this._maybeReadAndUpdateSecure();
     // Now save it - but don't wait on the _write promise - it's queued up as
     // a storage operation, so .finalize() will wait for completion, but no need
     // for us to.
     this._write();
   }),
 
   _clearCachedData() {
+    this.cachedMemory = {};
     this.cachedPlain = {};
     // If we don't have secure storage available we have cachedPlain and
     // cachedSecure be the same object.
     this.cachedSecure = this.secureStorage == null ? this.cachedPlain : {};
   },
 
   /* Reads the plain storage and caches the read values in this.cachedPlain.
      Only ever called once and unlike the "secure" storage, is expected to never
--- a/services/fxaccounts/tests/xpcshell/test_accounts.js
+++ b/services/fxaccounts/tests/xpcshell/test_accounts.js
@@ -150,17 +150,17 @@ function MockFxAccounts() {
     _now_is: new Date(),
     now: function () {
       return this._now_is;
     },
     newAccountState(credentials) {
       // we use a real accountState but mocked storage.
       let storage = new MockStorageManager();
       storage.initialize(credentials);
-      return new AccountState(this, storage);
+      return new AccountState(storage);
     },
     getCertificateSigned: function (sessionToken, serializedPublicKey) {
       _("mock getCertificateSigned\n");
       this._getCertificateSigned_calls.push([sessionToken, serializedPublicKey]);
       return this._d_signCertificate.promise;
     },
     fxAccountsClient: new MockFxAccountsClient()
   });
@@ -197,17 +197,17 @@ add_test(function test_non_https_remote_
 add_task(function test_get_signed_in_user_initially_unset() {
   // This test, unlike many of the the rest, uses a (largely) un-mocked
   // FxAccounts instance.
   let account = new FxAccounts({
     newAccountState(credentials) {
       // we use a real accountState but mocked storage.
       let storage = new MockStorageManager();
       storage.initialize(credentials);
-      return new AccountState(this, storage);
+      return new AccountState(storage);
     },
   });
   let credentials = {
     email: "foo@example.com",
     uid: "1234@lcip.org",
     assertion: "foobar",
     sessionToken: "dead",
     kA: "beef",
@@ -246,17 +246,17 @@ add_task(function* test_getCertificate()
   // This test, unlike many of the the rest, uses a (largely) un-mocked
   // FxAccounts instance.
   // We do mock the storage to keep the test fast on b2g.
   let fxa = new FxAccounts({
     newAccountState(credentials) {
       // we use a real accountState but mocked storage.
       let storage = new MockStorageManager();
       storage.initialize(credentials);
-      return new AccountState(this, storage);
+      return new AccountState(storage);
     },
   });
   let credentials = {
     email: "foo@example.com",
     uid: "1234@lcip.org",
     assertion: "foobar",
     sessionToken: "dead",
     kA: "beef",
@@ -267,17 +267,17 @@ add_task(function* test_getCertificate()
 
   // Test that an expired cert throws if we're offline.
   fxa.internal.currentAccountState.cert = {
     validUntil: Date.parse("Mon, 13 Jan 2000 21:45:06 GMT")
   };
   let offline = Services.io.offline;
   Services.io.offline = true;
   // This call would break from missing parameters ...
-  yield fxa.internal.currentAccountState.getCertificate().then(
+  yield fxa.internal.getCertificate().then(
     result => {
       Services.io.offline = offline;
       do_throw("Unexpected success");
     },
     err => {
       Services.io.offline = offline;
       // ... so we have to check the error string.
       do_check_eq(err, "Error: OFFLINE");
@@ -500,18 +500,19 @@ add_task(function test_getAssertion() {
   fxa.internal._d_signCertificate.resolve("cert1");
   let assertion = yield d;
   do_check_eq(fxa.internal._getCertificateSigned_calls.length, 1);
   do_check_eq(fxa.internal._getCertificateSigned_calls[0][0], "sessionToken");
   do_check_neq(assertion, null);
   _("ASSERTION: " + assertion + "\n");
   let pieces = assertion.split("~");
   do_check_eq(pieces[0], "cert1");
-  let keyPair = fxa.internal.currentAccountState.keyPair;
-  let cert = fxa.internal.currentAccountState.cert;
+  let userData = yield fxa.getSignedInUser();
+  let keyPair = userData.keyPair;
+  let cert = userData.cert;
   do_check_neq(keyPair, undefined);
   _(keyPair.validUntil + "\n");
   let p2 = pieces[1].split(".");
   let header = JSON.parse(atob(p2[0]));
   _("HEADER: " + JSON.stringify(header) + "\n");
   do_check_eq(header.alg, "DS128");
   let payload = JSON.parse(atob(p2[1]));
   _("PAYLOAD: " + JSON.stringify(payload) + "\n");
@@ -548,19 +549,20 @@ add_task(function test_getAssertion() {
   header = JSON.parse(atob(p2[0]));
   payload = JSON.parse(atob(p2[1]));
   do_check_eq(payload.aud, "third.example.com");
 
   // The keypair and cert should have the same validity as before, but the
   // expiration time of the assertion should be different.  We compare this to
   // the initial start time, to which they are relative, not the current value
   // of "now".
+  userData = yield fxa.getSignedInUser();
 
-  keyPair = fxa.internal.currentAccountState.keyPair;
-  cert = fxa.internal.currentAccountState.cert;
+  keyPair = userData.keyPair;
+  cert = userData.cert;
   do_check_eq(keyPair.validUntil, start + KEY_LIFETIME);
   do_check_eq(cert.validUntil, start + CERT_LIFETIME);
   exp = Number(payload.exp);
   do_check_eq(exp, now + ASSERTION_LIFETIME);
 
   // Now we wait even longer, and expect both assertion and cert to expire.  So
   // we will have to get a new keypair and cert.
   now += ONE_DAY_MS;
@@ -571,18 +573,19 @@ add_task(function test_getAssertion() {
   do_check_eq(fxa.internal._getCertificateSigned_calls.length, 2);
   do_check_eq(fxa.internal._getCertificateSigned_calls[1][0], "sessionToken");
   pieces = assertion.split("~");
   do_check_eq(pieces[0], "cert2");
   p2 = pieces[1].split(".");
   header = JSON.parse(atob(p2[0]));
   payload = JSON.parse(atob(p2[1]));
   do_check_eq(payload.aud, "fourth.example.com");
-  keyPair = fxa.internal.currentAccountState.keyPair;
-  cert = fxa.internal.currentAccountState.cert;
+  userData = yield fxa.getSignedInUser();
+  keyPair = userData.keyPair;
+  cert = userData.cert;
   do_check_eq(keyPair.validUntil, now + KEY_LIFETIME);
   do_check_eq(cert.validUntil, now + CERT_LIFETIME);
   exp = Number(payload.exp);
 
   do_check_eq(exp, now + ASSERTION_LIFETIME);
   _("----- DONE ----\n");
 });
 
--- a/services/fxaccounts/tests/xpcshell/test_oauth_token_storage.js
+++ b/services/fxaccounts/tests/xpcshell/test_oauth_token_storage.js
@@ -80,17 +80,17 @@ MockFxAccountsClient.prototype = {
 
 function MockFxAccounts() {
   return new FxAccounts({
     fxAccountsClient: new MockFxAccountsClient(),
     newAccountState(credentials) {
       // we use a real accountState but mocked storage.
       let storage = new MockStorageManager();
       storage.initialize(credentials);
-      return new AccountState(this, storage);
+      return new AccountState(storage);
     },
   });
 }
 
 function* createMockFxA() {
   let fxa = new MockFxAccounts();
   let credentials = {
     email: "foo@example.com",
--- a/services/fxaccounts/tests/xpcshell/test_storage_manager.js
+++ b/services/fxaccounts/tests/xpcshell/test_storage_manager.js
@@ -46,19 +46,21 @@ function MockedSecureStorage(accountData
       accountData: accountData,
     }
   }
   this.data = data;
   this.numReads = 0;
 }
 
 MockedSecureStorage.prototype = {
+  fetchCount: 0,
   locked: false,
   STORAGE_LOCKED: function() {},
   get: Task.async(function* (uid, email) {
+    this.fetchCount++;
     if (this.locked) {
       throw new this.STORAGE_LOCKED();
     }
     this.numReads++;
     Assert.equal(this.numReads, 1, "should only ever be 1 read of unlocked data");
     return this.data;
   }),
 
@@ -80,17 +82,17 @@ function add_storage_task(testFunction) 
 
 // initialized without account data and there's nothing to read. Not logged in.
 add_storage_task(function* checkInitializedEmpty(sm) {
   if (sm.secureStorage) {
     sm.secureStorage = new MockedSecureStorage(null);
   }
   yield sm.initialize();
   Assert.strictEqual((yield sm.getAccountData()), null);
-  Assert.rejects(sm.updateAccountData({foo: "bar"}), "No user is logged in")
+  Assert.rejects(sm.updateAccountData({kA: "kA"}), "No user is logged in")
 });
 
 // Initialized with account data (ie, simulating a new user being logged in).
 // Should reflect the initial data and be written to storage.
 add_storage_task(function* checkNewUser(sm) {
   let initialAccountData = {
     uid: "uid",
     email: "someone@somewhere.com",
@@ -125,31 +127,31 @@ add_storage_task(function* checkEverythi
   }
   yield sm.initialize();
   let accountData = yield sm.getAccountData();
   Assert.ok(accountData, "read account data");
   Assert.equal(accountData.uid, "uid");
   Assert.equal(accountData.email, "someone@somewhere.com");
   // Update the data - we should be able to fetch it back and it should appear
   // in our storage.
-  yield sm.updateAccountData({verified: true, foo: "bar", kA: "kA"});
+  yield sm.updateAccountData({verified: true, kA: "kA", kB: "kB"});
   accountData = yield sm.getAccountData();
-  Assert.equal(accountData.foo, "bar");
+  Assert.equal(accountData.kB, "kB");
   Assert.equal(accountData.kA, "kA");
   // Check the new value was written to storage.
   yield sm._promiseStorageComplete; // storage is written in the background.
   // "verified" is a plain-text field.
   Assert.equal(sm.plainStorage.data.accountData.verified, true);
   // "kA" and "foo" are secure
   if (sm.secureStorage) {
     Assert.equal(sm.secureStorage.data.accountData.kA, "kA");
-    Assert.equal(sm.secureStorage.data.accountData.foo, "bar");
+    Assert.equal(sm.secureStorage.data.accountData.kB, "kB");
   } else {
     Assert.equal(sm.plainStorage.data.accountData.kA, "kA");
-    Assert.equal(sm.plainStorage.data.accountData.foo, "bar");
+    Assert.equal(sm.plainStorage.data.accountData.kB, "kB");
   }
 });
 
 add_storage_task(function* checkInvalidUpdates(sm) {
   sm.plainStorage = new MockedPlainStorage({uid: "uid", email: "someone@somewhere.com"})
   if (sm.secureStorage) {
     sm.secureStorage = new MockedSecureStorage(null);
   }
@@ -226,16 +228,63 @@ add_task(function* checkEverythingReadSe
 
   let accountData = yield sm.getAccountData();
   Assert.ok(accountData, "read account data");
   Assert.equal(accountData.uid, "uid");
   Assert.equal(accountData.email, "someone@somewhere.com");
   Assert.equal(accountData.kA, "kA");
 });
 
+add_task(function* checkMemoryFieldsNotReturnedByDefault() {
+  let sm = new FxAccountsStorageManager();
+  sm.plainStorage = new MockedPlainStorage({uid: "uid", email: "someone@somewhere.com"})
+  sm.secureStorage = new MockedSecureStorage({kA: "kA"});
+  yield sm.initialize();
+
+  // keyPair is a memory field.
+  yield sm.updateAccountData({keyPair: "the keypair value"});
+  let accountData = yield sm.getAccountData();
+
+  // Requesting everything should *not* return in memory fields.
+  Assert.strictEqual(accountData.keyPair, undefined);
+  // But requesting them specifically does get them.
+  accountData = yield sm.getAccountData("keyPair");
+  Assert.strictEqual(accountData.keyPair, "the keypair value");
+});
+
+add_task(function* checkExplicitGet() {
+  let sm = new FxAccountsStorageManager();
+  sm.plainStorage = new MockedPlainStorage({uid: "uid", email: "someone@somewhere.com"})
+  sm.secureStorage = new MockedSecureStorage({kA: "kA"});
+  yield sm.initialize();
+
+  let accountData = yield sm.getAccountData(["uid", "kA"]);
+  Assert.ok(accountData, "read account data");
+  Assert.equal(accountData.uid, "uid");
+  Assert.equal(accountData.kA, "kA");
+  // We didn't ask for email so shouldn't have got it.
+  Assert.strictEqual(accountData.email, undefined);
+});
+
+add_task(function* checkExplicitGetNoSecureRead() {
+  let sm = new FxAccountsStorageManager();
+  sm.plainStorage = new MockedPlainStorage({uid: "uid", email: "someone@somewhere.com"})
+  sm.secureStorage = new MockedSecureStorage({kA: "kA"});
+  yield sm.initialize();
+
+  Assert.equal(sm.secureStorage.fetchCount, 0);
+  // request 2 fields in secure storage - it should have caused a single fetch.
+  let accountData = yield sm.getAccountData(["email", "uid"]);
+  Assert.ok(accountData, "read account data");
+  Assert.equal(accountData.uid, "uid");
+  Assert.equal(accountData.email, "someone@somewhere.com");
+  Assert.strictEqual(accountData.kA, undefined);
+  Assert.equal(sm.secureStorage.fetchCount, 1);
+});
+
 add_task(function* checkLockedUpdates() {
   let sm = new FxAccountsStorageManager();
   sm.plainStorage = new MockedPlainStorage({uid: "uid", email: "someone@somewhere.com"})
   sm.secureStorage = new MockedSecureStorage({kA: "old-kA", kB: "kB"});
   sm.secureStorage.locked = true;
   yield sm.initialize();
 
   let accountData = yield sm.getAccountData();
--- a/services/sync/modules-testing/utils.js
+++ b/services/sync/modules-testing/utils.js
@@ -176,27 +176,27 @@ this.configureFxAccountIdentity = functi
     newAccountState(credentials) {
       // We only expect this to be called with null indicating the (mock)
       // storage should be read.
       if (credentials) {
         throw new Error("Not expecting to have credentials passed");
       }
       let storageManager = new MockFxaStorageManager();
       storageManager.initialize(config.fxaccount.user);
-      let accountState = new AccountState(this, storageManager);
-      // mock getCertificate
-      accountState.getCertificate = function(data, keyPair, mustBeValidUntil) {
-        accountState.cert = {
-          validUntil: fxa.internal.now() + CERT_LIFETIME,
-          cert: "certificate",
-        };
-        return Promise.resolve(this.cert.cert);
-      }
+      let accountState = new AccountState(storageManager);
       return accountState;
-    }
+    },
+    getCertificate(data, keyPair, mustBeValidUntil) {
+      let cert = {
+        validUntil: this.now() + CERT_LIFETIME,
+        cert: "certificate",
+      };
+      this.currentAccountState.updateUserAccountData({cert: cert});
+      return Promise.resolve(cert.cert);
+    },
   };
   fxa = new FxAccounts(MockInternal);
 
   let mockTSC = { // TokenServerClient
     getTokenFromBrowserIDAssertion: function(uri, assertion, cb) {
       config.fxaccount.token.uid = config.username;
       cb(null, config.fxaccount.token);
     },
--- a/services/sync/tests/unit/test_browserid_identity.js
+++ b/services/sync/tests/unit/test_browserid_identity.js
@@ -590,17 +590,17 @@ add_task(function test_getKeysMissing() 
     newAccountState(credentials) {
       // We only expect this to be called with null indicating the (mock)
       // storage should be read.
       if (credentials) {
         throw new Error("Not expecting to have credentials passed");
       }
       let storageManager = new MockFxaStorageManager();
       storageManager.initialize(identityConfig.fxaccount.user);
-      return new AccountState(this, storageManager);
+      return new AccountState(storageManager);
     },
   });
 
   // Add a mock to the currentAccountState object.
   fxa.internal.currentAccountState.getCertificate = function(data, keyPair, mustBeValidUntil) {
     this.cert = {
       validUntil: fxa.internal.now() + CERT_LIFETIME,
       cert: "certificate",
@@ -669,17 +669,17 @@ function* initializeIdentityWithHAWKResp
     newAccountState(credentials) {
       // We only expect this to be called with null indicating the (mock)
       // storage should be read.
       if (credentials) {
         throw new Error("Not expecting to have credentials passed");
       }
       let storageManager = new MockFxaStorageManager();
       storageManager.initialize(config.fxaccount.user);
-      return new AccountState(this, storageManager);
+      return new AccountState(storageManager);
     },
   }
   let fxa = new FxAccounts(internal);
 
   browseridManager._fxaService = fxa;
   browseridManager._signedInUser = null;
   yield browseridManager.initializeWithCurrentIdentity();
   yield Assert.rejects(browseridManager.whenReadyToAuthenticate.promise,
--- a/toolkit/components/places/AsyncFaviconHelpers.cpp
+++ b/toolkit/components/places/AsyncFaviconHelpers.cpp
@@ -343,17 +343,17 @@ OptimizeIconSize(IconData& aIcon,
 } // namespace
 
 
 ////////////////////////////////////////////////////////////////////////////////
 //// AsyncFaviconHelperBase
 
 AsyncFaviconHelperBase::AsyncFaviconHelperBase(
   nsCOMPtr<nsIFaviconDataCallback>& aCallback
-) : mDB(Database::GetDatabase())
+)
 {
   // Don't AddRef or Release in runnables for thread-safety.
   mCallback.swap(aCallback);
 }
 
 AsyncFaviconHelperBase::~AsyncFaviconHelperBase()
 {
   nsCOMPtr<nsIThread> thread;
@@ -446,30 +446,32 @@ AsyncFetchAndSetIconForPage::~AsyncFetch
 
 NS_IMETHODIMP
 AsyncFetchAndSetIconForPage::Run()
 {
   NS_PRECONDITION(!NS_IsMainThread(),
                   "This should not be called on the main thread");
 
   // Try to fetch the icon from the database.
-  nsresult rv = FetchIconInfo(mDB, mIcon);
+  nsRefPtr<Database> DB = Database::GetDatabase();
+  NS_ENSURE_STATE(DB);
+  nsresult rv = FetchIconInfo(DB, mIcon);
   NS_ENSURE_SUCCESS(rv, rv);
 
   bool isInvalidIcon = mIcon.data.IsEmpty() ||
                        (mIcon.expiration && PR_Now() > mIcon.expiration);
   bool fetchIconFromNetwork = mIcon.fetchMode == FETCH_ALWAYS ||
                               (mIcon.fetchMode == FETCH_IF_MISSING && isInvalidIcon);
 
   if (!fetchIconFromNetwork) {
     // There is already a valid icon or we don't want to fetch a new one,
     // directly proceed with association.
     nsRefPtr<AsyncAssociateIconToPage> event =
         new AsyncAssociateIconToPage(mIcon, mPage, mCallback);
-    mDB->DispatchToAsyncThread(event);
+    DB->DispatchToAsyncThread(event);
 
     return NS_OK;
   }
   else {
     // Fetch the icon from network.  When done this will associate the
     // icon to the page and notify.
     nsRefPtr<AsyncFetchAndSetIconFromNetwork> event =
       new AsyncFetchAndSetIconFromNetwork(mIcon, mPage, mFaviconLoadPrivate, mCallback);
@@ -690,19 +692,21 @@ AsyncFetchAndSetIconFromNetwork::OnStopR
   // If over the maximum size allowed, don't save data to the database to
   // avoid bloating it.
   if (mIcon.data.Length() > MAX_FAVICON_SIZE) {
     return NS_OK;
   }
 
   mIcon.status = ICON_STATUS_CHANGED;
 
+  nsRefPtr<Database> DB = Database::GetDatabase();
+  NS_ENSURE_STATE(DB);
   nsRefPtr<AsyncAssociateIconToPage> event =
     new AsyncAssociateIconToPage(mIcon, mPage, mCallback);
-  mDB->DispatchToAsyncThread(event);
+  DB->DispatchToAsyncThread(event);
 
   return NS_OK;
 }
 
 ////////////////////////////////////////////////////////////////////////////////
 //// AsyncAssociateIconToPage
 
 AsyncAssociateIconToPage::AsyncAssociateIconToPage(
@@ -720,64 +724,66 @@ AsyncAssociateIconToPage::~AsyncAssociat
 }
 
 NS_IMETHODIMP
 AsyncAssociateIconToPage::Run()
 {
   NS_PRECONDITION(!NS_IsMainThread(),
                   "This should not be called on the main thread");
 
-  nsresult rv = FetchPageInfo(mDB, mPage);
+  nsRefPtr<Database> DB = Database::GetDatabase();
+  NS_ENSURE_STATE(DB);
+  nsresult rv = FetchPageInfo(DB, mPage);
   if (rv == NS_ERROR_NOT_AVAILABLE){
     // We have never seen this page.  If we can add the page to history,
     // we will try to do it later, otherwise just bail out.
     if (!mPage.canAddToHistory) {
       return NS_OK;
     }
   }
   else {
     NS_ENSURE_SUCCESS(rv, rv);
   }
 
-  mozStorageTransaction transaction(mDB->MainConn(), false,
+  mozStorageTransaction transaction(DB->MainConn(), false,
                                     mozIStorageConnection::TRANSACTION_IMMEDIATE);
 
   // If there is no entry for this icon, or the entry is obsolete, replace it.
   if (mIcon.id == 0 || (mIcon.status & ICON_STATUS_CHANGED)) {
-    rv = SetIconInfo(mDB, mIcon);
+    rv = SetIconInfo(DB, mIcon);
     NS_ENSURE_SUCCESS(rv, rv);
 
     // Get the new icon id.  Do this regardless mIcon.id, since other code
     // could have added a entry before us.  Indeed we interrupted the thread
     // after the previous call to FetchIconInfo.
     mIcon.status = (mIcon.status & ~(ICON_STATUS_CACHED)) | ICON_STATUS_SAVED;
-    rv = FetchIconInfo(mDB, mIcon);
+    rv = FetchIconInfo(DB, mIcon);
     NS_ENSURE_SUCCESS(rv, rv);
   }
 
   // If the page does not have an id, don't try to insert a new one, cause we
   // don't know where the page comes from.  Not doing so we may end adding
   // a page that otherwise we'd explicitly ignore, like a POST or an error page.
   if (mPage.id == 0) {
     return NS_OK;
   }
 
   // Otherwise just associate the icon to the page, if needed.
   if (mPage.iconId != mIcon.id) {
     nsCOMPtr<mozIStorageStatement> stmt;
     if (mPage.id) {
-      stmt = mDB->GetStatement(
+      stmt = DB->GetStatement(
         "UPDATE moz_places SET favicon_id = :icon_id WHERE id = :page_id"
       );
       NS_ENSURE_STATE(stmt);
       rv = stmt->BindInt64ByName(NS_LITERAL_CSTRING("page_id"), mPage.id);
       NS_ENSURE_SUCCESS(rv, rv);
     }
     else {
-      stmt = mDB->GetStatement(
+      stmt = DB->GetStatement(
         "UPDATE moz_places SET favicon_id = :icon_id WHERE url = :page_url"
       );
       NS_ENSURE_STATE(stmt);
       rv = URIBinder::Bind(stmt, NS_LITERAL_CSTRING("page_url"), mPage.spec);
       NS_ENSURE_SUCCESS(rv, rv);
     }
     rv = stmt->BindInt64ByName(NS_LITERAL_CSTRING("icon_id"), mIcon.id);
     NS_ENSURE_SUCCESS(rv, rv);
@@ -841,18 +847,20 @@ AsyncGetFaviconURLForPage::~AsyncGetFavi
 }
 
 NS_IMETHODIMP
 AsyncGetFaviconURLForPage::Run()
 {
   NS_PRECONDITION(!NS_IsMainThread(),
                   "This should not be called on the main thread.");
 
+  nsRefPtr<Database> DB = Database::GetDatabase();
+  NS_ENSURE_STATE(DB);
   nsAutoCString iconSpec;
-  nsresult rv = FetchIconURL(mDB, mPageSpec, iconSpec);
+  nsresult rv = FetchIconURL(DB, mPageSpec, iconSpec);
   NS_ENSURE_SUCCESS(rv, rv);
 
   // Now notify our callback of the icon spec we retrieved, even if empty.
   IconData iconData;
   iconData.spec.Assign(iconSpec);
 
   PageData pageData;
   pageData.spec.Assign(mPageSpec);
@@ -906,28 +914,30 @@ AsyncGetFaviconDataForPage::~AsyncGetFav
 }
 
 NS_IMETHODIMP
 AsyncGetFaviconDataForPage::Run()
 {
   NS_PRECONDITION(!NS_IsMainThread(),
                   "This should not be called on the main thread.");
 
+  nsRefPtr<Database> DB = Database::GetDatabase();
+  NS_ENSURE_STATE(DB);
   nsAutoCString iconSpec;
-  nsresult rv = FetchIconURL(mDB, mPageSpec, iconSpec);
+  nsresult rv = FetchIconURL(DB, mPageSpec, iconSpec);
   NS_ENSURE_SUCCESS(rv, rv);
 
   IconData iconData;
   iconData.spec.Assign(iconSpec);
 
   PageData pageData;
   pageData.spec.Assign(mPageSpec);
 
   if (!iconSpec.IsEmpty()) {
-    rv = FetchIconInfo(mDB, iconData);
+    rv = FetchIconInfo(DB, iconData);
     if (NS_FAILED(rv)) {
       iconData.spec.Truncate();
     }
   }
 
   nsCOMPtr<nsIRunnable> event =
     new NotifyIconObservers(iconData, pageData, mCallback);
   rv = NS_DispatchToMainThread(event);
@@ -970,26 +980,28 @@ AsyncReplaceFaviconData::~AsyncReplaceFa
 }
 
 NS_IMETHODIMP
 AsyncReplaceFaviconData::Run()
 {
   NS_PRECONDITION(!NS_IsMainThread(),
                   "This should not be called on the main thread");
 
+  nsRefPtr<Database> DB = Database::GetDatabase();
+  NS_ENSURE_STATE(DB);
   IconData dbIcon;
   dbIcon.spec.Assign(mIcon.spec);
-  nsresult rv = FetchIconInfo(mDB, dbIcon);
+  nsresult rv = FetchIconInfo(DB, dbIcon);
   NS_ENSURE_SUCCESS(rv, rv);
 
   if (!dbIcon.id) {
     return NS_OK;
   }
 
-  rv = SetIconInfo(mDB, mIcon);
+  rv = SetIconInfo(DB, mIcon);
   NS_ENSURE_SUCCESS(rv, rv);
 
   // We can invalidate the cache version since we now persist the icon.
   nsCOMPtr<nsIRunnable> event = new RemoveIconDataCacheEntry(mIcon, mCallback);
   rv = NS_DispatchToMainThread(event);
   NS_ENSURE_SUCCESS(rv, rv);
 
   return NS_OK;
@@ -1091,18 +1103,21 @@ NotifyIconObservers::SendGlobalNotificat
   // If the page is bookmarked and the bookmarked url is different from the
   // updated one, start a new task to update its icon as well.
   if (!mPage.bookmarkedSpec.IsEmpty() &&
       !mPage.bookmarkedSpec.Equals(mPage.spec)) {
     // Create a new page struct to avoid polluting it with old data.
     PageData bookmarkedPage;
     bookmarkedPage.spec = mPage.bookmarkedSpec;
 
+    nsRefPtr<Database> DB = Database::GetDatabase();
+    if (!DB)
+      return;
     // This will be silent, so be sure to not pass in the current callback.
     nsCOMPtr<nsIFaviconDataCallback> nullCallback;
     nsRefPtr<AsyncAssociateIconToPage> event =
         new AsyncAssociateIconToPage(mIcon, bookmarkedPage, nullCallback);
-    mDB->DispatchToAsyncThread(event);
+    DB->DispatchToAsyncThread(event);
   }
 }
 
 } // namespace places
 } // namespace mozilla
--- a/toolkit/components/places/AsyncFaviconHelpers.h
+++ b/toolkit/components/places/AsyncFaviconHelpers.h
@@ -97,17 +97,16 @@ struct PageData
  */
 class AsyncFaviconHelperBase : public nsRunnable
 {
 protected:
   explicit AsyncFaviconHelperBase(nsCOMPtr<nsIFaviconDataCallback>& aCallback);
 
   virtual ~AsyncFaviconHelperBase();
 
-  nsRefPtr<Database> mDB;
   // Strong reference since we are responsible for its existence.
   nsCOMPtr<nsIFaviconDataCallback> mCallback;
 };
 
 /**
  * Async fetches icon from database or network, associates it with the required
  * page and finally notifies the change.
  */
--- a/toolkit/components/places/Database.cpp
+++ b/toolkit/components/places/Database.cpp
@@ -310,18 +310,18 @@ public:
   explicit DatabaseShutdown(Database* aDatabase);
 
   already_AddRefed<nsIAsyncShutdownClient> GetClient();
 
   /**
    * `true` if we have not started shutdown, i.e.  if
    * `BlockShutdown()` hasn't been called yet, false otherwise.
    */
-  bool IsStarted() const {
-    return mIsStarted;
+  static bool IsStarted() {
+    return sIsStarted;
   }
 
 private:
   nsCOMPtr<nsIAsyncShutdownBarrier> mBarrier;
   nsCOMPtr<nsIAsyncShutdownClient> mParentClient;
 
   // The owning database.
   // The cycle is broken in method Complete(), once the connection
@@ -349,31 +349,32 @@ private:
 
     // Execution of `Complete` in progress
     // a. `Complete` is starting.
     RECEIVED_STORAGESHUTDOWN_COMPLETE,
     // b. We have notified observers that Places as closed connection.
     NOTIFIED_OBSERVERS_PLACES_CONNECTION_CLOSED,
   };
   State mState;
-  bool mIsStarted;
 
   // As tests may resurrect a dead `Database`, we use a counter to
   // give the instances of `DatabaseShutdown` unique names.
   uint16_t mCounter;
   static uint16_t sCounter;
 
+  static Atomic<bool> sIsStarted;
+
   ~DatabaseShutdown() {}
 };
 uint16_t DatabaseShutdown::sCounter = 0;
+Atomic<bool> DatabaseShutdown::sIsStarted(false);
 
 DatabaseShutdown::DatabaseShutdown(Database* aDatabase)
   : mDatabase(aDatabase)
   , mState(NOT_STARTED)
-  , mIsStarted(false)
   , mCounter(sCounter++)
 {
   MOZ_ASSERT(NS_IsMainThread());
   nsCOMPtr<nsIAsyncShutdownService> asyncShutdownSvc = services::GetAsyncShutdown();
   MOZ_ASSERT(asyncShutdownSvc);
 
   if (asyncShutdownSvc) {
     DebugOnly<nsresult> rv = asyncShutdownSvc->MakeBarrier(
@@ -460,17 +461,17 @@ NS_IMETHODIMP DatabaseShutdown::GetState
 // of `this` barrier have completed their own shutdown.
 //
 // See `Done()` for step 2.
 NS_IMETHODIMP
 DatabaseShutdown::BlockShutdown(nsIAsyncShutdownClient* aParentClient)
 {
   mParentClient = aParentClient;
   mState = RECEIVED_BLOCK_SHUTDOWN;
-  mIsStarted = true;
+  sIsStarted = true;
 
   if (NS_WARN_IF(!mBarrier)) {
     return NS_ERROR_NOT_AVAILABLE;
   }
 
   // Wait until all clients have removed their blockers, then proceed
   // with own shutdown.
   DebugOnly<nsresult> rv = mBarrier->Wait(this);
@@ -657,16 +658,25 @@ Database::GetStatement(const nsACString&
 already_AddRefed<nsIAsyncShutdownClient>
 Database::GetConnectionShutdown()
 {
   MOZ_ASSERT(mConnectionShutdown);
 
   return mConnectionShutdown->GetClient();
 }
 
+// static
+already_AddRefed<Database>
+Database::GetDatabase()
+{
+  if (DatabaseShutdown::IsStarted())
+    return nullptr;
+  return GetSingleton();
+}
+
 nsresult
 Database::Init()
 {
   MOZ_ASSERT(NS_IsMainThread());
 
   nsCOMPtr<mozIStorageService> storage =
     do_GetService(MOZ_STORAGE_SERVICE_CONTRACTID);
   NS_ENSURE_STATE(storage);
--- a/toolkit/components/places/Database.h
+++ b/toolkit/components/places/Database.h
@@ -89,20 +89,17 @@ public:
    */
   already_AddRefed<nsIAsyncShutdownClient> GetConnectionShutdown();
 
   /**
    * Getter to use when instantiating the class.
    *
    * @return Singleton instance of this class.
    */
-  static already_AddRefed<Database> GetDatabase()
-  {
-    return GetSingleton();
-  }
+  static already_AddRefed<Database> GetDatabase();
 
   /**
    * Returns last known database status.
    *
    * @return one of the nsINavHistoryService::DATABASE_STATUS_* constants.
    */
   uint16_t GetDatabaseStatus() const
   {
--- a/toolkit/components/telemetry/TelemetrySend.jsm
+++ b/toolkit/components/telemetry/TelemetrySend.jsm
@@ -141,17 +141,16 @@ function gzipCompressString(string) {
                      .createInstance(Ci.nsIStringInputStream);
   stringStream.data = string;
   converter.onStartRequest(null, null);
   converter.onDataAvailable(null, null, stringStream, 0, string.length);
   converter.onStopRequest(null, null, null);
   return observer.buffer;
 }
 
-
 this.TelemetrySend = {
 
   /**
    * Age in ms of a pending ping to be considered overdue.
    */
   get OVERDUE_PING_FILE_AGE() {
     return OVERDUE_PING_FILE_AGE;
   },
@@ -386,18 +385,22 @@ let SendScheduler = {
       }
 
       // Get a list of pending pings, sorted by last modified, descending.
       // Filter out all the pings we can't send now. This addresses scenarios like "deletion" pings
       // which can be send even when upload is disabled.
       let pending = TelemetryStorage.getPendingPingList();
       let current = TelemetrySendImpl.getUnpersistedPings();
       this._log.trace("_doSendTask - pending: " + pending.length + ", current: " + current.length);
-      pending = pending.filter(p => TelemetrySendImpl.sendingEnabled(p));
-      current = current.filter(p => TelemetrySendImpl.sendingEnabled(p));
+      // Note that the two lists contain different kind of data. |pending| only holds ping
+      // info, while |current| holds actual ping data.
+      if (!TelemetrySendImpl.sendingEnabled()) {
+        pending = pending.filter(pingInfo => TelemetryStorage.isDeletionPing(pingInfo.id));
+        current = current.filter(p => isDeletionPing(p));
+      }
       this._log.trace("_doSendTask - can send - pending: " + pending.length + ", current: " + current.length);
 
       // Bail out if there is nothing to send.
       if ((pending.length == 0) && (current.length == 0)) {
         this._log.trace("_doSendTask - no pending pings, bailing out");
         this._sendTaskState = "bail out - no pings to send";
         return;
       }
@@ -707,17 +710,22 @@ let TelemetrySendImpl = {
 
     for (let current of currentPings) {
       let ping = current;
       let p = Task.spawn(function*() {
         try {
           yield this._doPing(ping, ping.id, false);
         } catch (ex) {
           this._log.info("sendPings - ping " + ping.id + " not sent, saving to disk", ex);
-          yield TelemetryStorage.savePendingPing(ping);
+          // Deletion pings must be saved to a special location.
+          if (isDeletionPing(ping)) {
+            yield TelemetryStorage.saveDeletionPing(ping);
+          } else {
+            yield TelemetryStorage.savePendingPing(ping);
+          }
         } finally {
           this._currentPings.delete(ping.id);
         }
       }.bind(this));
 
       this._trackPendingPingTask(p);
       pingSends.push(p);
     }
@@ -779,16 +787,19 @@ let TelemetrySendImpl = {
     hping.add(new Date() - startTime);
 
     if (!success) {
       // Let the scheduler know about send failures for triggering backoff timeouts.
       SendScheduler.notifySendsFailed();
     }
 
     if (success && isPersisted) {
+      if (TelemetryStorage.isDeletionPing(id)) {
+        return TelemetryStorage.removeDeletionPing();
+      }
       return TelemetryStorage.removePendingPing(id);
     } else {
       return Promise.resolve();
     }
   },
 
   _getSubmissionPath: function(ping) {
     // The new ping format contains an "application" section, the old one doesn't.
--- a/toolkit/components/telemetry/TelemetryStorage.jsm
+++ b/toolkit/components/telemetry/TelemetryStorage.jsm
@@ -29,26 +29,30 @@ const LOGGER_PREFIX = "TelemetryStorage:
 
 const Telemetry = Services.telemetry;
 const Utils = TelemetryUtils;
 
 // Compute the path of the pings archive on the first use.
 const DATAREPORTING_DIR = "datareporting";
 const PINGS_ARCHIVE_DIR = "archived";
 const ABORTED_SESSION_FILE_NAME = "aborted-session-ping";
+const DELETION_PING_FILE_NAME = "pending-deletion-ping";
 
 XPCOMUtils.defineLazyGetter(this, "gDataReportingDir", function() {
   return OS.Path.join(OS.Constants.Path.profileDir, DATAREPORTING_DIR);
 });
 XPCOMUtils.defineLazyGetter(this, "gPingsArchivePath", function() {
   return OS.Path.join(gDataReportingDir, PINGS_ARCHIVE_DIR);
 });
 XPCOMUtils.defineLazyGetter(this, "gAbortedSessionFilePath", function() {
   return OS.Path.join(gDataReportingDir, ABORTED_SESSION_FILE_NAME);
 });
+XPCOMUtils.defineLazyGetter(this, "gDeletionPingFilePath", function() {
+  return OS.Path.join(gDataReportingDir, DELETION_PING_FILE_NAME);
+});
 
 // Maxmimum time, in milliseconds, archive pings should be retained.
 const MAX_ARCHIVED_PINGS_RETENTION_MS = 180 * 24 * 60 * 60 * 1000;  // 180 days
 
 // Maximum space the archive can take on disk (in Bytes).
 const ARCHIVE_QUOTA_BYTES = 120 * 1024 * 1024; // 120 MB
 // Maximum space the outgoing pings can take on disk, for Desktop (in Bytes).
 const PENDING_PINGS_QUOTA_BYTES_DESKTOP = 15 * 1024 * 1024; // 15 MB
@@ -258,16 +262,40 @@ this.TelemetryStorage = {
    * @return {promise<object>} Promise that is resolved with the ping data if found.
    *                           Otherwise returns null.
    */
   loadAbortedSessionPing: function() {
     return TelemetryStorageImpl.loadAbortedSessionPing();
   },
 
   /**
+   * Save the deletion ping.
+   * @param ping The deletion ping.
+   * @return {Promise} A promise resolved when the ping is saved.
+   */
+  saveDeletionPing: function(ping) {
+    return TelemetryStorageImpl.saveDeletionPing(ping);
+  },
+
+  /**
+   * Remove the deletion ping.
+   * @return {Promise} Resolved when the ping is deleted from the disk.
+   */
+  removeDeletionPing: function() {
+    return TelemetryStorageImpl.removeDeletionPing();
+  },
+
+  /**
+   * Check if the ping id identifies a deletion ping.
+   */
+  isDeletionPing: function(aPingId) {
+    return TelemetryStorageImpl.isDeletionPing(aPingId);
+  },
+
+  /**
    * Remove the aborted-session ping if present.
    *
    * @return {promise} Promise that is resolved once the ping is removed.
    */
   removeAbortedSessionPing: function() {
     return TelemetryStorageImpl.removeAbortedSessionPing();
   },
 
@@ -477,16 +505,18 @@ SaveSerializer.prototype = {
       });
   },
 };
 
 let TelemetryStorageImpl = {
   _logger: null,
   // Used to serialize aborted session ping writes to disk.
   _abortedSessionSerializer: new SaveSerializer(),
+  // Used to serialize deletion ping writes to disk.
+  _deletionPingSerializer: new SaveSerializer(),
 
   // Tracks the archived pings in a Map of (id -> {timestampCreated, type}).
   // We use this to cache info on archived pings to avoid scanning the disk more than once.
   _archivedPings: new Map(),
   // A set of promises for pings currently being archived
   _activelyArchiving: new Set(),
   // Track the archive loading task to prevent multiple tasks from being executed.
   _scanArchiveTask: null,
@@ -516,16 +546,17 @@ let TelemetryStorageImpl = {
   /**
    * Shutdown & block on any outstanding async activity in this module.
    *
    * @return {Promise} Promise that is resolved when shutdown is complete.
    */
   shutdown: Task.async(function*() {
     this._shutdown = true;
     yield this._abortedSessionSerializer.flushTasks();
+    yield this._deletionPingSerializer.flushTasks();
     // If the tasks for archive cleaning or pending ping quota are still running, block on
     // them. They will bail out as soon as possible.
     yield this._cleanArchiveTask;
     yield this._enforcePendingPingsQuotaTask;
   }),
 
   /**
    * Save an archived ping to disk.
@@ -1220,16 +1251,28 @@ let TelemetryStorageImpl = {
 
       this._pendingPings.set(id, {
         path: file.path,
         lastModificationDate: info.lastModificationDate.getTime(),
       });
     }
 
     yield iter.close();
+
+    // Explicitly load the deletion ping from its known path, if it's there.
+    if (yield OS.File.exists(gDeletionPingFilePath)) {
+      this._log.trace("_scanPendingPings - Adding pending deletion ping.");
+      // We can't get the ping id or the last modification date without hitting the disk.
+      // Since deletion has a special handling, we don't really need those.
+      this._pendingPings.set(Utils.generateUUID(), {
+        path: gDeletionPingFilePath,
+        lastModificationDate: Date.now(),
+      });
+    }
+
     this._scannedPendingDirectory = true;
     return this._buildPingList();
   }),
 
   _buildPingList: function() {
     const list = [for (p of this._pendingPings) {
       id: p[0],
       lastModificationDate: p[1].lastModificationDate,
@@ -1374,16 +1417,60 @@ let TelemetryStorageImpl = {
         this._log.trace("removeAbortedSessionPing - success");
       } catch (ex if ex.becauseNoSuchFile) {
         this._log.trace("removeAbortedSessionPing - no such file");
       } catch (ex) {
         this._log.error("removeAbortedSessionPing - error removing ping", ex)
       }
     }.bind(this)));
   },
+
+  /**
+   * Save the deletion ping.
+   * @param ping The deletion ping.
+   * @return {Promise} Resolved when the ping is saved.
+   */
+  saveDeletionPing: Task.async(function*(ping) {
+    this._log.trace("saveDeletionPing - ping path: " + gDeletionPingFilePath);
+    yield OS.File.makeDir(gDataReportingDir, { ignoreExisting: true });
+
+    return this._deletionPingSerializer.enqueueTask(() =>
+      this.savePingToFile(ping, gDeletionPingFilePath, true));
+  }),
+
+  /**
+   * Remove the deletion ping.
+   * @return {Promise} Resolved when the ping is deleted from the disk.
+   */
+  removeDeletionPing: Task.async(function*() {
+    return this._deletionPingSerializer.enqueueTask(Task.async(function*() {
+      try {
+        yield OS.File.remove(gDeletionPingFilePath, { ignoreAbsent: false });
+        this._log.trace("removeDeletionPing - success");
+      } catch (ex if ex.becauseNoSuchFile) {
+        this._log.trace("removeDeletionPing - no such file");
+      } catch (ex) {
+        this._log.error("removeDeletionPing - error removing ping", ex)
+      }
+    }.bind(this)));
+  }),
+
+  isDeletionPing: function(aPingId) {
+    this._log.trace("isDeletionPing - id: " + aPingId);
+    let pingInfo = this._pendingPings.get(aPingId);
+    if (!pingInfo) {
+      return false;
+    }
+
+    if (pingInfo.path != gDeletionPingFilePath) {
+      return false;
+    }
+
+    return true;
+  },
 };
 
 ///// Utility functions
 
 function pingFilePath(ping) {
   // Support legacy ping formats, who don't have an "id" field, but a "slug" field.
   let pingIdentifier = (ping.slug) ? ping.slug : ping.id;
   return OS.Path.join(TelemetryStorage.pingDirectoryPath, pingIdentifier);
--- a/toolkit/components/telemetry/tests/unit/test_TelemetryController.js
+++ b/toolkit/components/telemetry/tests/unit/test_TelemetryController.js
@@ -147,21 +147,47 @@ add_task(function* test_simplePing() {
 add_task(function* test_deletionPing() {
   const isUnified = Preferences.get(PREF_UNIFIED, false);
   if (!isUnified) {
     // Skipping the test if unified telemetry is off, as no deletion ping will
     // be generated.
     return;
   }
 
+  const PREF_TELEMETRY_SERVER = "toolkit.telemetry.server";
+
   // Disable FHR upload: this should trigger a deletion ping.
   Preferences.set(PREF_FHR_UPLOAD_ENABLED, false);
 
   let ping = yield PingServer.promiseNextPing();
   checkPingFormat(ping, DELETION_PING_TYPE, true, false);
+  // Wait on ping activity to settle.
+  yield TelemetrySend.testWaitOnOutgoingPings();
+
+  // Restore FHR Upload.
+  Preferences.set(PREF_FHR_UPLOAD_ENABLED, true);
+
+  // Simulate a failure in sending the deletion ping by disabling the HTTP server.
+  yield PingServer.stop();
+  // Disable FHR upload to send a deletion ping again.
+  Preferences.set(PREF_FHR_UPLOAD_ENABLED, false);
+  // Wait for the send task to terminate, flagging it to do so at the next opportunity and
+  // cancelling any timeouts.
+  yield TelemetryController.reset();
+
+  // Enable the ping server again.
+  PingServer.start();
+  // We set the new server using the pref, otherwise it would get reset with
+  // |TelemetryController.reset|.
+  Preferences.set(PREF_TELEMETRY_SERVER, "http://localhost:" + PingServer.port);
+
+  // Reset the controller to spin the ping sending task.
+  yield TelemetryController.reset();
+  ping = yield PingServer.promiseNextPing();
+  checkPingFormat(ping, DELETION_PING_TYPE, true, false);
 
   // Restore FHR Upload.
   Preferences.set(PREF_FHR_UPLOAD_ENABLED, true);
 });
 
 add_task(function* test_pingHasClientId() {
   // Send a ping with a clientId.
   yield sendPing(true, false);
@@ -257,17 +283,17 @@ add_task(function* test_archivePings() {
     "TelemetryController should still archive pings if ping upload is enabled.");
 });
 
 // Test that we fuzz the submission time around midnight properly
 // to avoid overloading the telemetry servers.
 add_task(function* test_midnightPingSendFuzzing() {
   const fuzzingDelay = 60 * 60 * 1000;
   fakeMidnightPingFuzzingDelay(fuzzingDelay);
-  let now = new Date(2030, 5, 1, 11, 00, 0);
+  let now = new Date(2030, 5, 1, 11, 0, 0);
   fakeNow(now);
 
   let waitForTimer = () => new Promise(resolve => {
     fakePingSendTimer((callback, timeout) => {
       resolve([callback, timeout]);
     }, () => {});
   });
 
new file mode 100644
--- /dev/null
+++ b/toolkit/components/url-classifier/PrivateBrowsingTrackingProtectionWhitelist.js
@@ -0,0 +1,68 @@
+/* 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/. */
+
+const Ci = Components.interfaces;
+const Cu = Components.utils;
+
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+Cu.import("resource://gre/modules/Services.jsm");
+
+function PrivateBrowsingTrackingProtectionWhitelist() {
+  // The list of URIs explicitly excluded from tracking protection.
+  this._allowlist = [];
+
+  Services.obs.addObserver(this, "last-pb-context-exited", true);
+}
+
+PrivateBrowsingTrackingProtectionWhitelist.prototype = {
+  classID: Components.ID("{a319b616-c45d-4037-8d86-01c592b5a9af}"),
+  QueryInterface: XPCOMUtils.generateQI([Ci.nsIPrivateBrowsingTrackingProtectionWhitelist,
+                                         Ci.nsIObserver,
+                                         Ci.nsISupportsWeakReference,
+                                         Ci.nsISupports]),
+  _xpcom_factory: XPCOMUtils.generateSingletonFactory(PrivateBrowsingTrackingProtectionWhitelist),
+
+  /**
+   * Add the provided URI to the list of allowed tracking sites.
+   *
+   * @param uri nsIURI
+   *        The URI to add to the list.
+   */
+  addToAllowList(uri) {
+    if (this._allowlist.indexOf(uri.spec) === -1) {
+      this._allowlist.push(uri.spec);
+    }
+  },
+
+  /**
+   * Remove the provided URI from the list of allowed tracking sites.
+   *
+   * @param uri nsIURI
+   *        The URI to add to the list.
+   */
+  removeFromAllowList(uri) {
+    let index = this._allowlist.indexOf(uri.spec);
+    if (index !== -1) {
+      this._allowlist.splice(index, 1);
+    }
+  },
+
+  /**
+   * Check if the provided URI exists in the list of allowed tracking sites.
+   *
+   * @param uri nsIURI
+   *        The URI to add to the list.
+   */
+  existsInAllowList(uri) {
+    return this._allowlist.indexOf(uri.spec) !== -1;
+  },
+
+  observe: function (subject, topic, data) {
+    if (topic == "last-pb-context-exited") {
+      this._allowlist = [];
+    }
+  }
+};
+
+this.NSGetFactory = XPCOMUtils.generateNSGetFactory([PrivateBrowsingTrackingProtectionWhitelist]);
--- a/toolkit/components/url-classifier/moz.build
+++ b/toolkit/components/url-classifier/moz.build
@@ -2,16 +2,17 @@
 # vim: set filetype=python:
 # 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/.
 
 TEST_DIRS += ['tests']
 
 XPIDL_SOURCES += [
+    'nsIPrivateBrowsingTrackingProtectionWhitelist.idl',
     'nsIUrlClassifierDBService.idl',
     'nsIUrlClassifierHashCompleter.idl',
     'nsIUrlClassifierPrefixSet.idl',
     'nsIUrlClassifierStreamUpdater.idl',
     'nsIUrlClassifierUtils.idl',
     'nsIUrlListManager.idl',
 ]
 
@@ -37,16 +38,17 @@ SOURCES += [
 # contains variables that conflict with LookupCache.cpp
 SOURCES += [
     'HashStore.cpp',
 ]
 
 EXTRA_COMPONENTS += [
     'nsURLClassifier.manifest',
     'nsUrlClassifierHashCompleter.js',
+    'PrivateBrowsingTrackingProtectionWhitelist.js',
 ]
 
 # Same as JS components that are run through the pre-processor.
 EXTRA_PP_COMPONENTS += [
     'nsUrlClassifierLib.js',
     'nsUrlClassifierListManager.js',
 ]
 
new file mode 100644
--- /dev/null
+++ b/toolkit/components/url-classifier/nsIPrivateBrowsingTrackingProtectionWhitelist.idl
@@ -0,0 +1,46 @@
+/* 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/. */
+
+#include "nsISupports.idl"
+
+interface nsIURI;
+
+/**
+ * The Private Browsing Tracking Protection service checks a URI against an
+ * in-memory list of tracking sites.
+ */
+[scriptable, uuid(c77ddfac-6cd6-43a9-84e8-91682a1a7b18)]
+interface nsIPrivateBrowsingTrackingProtectionWhitelist : nsISupports
+{
+  /**
+   * Add a URI to the list of allowed tracking sites in Private Browsing mode
+   * (essentially a tracking whitelist). This operation will cause the URI to
+   * be registered if it does not currently exist. If it already exists, then
+   * the operation is essentially a no-op.
+   *
+   * @param uri         the uri to add to the list
+   */
+  void addToAllowList(in nsIURI uri);
+
+  /**
+   * Remove a URI from the list of allowed tracking sites in Private Browsing
+   * mode (the tracking whitelist). If the URI is not already in the list,
+   * then the operation is essentially a no-op.
+   *
+   * @param uri         the uri to remove from the list
+   */
+  void removeFromAllowList(in nsIURI uri);
+
+  /**
+   * Check if a URI exists in the list of allowed tracking sites in Private
+   * Browsing mode (the tracking whitelist).
+   *
+   * @param uri         the uri to look for in the list
+   */
+  bool existsInAllowList(in nsIURI uri);
+};
+
+%{ C++
+#define NS_PBTRACKINGPROTECTIONWHITELIST_CONTRACTID "@mozilla.org/url-classifier/pbm-tp-whitelist;1"
+%}
--- a/toolkit/components/url-classifier/nsURLClassifier.manifest
+++ b/toolkit/components/url-classifier/nsURLClassifier.manifest
@@ -1,6 +1,8 @@
 component {26a4a019-2827-4a89-a85c-5931a678823a} nsUrlClassifierLib.js
 contract @mozilla.org/url-classifier/jslib;1 {26a4a019-2827-4a89-a85c-5931a678823a}
 component {ca168834-cc00-48f9-b83c-fd018e58cae3} nsUrlClassifierListManager.js
 contract @mozilla.org/url-classifier/listmanager;1 {ca168834-cc00-48f9-b83c-fd018e58cae3}
 component {9111de73-9322-4bfc-8b65-2b727f3e6ec8} nsUrlClassifierHashCompleter.js
 contract @mozilla.org/url-classifier/hashcompleter;1 {9111de73-9322-4bfc-8b65-2b727f3e6ec8}
+component {a319b616-c45d-4037-8d86-01c592b5a9af} PrivateBrowsingTrackingProtectionWhitelist.js
+contract @mozilla.org/url-classifier/pbm-tp-whitelist;1 {a319b616-c45d-4037-8d86-01c592b5a9af}
--- a/toolkit/devtools/server/actors/utils/make-debugger.js
+++ b/toolkit/devtools/server/actors/utils/make-debugger.js
@@ -63,25 +63,25 @@ const { reportException } = require("dev
  *              |findDebuggees|) to the |Debugger| instance.
  */
 module.exports = function makeDebugger({ findDebuggees, shouldAddNewGlobalAsDebuggee }) {
   const dbg = new Debugger();
   EventEmitter.decorate(dbg);
 
   dbg.uncaughtExceptionHook = reportDebuggerHookException;
 
-  dbg.onNewGlobalObject = global => {
+  dbg.onNewGlobalObject = function(global) {
     if (shouldAddNewGlobalAsDebuggee(global)) {
-      safeAddDebuggee(dbg, global);
+      safeAddDebuggee(this, global);
     }
   };
 
-  dbg.addDebuggees = () => {
-    for (let global of findDebuggees(dbg)) {
-      safeAddDebuggee(dbg, global);
+  dbg.addDebuggees = function() {
+    for (let global of findDebuggees(this)) {
+      safeAddDebuggee(this, global);
     }
   };
 
   return dbg;
 };
 
 const reportDebuggerHookException = e => reportException("Debugger Hook", e);
 
--- a/toolkit/modules/PrivateBrowsingUtils.jsm
+++ b/toolkit/modules/PrivateBrowsingUtils.jsm
@@ -46,16 +46,28 @@ this.PrivateBrowsingUtils = {
   },
 
   privacyContextFromWindow: function pbu_privacyContextFromWindow(aWindow) {
     return aWindow.QueryInterface(Ci.nsIInterfaceRequestor)
                   .getInterface(Ci.nsIWebNavigation)
                   .QueryInterface(Ci.nsILoadContext);
   },
 
+  addToTrackingAllowlist(aURI) {
+    let pbmtpWhitelist = Cc["@mozilla.org/url-classifier/pbm-tp-whitelist;1"]
+                           .getService(Ci.nsIPrivateBrowsingTrackingProtectionWhitelist);
+    pbmtpWhitelist.addToAllowList(aURI);
+  },
+
+  removeFromTrackingAllowlist(aURI) {
+    let pbmtpWhitelist = Cc["@mozilla.org/url-classifier/pbm-tp-whitelist;1"]
+                           .getService(Ci.nsIPrivateBrowsingTrackingProtectionWhitelist);
+    pbmtpWhitelist.removeFromAllowList(aURI);
+  },
+
   get permanentPrivateBrowsing() {
     try {
       return gTemporaryAutoStartMode ||
              Services.prefs.getBoolPref(kAutoStartPref);
     } catch (e) {
       // The pref does not exist
       return false;
     }
--- a/toolkit/mozapps/extensions/amWebInstallListener.js
+++ b/toolkit/mozapps/extensions/amWebInstallListener.js
@@ -114,30 +114,30 @@ Installer.prototype = {
         if (install.addon.appDisabled)
           failed.push(install);
         else
           installs.push(install);
 
         if (install.linkedInstalls) {
           install.linkedInstalls.forEach(function(aInstall) {
             aInstall.addListener(this);
-            // App disabled items are not compatible and so fail to install
-            if (aInstall.addon.appDisabled)
+            // Corrupt or incompatible items fail to install
+            if (aInstall.state == AddonManager.STATE_DOWNLOAD_FAILED || aInstall.addon.appDisabled)
               failed.push(aInstall);
             else
               installs.push(aInstall);
           }, this);
         }
         break;
       case AddonManager.STATE_CANCELLED:
         // Just ignore cancelled downloads
         break;
       default:
         logger.warn("Download of " + install.sourceURI.spec + " in unexpected state " +
-             install.state);
+                    install.state);
       }
     }
 
     this.isDownloading = false;
     this.downloads = installs;
 
     if (failed.length > 0) {
       // Stop listening and cancel any installs that are failed because of
--- a/toolkit/mozapps/extensions/internal/XPIProvider.jsm
+++ b/toolkit/mozapps/extensions/internal/XPIProvider.jsm
@@ -5121,17 +5121,17 @@ AddonInstall.prototype = {
         this.state = AddonManager.STATE_DOWNLOAD_FAILED;
         this.error = AddonManager.ERROR_INCORRECT_HASH;
         aCallback(this);
         return;
       }
     }
 
     let self = this;
-    this.loadManifest().then(() => {
+    this.loadManifest(this.file).then(() => {
       XPIDatabase.getVisibleAddonForID(self.addon.id, function initLocalInstall_getVisibleAddon(aAddon) {
         self.existingAddon = aAddon;
         if (aAddon)
           applyBlocklistChanges(aAddon, self.addon);
         self.addon.updateDate = Date.now();
         self.addon.installDate = aAddon ? aAddon.installDate : self.addon.updateDate;
 
         if (!self.addon.isCompatible) {
@@ -5157,16 +5157,20 @@ AddonInstall.prototype = {
 
           aCallback(self);
         }
       });
     }, ([error, message]) => {
       logger.warn("Invalid XPI", message);
       this.state = AddonManager.STATE_DOWNLOAD_FAILED;
       this.error = error;
+      AddonManagerPrivate.callInstallListeners("onNewInstall",
+                                               self.listeners,
+                                               self.wrapper);
+
       aCallback(this);
     });
   },
 
   /**
    * Initialises this install to be a download from a remote url.
    *
    * @param  aCallback
@@ -5333,132 +5337,132 @@ AddonInstall.prototype = {
    */
   updateAddonURIs: function AI_updateAddonURIs() {
     this.addon.sourceURI = this.sourceURI.spec;
     if (this.releaseNotesURI)
       this.addon.releaseNotesURI = this.releaseNotesURI.spec;
   },
 
   /**
+   * Fills out linkedInstalls with AddonInstall instances for the other files
+   * in a multi-package XPI.
+   *
+   * @param  aFiles
+   *         An array of { entryName, file } for each remaining file from the
+   *         multi-package XPI.
+   */
+  _createLinkedInstalls: Task.async(function* AI_createLinkedInstalls(aFiles) {
+    if (aFiles.length == 0)
+      return;
+
+    // Create new AddonInstall instances for every remaining file
+    if (!this.linkedInstalls)
+      this.linkedInstalls = [];
+
+    for (let { entryName, file } of aFiles) {
+      logger.debug("Creating linked install from " + entryName);
+      let install = yield new Promise(resolve => AddonInstall.createInstall(resolve, file));
+
+      // Make the new install own its temporary file
+      install.ownsTempFile = true;
+
+      this.linkedInstalls.push(install);
+
+      // If one of the internal XPIs was multipackage then move its linked
+      // installs to the outer install
+      if (install.linkedInstalls) {
+        this.linkedInstalls.push(...install.linkedInstalls);
+        install.linkedInstalls = null;
+      }
+
+      install.sourceURI = this.sourceURI;
+      install.releaseNotesURI = this.releaseNotesURI;
+      if (install.state != AddonManager.STATE_DOWNLOAD_FAILED)
+        install.updateAddonURIs();
+    }
+  }),
+
+  /**
    * Loads add-on manifests from a multi-package XPI file. Each of the
    * XPI and JAR files contained in the XPI will be extracted. Any that
    * do not contain valid add-ons will be ignored. The first valid add-on will
    * be installed by this AddonInstall instance, the rest will have new
    * AddonInstall instances created for them.
    *
    * @param  aZipReader
    *         An open nsIZipReader for the multi-package XPI's files. This will
    *         be closed before this method returns.
-   * @param  aCallback
-   *         A function to call when all of the add-on manifests have been
-   *         loaded. Because this loadMultipackageManifests is an internal API
-   *         we don't exception-wrap this callback
    */
   _loadMultipackageManifests: Task.async(function* AI_loadMultipackageManifests(aZipReader) {
     let files = [];
     let entries = aZipReader.findEntries("(*.[Xx][Pp][Ii]|*.[Jj][Aa][Rr])");
     while (entries.hasMore()) {
       let entryName = entries.getNext();
-      var target = getTemporaryFile();
+      let file = getTemporaryFile();
       try {
-        aZipReader.extract(entryName, target);
-        files.push(target);
+        aZipReader.extract(entryName, file);
+        files.push({ entryName, file });
       }
       catch (e) {
         logger.warn("Failed to extract " + entryName + " from multi-package " +
              "XPI", e);
-        target.remove(false);
+        file.remove(false);
       }
     }
 
     aZipReader.close();
 
     if (files.length == 0) {
-      throw new Error("Multi-package XPI does not contain any packages " +
-                      "to install");
+      return Promise.reject([AddonManager.ERROR_CORRUPT_FILE,
+                             "Multi-package XPI does not contain any packages to install"]);
     }
 
     let addon = null;
 
-    // Find the first file that has a valid install manifest and use it for
+    // Find the first file that is a valid install and use it for
     // the add-on that this AddonInstall instance will install.
-    while (files.length > 0) {
+    for (let { entryName, file } of files) {
       this.removeTemporaryFile();
-      this.file = files.shift();
-      this.ownsTempFile = true;
       try {
-        addon = yield loadManifestFromZipFile(this.file);
-        break;
+        yield this.loadManifest(file);
+        logger.debug("Base multi-package XPI install came from " + entryName);
+        this.file = file;
+        this.ownsTempFile = true;
+
+        yield this._createLinkedInstalls(files.filter(f => f.file != file));
+        return;
       }
       catch (e) {
-        logger.warn(this.file.leafName + " cannot be installed from multi-package " +
-             "XPI", e);
-      }
-    }
-
-    if (!addon) {
-      // No valid add-on was found
-      return;
-    }
-
-    this.addon = addon;
-
-    this.updateAddonURIs();
-
-    this.addon._install = this;
-    this.name = this.addon.selectedLocale.name;
-    this.type = this.addon.type;
-    this.version = this.addon.version;
-
-    // Setting the iconURL to something inside the XPI locks the XPI and
-    // makes it impossible to delete on Windows.
-    //let newIcon = createWrapper(this.addon).iconURL;
-    //if (newIcon)
-    //  this.iconURL = newIcon;
-
-    // Create new AddonInstall instances for every remaining file
-    if (files.length > 0) {
-      this.linkedInstalls = [];
-      let self = this;
-      for (let file of files) {
-        let install = yield new Promise(resolve => AddonInstall.createInstall(resolve, file));
-
-        // Ignore bad add-ons (createInstall will have logged the error)
-        if (install.state == AddonManager.STATE_DOWNLOAD_FAILED) {
-          // Manually remove the temporary file
-          file.remove(true);
-        }
-        else {
-          // Make the new install own its temporary file
-          install.ownsTempFile = true;
-
-          self.linkedInstalls.push(install)
-
-          install.sourceURI = self.sourceURI;
-          install.releaseNotesURI = self.releaseNotesURI;
-          install.updateAddonURIs();
-        }
-      }
-    }
+        // _createLinkedInstalls will log errors when it tries to process this
+        // file
+      }
+    }
+
+    // No valid add-on was found, delete all the temporary files
+    for (let { file } of files)
+      file.remove(true);
+
+    return Promise.reject([AddonManager.ERROR_CORRUPT_FILE,
+                           "Multi-package XPI does not contain any valid packages to install"]);
   }),
 
   /**
    * Called after the add-on is a local file and the signature and install
    * manifest can be read.
    *
    * @param  aCallback
    *         A function to call when the manifest has been loaded
    * @throws if the add-on does not contain a valid install manifest or the
    *         XPI is incorrectly signed
    */
-  loadManifest: Task.async(function* AI_loadManifest() {
+  loadManifest: Task.async(function* AI_loadManifest(file) {
     let zipreader = Cc["@mozilla.org/libjar/zip-reader;1"].
                     createInstance(Ci.nsIZipReader);
     try {
-      zipreader.open(this.file);
+      zipreader.open(file);
     }
     catch (e) {
       zipreader.close();
       return Promise.reject([AddonManager.ERROR_CORRUPT_FILE, e]);
     }
 
     try {
       // loadManifestFromZipReader performs the certificate verification for us
@@ -5766,17 +5770,17 @@ AddonInstall.prototype = {
         if (this.hash && calculatedHash != this.hash.data) {
           this.downloadFailed(AddonManager.ERROR_INCORRECT_HASH,
                               "Downloaded file hash (" + calculatedHash +
                               ") did not match provided hash (" + this.hash.data + ")");
           return;
         }
 
         let self = this;
-        this.loadManifest().then(() => {
+        this.loadManifest(this.file).then(() => {
           if (self.addon.isCompatible) {
             self.downloadCompleted();
           }
           else {
             // TODO Should we send some event here (bug 557716)?
             self.state = AddonManager.STATE_CHECKING;
             new UpdateChecker(self.addon, {
               onUpdateFinished: function onStopRequest_onUpdateFinished(aAddon) {
@@ -5855,19 +5859,20 @@ AddonInstall.prototype = {
                                                    self.wrapper)) {
         // If a listener changed our state then do not proceed with the install
         if (self.state != AddonManager.STATE_DOWNLOADED)
           return;
 
         self.install();
 
         if (self.linkedInstalls) {
-          self.linkedInstalls.forEach(function(aInstall) {
-            aInstall.install();
-          });
+          for (let install of self.linkedInstalls) {
+            if (install.state == AddonManager.STATE_DOWNLOADED)
+              install.install();
+          }
         }
       }
     });
   },
 
   // TODO This relies on the assumption that we are always installing into the
   // highest priority install location so the resulting add-on will be visible
   // overriding any existing copy in another install location (bug 557710).
new file mode 100644
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/addons/test_install7/addon1.xpi
@@ -0,0 +1,1 @@
+This isn't a valid zip file.
\ No newline at end of file
new file mode 100644
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/addons/test_install7/addon2.xpi
@@ -0,0 +1,1 @@
+This isn't a valid zip file.
\ No newline at end of file
new file mode 100644
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/addons/test_install7/install.rdf
@@ -0,0 +1,10 @@
+<?xml version="1.0"?>
+
+<!-- A multi-package XPI -->
+<RDF xmlns="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+     xmlns:em="http://www.mozilla.org/2004/em-rdf#">
+
+  <Description about="urn:mozilla:install-manifest">
+    <em:type>32</em:type>
+  </Description>
+</RDF>
new file mode 100644
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/addons/test_install8/install.rdf
@@ -0,0 +1,10 @@
+<?xml version="1.0"?>
+
+<!-- A multi-package XPI -->
+<RDF xmlns="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+     xmlns:em="http://www.mozilla.org/2004/em-rdf#">
+
+  <Description about="urn:mozilla:install-manifest">
+    <em:type>32</em:type>
+  </Description>
+</RDF>
new file mode 100644
index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..6e23eb214c8f3564f7f6ac508ff66c8780a13239
GIT binary patch
literal 6443
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
AS^xk5
new file mode 100644
index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..0ba0f30d1ac93ad3851a6f20bbd4f319b57e8201
GIT binary patch
literal 6563
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
new file mode 100644
index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..33101f63c06d75b719177863779b8ffbb5b43b17
GIT binary patch
literal 6425
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
new file mode 100644
index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..3146870d86bc439a2a2e0985c2515ebe7d418483
GIT binary patch
literal 2436
zc$^FHW@h1H0D-WW8*bahw{PcSWMBYcZUz~K%)H`~#GD+xqLj4I5KabW=O106AY59(
z&A`a=4XBDm1gO=<z9KhAp)9qiI5R)bR>@G$K*`>Yi_1n)S69JNA-6QABvZE_F*!Ri
zJyjthz*9k2*N)33$i+<os4cJ9Rw<*Tq`*pFzr4I$uiRKKzbIYb(9+UU-@r)U$VeAx
zw{CG|UP)qwZeFpn5|;u9fHhjB=A!7$&9BPL$w>qoU}RumqMw=z(ygonbc=$5jZ12A
za#3bM3DAuSiAnjTCALbXMR`_GT~<)P>gFcqWu~PTmjHEvY_dtswJNDBNVPLIveAdJ
zLB{DLnFsWaKF~jQTmkUld=@#w&C#(ctA&Mu0fZ%h!C9JDoSB}Nni8LspI=g3Qj}N_
zZ=_dIkO>Z3gAS`ur^pucumzbY!obA9!H|^M<W|q^`c?-hwHjz5570!oL3&xmU{k^u
z$%YmKF`6k`C;NLJb`UuF{-=nu-`Xj?6<aEV`1h`ecxjq5IaB!No0hpZ*#!6R7M`_D
zVbPcV?RSr#$-iH{;ro>>PcCxL*4UaH{Mz<}2JZ<G=f>@k+xTXfhPbuQIcu2nis|N_
z!pVKhJ%c8sHy%BFy5`##j`D)VQPU1;uR7JF>l}FV)?uvx=G+6K7bb}|H8$GBJZ03K
zJpYTgPeN?^eMt${C7YPeZBkkNf_r7@u4~)Z6feHiHTOVSF^@dwfqUWqS)}jIudKZG
zStC}6(|g0(73#(c8_ra7bNQ-Uf0ue*5#YDZW<pi6+tj0Bmc2X9PJPGhH~IXN3ytSk
zI^SB#MyTd~xWmMzEUuH_{%3K6mY3(;{Ta;H-K}J8p2+Rae;8`1`-ksm^{*HIUkc0I
zJaF$gtG0zf{i_Guh!BqjhWMx7KXiP7!MK45dzJ{o9^kE$PUjsm5ODqelgoUKY`aLP
z64yeHDWSU^bHA$@CY<a)G0!A(|GvJ@3@%Hn5;e=u&6SK-jou!#KtE0AdY0(9#TGHo
z9VfiqZv8~-)Vf2)4pW6LnfP9@al19=M9t2aNfwHpO}>>!zC|<UZrrrlz~S!4V^dB_
zufJj+!|_U5rErgePuaW37snK*<j&OPS$|+{;|s^@8@RF;UphFoD7BHRQ8o0>2_1`Y
zbIoL<r>T5?mGAVbZPk=(OO@KwpLvS>zV*YFk$vsKriGz=%hKlU^l<FAP3S3e({|8b
z<&w|yVj7dHMEorct;m1xt~t)$bR_DhTil!pyR<Yn^#9Gab5SXAJQ^A8A^vW{KDnI@
zdww>t2XilYb|fJ<v`JiKS^1qb%dFs#+|V1w!jI<faBF|^s@Nm*VWaj#u_bR;*nfTC
zD)C*Zyu0#Ke>+QgiQ66hne6)^xe6Eo3~j&|solCh+Z@OSVGdx7lmIiUUP(m>I67tn
zqhdB@bcAGN7At@hDx_uRq$(sQ<|!nlDkSEVq!y*7D3s(YBo(D5W-EZQXJ%fyLUKlG
za(1yEFgr6c$uZ-~eiGn}4&*W{X#}xQGb1Y`Gh$?Cgo&8R3)#dXAcd0HSRsiGtAV&e
z2iZUtW(K6NVugeihH0#9ARWv=_z_6gGBZHSAV{g^UVFn$u;PIN2hchY<^+X*Mrv*<
zyclyc>JE(pVw}a83($zURRv#SfKnfEWGV-s0S=gkv<CV5HycRo`>d^HJ)LQGyWlaC
znbs@YQ>#oxcU@efyQEhA#ro|A3!>AK@@wb!e}BpRa@SO*TEpCmrD1g<E&r@O?<v`S
zQ>j~H^C#~HE5#+5vlh%`pK3QL1DK->)Czk_3lBU4<tQWL`_6Y?wsKAmkBa<TpqP;_
z(EhN=;O5HoeX;EUyAsu|?_rZ>zIgr>i}wE|p++l&*6{==ZdlSKKZ9}gE3>6OYI{yB
zNSS2YFn9K8TTRQu!lf1IX8ecbq~G70S=erP>y;N{RS;`cqJz1J#-SUQz-+Yb{k5-b
ze@mITzqjS&A69VR(-Kp}>Z7d{XFXldwzDlEyu+N+uhg+|@7sBs7e)j=4Vf~vF(r|I
zqlW6-i^ng7CrH2JoqK5~<5hRI{!-lvzt~N$`wh=6TX);aTostbcDc7cc~zTX`HEA#
zIa?|B_|LyHU$7<X6$!6N|MyYgecN22-~Q?M^pO)LW)z_(b@YUZC9+^ii4h0`f%HN~
z2BhSQy~P2sh5@HFSlS-Q24dE^$OifYwcxISSs^tr9@8<?KC<bmK+huO1Xf5+z>~fH
K0qFt`5Dx%QjUa&l
--- a/toolkit/mozapps/extensions/test/xpcshell/data/test_gfxBlacklist_AllOS.xml
+++ b/toolkit/mozapps/extensions/test/xpcshell/data/test_gfxBlacklist_AllOS.xml
@@ -130,16 +130,42 @@
       </devices>
       <feature> WEBRTC_HW_ACCELERATION </feature>
       <featureStatus> BLOCKED_DRIVER_VERSION </featureStatus>
     </gfxBlacklistEntry>
 
     <gfxBlacklistEntry>
       <os>All</os>
       <vendor>0xabcd</vendor>
+      <versionRange minVersion="42.0" maxVersion="13.0b2"/>
+      <devices>
+        <device>0x2783</device>
+        <device>0x1234</device>
+        <device>0x2782</device>
+      </devices>
+      <feature> WEBRTC_HW_ACCELERATION_ENCODE </feature>
+      <featureStatus> BLOCKED_DRIVER_VERSION </featureStatus>
+    </gfxBlacklistEntry>
+
+    <gfxBlacklistEntry>
+      <os>All</os>
+      <vendor>0xabcd</vendor>
+      <versionRange minVersion="42.0" maxVersion="13.0b2"/>
+      <devices>
+        <device>0x2783</device>
+        <device>0x1234</device>
+        <device>0x2782</device>
+      </devices>
+      <feature> WEBRTC_HW_ACCELERATION_DECODE </feature>
+      <featureStatus> BLOCKED_DRIVER_VERSION </featureStatus>
+    </gfxBlacklistEntry>
+
+    <gfxBlacklistEntry>
+      <os>All</os>
+      <vendor>0xabcd</vendor>
       <versionRange minVersion="17.2a2" maxVersion="15.0"/>
       <devices>
         <device>0x2783</device>
         <device>0x1234</device>
         <device>0x2782</device>
       </devices>
       <feature> DIRECT3D_11_LAYERS </feature>
       <featureStatus> BLOCKED_DRIVER_VERSION </featureStatus>
--- a/toolkit/mozapps/extensions/test/xpcshell/head_addons.js
+++ b/toolkit/mozapps/extensions/test/xpcshell/head_addons.js
@@ -1103,19 +1103,23 @@ const AddonListener = {
     do_check_eq("onOperationCancelled", event);
     return check_test_completed(arguments);
   }
 };
 
 const InstallListener = {
   onNewInstall: function(install) {
     if (install.state != AddonManager.STATE_DOWNLOADED &&
+        install.state != AddonManager.STATE_DOWNLOAD_FAILED &&
         install.state != AddonManager.STATE_AVAILABLE)
       do_throw("Bad install state " + install.state);
-    do_check_eq(install.error, 0);
+    if (install.state != AddonManager.STATE_DOWNLOAD_FAILED)
+      do_check_eq(install.error, 0);
+    else
+      do_check_neq(install.error, 0);
     do_check_eq("onNewInstall", getExpectedInstall());
     return check_test_completed(arguments);
   },
 
   onDownloadStarted: function(install) {
     do_check_eq(install.state, AddonManager.STATE_DOWNLOADING);
     do_check_eq(install.error, 0);
     do_check_eq("onDownloadStarted", getExpectedInstall());
--- a/toolkit/mozapps/extensions/test/xpcshell/test_gfxBlacklist_Version.js
+++ b/toolkit/mozapps/extensions/test/xpcshell/test_gfxBlacklist_Version.js
@@ -102,16 +102,22 @@ function run_test() {
     do_check_eq(status, Ci.nsIGfxInfo.FEATURE_BLOCKED_DRIVER_VERSION);
 
     status = gfxInfo.getFeatureStatus(Ci.nsIGfxInfo.FEATURE_STAGEFRIGHT);
     do_check_eq(status, Ci.nsIGfxInfo.FEATURE_STATUS_OK);
 
     status = gfxInfo.getFeatureStatus(Ci.nsIGfxInfo.FEATURE_WEBRTC_HW_ACCELERATION);
     do_check_eq(status, Ci.nsIGfxInfo.FEATURE_STATUS_OK);
 
+    status = gfxInfo.getFeatureStatus(Ci.nsIGfxInfo.FEATURE_WEBRTC_HW_ACCELERATION_ENCODE);
+    do_check_eq(status, Ci.nsIGfxInfo.FEATURE_STATUS_OK);
+
+    status = gfxInfo.getFeatureStatus(Ci.nsIGfxInfo.FEATURE_WEBRTC_HW_ACCELERATION_DECODE);
+    do_check_eq(status, Ci.nsIGfxInfo.FEATURE_STATUS_OK);
+
     status = gfxInfo.getFeatureStatus(Ci.nsIGfxInfo.FEATURE_DIRECT3D_11_LAYERS);
     do_check_eq(status, Ci.nsIGfxInfo.FEATURE_STATUS_OK);
 
     status = gfxInfo.getFeatureStatus(Ci.nsIGfxInfo.FEATURE_HARDWARE_VIDEO_DECODING);
     do_check_eq(status, Ci.nsIGfxInfo.FEATURE_STATUS_OK);
 
     status = gfxInfo.getFeatureStatus(Ci.nsIGfxInfo.FEATURE_DIRECT3D_11_ANGLE);
     do_check_eq(status, Ci.nsIGfxInfo.FEATURE_STATUS_OK);
--- a/toolkit/mozapps/extensions/test/xpcshell/test_install.js
+++ b/toolkit/mozapps/extensions/test/xpcshell/test_install.js
@@ -645,28 +645,41 @@ function check_test_10(install) {
 
 // Tests that a multi-package install shows up as multiple installs with the
 // correct sourceURI.
 function run_test_11() {
   prepare_test({ }, [
     "onNewInstall",
     "onNewInstall",
     "onNewInstall",
+    "onNewInstall",
+    "onNewInstall",
     "onNewInstall"
   ]);
 
   AddonManager.getInstallForFile(do_get_addon("test_install4"), function(install) {
     ensure_test_completed();
     do_check_neq(install, null);
     do_check_neq(install.linkedInstalls, null);
-    do_check_eq(install.linkedInstalls.length, 3);
+    do_check_eq(install.linkedInstalls.length, 5);
 
     // Might be in any order so sort them based on ID
     let installs = [install].concat(install.linkedInstalls);
     installs.sort(function(a, b) {
+      if (a.state != b.state) {
+        if (a.state == AddonManager.STATE_DOWNLOAD_FAILED)
+          return 1;
+        else if (b.state == AddonManager.STATE_DOWNLOAD_FAILED)
+          return -1;
+      }
+
+      // Don't care what order the failed installs show up in
+      if (a.state == AddonManager.STATE_DOWNLOAD_FAILED)
+        return 0;
+
       if (a.addon.id < b.addon.id)
         return -1;
       if (a.addon.id > b.addon.id)
         return 1;
       return 0;
     });
 
     // Comes from addon4.xpi and is made compatible by an update check
@@ -704,16 +717,22 @@ function run_test_11() {
     do_check_eq(installs[3].addon.id, "addon7@tests.mozilla.org");
     do_check_false(installs[3].addon.appDisabled);
     do_check_eq(installs[3].version, "5.0");
     do_check_eq(installs[3].name, "Multi Test 4");
     do_check_eq(installs[3].state, AddonManager.STATE_DOWNLOADED);
     do_check_true(hasFlag(installs[3].addon.operationsRequiringRestart,
                           AddonManager.OP_NEEDS_RESTART_INSTALL));
 
+    do_check_eq(installs[4].state, AddonManager.STATE_DOWNLOAD_FAILED);
+    do_check_eq(installs[4].error, AddonManager.ERROR_CORRUPT_FILE);
+
+    do_check_eq(installs[5].state, AddonManager.STATE_DOWNLOAD_FAILED);
+    do_check_eq(installs[5].error, AddonManager.ERROR_CORRUPT_FILE);
+
     AddonManager.getAllInstalls(function(aInstalls) {
       do_check_eq(aInstalls.length, 4);
 
       prepare_test({
         "addon4@tests.mozilla.org": [
           "onInstalling"
         ],
         "addon5@tests.mozilla.org": [
@@ -810,16 +829,18 @@ function run_test_12() {
         "onInstalling"
       ]
     }, {
       "NO_ID": [
         "onDownloadStarted",
         "onNewInstall",
         "onNewInstall",
         "onNewInstall",
+        "onNewInstall",
+        "onNewInstall",
         "onDownloadEnded"
       ],
       "addon4@tests.mozilla.org": [
         "onInstallStarted",
         "onInstallEnded"
       ],
       "addon5@tests.mozilla.org": [
         "onInstallStarted",
@@ -834,21 +855,32 @@ function run_test_12() {
         "onInstallEnded"
       ]
     }, callback_soon(check_test_12));
     install.install();
   }, "application/x-xpinstall", null, "Multi Test 4");
 }
 
 function check_test_12() {
-  do_check_eq(gInstall.linkedInstalls.length, 3);
+  do_check_eq(gInstall.linkedInstalls.length, 5);
 
   // Might be in any order so sort them based on ID
   let installs = [gInstall].concat(gInstall.linkedInstalls);
   installs.sort(function(a, b) {
+    if (a.state != b.state) {
+      if (a.state == AddonManager.STATE_DOWNLOAD_FAILED)
+        return 1;
+      else if (b.state == AddonManager.STATE_DOWNLOAD_FAILED)
+        return -1;
+    }
+
+    // Don't care what order the failed installs show up in
+    if (a.state == AddonManager.STATE_DOWNLOAD_FAILED)
+      return 0;
+
     if (a.addon.id < b.addon.id)
       return -1;
     if (a.addon.id > b.addon.id)
       return 1;
     return 0;
   });
 
   // Comes from addon4.xpi and is made compatible by an update check
@@ -878,16 +910,22 @@ function check_test_12() {
   // Comes from addon7.jar and is made compatible by an update check
   do_check_eq(installs[3].sourceURI, gInstall.sourceURI);
   do_check_eq(installs[3].addon.id, "addon7@tests.mozilla.org");
   do_check_false(installs[3].addon.appDisabled);
   do_check_eq(installs[3].version, "5.0");
   do_check_eq(installs[3].name, "Multi Test 4");
   do_check_eq(installs[3].state, AddonManager.STATE_INSTALLED);
 
+  do_check_eq(installs[4].state, AddonManager.STATE_DOWNLOAD_FAILED);
+  do_check_eq(installs[4].error, AddonManager.ERROR_CORRUPT_FILE);
+
+  do_check_eq(installs[5].state, AddonManager.STATE_DOWNLOAD_FAILED);
+  do_check_eq(installs[5].error, AddonManager.ERROR_CORRUPT_FILE);
+
   restartManager();
 
   AddonManager.getAddonsByIDs(["addon4@tests.mozilla.org",
                                "addon5@tests.mozilla.org",
                                "addon6@tests.mozilla.org",
                                "addon7@tests.mozilla.org"],
                                function([a4, a5, a6, a7]) {
     do_check_neq(a4, null);
@@ -1750,12 +1788,50 @@ function check_test_29(install) {
   //ensure_test_completed();
   do_check_eq(install.state, AddonManager.STATE_DOWNLOADED);
   do_check_neq(install.addon, null);
   do_check_false(install.addon.isCompatible);
   do_check_true(install.addon.appDisabled);
 
   prepare_test({}, [
     "onDownloadCancelled"
-  ], do_test_finished);
+  ], run_test_30);
   install.cancel();
   return false;
 }
+
+// Tests that a multi-package XPI with no add-ons inside shows up as a
+// corrupt file
+function run_test_30() {
+  prepare_test({ }, [
+    "onNewInstall"
+  ]);
+
+  AddonManager.getInstallForFile(do_get_addon("test_install7"), function(install) {
+    ensure_test_completed();
+
+    do_check_neq(install, null);
+    do_check_eq(install.state, AddonManager.STATE_DOWNLOAD_FAILED);
+    do_check_eq(install.error, AddonManager.ERROR_CORRUPT_FILE);
+    do_check_eq(install.linkedInstalls, null);
+
+    run_test_31();
+  });
+}
+
+// Tests that a multi-package XPI with no valid add-ons inside shows up as a
+// corrupt file
+function run_test_31() {
+  prepare_test({ }, [
+    "onNewInstall"
+  ]);
+
+  AddonManager.getInstallForFile(do_get_addon("test_install8"), function(install) {
+    ensure_test_completed();
+
+    do_check_neq(install, null);
+    do_check_eq(install.state, AddonManager.STATE_DOWNLOAD_FAILED);
+    do_check_eq(install.error, AddonManager.ERROR_CORRUPT_FILE);
+    do_check_eq(install.linkedInstalls, null);
+
+    end_test();
+  });
+}
--- a/toolkit/mozapps/extensions/test/xpcshell/test_install_strictcompat.js
+++ b/toolkit/mozapps/extensions/test/xpcshell/test_install_strictcompat.js
@@ -634,28 +634,41 @@ function check_test_10(install) {
 
 // Tests that a multi-package install shows up as multiple installs with the
 // correct sourceURI.
 function run_test_11() {
   prepare_test({ }, [
     "onNewInstall",
     "onNewInstall",
     "onNewInstall",
+    "onNewInstall",
+    "onNewInstall",
     "onNewInstall"
   ]);
 
   AddonManager.getInstallForFile(do_get_addon("test_install4"), function(install) {
     ensure_test_completed();
     do_check_neq(install, null);
     do_check_neq(install.linkedInstalls, null);
-    do_check_eq(install.linkedInstalls.length, 3);
+    do_check_eq(install.linkedInstalls.length, 5);
 
     // Might be in any order so sort them based on ID
     let installs = [install].concat(install.linkedInstalls);
     installs.sort(function(a, b) {
+      if (a.state != b.state) {
+        if (a.state == AddonManager.STATE_DOWNLOAD_FAILED)
+          return 1;
+        else if (b.state == AddonManager.STATE_DOWNLOAD_FAILED)
+          return -1;
+      }
+
+      // Don't care what order the failed installs show up in
+      if (a.state == AddonManager.STATE_DOWNLOAD_FAILED)
+        return 0;
+
       if (a.addon.id < b.addon.id)
         return -1;
       if (a.addon.id > b.addon.id)
         return 1;
       return 0;
     });
 
     // Comes from addon4.xpi and is made compatible by an update check
@@ -693,16 +706,22 @@ function run_test_11() {
     do_check_eq(installs[3].addon.id, "addon7@tests.mozilla.org");
     do_check_false(installs[3].addon.appDisabled);
     do_check_eq(installs[3].version, "5.0");
     do_check_eq(installs[3].name, "Multi Test 4");
     do_check_eq(installs[3].state, AddonManager.STATE_DOWNLOADED);
     do_check_true(hasFlag(installs[3].addon.operationsRequiringRestart,
                           AddonManager.OP_NEEDS_RESTART_INSTALL));
 
+    do_check_eq(installs[4].state, AddonManager.STATE_DOWNLOAD_FAILED);
+    do_check_eq(installs[4].error, AddonManager.ERROR_CORRUPT_FILE);
+
+    do_check_eq(installs[5].state, AddonManager.STATE_DOWNLOAD_FAILED);
+    do_check_eq(installs[5].error, AddonManager.ERROR_CORRUPT_FILE);
+
     AddonManager.getAllInstalls(function(aInstalls) {
       do_check_eq(aInstalls.length, 4);
 
       prepare_test({
         "addon4@tests.mozilla.org": [
           "onInstalling"
         ],
         "addon5@tests.mozilla.org": [
@@ -801,16 +820,18 @@ function run_test_12() {
         "onInstalling"
       ]
     }, {
       "NO_ID": [
         "onDownloadStarted",
         "onNewInstall",
         "onNewInstall",
         "onNewInstall",
+        "onNewInstall",
+        "onNewInstall",
         "onDownloadEnded"
       ],
       "addon4@tests.mozilla.org": [
         "onInstallStarted",
         "onInstallEnded"
       ],
       "addon5@tests.mozilla.org": [
         "onInstallStarted",
@@ -825,21 +846,32 @@ function run_test_12() {
         "onInstallEnded"
       ]
     }, callback_soon(check_test_12));
     install.install();
   }, "application/x-xpinstall", null, "Multi Test 4");
 }
 
 function check_test_12() {
-  do_check_eq(gInstall.linkedInstalls.length, 3);
+  do_check_eq(gInstall.linkedInstalls.length, 5);
 
   // Might be in any order so sort them based on ID
   let installs = [gInstall].concat(gInstall.linkedInstalls);
   installs.sort(function(a, b) {
+    if (a.state != b.state) {
+      if (a.state == AddonManager.STATE_DOWNLOAD_FAILED)
+        return 1;
+      else if (b.state == AddonManager.STATE_DOWNLOAD_FAILED)
+        return -1;
+    }
+
+    // Don't care what order the failed installs show up in
+    if (a.state == AddonManager.STATE_DOWNLOAD_FAILED)
+      return 0;
+
     if (a.addon.id < b.addon.id)
       return -1;
     if (a.addon.id > b.addon.id)
       return 1;
     return 0;
   });
 
   // Comes from addon4.xpi and is made compatible by an update check
@@ -869,16 +901,22 @@ function check_test_12() {
   // Comes from addon7.jar and is made compatible by an update check
   do_check_eq(installs[3].sourceURI, gInstall.sourceURI);
   do_check_eq(installs[3].addon.id, "addon7@tests.mozilla.org");
   do_check_false(installs[3].addon.appDisabled);
   do_check_eq(installs[3].version, "5.0");
   do_check_eq(installs[3].name, "Multi Test 4");
   do_check_eq(installs[3].state, AddonManager.STATE_INSTALLED);
 
+  do_check_eq(installs[4].state, AddonManager.STATE_DOWNLOAD_FAILED);
+  do_check_eq(installs[4].error, AddonManager.ERROR_CORRUPT_FILE);
+
+  do_check_eq(installs[5].state, AddonManager.STATE_DOWNLOAD_FAILED);
+  do_check_eq(installs[5].error, AddonManager.ERROR_CORRUPT_FILE);
+
   restartManager();
 
   AddonManager.getAddonsByIDs(["addon4@tests.mozilla.org",
                                "addon5@tests.mozilla.org",
                                "addon6@tests.mozilla.org",
                                "addon7@tests.mozilla.org"],
                                function([a4, a5, a6, a7]) {
     do_check_neq(a4, null);
@@ -1645,10 +1683,48 @@ function finish_test_27(aInstall) {
   }, [
     "onInstallCancelled"
   ]);
 
   aInstall.cancel();
 
   ensure_test_completed();
 
-  end_test();
+  run_test_30();
 }
+
+// Tests that a multi-package XPI with no add-ons inside shows up as a
+// corrupt file
+function run_test_30() {
+  prepare_test({ }, [
+    "onNewInstall"
+  ]);
+
+  AddonManager.getInstallForFile(do_get_addon("test_install7"), function(install) {
+    ensure_test_completed();