Merge m-c to inbound. a=merge
authorRyan VanderMeulen <ryanvm@gmail.com>
Tue, 24 Mar 2015 11:58:07 -0400
changeset 264227 47fa87252df0352a66c4698ceee71e351e8f2c8b
parent 264226 d026794b4c0b4a95414deaab831da79c27232586 (current diff)
parent 264163 2e22440310321d890737f32af5ff31549ec70c6d (diff)
child 264228 ee8dc146ae8249d75410b3ede0650eb98564fc2c
push id4718
push userraliiev@mozilla.com
push dateMon, 11 May 2015 18:39:53 +0000
treeherdermozilla-beta@c20c4ef55f08 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersmerge
milestone39.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
browser/devtools/shared/getjson.js
browser/devtools/webide/modules/remote-resources.js
modules/libpref/init/all.js
testing/marionette/common.js
testing/marionette/elements.js
testing/marionette/frame-manager.js
testing/marionette/listener.js
testing/marionette/sendkeys.js
testing/marionette/simpletest.js
toolkit/components/telemetry/Histograms.json
--- a/b2g/config/dolphin/sources.xml
+++ b/b2g/config/dolphin/sources.xml
@@ -10,17 +10,17 @@
   <!--original fetch url was git://codeaurora.org/-->
   <remote fetch="https://git.mozilla.org/external/caf" name="caf"/>
   <!--original fetch url was https://git.mozilla.org/releases-->
   <remote fetch="https://git.mozilla.org/releases" name="mozillaorg"/>
   <!-- B2G specific things. -->
   <project name="platform_build" path="build" remote="b2g" revision="ef937d1aca7c4cf89ecb5cc43ae8c21c2000a9db">
     <copyfile dest="Makefile" src="core/root.mk"/>
   </project>
-  <project name="gaia" path="gaia" remote="mozillaorg" revision="efebbafd12fc42ddcd378948b683a51106517660"/>
+  <project name="gaia" path="gaia" remote="mozillaorg" revision="00d50b78a7a99de5b0b55309a467807c677e2a66"/>
   <project name="fake-libdvm" path="dalvik" remote="b2g" revision="d50ae982b19f42f0b66d08b9eb306be81687869f"/>
   <project name="gonk-misc" path="gonk-misc" remote="b2g" revision="86cd7486d8e50eaac8ef6fe2f51f09d25194577b"/>
   <project name="librecovery" path="librecovery" remote="b2g" revision="1b3591a50ed352fc6ddb77462b7b35d0bfa555a3"/>
   <project name="moztt" path="external/moztt" remote="b2g" revision="ed2cf97a6c37a4bbd0bbbbffe06ec7136d8c79ff"/>
   <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="b685e3aab4fde7624d78993877a8f7910f2a5f06"/>
--- 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="efebbafd12fc42ddcd378948b683a51106517660"/>
+  <project name="gaia.git" path="gaia" remote="mozillaorg" revision="00d50b78a7a99de5b0b55309a467807c677e2a66"/>
   <project name="gonk-misc" path="gonk-misc" remote="b2g" revision="86cd7486d8e50eaac8ef6fe2f51f09d25194577b"/>
   <project name="rilproxy" path="rilproxy" remote="b2g" revision="5ef30994f4778b4052e58a4383dbe7890048c87e"/>
   <project name="platform_hardware_ril" path="hardware/ril" remote="b2g" revision="93f9ba577f68d772093987c2f1c0a4ae293e1802"/>
   <project name="platform_external_qemu" path="external/qemu" remote="b2g" revision="527d1c939ee57deb7192166e56e2a3fffa8cb087"/>
   <project name="moztt" path="external/moztt" remote="b2g" revision="ed2cf97a6c37a4bbd0bbbbffe06ec7136d8c79ff"/>
   <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,17 +12,17 @@
   <!--original fetch url was https://git.mozilla.org/releases-->
   <remote fetch="https://git.mozilla.org/releases" name="mozillaorg"/>
   <!-- B2G specific things. -->
   <project name="platform_build" path="build" remote="b2g" revision="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="efebbafd12fc42ddcd378948b683a51106517660"/>
+  <project name="gaia" path="gaia" remote="mozillaorg" revision="00d50b78a7a99de5b0b55309a467807c677e2a66"/>
   <project name="gonk-misc" path="gonk-misc" remote="b2g" revision="86cd7486d8e50eaac8ef6fe2f51f09d25194577b"/>
   <project name="moztt" path="external/moztt" remote="b2g" revision="ed2cf97a6c37a4bbd0bbbbffe06ec7136d8c79ff"/>
   <project name="apitrace" path="external/apitrace" remote="apitrace" revision="b685e3aab4fde7624d78993877a8f7910f2a5f06"/>
   <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"/>
@@ -130,12 +130,12 @@
   <project name="android-development" path="development" remote="b2g" revision="dab55669da8f48b6e57df95d5af9f16b4a87b0b1"/>
   <project name="device/generic/armv7-a-neon" path="device/generic/armv7-a-neon" revision="3a9a17613cc685aa232432566ad6cc607eab4ec1"/>
   <project name="device_generic_goldfish" path="device/generic/goldfish" remote="b2g" revision="197cd9492b9fadaa915c5daf36ff557f8f4a8d1c"/>
   <project name="platform/external/libnfc-nci" path="external/libnfc-nci" revision="7d33aaf740bbf6c7c6e9c34a92b371eda311b66b"/>
   <project name="libnfcemu" path="external/libnfcemu" remote="b2g" revision="125ccf9bd5986c7728ea44508b3e1d1185ac028b"/>
   <project name="platform_external_qemu" path="external/qemu" remote="b2g" revision="c5f8d282efe4a4e8b1e31a37300944e338e60e4f"/>
   <project name="platform/external/wpa_supplicant_8" path="external/wpa_supplicant_8" revision="0e56e450367cd802241b27164a2979188242b95f"/>
   <project name="platform_hardware_ril" path="hardware/ril" remote="b2g" revision="9f28c4faea3b2f01db227b2467b08aeba96d9bec"/>
-  <project name="platform_system_nfcd" path="system/nfcd" remote="b2g" revision="ab0e87a2be365b9c7470a9deca1eeaaa2f962b18"/>
+  <project name="platform_system_nfcd" path="system/nfcd" remote="b2g" revision="306b3290ea22211397d3daf49a42258f4638e4b7"/>
   <project name="android-sdk" path="sdk" remote="b2g" revision="8b1365af38c9a653df97349ee53a3f5d64fd590a"/>
   <project name="darwinstreamingserver" path="system/darwinstreamingserver" remote="b2g" revision="cf85968c7f85e0ec36e72c87ceb4837a943b8af6"/>
 </manifest>
--- a/b2g/config/emulator-kk/sources.xml
+++ b/b2g/config/emulator-kk/sources.xml
@@ -10,17 +10,17 @@
   <!--original fetch url was git://codeaurora.org/-->
   <remote fetch="https://git.mozilla.org/external/caf" name="caf"/>
   <!--original fetch url was https://git.mozilla.org/releases-->
   <remote fetch="https://git.mozilla.org/releases" name="mozillaorg"/>
   <!-- B2G specific things. -->
   <project name="platform_build" path="build" remote="b2g" revision="ef937d1aca7c4cf89ecb5cc43ae8c21c2000a9db">
     <copyfile dest="Makefile" src="core/root.mk"/>
   </project>
-  <project name="gaia" path="gaia" remote="mozillaorg" revision="efebbafd12fc42ddcd378948b683a51106517660"/>
+  <project name="gaia" path="gaia" remote="mozillaorg" revision="00d50b78a7a99de5b0b55309a467807c677e2a66"/>
   <project name="fake-libdvm" path="dalvik" remote="b2g" revision="d50ae982b19f42f0b66d08b9eb306be81687869f"/>
   <project name="gonk-misc" path="gonk-misc" remote="b2g" revision="86cd7486d8e50eaac8ef6fe2f51f09d25194577b"/>
   <project name="librecovery" path="librecovery" remote="b2g" revision="1b3591a50ed352fc6ddb77462b7b35d0bfa555a3"/>
   <project name="moztt" path="external/moztt" remote="b2g" revision="ed2cf97a6c37a4bbd0bbbbffe06ec7136d8c79ff"/>
   <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="b685e3aab4fde7624d78993877a8f7910f2a5f06"/>
--- a/b2g/config/emulator-l/sources.xml
+++ b/b2g/config/emulator-l/sources.xml
@@ -10,17 +10,17 @@
   <!--original fetch url was git://codeaurora.org/-->
   <remote fetch="https://git.mozilla.org/external/caf" name="caf"/>
   <!--original fetch url was https://git.mozilla.org/releases-->
   <remote fetch="https://git.mozilla.org/releases" name="mozillaorg"/>
   <!-- B2G specific things. -->
   <project name="platform_build" path="build" remote="b2g" revision="52775e03a2d8532429dff579cb2cd56718e488c3">
     <copyfile dest="Makefile" src="core/root.mk"/>
   </project>
-  <project name="gaia" path="gaia" remote="mozillaorg" revision="efebbafd12fc42ddcd378948b683a51106517660"/>
+  <project name="gaia" path="gaia" remote="mozillaorg" revision="00d50b78a7a99de5b0b55309a467807c677e2a66"/>
   <project name="fake-libdvm" path="dalvik" remote="b2g" revision="d50ae982b19f42f0b66d08b9eb306be81687869f"/>
   <project name="gonk-misc" path="gonk-misc" remote="b2g" revision="86cd7486d8e50eaac8ef6fe2f51f09d25194577b"/>
   <project name="librecovery" path="librecovery" remote="b2g" revision="1b3591a50ed352fc6ddb77462b7b35d0bfa555a3"/>
   <project name="moztt" path="external/moztt" remote="b2g" revision="ed2cf97a6c37a4bbd0bbbbffe06ec7136d8c79ff"/>
   <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="b685e3aab4fde7624d78993877a8f7910f2a5f06"/>
--- 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="efebbafd12fc42ddcd378948b683a51106517660"/>
+  <project name="gaia.git" path="gaia" remote="mozillaorg" revision="00d50b78a7a99de5b0b55309a467807c677e2a66"/>
   <project name="gonk-misc" path="gonk-misc" remote="b2g" revision="86cd7486d8e50eaac8ef6fe2f51f09d25194577b"/>
   <project name="rilproxy" path="rilproxy" remote="b2g" revision="5ef30994f4778b4052e58a4383dbe7890048c87e"/>
   <project name="platform_hardware_ril" path="hardware/ril" remote="b2g" revision="93f9ba577f68d772093987c2f1c0a4ae293e1802"/>
   <project name="platform_external_qemu" path="external/qemu" remote="b2g" revision="527d1c939ee57deb7192166e56e2a3fffa8cb087"/>
   <project name="moztt" path="external/moztt" remote="b2g" revision="ed2cf97a6c37a4bbd0bbbbffe06ec7136d8c79ff"/>
   <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,17 +10,17 @@
   <!--original fetch url was git://codeaurora.org/-->
   <remote fetch="https://git.mozilla.org/external/caf" name="caf"/>
   <!--original fetch url was https://git.mozilla.org/releases-->
   <remote fetch="https://git.mozilla.org/releases" name="mozillaorg"/>
   <!-- B2G specific things. -->
   <project name="platform_build" path="build" remote="b2g" revision="ef937d1aca7c4cf89ecb5cc43ae8c21c2000a9db">
     <copyfile dest="Makefile" src="core/root.mk"/>
   </project>
-  <project name="gaia" path="gaia" remote="mozillaorg" revision="efebbafd12fc42ddcd378948b683a51106517660"/>
+  <project name="gaia" path="gaia" remote="mozillaorg" revision="00d50b78a7a99de5b0b55309a467807c677e2a66"/>
   <project name="fake-libdvm" path="dalvik" remote="b2g" revision="d50ae982b19f42f0b66d08b9eb306be81687869f"/>
   <project name="gonk-misc" path="gonk-misc" remote="b2g" revision="86cd7486d8e50eaac8ef6fe2f51f09d25194577b"/>
   <project name="librecovery" path="librecovery" remote="b2g" revision="1b3591a50ed352fc6ddb77462b7b35d0bfa555a3"/>
   <project name="moztt" path="external/moztt" remote="b2g" revision="ed2cf97a6c37a4bbd0bbbbffe06ec7136d8c79ff"/>
   <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="b685e3aab4fde7624d78993877a8f7910f2a5f06"/>
@@ -141,13 +141,13 @@
   <project name="platform/hardware/qcom/camera" path="hardware/qcom/camera" revision="2a1ded216a91bf62a72b1640cf01ab4998f37028"/>
   <project name="platform/hardware/qcom/display" path="hardware/qcom/display" revision="a74adcf8d88320d936daa8d20ce88ca0107fb916"/>
   <project name="platform/hardware/qcom/gps" path="hardware/qcom/gps" revision="9883ea57b0668d8f60dba025d4522dfa69a1fbfa"/>
   <project name="platform/hardware/qcom/media" path="hardware/qcom/media" revision="a558dc844bf5144fc38603fd8f4df8d9557052a5"/>
   <project name="platform/hardware/qcom/wlan" path="hardware/qcom/wlan" revision="57ee1320ed7b4a1a1274d8f3f6c177cd6b9becb2"/>
   <project name="platform/hardware/ril" path="hardware/ril" revision="12b1977cc704b35f2e9db2bb423fa405348bc2f3"/>
   <project name="platform/system/bluetooth" path="system/bluetooth" revision="985bf15264d865fe7b9c5b45f61c451cbaafa43d"/>
   <project name="platform/system/core" path="system/core" revision="42839aedcf70bf6bc92a3b7ea4a5cc9bf9aef3f9"/>
-  <project name="platform_system_nfcd" path="system/nfcd" remote="b2g" revision="ab0e87a2be365b9c7470a9deca1eeaaa2f962b18"/>
+  <project name="platform_system_nfcd" path="system/nfcd" remote="b2g" revision="306b3290ea22211397d3daf49a42258f4638e4b7"/>
   <project name="platform/system/qcom" path="system/qcom" revision="63e3f6f176caad587d42bba4c16b66d953fb23c2"/>
   <project name="platform/vendor/qcom-opensource/wlan/prima" path="vendor/qcom/opensource/wlan/prima" revision="d8952a42771045fca73ec600e2b42a4c7129d723"/>
   <project name="platform/vendor/qcom/msm8610" path="device/qcom/msm8610" revision="4c187c1f3a0dffd8e51a961735474ea703535b99"/>
 </manifest>
--- a/b2g/config/flame/sources.xml
+++ b/b2g/config/flame/sources.xml
@@ -12,17 +12,17 @@
   <!--original fetch url was https://git.mozilla.org/releases-->
   <remote fetch="https://git.mozilla.org/releases" name="mozillaorg"/>
   <!-- B2G specific things. -->
   <project name="platform_build" path="build" remote="b2g" revision="4efd19d199ae52656604f794c5a77518400220fd">
     <copyfile dest="Makefile" src="core/root.mk"/>
   </project>
   <project name="librecovery" path="librecovery" remote="b2g" revision="1b3591a50ed352fc6ddb77462b7b35d0bfa555a3"/>
   <project name="fake-libdvm" path="dalvik" remote="b2g" revision="d50ae982b19f42f0b66d08b9eb306be81687869f"/>
-  <project name="gaia" path="gaia" remote="mozillaorg" revision="efebbafd12fc42ddcd378948b683a51106517660"/>
+  <project name="gaia" path="gaia" remote="mozillaorg" revision="00d50b78a7a99de5b0b55309a467807c677e2a66"/>
   <project name="gonk-misc" path="gonk-misc" remote="b2g" revision="86cd7486d8e50eaac8ef6fe2f51f09d25194577b"/>
   <project name="moztt" path="external/moztt" remote="b2g" revision="ed2cf97a6c37a4bbd0bbbbffe06ec7136d8c79ff"/>
   <project name="apitrace" path="external/apitrace" remote="apitrace" revision="b685e3aab4fde7624d78993877a8f7910f2a5f06"/>
   <project name="valgrind" path="external/valgrind" remote="b2g" revision="daa61633c32b9606f58799a3186395fd2bbb8d8c"/>
   <project name="vex" path="external/VEX" remote="b2g" revision="47f031c320888fe9f3e656602588565b52d43010"/>
   <!-- Stock Android things -->
   <project groups="linux" name="platform/prebuilts/clang/linux-x86/3.1" path="prebuilts/clang/linux-x86/3.1" revision="e95b4ce22c825da44d14299e1190ea39a5260bde"/>
   <project groups="linux" name="platform/prebuilts/clang/linux-x86/3.2" path="prebuilts/clang/linux-x86/3.2" revision="471afab478649078ad7c75ec6b252481a59e19b8"/>
@@ -140,13 +140,13 @@
   <project name="platform/hardware/qcom/camera" path="hardware/qcom/camera" revision="5e110615212302c5d798a3c223dcee458817651c"/>
   <project name="platform/hardware/qcom/display" path="hardware/qcom/display" revision="fa9ffd47948eb24466de227e48fe9c4a7c5e7711"/>
   <project name="platform/hardware/qcom/gps" path="hardware/qcom/gps" revision="cd76b19aafd4229ccf83853d02faef8c51ca8b34"/>
   <project name="platform/hardware/qcom/media" path="hardware/qcom/media" revision="8a0d0b0d9889ef99c4c6317c810db4c09295f15a"/>
   <project name="platform/hardware/qcom/wlan" path="hardware/qcom/wlan" revision="2208fa3537ace873b8f9ec2355055761c79dfd5f"/>
   <project name="platform/hardware/ril" path="hardware/ril" revision="c4e2ac95907a5519a0e09f01a0d8e27fec101af0"/>
   <project name="platform/system/bluetooth" path="system/bluetooth" revision="e1eb226fa3ad3874ea7b63c56a9dc7012d7ff3c2"/>
   <project name="platform/system/core" path="system/core" revision="adc485d8755af6a61641d197de7cfef667722580"/>
-  <project name="platform_system_nfcd" path="system/nfcd" remote="b2g" revision="ab0e87a2be365b9c7470a9deca1eeaaa2f962b18"/>
+  <project name="platform_system_nfcd" path="system/nfcd" remote="b2g" revision="306b3290ea22211397d3daf49a42258f4638e4b7"/>
   <project name="platform/system/qcom" path="system/qcom" revision="1cdab258b15258b7f9657da70e6f06ebd5a2fc25"/>
   <project name="platform/vendor/qcom/msm8610" path="device/qcom/msm8610" revision="4ae5df252123591d5b941191790e7abed1bce5a4"/>
   <project name="platform/vendor/qcom-opensource/wlan/prima" path="vendor/qcom/opensource/wlan/prima" revision="ce18b47b4a4f93a581d672bbd5cb6d12fe796ca9"/>
 </manifest>
--- a/b2g/config/gaia.json
+++ b/b2g/config/gaia.json
@@ -1,9 +1,9 @@
 {
     "git": {
-        "git_revision": "efebbafd12fc42ddcd378948b683a51106517660", 
+        "git_revision": "00d50b78a7a99de5b0b55309a467807c677e2a66", 
         "remote": "https://git.mozilla.org/releases/gaia.git", 
         "branch": ""
     }, 
-    "revision": "d8e53e5d917b1ce79aea842e8340ce82799cac3e", 
+    "revision": "c1db04cf683e85135ca587f1f414d4456b20cfa0", 
     "repo_path": "integration/gaia-central"
 }
--- a/b2g/config/nexus-4/sources.xml
+++ b/b2g/config/nexus-4/sources.xml
@@ -12,17 +12,17 @@
   <!--original fetch url was https://git.mozilla.org/releases-->
   <remote fetch="https://git.mozilla.org/releases" name="mozillaorg"/>
   <!-- B2G specific things. -->
   <project name="platform_build" path="build" remote="b2g" revision="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="efebbafd12fc42ddcd378948b683a51106517660"/>
+  <project name="gaia" path="gaia" remote="mozillaorg" revision="00d50b78a7a99de5b0b55309a467807c677e2a66"/>
   <project name="gonk-misc" path="gonk-misc" remote="b2g" revision="86cd7486d8e50eaac8ef6fe2f51f09d25194577b"/>
   <project name="moztt" path="external/moztt" remote="b2g" revision="ed2cf97a6c37a4bbd0bbbbffe06ec7136d8c79ff"/>
   <project name="apitrace" path="external/apitrace" remote="apitrace" revision="b685e3aab4fde7624d78993877a8f7910f2a5f06"/>
   <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"/>
@@ -125,17 +125,17 @@
   <project name="platform/system/netd" path="system/netd" revision="56112dd7b811301b718d0643a82fd5cac9522073"/>
   <project name="platform/system/security" path="system/security" revision="f48ff68fedbcdc12b570b7699745abb6e7574907"/>
   <project name="platform/system/vold" path="system/vold" revision="8de05d4a52b5a91e7336e6baa4592f945a6ddbea"/>
   <default remote="caf" revision="refs/tags/android-4.3_r2.1" sync-j="4"/>
   <!-- Nexus 4 specific things -->
   <project name="device-mako" path="device/lge/mako" remote="b2g" revision="78d17f0c117f0c66dd55ee8d5c5dde8ccc93ecba"/>
   <project name="device/generic/armv7-a-neon" path="device/generic/armv7-a-neon" revision="3a9a17613cc685aa232432566ad6cc607eab4ec1"/>
   <project name="device/lge/mako-kernel" path="device/lge/mako-kernel" revision="d1729e53d71d711c8fde25eab8728ff2b9b4df0e"/>
-  <project name="platform_system_nfcd" path="system/nfcd" remote="b2g" revision="ab0e87a2be365b9c7470a9deca1eeaaa2f962b18"/>
+  <project name="platform_system_nfcd" path="system/nfcd" remote="b2g" revision="306b3290ea22211397d3daf49a42258f4638e4b7"/>
   <project name="platform/external/libnfc-nci" path="external/libnfc-nci" revision="7d33aaf740bbf6c7c6e9c34a92b371eda311b66b"/>
   <project name="platform/external/wpa_supplicant_8" path="external/wpa_supplicant_8" revision="0e56e450367cd802241b27164a2979188242b95f"/>
   <project name="platform/hardware/broadcom/wlan" path="hardware/broadcom/wlan" revision="0e1929fa3aa38bf9d40e9e953d619fab8164c82e"/>
   <project name="platform/hardware/qcom/audio" path="hardware/qcom/audio" revision="b0a528d839cfd9d170d092fe3743b5252b4243a6"/>
   <project name="platform/hardware/qcom/bt" path="hardware/qcom/bt" revision="380945eaa249a2dbdde0daa4c8adb8ca325edba6"/>
   <project name="platform/hardware/qcom/display" path="hardware/qcom/display" revision="6f3b0272cefaffeaed2a7d2bb8f633059f163ddc"/>
   <project name="platform/hardware/qcom/keymaster" path="hardware/qcom/keymaster" revision="16da8262c997a5a0d797885788a64a0771b26910"/>
   <project name="platform/hardware/qcom/media" path="hardware/qcom/media" revision="689b476ba3eb46c34b81343295fe144a0e81a18e"/>
--- a/b2g/config/nexus-5-l/sources.xml
+++ b/b2g/config/nexus-5-l/sources.xml
@@ -10,17 +10,17 @@
   <!--original fetch url was git://codeaurora.org/-->
   <remote fetch="https://git.mozilla.org/external/caf" name="caf"/>
   <!--original fetch url was https://git.mozilla.org/releases-->
   <remote fetch="https://git.mozilla.org/releases" name="mozillaorg"/>
   <!-- B2G specific things. -->
   <project name="platform_build" path="build" remote="b2g" revision="52775e03a2d8532429dff579cb2cd56718e488c3">
     <copyfile dest="Makefile" src="core/root.mk"/>
   </project>
-  <project name="gaia" path="gaia" remote="mozillaorg" revision="efebbafd12fc42ddcd378948b683a51106517660"/>
+  <project name="gaia" path="gaia" remote="mozillaorg" revision="00d50b78a7a99de5b0b55309a467807c677e2a66"/>
   <project name="fake-libdvm" path="dalvik" remote="b2g" revision="d50ae982b19f42f0b66d08b9eb306be81687869f"/>
   <project name="gonk-misc" path="gonk-misc" remote="b2g" revision="86cd7486d8e50eaac8ef6fe2f51f09d25194577b"/>
   <project name="librecovery" path="librecovery" remote="b2g" revision="1b3591a50ed352fc6ddb77462b7b35d0bfa555a3"/>
   <project name="moztt" path="external/moztt" remote="b2g" revision="ed2cf97a6c37a4bbd0bbbbffe06ec7136d8c79ff"/>
   <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="b685e3aab4fde7624d78993877a8f7910f2a5f06"/>
@@ -151,10 +151,10 @@
   <project name="hardware_qcom_display" path="hardware/qcom/display" remote="b2g" revision="c43952000d57f08b93a0e4fb77052871ce587976"/>
   <project name="platform/hardware/qcom/keymaster" path="hardware/qcom/keymaster" revision="028649652cd8f8f18cfb47d34bd78c435eb030ca"/>
   <project name="platform/hardware/qcom/media" path="hardware/qcom/media" revision="758a80fbb178b5663d4edbb46944b2dc553cb1ca"/>
   <project name="platform/hardware/qcom/msm8x74" path="hardware/qcom/msm8x74" revision="aa0124820e22302149b1f2db603a9a72f1972527"/>
   <project name="platform/hardware/qcom/power" path="hardware/qcom/power" revision="37499eb89f31233135ca73b830b067ab24dc1be2"/>
   <project name="platform/hardware/qcom/sensors" path="hardware/qcom/sensors" revision="3724fd91ef5183684d97e2bf1d7ff948faabe090"/>
   <project name="platform/hardware/qcom/wlan" path="hardware/qcom/wlan" revision="2e54754cc0529d26ccac37ed291600048adbf6c0"/>
   <project name="platform/hardware/ril" path="hardware/ril" revision="71dfa8228ad0d6cdf6bac0426ac59404ab74b7f3"/>
-  <project name="platform_system_nfcd" path="system/nfcd" remote="b2g" revision="ab0e87a2be365b9c7470a9deca1eeaaa2f962b18"/>
+  <project name="platform_system_nfcd" path="system/nfcd" remote="b2g" revision="306b3290ea22211397d3daf49a42258f4638e4b7"/>
 </manifest>
--- a/browser/app/profile/firefox.js
+++ b/browser/app/profile/firefox.js
@@ -1293,17 +1293,17 @@ pref("services.sync.prefs.sync.dom.disab
 pref("services.sync.prefs.sync.dom.disable_window_flip", true);
 pref("services.sync.prefs.sync.dom.disable_window_move_resize", true);
 pref("services.sync.prefs.sync.dom.event.contextmenu.enabled", true);
 pref("services.sync.prefs.sync.extensions.personas.current", true);
 pref("services.sync.prefs.sync.extensions.update.enabled", true);
 pref("services.sync.prefs.sync.intl.accept_languages", true);
 pref("services.sync.prefs.sync.javascript.enabled", true);
 pref("services.sync.prefs.sync.layout.spellcheckDefault", true);
-pref("services.sync.prefs.sync.lightweightThemes.isThemeSelected", true);
+pref("services.sync.prefs.sync.lightweightThemes.selectedThemeID", true);
 pref("services.sync.prefs.sync.lightweightThemes.usedThemes", true);
 pref("services.sync.prefs.sync.network.cookie.cookieBehavior", true);
 pref("services.sync.prefs.sync.network.cookie.lifetimePolicy", true);
 pref("services.sync.prefs.sync.permissions.default.image", true);
 pref("services.sync.prefs.sync.pref.advanced.images.disable_button.view_image", true);
 pref("services.sync.prefs.sync.pref.advanced.javascript.disable_button.advanced", true);
 pref("services.sync.prefs.sync.pref.downloads.disable_button.edit_actions", true);
 pref("services.sync.prefs.sync.pref.privacy.disable_button.cookie_exceptions", true);
@@ -1879,8 +1879,9 @@ pref("dom.ipc.reportProcessHangs", true)
 pref("reader.parse-on-load.enabled", false);
 #endif
 
 // Enable ReadingList browser UI by default.
 pref("browser.readinglist.enabled", true);
 pref("browser.readinglist.sidebarEverOpened", false);
 // Enable the readinglist engine by default.
 pref("readinglist.scheduler.enabled", true);
+pref("readinglist.server", "https://readinglist.services.mozilla.com/v1");
--- a/browser/base/content/browser-devedition.js
+++ b/browser/base/content/browser-devedition.js
@@ -4,17 +4,17 @@
 
 /**
  * Listeners for the DevEdition theme.  This adds an extra stylesheet
  * to browser.xul if a pref is set and no other themes are applied.
  */
 let DevEdition = {
   _prefName: "browser.devedition.theme.enabled",
   _themePrefName: "general.skins.selectedSkin",
-  _lwThemePrefName: "lightweightThemes.isThemeSelected",
+  _lwThemePrefName: "lightweightThemes.selectedThemeID",
   _devtoolsThemePrefName: "devtools.theme",
 
   styleSheetLocation: "chrome://browser/skin/devedition.css",
   styleSheet: null,
 
   init: function () {
     this._updateDevtoolsThemeAttribute();
     this._updateStyleSheetFromPrefs();
@@ -71,17 +71,17 @@ let DevEdition = {
     document.documentElement.setAttribute("devtoolstheme", devtoolsTheme);
     this._inferBrightness();
     this._updateStyleSheetFromPrefs();
   },
 
   _updateStyleSheetFromPrefs: function() {
     let lightweightThemeSelected = false;
     try {
-      lightweightThemeSelected = Services.prefs.getBoolPref(this._lwThemePrefName);
+      lightweightThemeSelected = !!Services.prefs.getCharPref(this._lwThemePrefName);
     } catch(e) {}
 
     let defaultThemeSelected = false;
     try {
        defaultThemeSelected = Services.prefs.getCharPref(this._themePrefName) == "classic/1.0";
     } catch(e) {}
 
     let deveditionThemeEnabled = Services.prefs.getBoolPref(this._prefName) &&
--- a/browser/base/content/browser-syncui.js
+++ b/browser/base/content/browser-syncui.js
@@ -452,17 +452,17 @@ let gSyncUI = {
 
   // Return true if the reading-list is in a "prolonged" error state. That
   // engine doesn't impose what that means, so calculate it here. For
   // consistency, we just use the sync prefs.
   isProlongedReadingListError() {
     let lastSync, threshold, prolonged;
     try {
       lastSync = new Date(Services.prefs.getCharPref("readinglist.scheduler.lastSync"));
-      threshold = new Date(Date.now() - Services.prefs.getIntPref("services.sync.errorhandler.networkFailureReportTimeout"));
+      threshold = new Date(Date.now() - Services.prefs.getIntPref("services.sync.errorhandler.networkFailureReportTimeout") * 1000);
       prolonged = lastSync <= threshold;
     } catch (ex) {
       // no pref, assume not prolonged.
       prolonged = false;
     }
     this.log.debug("isProlongedReadingListError has last successful sync at ${lastSync}, threshold is ${threshold}, prolonged=${prolonged}",
                    {lastSync, threshold, prolonged});
     return prolonged;
--- a/browser/base/content/test/general/browser_devedition.js
+++ b/browser/base/content/test/general/browser_devedition.js
@@ -1,44 +1,48 @@
 /*
  * Testing changes for Developer Edition theme.
  * A special stylesheet should be added to the browser.xul document
  * when browser.devedition.theme.enabled is set to true and no themes
  * are applied.
  */
 
 const PREF_DEVEDITION_THEME = "browser.devedition.theme.enabled";
-const PREF_LWTHEME = "lightweightThemes.isThemeSelected";
+const PREF_LWTHEME = "lightweightThemes.selectedThemeID";
+const PREF_LWTHEME_USED_THEMES = "lightweightThemes.usedThemes";
 const PREF_DEVTOOLS_THEME = "devtools.theme";
+const {LightweightThemeManager} = Components.utils.import("resource://gre/modules/LightweightThemeManager.jsm", {});
 
 registerCleanupFunction(() => {
   // Set preferences back to their original values
+  LightweightThemeManager.currentTheme = null;
   Services.prefs.clearUserPref(PREF_DEVEDITION_THEME);
   Services.prefs.clearUserPref(PREF_LWTHEME);
   Services.prefs.clearUserPref(PREF_DEVTOOLS_THEME);
+  Services.prefs.clearUserPref(PREF_LWTHEME_USED_THEMES);
 });
 
 add_task(function* startTests() {
   Services.prefs.setCharPref(PREF_DEVTOOLS_THEME, "dark");
 
   info ("Setting browser.devedition.theme.enabled to false.");
   Services.prefs.setBoolPref(PREF_DEVEDITION_THEME, false);
   ok (!DevEdition.styleSheet, "There is no devedition style sheet when the pref is false.");
 
   info ("Setting browser.devedition.theme.enabled to true.");
   Services.prefs.setBoolPref(PREF_DEVEDITION_THEME, true);
   ok (DevEdition.styleSheet, "There is a devedition stylesheet when no themes are applied and pref is set.");
 
   info ("Adding a lightweight theme.");
-  Services.prefs.setBoolPref(PREF_LWTHEME, true);
+  LightweightThemeManager.currentTheme = dummyLightweightTheme("preview0");
   ok (!DevEdition.styleSheet, "The devedition stylesheet has been removed when a lightweight theme is applied.");
 
   info ("Removing a lightweight theme.");
   let onAttributeAdded = waitForBrightTitlebarAttribute();
-  Services.prefs.setBoolPref(PREF_LWTHEME, false);
+  LightweightThemeManager.currentTheme = null;
   ok (DevEdition.styleSheet, "The devedition stylesheet has been added when a lightweight theme is removed.");
   yield onAttributeAdded;
 
   is (document.documentElement.getAttribute("brighttitlebarforeground"), "true",
      "The brighttitlebarforeground attribute is set on the window.");
 
   info ("Setting browser.devedition.theme.enabled to false.");
   Services.prefs.setBoolPref(PREF_DEVEDITION_THEME, false);
@@ -80,26 +84,24 @@ add_task(function* testDevtoolsTheme() {
   ok (!document.documentElement.hasAttribute("brighttitlebarforeground"),
      "The brighttitlebarforeground attribute is not set on the window with light devtools theme.");
 });
 
 function dummyLightweightTheme(id) {
   return {
     id: id,
     name: id,
-    headerURL: "http://lwttest.invalid/a.png",
-    footerURL: "http://lwttest.invalid/b.png",
+    headerURL: "resource:///chrome/browser/content/browser/defaultthemes/1.header.jpg",
+    iconURL: "resource:///chrome/browser/content/browser/defaultthemes/1.icon.jpg",
     textcolor: "red",
     accentcolor: "blue"
   };
 }
 
 add_task(function* testLightweightThemePreview() {
-  let {LightweightThemeManager} = Components.utils.import("resource://gre/modules/LightweightThemeManager.jsm", {});
-
   info ("Turning the pref on, then previewing lightweight themes");
   Services.prefs.setBoolPref(PREF_DEVEDITION_THEME, true);
   ok (DevEdition.styleSheet, "The devedition stylesheet is enabled.");
   LightweightThemeManager.previewTheme(dummyLightweightTheme("preview0"));
   ok (!DevEdition.styleSheet, "The devedition stylesheet is not enabled after a lightweight theme preview.");
   LightweightThemeManager.resetPreview();
   LightweightThemeManager.previewTheme(dummyLightweightTheme("preview1"));
   ok (!DevEdition.styleSheet, "The devedition stylesheet is not enabled after a second lightweight theme preview.");
--- a/browser/components/customizableui/test/browser_1007336_lwthemes_in_customize_mode.js
+++ b/browser/components/customizableui/test/browser_1007336_lwthemes_in_customize_mode.js
@@ -44,17 +44,17 @@ add_task(function () {
   ok(installedThemeId.startsWith(firstLWThemeId),
      "The second theme in the 'My Themes' section should be the newly installed theme: " +
      "Installed theme id: " + installedThemeId + "; First theme ID: " + firstLWThemeId);
   is(header.nextSibling.nextSibling.nextSibling, recommendedHeader,
      "There should be two themes in the 'My Themes' section");
 
   let defaultTheme = header.nextSibling;
   defaultTheme.doCommand();
-  is(Services.prefs.getBoolPref("lightweightThemes.isThemeSelected"), false, "No lwtheme should be selected");
+  is(Services.prefs.prefHasUserValue("lightweightThemes.selectedThemeID"), false, "No lwtheme should be selected");
 });
 
 add_task(function asyncCleanup() {
   yield endCustomizing();
 
   Services.prefs.clearUserPref("lightweightThemes.usedThemes");
   Services.prefs.clearUserPref("lightweightThemes.recommendedThemes");
 })
\ No newline at end of file
--- a/browser/components/loop/MozLoopAPI.jsm
+++ b/browser/components/loop/MozLoopAPI.jsm
@@ -634,16 +634,23 @@ function injectLoopAPI(targetWindow) {
 
     TWO_WAY_MEDIA_CONN_LENGTH: {
       enumerable: true,
       get: function() {
         return Cu.cloneInto(TWO_WAY_MEDIA_CONN_LENGTH, targetWindow);
       }
     },
 
+    SHARING_STATE_CHANGE: {
+      enumerable: true,
+      get: function() {
+        return Cu.cloneInto(SHARING_STATE_CHANGE, targetWindow);
+      }
+    },
+
     fxAEnabled: {
       enumerable: true,
       get: function() {
         return MozLoopService.fxAEnabled;
       },
     },
 
     logInToFxA: {
--- a/browser/components/loop/MozLoopService.jsm
+++ b/browser/components/loop/MozLoopService.jsm
@@ -24,30 +24,44 @@ const LOOP_SESSION_TYPE = {
  */
 const TWO_WAY_MEDIA_CONN_LENGTH = {
   SHORTER_THAN_10S: "SHORTER_THAN_10S",
   BETWEEN_10S_AND_30S: "BETWEEN_10S_AND_30S",
   BETWEEN_30S_AND_5M: "BETWEEN_30S_AND_5M",
   MORE_THAN_5M: "MORE_THAN_5M",
 };
 
+/**
+ * Buckets that we segment sharing state change telemetry probes into.
+ *
+ * @type {{WINDOW_ENABLED: String, WINDOW_DISABLED: String,
+ *   BROWSER_ENABLED: String, BROWSER_DISABLED: String}}
+ */
+const SHARING_STATE_CHANGE = {
+  WINDOW_ENABLED: "WINDOW_ENABLED",
+  WINDOW_DISABLED: "WINDOW_DISABLED",
+  BROWSER_ENABLED: "BROWSER_ENABLED",
+  BROWSER_DISABLED: "BROWSER_DISABLED"
+};
+
 // See LOG_LEVELS in Console.jsm. Common examples: "All", "Info", "Warn", & "Error".
 const PREF_LOG_LEVEL = "loop.debug.loglevel";
 
 Cu.import("resource://gre/modules/Services.jsm");
 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
 Cu.import("resource://gre/modules/Promise.jsm");
 Cu.import("resource://gre/modules/osfile.jsm", this);
 Cu.import("resource://gre/modules/Task.jsm");
 Cu.import("resource://gre/modules/Timer.jsm");
 Cu.import("resource://gre/modules/FxAccountsOAuthClient.jsm");
 
 Cu.importGlobalProperties(["URL"]);
 
-this.EXPORTED_SYMBOLS = ["MozLoopService", "LOOP_SESSION_TYPE", "TWO_WAY_MEDIA_CONN_LENGTH"];
+this.EXPORTED_SYMBOLS = ["MozLoopService", "LOOP_SESSION_TYPE",
+  "TWO_WAY_MEDIA_CONN_LENGTH", "SHARING_STATE_CHANGE"];
 
 XPCOMUtils.defineLazyModuleGetter(this, "injectLoopAPI",
   "resource:///modules/loop/MozLoopAPI.jsm");
 
 XPCOMUtils.defineLazyModuleGetter(this, "convertToRTCStatsReport",
   "resource://gre/modules/media/RTCStatsReport.jsm");
 
 XPCOMUtils.defineLazyModuleGetter(this, "Chat", "resource:///modules/Chat.jsm");
--- a/browser/components/loop/content/shared/js/actions.js
+++ b/browser/components/loop/content/shared/js/actions.js
@@ -356,17 +356,19 @@ loop.shared.actions = (function() {
     EmailRoomUrl: Action.define("emailRoomUrl", {
       roomUrl: String
     }),
 
     /**
      * XXX: should move to some roomActions module - refs bug 1079284
      */
     RoomFailure: Action.define("roomFailure", {
-      error: Object
+      error: Object,
+      // True when the failures occurs in the join room request to the loop-server.
+      failedJoinRequest: Boolean
     }),
 
     /**
      * Sets up the room information when it is received.
      * XXX: should move to some roomActions module - refs bug 1079284
      *
      * @see https://wiki.mozilla.org/Loop/Architecture/Rooms#GET_.2Frooms.2F.7Btoken.7D
      */
--- a/browser/components/loop/content/shared/js/activeRoomStore.js
+++ b/browser/components/loop/content/shared/js/activeRoomStore.js
@@ -100,17 +100,17 @@ loop.store.ActiveRoomStore = (function()
         actionData.error);
 
       this.setStoreState({
         error: actionData.error,
         failureReason: getReason(actionData.error.errno)
       });
 
       this._leaveRoom(actionData.error.errno === REST_ERRNOS.ROOM_FULL ?
-          ROOM_STATES.FULL : ROOM_STATES.FAILED);
+          ROOM_STATES.FULL : ROOM_STATES.FAILED, actionData.failedJoinRequest);
     },
 
     /**
      * Registers the actions with the dispatcher that this store is interested
      * in after the initial setup has been performed.
      */
     _registerPostSetupActions: function() {
       this.dispatcher.register(this, [
@@ -156,17 +156,20 @@ loop.store.ActiveRoomStore = (function()
         roomState: ROOM_STATES.GATHER,
         windowId: actionData.windowId
       });
 
       // Get the window data from the mozLoop api.
       this._mozLoop.rooms.get(actionData.roomToken,
         function(error, roomData) {
           if (error) {
-            this.dispatchAction(new sharedActions.RoomFailure({error: error}));
+            this.dispatchAction(new sharedActions.RoomFailure({
+              error: error,
+              failedJoinRequest: false
+            }));
             return;
           }
 
           this.dispatchAction(new sharedActions.SetupRoomInfo({
             roomToken: actionData.roomToken,
             roomName: roomData.roomName,
             roomOwner: roomData.roomOwner,
             roomUrl: roomData.roomUrl
@@ -288,17 +291,25 @@ loop.store.ActiveRoomStore = (function()
      * granted and starts joining the room.
      */
     gotMediaPermission: function() {
       this.setStoreState({roomState: ROOM_STATES.JOINING});
 
       this._mozLoop.rooms.join(this._storeState.roomToken,
         function(error, responseData) {
           if (error) {
-            this.dispatchAction(new sharedActions.RoomFailure({error: error}));
+            this.dispatchAction(new sharedActions.RoomFailure({
+              error: error,
+              // This is an explicit flag to avoid the leave happening if join
+              // fails. We can't track it on ROOM_STATES.JOINING as the user
+              // might choose to leave the room whilst the XHR is in progress
+              // which would then mean we'd run the race condition of not
+              // notifying the server of a leave.
+              failedJoinRequest: true
+            }));
             return;
           }
 
           this.dispatchAction(new sharedActions.JoinedRoom({
             apiKey: responseData.apiKey,
             sessionToken: responseData.sessionToken,
             sessionId: responseData.sessionId,
             expires: responseData.expires
@@ -550,31 +561,37 @@ loop.store.ActiveRoomStore = (function()
      * Refreshes the membership of the room with the server, and then
      * sets up the refresh for the next cycle.
      */
     _refreshMembership: function() {
       this._mozLoop.rooms.refreshMembership(this._storeState.roomToken,
         this._storeState.sessionToken,
         function(error, responseData) {
           if (error) {
-            this.dispatchAction(new sharedActions.RoomFailure({error: error}));
+            this.dispatchAction(new sharedActions.RoomFailure({
+              error: error,
+              failedJoinRequest: false
+            }));
             return;
           }
 
           this._setRefreshTimeout(responseData.expires);
         }.bind(this));
     },
 
     /**
      * Handles leaving a room. Clears any membership timeouts, then
      * signals to the server the leave of the room.
      *
-     * @param {ROOM_STATES} nextState The next state to switch to.
+     * @param {ROOM_STATES} nextState         The next state to switch to.
+     * @param {Boolean}     failedJoinRequest Optional. Set to true if the join
+     *                                        request to loop-server failed. It
+     *                                        will skip the leave message.
      */
-    _leaveRoom: function(nextState) {
+    _leaveRoom: function(nextState, failedJoinRequest) {
       if (loop.standaloneMedia) {
         loop.standaloneMedia.multiplexGum.reset();
       }
 
       this._mozLoop.setScreenShareState(
         this.getStoreState().windowId,
         false);
 
@@ -587,20 +604,21 @@ loop.store.ActiveRoomStore = (function()
       // We probably don't need to end screen share separately, but lets be safe.
       this._sdkDriver.disconnectSession();
 
       if (this._timeout) {
         clearTimeout(this._timeout);
         delete this._timeout;
       }
 
-      if (this._storeState.roomState === ROOM_STATES.JOINING ||
-          this._storeState.roomState === ROOM_STATES.JOINED ||
-          this._storeState.roomState === ROOM_STATES.SESSION_CONNECTED ||
-          this._storeState.roomState === ROOM_STATES.HAS_PARTICIPANTS) {
+      if (!failedJoinRequest &&
+          (this._storeState.roomState === ROOM_STATES.JOINING ||
+           this._storeState.roomState === ROOM_STATES.JOINED ||
+           this._storeState.roomState === ROOM_STATES.SESSION_CONNECTED ||
+           this._storeState.roomState === ROOM_STATES.HAS_PARTICIPANTS)) {
         this._mozLoop.rooms.leave(this._storeState.roomToken,
           this._storeState.sessionToken);
       }
 
       this.setStoreState({roomState: nextState});
     },
 
     /**
--- a/browser/components/loop/content/shared/js/otSdkDriver.js
+++ b/browser/components/loop/content/shared/js/otSdkDriver.js
@@ -160,16 +160,18 @@ loop.OTSdkDriver = (function() {
       }
 
       var config = _.extend(this._getCopyPublisherConfig(), options);
 
       this.screenshare = this.sdk.initPublisher(this.getScreenShareElementFunc(),
         config);
       this.screenshare.on("accessAllowed", this._onScreenShareGranted.bind(this));
       this.screenshare.on("accessDenied", this._onScreenShareDenied.bind(this));
+
+      this._noteSharingState(options.videoSource, true);
     },
 
     /**
      * Initiates switching the browser window that is being shared.
      *
      * @param {Integer} windowId  The windowId of the browser.
      */
     switchAcquiredWindow: function(windowId) {
@@ -191,16 +193,17 @@ loop.OTSdkDriver = (function() {
       if (!this.screenshare) {
         return false;
       }
 
       this.session.unpublish(this.screenshare);
       this.screenshare.off("accessAllowed accessDenied");
       this.screenshare.destroy();
       delete this.screenshare;
+      this._noteSharingState(this._windowId ? "browser" : "window", false);
       delete this._windowId;
       return true;
     },
 
     /**
      * Connects a session for the SDK, listening to the required events.
      *
      * sessionData items:
@@ -643,25 +646,25 @@ loop.OTSdkDriver = (function() {
      * Wrapper for adding a keyed value that also updates
      * connectionLengthNoted calls and sets the twoWayMediaStartTime to
      * this.CONNECTION_START_TIME_ALREADY_NOTED.
      *
      * @param {number} callLengthSeconds  the call length in seconds
      * @private
      */
     _noteConnectionLength: function(callLengthSeconds) {
+      var buckets = this.mozLoop.TWO_WAY_MEDIA_CONN_LENGTH;
 
-      var bucket = this.mozLoop.TWO_WAY_MEDIA_CONN_LENGTH.SHORTER_THAN_10S;
-
+      var bucket = buckets.SHORTER_THAN_10S;
       if (callLengthSeconds >= 10 && callLengthSeconds <= 30) {
-        bucket = this.mozLoop.TWO_WAY_MEDIA_CONN_LENGTH.BETWEEN_10S_AND_30S;
+        bucket = buckets.BETWEEN_10S_AND_30S;
       } else if (callLengthSeconds > 30 && callLengthSeconds <= 300) {
-        bucket = this.mozLoop.TWO_WAY_MEDIA_CONN_LENGTH.BETWEEN_30S_AND_5M;
+        bucket = buckets.BETWEEN_30S_AND_5M;
       } else if (callLengthSeconds > 300) {
-        bucket = this.mozLoop.TWO_WAY_MEDIA_CONN_LENGTH.MORE_THAN_5M;
+        bucket = buckets.MORE_THAN_5M;
       }
 
       this.mozLoop.telemetryAddKeyedValue("LOOP_TWO_WAY_MEDIA_CONN_LENGTH",
         bucket);
       this._setTwoWayMediaStartTime(this.CONNECTION_START_TIME_ALREADY_NOTED);
 
       this._connectionLengthNotedCalls++;
       if (this._debugTwoWayMediaTelemetry) {
@@ -700,14 +703,39 @@ loop.OTSdkDriver = (function() {
       var callLengthSeconds = (endTime - startTime) / 1000;
       this._noteConnectionLength(callLengthSeconds);
     },
 
     /**
      * If set to true, make it easy to test/verify 2-way media connection
      * telemetry code operation by viewing the logs.
      */
-    _debugTwoWayMediaTelemetry: false
+    _debugTwoWayMediaTelemetry: false,
+
+    /**
+     * Note the sharing state. If this.mozLoop is not defined, we're assumed to
+     * be running in the standalone client and return immediately.
+     *
+     * @param  {String}  type    Type of sharing that was flipped. May be 'window'
+     *                           or 'tab'.
+     * @param  {Boolean} enabled Flag that tells us if the feature was flipped on
+     *                           or off.
+     * @private
+     */
+    _noteSharingState: function(type, enabled) {
+      if (!this.mozLoop) {
+        return;
+      }
+
+      var bucket = this.mozLoop.SHARING_STATE_CHANGE[type.toUpperCase() + "_" +
+        (enabled ? "ENABLED" : "DISABLED")];
+      if (!bucket) {
+        console.error("No sharing state bucket found for '" + type + "'");
+        return;
+      }
+
+      this.mozLoop.telemetryAddKeyedValue("LOOP_SHARING_STATE_CHANGE", bucket);
+    }
   };
 
   return OTSdkDriver;
 
 })();
--- a/browser/components/loop/test/mochitest/browser_mozLoop_telemetry.js
+++ b/browser/components/loop/test/mochitest/browser_mozLoop_telemetry.js
@@ -42,8 +42,34 @@ add_task(function* test_mozLoop_telemetr
   }
 
   let snapshot = histogram.snapshot();
   is(snapshot["SHORTER_THAN_10S"].sum, 1, "TWO_WAY_MEDIA_CONN_LENGTH.SHORTER_THAN_10S");
   is(snapshot["BETWEEN_10S_AND_30S"].sum, 2, "TWO_WAY_MEDIA_CONN_LENGTH.BETWEEN_10S_AND_30S");
   is(snapshot["BETWEEN_30S_AND_5M"].sum, 3, "TWO_WAY_MEDIA_CONN_LENGTH.BETWEEN_30S_AND_5M");
   is(snapshot["MORE_THAN_5M"].sum, 4, "TWO_WAY_MEDIA_CONN_LENGTH.MORE_THAN_5M");
 });
+
+add_task(function* test_mozLoop_telemetryAdd_sharing_buckets() {
+  let histogramId = "LOOP_SHARING_STATE_CHANGE";
+  let histogram = Services.telemetry.getKeyedHistogramById(histogramId);
+  const SHARING_STATES = gMozLoopAPI.SHARING_STATE_CHANGE;
+
+  histogram.clear();
+  for (let value of [SHARING_STATES.WINDOW_ENABLED,
+                     SHARING_STATES.WINDOW_DISABLED,
+                     SHARING_STATES.WINDOW_DISABLED,
+                     SHARING_STATES.BROWSER_ENABLED,
+                     SHARING_STATES.BROWSER_ENABLED,
+                     SHARING_STATES.BROWSER_ENABLED,
+                     SHARING_STATES.BROWSER_DISABLED,
+                     SHARING_STATES.BROWSER_DISABLED,
+                     SHARING_STATES.BROWSER_DISABLED,
+                     SHARING_STATES.BROWSER_DISABLED]) {
+    gMozLoopAPI.telemetryAddKeyedValue(histogramId, value);
+  }
+
+  let snapshot = histogram.snapshot();
+  Assert.strictEqual(snapshot["WINDOW_ENABLED"].sum, 1, "SHARING_STATE_CHANGE.WINDOW_ENABLED");
+  Assert.strictEqual(snapshot["WINDOW_DISABLED"].sum, 2, "SHARING_STATE_CHANGE.WINDOW_DISABLED");
+  Assert.strictEqual(snapshot["BROWSER_ENABLED"].sum, 3, "SHARING_STATE_CHANGE.BROWSER_ENABLED");
+  Assert.strictEqual(snapshot["BROWSER_DISABLED"].sum, 4, "SHARING_STATE_CHANGE.BROWSER_DISABLED");
+});
--- a/browser/components/loop/test/shared/activeRoomStore_test.js
+++ b/browser/components/loop/test/shared/activeRoomStore_test.js
@@ -90,107 +90,149 @@ describe("loop.store.ActiveRoomStore", f
       store.setStoreState({
         roomState: ROOM_STATES.JOINED,
         roomToken: "fakeToken",
         sessionToken: "1627384950"
       });
     });
 
     it("should log the error", function() {
-      store.roomFailure({error: fakeError});
+      store.roomFailure(new sharedActions.RoomFailure({
+        error: fakeError,
+        failedJoinRequest: false
+      }));
 
       sinon.assert.calledOnce(console.error);
       sinon.assert.calledWith(console.error,
         sinon.match(ROOM_STATES.JOINED), fakeError);
     });
 
     it("should set the state to `FULL` on server error room full", function() {
       fakeError.errno = REST_ERRNOS.ROOM_FULL;
 
-      store.roomFailure({error: fakeError});
+      store.roomFailure(new sharedActions.RoomFailure({
+        error: fakeError,
+        failedJoinRequest: false
+      }));
 
       expect(store._storeState.roomState).eql(ROOM_STATES.FULL);
     });
 
     it("should set the state to `FAILED` on generic error", function() {
-      store.roomFailure({error: fakeError});
+      store.roomFailure(new sharedActions.RoomFailure({
+        error: fakeError,
+        failedJoinRequest: false
+      }));
 
       expect(store._storeState.roomState).eql(ROOM_STATES.FAILED);
       expect(store._storeState.failureReason).eql(FAILURE_DETAILS.UNKNOWN);
     });
 
     it("should set the failureReason to EXPIRED_OR_INVALID on server error: " +
       "invalid token", function() {
         fakeError.errno = REST_ERRNOS.INVALID_TOKEN;
 
-        store.roomFailure({error: fakeError});
+        store.roomFailure(new sharedActions.RoomFailure({
+          error: fakeError,
+          failedJoinRequest: false
+        }));
 
         expect(store._storeState.roomState).eql(ROOM_STATES.FAILED);
         expect(store._storeState.failureReason).eql(FAILURE_DETAILS.EXPIRED_OR_INVALID);
       });
 
     it("should set the failureReason to EXPIRED_OR_INVALID on server error: " +
       "expired", function() {
         fakeError.errno = REST_ERRNOS.EXPIRED;
 
-        store.roomFailure({error: fakeError});
+        store.roomFailure(new sharedActions.RoomFailure({
+          error: fakeError,
+          failedJoinRequest: false
+        }));
 
         expect(store._storeState.roomState).eql(ROOM_STATES.FAILED);
         expect(store._storeState.failureReason).eql(FAILURE_DETAILS.EXPIRED_OR_INVALID);
       });
 
     it("should reset the multiplexGum", function() {
-      store.roomFailure({error: fakeError});
+      store.roomFailure(new sharedActions.RoomFailure({
+        error: fakeError,
+        failedJoinRequest: false
+      }));
 
       sinon.assert.calledOnce(fakeMultiplexGum.reset);
     });
 
     it("should set screen sharing inactive", function() {
       store.setStoreState({windowId: "1234"});
 
-      store.roomFailure({error: fakeError});
+      store.roomFailure(new sharedActions.RoomFailure({
+        error: fakeError,
+        failedJoinRequest: false
+      }));
 
       sinon.assert.calledOnce(fakeMozLoop.setScreenShareState);
       sinon.assert.calledWithExactly(fakeMozLoop.setScreenShareState, "1234", false);
     });
 
     it("should disconnect from the servers via the sdk", function() {
-      store.roomFailure({error: fakeError});
+      store.roomFailure(new sharedActions.RoomFailure({
+        error: fakeError,
+        failedJoinRequest: false
+      }));
 
       sinon.assert.calledOnce(fakeSdkDriver.disconnectSession);
     });
 
     it("should clear any existing timeout", function() {
       sandbox.stub(window, "clearTimeout");
       store._timeout = {};
 
-      store.roomFailure({error: fakeError});
+      store.roomFailure(new sharedActions.RoomFailure({
+        error: fakeError,
+        failedJoinRequest: false
+      }));
 
       sinon.assert.calledOnce(clearTimeout);
     });
 
     it("should remove the sharing listener", function() {
       // Setup the listener.
       store.startScreenShare(new sharedActions.StartScreenShare({
         type: "browser"
       }));
 
       // Now simulate room failure.
-      store.roomFailure({error: fakeError});
+      store.roomFailure(new sharedActions.RoomFailure({
+        error: fakeError,
+        failedJoinRequest: false
+      }));
 
       sinon.assert.calledOnce(fakeMozLoop.removeBrowserSharingListener);
     });
 
     it("should call mozLoop.rooms.leave", function() {
-      store.roomFailure({error: fakeError});
+      store.roomFailure(new sharedActions.RoomFailure({
+        error: fakeError,
+        failedJoinRequest: false
+      }));
 
       sinon.assert.calledOnce(fakeMozLoop.rooms.leave);
       sinon.assert.calledWithExactly(fakeMozLoop.rooms.leave,
         "fakeToken", "1627384950");
     });
+
+    it("should not call mozLoop.rooms.leave if failedJoinRequest is true", function() {
+      store.roomFailure(new sharedActions.RoomFailure({
+        error: fakeError,
+        failedJoinRequest: true
+      }));
+
+      sinon.assert.notCalled(fakeMozLoop.rooms.leave);
+    });
   });
 
   describe("#setupWindowData", function() {
     var fakeToken, fakeRoomData;
 
     beforeEach(function() {
       fakeToken = "337-ff-54";
       fakeRoomData = {
@@ -266,17 +308,18 @@ describe("loop.store.ActiveRoomStore", f
           windowId: "42",
           type: "room",
           roomToken: fakeToken
         }));
 
         sinon.assert.calledOnce(dispatcher.dispatch);
         sinon.assert.calledWithExactly(dispatcher.dispatch,
           new sharedActions.RoomFailure({
-            error: fakeError
+            error: fakeError,
+            failedJoinRequest: false
           }));
       });
   });
 
   describe("#fetchServerData", function() {
     it("should save the token", function() {
       store.fetchServerData(new sharedActions.FetchServerData({
         windowType: "room",
@@ -444,17 +487,20 @@ describe("loop.store.ActiveRoomStore", f
       var fakeError = new Error("fake");
 
       fakeMozLoop.rooms.join.callsArgWith(1, fakeError);
 
       store.gotMediaPermission();
 
       sinon.assert.calledOnce(dispatcher.dispatch);
       sinon.assert.calledWith(dispatcher.dispatch,
-        new sharedActions.RoomFailure({error: fakeError}));
+        new sharedActions.RoomFailure({
+          error: fakeError,
+          failedJoinRequest: true
+        }));
     });
   });
 
   describe("#joinedRoom", function() {
     var fakeJoinedData;
 
     beforeEach(function() {
       fakeJoinedData = {
@@ -577,17 +623,18 @@ describe("loop.store.ActiveRoomStore", f
 
         // Clock tick for the first expiry time (which
         // sets up the refreshMembership).
         sandbox.clock.tick(fakeJoinedData.expires * 1000);
 
         sinon.assert.calledOnce(dispatcher.dispatch);
         sinon.assert.calledWith(dispatcher.dispatch,
           new sharedActions.RoomFailure({
-            error: fakeError
+            error: fakeError,
+            failedJoinRequest: false
           }));
     });
   });
 
   describe("#connectedToSdkServers", function() {
     it("should set the state to `SESSION_CONNECTED`", function() {
       store.connectedToSdkServers(new sharedActions.ConnectedToSdkServers());
 
--- a/browser/components/loop/test/shared/otSdkDriver_test.js
+++ b/browser/components/loop/test/shared/otSdkDriver_test.js
@@ -66,16 +66,22 @@ describe("loop.OTSdkDriver", function ()
 
     mozLoop = {
       telemetryAddKeyedValue: sinon.stub(),
       TWO_WAY_MEDIA_CONN_LENGTH: {
         SHORTER_THAN_10S: "SHORTER_THAN_10S",
         BETWEEN_10S_AND_30S: "BETWEEN_10S_AND_30S",
         BETWEEN_30S_AND_5M: "BETWEEN_30S_AND_5M",
         MORE_THAN_5M: "MORE_THAN_5M"
+      },
+      SHARING_STATE_CHANGE: {
+        WINDOW_ENABLED: "WINDOW_ENABLED",
+        WINDOW_DISABLED: "WINDOW_DISABLED",
+        BROWSER_ENABLED: "BROWSER_ENABLED",
+        BROWSER_DISABLED: "BROWSER_DISABLED"
       }
     };
 
     driver = new loop.OTSdkDriver({
       dispatcher: dispatcher,
       sdk: sdk,
       mozLoop: mozLoop,
       isDesktop: true
@@ -184,16 +190,17 @@ describe("loop.OTSdkDriver", function ()
     });
   });
 
   describe("#startScreenShare", function() {
     var fakeElement;
 
     beforeEach(function() {
       sandbox.stub(dispatcher, "dispatch");
+      sandbox.stub(driver, "_noteSharingState");
 
       fakeElement = {
         className: "fakeVideo"
       };
 
       driver.getScreenShareElementFunc = function() {
         return fakeElement;
       };
@@ -209,16 +216,29 @@ describe("loop.OTSdkDriver", function ()
           scrollWithPage: true
         }
       };
       driver.startScreenShare(options);
 
       sinon.assert.calledOnce(sdk.initPublisher);
       sinon.assert.calledWithMatch(sdk.initPublisher, fakeElement, options);
     });
+
+    it("should log a telemetry action", function() {
+      var options = {
+        videoSource: "browser",
+        constraints: {
+          browserWindow: 42,
+          scrollWithPage: true
+        }
+      };
+      driver.startScreenShare(options);
+
+      sinon.assert.calledWithExactly(driver._noteSharingState, "browser", true);
+    });
   });
 
   describe("#switchAcquiredWindow", function() {
     beforeEach(function() {
       var options = {
         videoSource: "browser",
         constraints: {
           browserWindow: 42,
@@ -246,36 +266,80 @@ describe("loop.OTSdkDriver", function ()
       sinon.assert.notCalled(publisher._.switchAcquiredWindow);
     });
   });
 
   describe("#endScreenShare", function() {
     beforeEach(function() {
       driver.getScreenShareElementFunc = function() {};
 
+      sandbox.stub(dispatcher, "dispatch");
+      sandbox.stub(driver, "_noteSharingState");
+    });
+
+    it("should unpublish the share", function() {
       driver.startScreenShare({
         videoSource: "window"
       });
-
-      sandbox.stub(dispatcher, "dispatch");
+      driver.session = session;
 
-      driver.session = session;
-    });
-
-    it("should unpublish the share", function() {
       driver.endScreenShare(new sharedActions.EndScreenShare());
 
       sinon.assert.calledOnce(session.unpublish);
     });
 
+    it("should log a telemetry action", function() {
+      driver.startScreenShare({
+        videoSource: "window"
+      });
+      driver.session = session;
+
+      driver.endScreenShare(new sharedActions.EndScreenShare());
+
+      sinon.assert.calledWithExactly(driver._noteSharingState, "window", false);
+    });
+
     it("should destroy the share", function() {
+      driver.startScreenShare({
+        videoSource: "window"
+      });
+      driver.session = session;
+
       expect(driver.endScreenShare()).to.equal(true);
 
       sinon.assert.calledOnce(publisher.destroy);
     });
+
+    it("should unpublish the share too when type is 'browser'", function() {
+      driver.startScreenShare({
+        videoSource: "browser",
+        constraints: {
+          browserWindow: 42
+        }
+      });
+      driver.session = session;
+
+      driver.endScreenShare(new sharedActions.EndScreenShare());
+
+      sinon.assert.calledOnce(session.unpublish);
+    });
+
+    it("should log a telemetry action too when type is 'browser'", function() {
+      driver.startScreenShare({
+        videoSource: "browser",
+        constraints: {
+          browserWindow: 42
+        }
+      });
+      driver.session = session;
+
+      driver.endScreenShare(new sharedActions.EndScreenShare());
+
+      sinon.assert.calledWithExactly(driver._noteSharingState, "browser", false);
+    });
   });
 
   describe("#connectSession", function() {
     it("should initialise a new session", function() {
       driver.connectSession(sessionData);
 
       sinon.assert.calledOnce(sdk.initSession);
       sinon.assert.calledWithExactly(sdk.initSession, "3216549870");
@@ -426,16 +490,54 @@ describe("loop.OTSdkDriver", function ()
         driver._isDesktop = false;
 
         driver._noteConnectionLengthIfNeeded(startTimeMS, endTimeMS);
 
         sinon.assert.notCalled(mozLoop.telemetryAddKeyedValue);
       });
   });
 
+  describe("#_noteSharingState", function() {
+    it("should record enabled sharing states for window", function() {
+      driver._noteSharingState("window", true);
+
+      sinon.assert.calledOnce(mozLoop.telemetryAddKeyedValue);
+      sinon.assert.calledWithExactly(mozLoop.telemetryAddKeyedValue,
+        "LOOP_SHARING_STATE_CHANGE",
+        mozLoop.SHARING_STATE_CHANGE.WINDOW_ENABLED);
+    });
+
+    it("should record enabled sharing states for browser", function() {
+      driver._noteSharingState("browser", true);
+
+      sinon.assert.calledOnce(mozLoop.telemetryAddKeyedValue);
+      sinon.assert.calledWithExactly(mozLoop.telemetryAddKeyedValue,
+        "LOOP_SHARING_STATE_CHANGE",
+        mozLoop.SHARING_STATE_CHANGE.BROWSER_ENABLED);
+    });
+
+    it("should record disabled sharing states for window", function() {
+      driver._noteSharingState("window", false);
+
+      sinon.assert.calledOnce(mozLoop.telemetryAddKeyedValue);
+      sinon.assert.calledWithExactly(mozLoop.telemetryAddKeyedValue,
+        "LOOP_SHARING_STATE_CHANGE",
+        mozLoop.SHARING_STATE_CHANGE.WINDOW_DISABLED);
+    });
+
+    it("should record disabled sharing states for browser", function() {
+      driver._noteSharingState("browser", false);
+
+      sinon.assert.calledOnce(mozLoop.telemetryAddKeyedValue);
+      sinon.assert.calledWithExactly(mozLoop.telemetryAddKeyedValue,
+        "LOOP_SHARING_STATE_CHANGE",
+        mozLoop.SHARING_STATE_CHANGE.BROWSER_DISABLED);
+    });
+  });
+
   describe("#forceDisconnectAll", function() {
     it("should not disconnect anything when not connected", function() {
       driver.session = session;
       driver.forceDisconnectAll(function() {});
 
       sinon.assert.notCalled(session.forceDisconnect);
     });
 
--- a/browser/components/readinglist/ReadingList.jsm
+++ b/browser/components/readinglist/ReadingList.jsm
@@ -53,16 +53,27 @@ const ITEM_RECORD_PROPERTIES = `
   addedBy
   addedOn
   storedOn
   markedReadBy
   markedReadOn
   readPosition
 `.trim().split(/\s+/);
 
+// Article objects that are passed to ReadingList.addItem may contain
+// some properties that are known but are not currently stored in the
+// ReadingList records. This is the list of properties that are knowingly
+// disregarded before the item is normalized.
+const ITEM_DISREGARDED_PROPERTIES = `
+  byline
+  dir
+  content
+  length
+`.trim().split(/\s+/);
+
 /**
  * A reading list contains ReadingListItems.
  *
  * A list maintains only one copy of an item per URL.  So if for example you use
  * an iterator to get two references to items with the same URL, your references
  * actually refer to the same JS object.
  *
  * Options Objects
@@ -848,16 +859,19 @@ ReadingListItemIterator.prototype = {
  * aren't in ITEM_RECORD_PROPERTIES.
  *
  * @param record A non-normalized record object.
  * @return The new normalized record.
  */
 function normalizeRecord(nonNormalizedRecord) {
   let record = {};
   for (let prop in nonNormalizedRecord) {
+    if (ITEM_DISREGARDED_PROPERTIES.includes(prop)) {
+      continue;
+    }
     if (!ITEM_RECORD_PROPERTIES.includes(prop)) {
       throw new Error("Unrecognized item property: " + prop);
     }
     switch (prop) {
     case "url":
     case "resolvedURL":
       if (nonNormalizedRecord[prop]) {
         record[prop] = normalizeURI(nonNormalizedRecord[prop]).spec;
new file mode 100644
--- /dev/null
+++ b/browser/components/readinglist/ServerClient.jsm
@@ -0,0 +1,166 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+// The client used to access the ReadingList server.
+
+"use strict";
+
+const { classes: Cc, interfaces: Ci, utils: Cu } = Components;
+
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+Cu.import("resource://gre/modules/Services.jsm");
+Cu.import("resource://gre/modules/Log.jsm");
+Cu.import("resource://gre/modules/Task.jsm");
+
+XPCOMUtils.defineLazyModuleGetter(this, "RESTRequest", "resource://services-common/rest.js");
+XPCOMUtils.defineLazyModuleGetter(this, "CommonUtils", "resource://services-common/utils.js");
+XPCOMUtils.defineLazyModuleGetter(this, "fxAccounts", "resource://gre/modules/FxAccounts.jsm");
+
+let log = Log.repository.getLogger("readinglist.serverclient");
+
+const OAUTH_SCOPE = "readinglist"; // The "scope" on the oauth token we request.
+
+this.EXPORTED_SYMBOLS = [
+  "ServerClient",
+];
+
+// utf-8 joy. rest.js, which we use for the underlying requests, does *not*
+// encode the request as utf-8 even though it wants to know the encoding.
+// It does, however, explicitly decode the response.  This seems insane, but is
+// what it is.
+// The end result being we need to utf-8 the request and let the response take
+// care of itself.
+function objectToUTF8Json(obj) {
+  // FTR, unescape(encodeURIComponent(JSON.stringify(obj))) also works ;)
+  return CommonUtils.encodeUTF8(JSON.stringify(obj));
+}
+
+function ServerClient(fxa = fxAccounts) {
+  this.fxa = fxa;
+}
+
+ServerClient.prototype = {
+
+  request(options) {
+    return this._request(options.path, options.method, options.body, options.headers);
+  },
+
+  get serverURL() {
+    return Services.prefs.getCharPref("readinglist.server");
+  },
+
+  _getURL(path) {
+    let result = this.serverURL;
+    // we expect the path to have a leading slash, so remove any trailing
+    // slashes on the pref.
+    if (result.endsWith("/")) {
+      result = result.slice(0, -1);
+    }
+    return result + path;
+  },
+
+  // Hook points for testing.
+  _getToken() {
+    // Assume token-caching is in place - if it's not we should avoid doing
+    // this each request.
+    return this.fxa.getOAuthToken({scope: OAUTH_SCOPE});
+  },
+
+  _removeToken(token) {
+    // XXX - remove this check once tokencaching landsin FxA.
+    if (!this.fxa.removeCachedOAuthToken) {
+      dump("XXX - token caching support is yet to land - can't remove token!");
+      return;
+    }
+    return this.fxa.removeCachedOAuthToken({token});
+  },
+
+  // Converts an error from the RESTRequest object to an error we export.
+  _convertRestError(error) {
+    return error; // XXX - errors?
+  },
+
+  // Converts an error from a try/catch handler to an error we export.
+  _convertJSError(error) {
+    return error; // XXX - errors?
+  },
+
+  /*
+   * Perform a request - handles authentication
+   */
+  _request: Task.async(function* (path, method, body, headers) {
+    let token = yield this._getToken();
+    let response = yield this._rawRequest(path, method, body, headers, token);
+    log.debug("initial request got status ${status}", response);
+    if (response.status == 401) {
+      // an auth error - assume our token has expired or similar.
+      this._removeToken(token);
+      token = yield this._getToken();
+      response = yield this._rawRequest(path, method, body, headers, token);
+      log.debug("retry of request got status ${status}", response);
+    }
+    return response;
+  }),
+
+  /*
+   * Perform a request *without* abstractions such as auth etc
+   *
+   * On success (which *includes* non-200 responses) returns an object like:
+   * {
+   *   status: 200, # http status code
+   *   headers: {}, # header values keyed by header name.
+   *   body: {},    # parsed json
+   }
+   */
+
+  _rawRequest(path, method, body, headers, oauthToken) {
+    return new Promise((resolve, reject) => {
+      let url = this._getURL(path);
+      log.debug("dispatching request to", url);
+      let request = new RESTRequest(url);
+      method = method.toUpperCase();
+
+      request.setHeader("Accept", "application/json");
+      request.setHeader("Content-Type", "application/json; charset=utf-8");
+      request.setHeader("Authorization", "Bearer " + oauthToken);
+      // and additional header specified for this request.
+      if (headers) {
+        for (let [headerName, headerValue] in Iterator(headers)) {
+          log.trace("Caller specified header: ${headerName}=${headerValue}", {headerName, headerValue});
+          request.setHeader(headerName, headerValue);
+        }
+      }
+
+      request.onComplete = error => {
+        if (error) {
+          return reject(this._convertRestError(error));
+        }
+
+        let response = request.response;
+        log.debug("received response status: ${status} ${statusText}", response);
+        // Handle response status codes we know about
+        let result = {
+          status: response.status,
+          headers: response.headers
+        };
+        try {
+          if (response.body) {
+            result.body = JSON.parse(response.body);
+          }
+        } catch (e) {
+          log.info("Failed to parse JSON body |${body}|: ${e}",
+                    {body: response.body, e});
+          // We don't reject due to this (and don't even make a huge amount of
+          // log noise - eg, a 50X error from a load balancer etc may not write
+          // JSON.
+        }
+
+        resolve(result);
+      }
+      // We are assuming the body has already been decoded and thus contains
+      // unicode, but the server expects utf-8. encodeURIComponent does that.
+      request.dispatch(method, objectToUTF8Json(body));
+    });
+  },
+};
--- a/browser/components/readinglist/moz.build
+++ b/browser/components/readinglist/moz.build
@@ -1,25 +1,23 @@
 # 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/.
 
 JAR_MANIFESTS += ['jar.mn']
 
 EXTRA_JS_MODULES.readinglist += [
     'ReadingList.jsm',
+    'Scheduler.jsm',
+    'ServerClient.jsm',
     'SQLiteStore.jsm',
 ]
 
 TESTING_JS_MODULES += [
     'test/ReadingListTestUtils.jsm',
 ]
 
 BROWSER_CHROME_MANIFESTS += ['test/browser/browser.ini']
 
 XPCSHELL_TESTS_MANIFESTS += ['test/xpcshell/xpcshell.ini']
 
-EXTRA_JS_MODULES.readinglist += [
-    'Scheduler.jsm',
-]
-
 with Files('**'):
     BUG_COMPONENT = ('Firefox', 'Reading List')
--- a/browser/components/readinglist/sidebar.js
+++ b/browser/components/readinglist/sidebar.js
@@ -156,16 +156,17 @@ let RLSidebar = {
     itemNode.querySelector(".item-domain").textContent = domain;
 
     let thumb = itemNode.querySelector(".item-thumb-container");
     if (item.preview) {
       thumb.style.backgroundImage = "url(" + item.preview + ")";
     } else {
       thumb.style.removeProperty("background-image");
     }
+    thumb.classList.toggle("preview-available", !!item.preview);
   },
 
   /**
    * Ensure that the list is populated with the correct items.
    */
   ensureListItems: Task.async(function* () {
     yield ReadingList.forEachItem(item => {
       // TODO: Should be batch inserting via DocumentFragment
@@ -195,17 +196,17 @@ let RLSidebar = {
   },
 
   set activeItem(node) {
     if (node && node.parentNode != this.list) {
       log.error(`Unable to set activeItem to invalid node ${node}`);
       return;
     }
 
-    log.debug(`Setting activeItem: ${node ? node.id : null}`);
+    log.trace(`Setting activeItem: ${node ? node.id : null}`);
 
     if (node && node.classList.contains("active")) {
       return;
     }
 
     let prevItem = document.querySelector("#list > .item.active");
     if (prevItem) {
       prevItem.classList.remove("active");
@@ -228,17 +229,17 @@ let RLSidebar = {
   },
 
   set selectedItem(node) {
     if (node && node.parentNode != this.list) {
       log.error(`Unable to set selectedItem to invalid node ${node}`);
       return;
     }
 
-    log.debug(`Setting activeItem: ${node ? node.id : null}`);
+    log.trace(`Setting selectedItem: ${node ? node.id : null}`);
 
     let prevItem = document.querySelector("#list > .item.selected");
     if (prevItem) {
       prevItem.classList.remove("selected");
     }
 
     if (node) {
       node.classList.add("selected");
@@ -265,17 +266,17 @@ let RLSidebar = {
       if (item.classList.contains("selected")) {
         return i;
       }
     }
     return -1;
   },
 
   set selectedIndex(index) {
-    log.debug(`Setting selectedIndex: ${index}`);
+    log.trace(`Setting selectedIndex: ${index}`);
 
     if (index == -1) {
       this.selectedItem = null;
       return;
     }
 
     let item = this.list.children.item(index);
     if (!item) {
--- a/browser/components/readinglist/test/xpcshell/head.js
+++ b/browser/components/readinglist/test/xpcshell/head.js
@@ -1,7 +1,56 @@
 /* Any copyright is dedicated to the Public Domain.
    http://creativecommons.org/publicdomain/zero/1.0/ */
 
 const {classes: Cc, interfaces: Ci, utils: Cu, results: Cr} = Components;
 
 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
 Cu.import("resource://gre/modules/Services.jsm");
+
+do_get_profile(); // fxa needs a profile directory for storage.
+
+Cu.import("resource://gre/modules/FxAccounts.jsm");
+Cu.import("resource://gre/modules/FxAccountsClient.jsm");
+
+// Create a mocked FxAccounts object with a signed-in, verified user.
+function* createMockFxA() {
+
+  function MockFxAccountsClient() {
+    this._email = "nobody@example.com";
+    this._verified = false;
+
+    this.accountStatus = function(uid) {
+      let deferred = Promise.defer();
+      deferred.resolve(!!uid && (!this._deletedOnServer));
+      return deferred.promise;
+    };
+
+    this.signOut = function() { return Promise.resolve(); };
+
+    FxAccountsClient.apply(this);
+  }
+
+  MockFxAccountsClient.prototype = {
+    __proto__: FxAccountsClient.prototype
+  }
+
+  function MockFxAccounts() {
+    return new FxAccounts({
+      fxAccountsClient: new MockFxAccountsClient(),
+      getAssertion: () => Promise.resolve("assertion"),
+    });
+  }
+
+  let fxa = new MockFxAccounts();
+  let credentials = {
+    email: "foo@example.com",
+    uid: "1234@lcip.org",
+    assertion: "foobar",
+    sessionToken: "dead",
+    kA: "beef",
+    kB: "cafe",
+    verified: true
+  };
+
+  yield fxa.setSignedInUser(credentials);
+  return fxa;
+}
new file mode 100644
--- /dev/null
+++ b/browser/components/readinglist/test/xpcshell/test_ServerClient.js
@@ -0,0 +1,209 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+Cu.import("resource://testing-common/httpd.js");
+Cu.import("resource:///modules/readinglist/ServerClient.jsm");
+Cu.import("resource://gre/modules/Log.jsm");
+
+let appender = new Log.DumpAppender();
+for (let logName of ["FirefoxAccounts", "readinglist.serverclient"]) {
+  Log.repository.getLogger(logName).addAppender(appender);
+}
+
+// Some test servers we use.
+let Server = function(handlers) {
+  this._server = null;
+  this._handlers = handlers;
+}
+
+Server.prototype = {
+  start() {
+    this._server = new HttpServer();
+    for (let [path, handler] in Iterator(this._handlers)) {
+      // httpd.js seems to swallow exceptions
+      let thisHandler = handler;
+      let wrapper = (request, response) => {
+        try {
+          thisHandler(request, response);
+        } catch (ex) {
+          print("**** Handler for", path, "failed:", ex, ex.stack);
+          throw ex;
+        }
+      }
+      this._server.registerPathHandler(path, wrapper);
+    }
+    this._server.start(-1);
+  },
+
+  stop() {
+    return new Promise(resolve => {
+      this._server.stop(resolve);
+      this._server = null;
+    });
+  },
+
+  get host() {
+    return "http://localhost:" + this._server.identity.primaryPort;
+  },
+};
+
+// An OAuth server that hands out tokens.
+function OAuthTokenServer() {
+  let server;
+  let handlers = {
+    "/v1/authorization": (request, response) => {
+      response.setStatusLine("1.1", 200, "OK");
+      let token = "token" + server.numTokenFetches;
+      print("Test OAuth server handing out token", token);
+      server.numTokenFetches += 1;
+      server.activeTokens.add(token);
+      response.write(JSON.stringify({access_token: token}));
+    },
+    "/v1/destroy": (request, response) => {
+      // Getting the body seems harder than it should be!
+      let sis = Cc["@mozilla.org/scriptableinputstream;1"]
+                .createInstance(Ci.nsIScriptableInputStream);
+      sis.init(request.bodyInputStream);
+      let body = JSON.parse(sis.read(sis.available()));
+      sis.close();
+      let token = body.token;
+      ok(server.activeTokens.delete(token));
+      print("after destroy have", server.activeTokens.size, "tokens left.")
+      response.setStatusLine("1.1", 200, "OK");
+      response.write('{}');
+    },
+  }
+  server = new Server(handlers);
+  server.numTokenFetches = 0;
+  server.activeTokens = new Set();
+  return server;
+}
+
+// The tests.
+function run_test() {
+  run_next_test();
+}
+
+// Arrange for the first token we hand out to be rejected - the client should
+// notice the 401 and silently get a new token and retry the request.
+add_task(function testAuthRetry() {
+  let handlers = {
+    "/v1/batch": (request, response) => {
+      // We know the first token we will get is "token0", so we simulate that
+      // "expiring" by only accepting "token1". Then we just echo the response
+      // back.
+      let authHeader;
+      try {
+        authHeader = request.getHeader("Authorization");
+      } catch (ex) {}
+      if (authHeader != "Bearer token1") {
+        response.setStatusLine("1.1", 401, "Unauthorized");
+        response.write("wrong token");
+        return;
+      }
+      response.setStatusLine("1.1", 200, "OK");
+      response.write(JSON.stringify({ok: true}));
+    }
+  };
+  let rlserver = new Server(handlers);
+  rlserver.start();
+  let authServer = OAuthTokenServer();
+  authServer.start();
+  try {
+    Services.prefs.setCharPref("readinglist.server", rlserver.host + "/v1");
+    Services.prefs.setCharPref("identity.fxaccounts.remote.oauth.uri", authServer.host + "/v1");
+
+    let fxa = yield createMockFxA();
+    let sc = new ServerClient(fxa);
+
+    let response = yield sc.request({
+      path: "/batch",
+      method: "post",
+      body: {foo: "bar"},
+    });
+    equal(response.status, 200, "got the 200 we expected");
+    equal(authServer.numTokenFetches, 2, "took 2 tokens to get the 200")
+    deepEqual(response.body, {ok: true});
+  } finally {
+    yield authServer.stop();
+    yield rlserver.stop();
+  }
+});
+
+// Check that specified headers are seen by the server, and that server headers
+// in the response are seen by the client.
+add_task(function testHeaders() {
+  let handlers = {
+    "/v1/batch": (request, response) => {
+      ok(request.hasHeader("x-foo"), "got our foo header");
+      equal(request.getHeader("x-foo"), "bar", "foo header has the correct value");
+      response.setHeader("Server-Sent-Header", "hello");
+      response.setStatusLine("1.1", 200, "OK");
+      response.write("{}");
+    }
+  };
+  let rlserver = new Server(handlers);
+  rlserver.start();
+  try {
+    Services.prefs.setCharPref("readinglist.server", rlserver.host + "/v1");
+
+    let fxa = yield createMockFxA();
+    let sc = new ServerClient(fxa);
+    sc._getToken = () => Promise.resolve();
+
+    let response = yield sc.request({
+      path: "/batch",
+      method: "post",
+      headers: {"X-Foo": "bar"},
+      body: {foo: "bar"}});
+    equal(response.status, 200, "got the 200 we expected");
+    equal(response.headers["server-sent-header"], "hello", "got the server header");
+  } finally {
+    yield rlserver.stop();
+  }
+});
+
+// Check that unicode ends up as utf-8 in requests, and vice-versa in responses.
+// (Note the ServerClient assumes all strings in and out are UCS, and thus have
+// already been encoded/decoded (ie, it never expects to receive stuff already
+// utf-8 encoded, and never returns utf-8 encoded responses.)
+add_task(function testUTF8() {
+  let handlers = {
+    "/v1/hello": (request, response) => {
+      // Get the body as bytes.
+      let sis = Cc["@mozilla.org/scriptableinputstream;1"]
+                .createInstance(Ci.nsIScriptableInputStream);
+      sis.init(request.bodyInputStream);
+      let body = sis.read(sis.available());
+      sis.close();
+      // The client sent "{"copyright: "\xa9"} where \xa9 is the copyright symbol.
+      // It should have been encoded as utf-8 which is \xc2\xa9
+      equal(body, '{"copyright":"\xc2\xa9"}', "server saw utf-8 encoded data");
+      // and just write it back unchanged.
+      response.setStatusLine("1.1", 200, "OK");
+      response.write(body);
+    }
+  };
+  let rlserver = new Server(handlers);
+  rlserver.start();
+  try {
+    Services.prefs.setCharPref("readinglist.server", rlserver.host + "/v1");
+
+    let fxa = yield createMockFxA();
+    let sc = new ServerClient(fxa);
+    sc._getToken = () => Promise.resolve();
+
+    let body = {copyright: "\xa9"}; // see above - \xa9 is the copyright symbol
+    let response = yield sc.request({
+      path: "/hello",
+      method: "post",
+      body: body
+    });
+    equal(response.status, 200, "got the 200 we expected");
+    deepEqual(response.body, body);
+  } finally {
+    yield rlserver.stop();
+  }
+});
--- a/browser/components/readinglist/test/xpcshell/xpcshell.ini
+++ b/browser/components/readinglist/test/xpcshell/xpcshell.ini
@@ -1,7 +1,8 @@
 [DEFAULT]
 head = head.js
 firefox-appdir = browser
 
 [test_ReadingList.js]
+[test_ServerClient.js]
 [test_scheduler.js]
 [test_SQLiteStore.js]
--- a/browser/components/sessionstore/SessionStore.jsm
+++ b/browser/components/sessionstore/SessionStore.jsm
@@ -40,17 +40,17 @@ const WINDOW_ATTRIBUTES = ["width", "hei
 
 // Hideable window features to (re)store
 // Restored in restoreWindowFeatures()
 const WINDOW_HIDEABLE_FEATURES = [
   "menubar", "toolbar", "locationbar", "personalbar", "statusbar", "scrollbars"
 ];
 
 // Messages that will be received via the Frame Message Manager.
-const FMM_MESSAGES = [
+const MESSAGES = [
   // The content script gives us a reference to an object that performs
   // synchronous collection of session data.
   "SessionStore:setupSyncHandler",
 
   // The content script sends us data that has been invalidated and needs to
   // be saved to disk.
   "SessionStore:update",
 
@@ -65,37 +65,39 @@ const FMM_MESSAGES = [
   // consider restoring another tab in the queue. The document has
   // been restored, and forms have been filled. We trigger
   // SSTabRestored at this time.
   "SessionStore:restoreTabContentComplete",
 
   // A tab that is being restored was reloaded. We call restoreTabContent to
   // finish restoring it right away.
   "SessionStore:reloadPendingTab",
+
+  // A crashed tab was revived by navigating to a different page. Remove its
+  // browser from the list of crashed browsers to stop ignoring its messages.
+  "SessionStore:crashedTabRevived",
 ];
 
 // The list of messages we accept from <xul:browser>s that have no tab
 // assigned. Those are for example the ones that preload about:newtab pages.
-const FMM_NOTAB_MESSAGES = new Set([
+const NOTAB_MESSAGES = new Set([
   // For a description see above.
   "SessionStore:setupSyncHandler",
 
   // For a description see above.
   "SessionStore:update",
 ]);
 
-// Messages that will be received via the Parent Process Message Manager.
-const PPMM_MESSAGES = [
-  // A tab is being revived from the crashed state. The sender of this
-  // message should actually be running in the parent process, since this
-  // will be the crashed tab interface. We use the Child and Parent Process
-  // Message Managers because the message is sent during framescript unload
-  // when the Frame Message Manager is not available.
-  "SessionStore:RemoteTabRevived",
-];
+// The list of messages we want to receive even during the short period after a
+// frame has been removed from the DOM and before its frame script has finished
+// unloading.
+const CLOSED_MESSAGES = new Set([
+  // For a description see above.
+  "SessionStore:crashedTabRevived",
+]);
 
 // These are tab events that we listen to.
 const TAB_EVENTS = [
   "TabOpen", "TabClose", "TabSelect", "TabShow", "TabHide", "TabPinned",
   "TabUnpinned"
 ];
 
 // The number of milliseconds in a day
@@ -418,18 +420,16 @@ let SessionStoreInternal = {
       throw new Error("SessionStore.init() must only be called once!");
     }
 
     TelemetryTimestamps.add("sessionRestoreInitialized");
     OBSERVING.forEach(function(aTopic) {
       Services.obs.addObserver(this, aTopic, true);
     }, this);
 
-    PPMM_MESSAGES.forEach(msg => ppmm.addMessageListener(msg, this));
-
     this._initPrefs();
     this._initialized = true;
   },
 
   /**
    * Initialize the session using the state provided by SessionStartup
    */
   initSession: function () {
@@ -549,18 +549,16 @@ let SessionStoreInternal = {
       SessionSaver.run();
     }
 
     // clear out priority queue in case it's still holding refs
     TabRestoreQueue.reset();
 
     // Make sure to cancel pending saves.
     SessionSaver.cancel();
-
-    PPMM_MESSAGES.forEach(msg => ppmm.removeMessageListener(msg, this));
   },
 
   /**
    * Handle notifications
    */
   observe: function ssi_observe(aSubject, aTopic, aData) {
     switch (aTopic) {
       case "browser-window-before-show": // catch new windows
@@ -597,31 +595,25 @@ let SessionStoreInternal = {
   },
 
   /**
    * This method handles incoming messages sent by the session store content
    * script via the Frame Message Manager or Parent Process Message Manager,
    * and thus enables communication with OOP tabs.
    */
   receiveMessage(aMessage) {
-    // We'll deal with any Parent Process Message Manager messages first...
-    if (aMessage.name == "SessionStore:RemoteTabRevived") {
-      this._crashedBrowsers.delete(aMessage.objects.browser.permanentKey);
-      return;
-    }
-
     // If we got here, that means we're dealing with a frame message
     // manager message, so the target will be a <xul:browser>.
     var browser = aMessage.target;
     var win = browser.ownerDocument.defaultView;
     let tab = win.gBrowser.getTabForBrowser(browser);
 
     // Ensure we receive only specific messages from <xul:browser>s that
     // have no tab assigned, e.g. the ones that preload about:newtab pages.
-    if (!tab && !FMM_NOTAB_MESSAGES.has(aMessage.name)) {
+    if (!tab && !NOTAB_MESSAGES.has(aMessage.name)) {
       throw new Error(`received unexpected message '${aMessage.name}' ` +
                       `from a browser that has no tab`);
     }
 
     switch (aMessage.name) {
       case "SessionStore:setupSyncHandler":
         TabState.setSyncHandler(browser, aMessage.objects.handler);
         break;
@@ -704,16 +696,19 @@ let SessionStoreInternal = {
         break;
       case "SessionStore:reloadPendingTab":
         if (this.isCurrentEpoch(browser, aMessage.data.epoch)) {
           if (browser.__SS_restoreState == TAB_STATE_NEEDS_RESTORE) {
             this.restoreTabContent(tab);
           }
         }
         break;
+      case "SessionStore:crashedTabRevived":
+        this._crashedBrowsers.delete(browser.permanentKey);
+        break;
       default:
         throw new Error(`received unknown message '${aMessage.name}'`);
         break;
     }
   },
 
   /**
    * Record telemetry measurements stored in an object.
@@ -794,17 +789,20 @@ let SessionStoreInternal = {
     if (RunState.isQuitting)
       return;
 
     // Assign the window a unique identifier we can use to reference
     // internal data about the window.
     aWindow.__SSi = this._generateWindowID();
 
     let mm = aWindow.getGroupMessageManager("browsers");
-    FMM_MESSAGES.forEach(msg => mm.addMessageListener(msg, this));
+    MESSAGES.forEach(msg => {
+      let listenWhenClosed = CLOSED_MESSAGES.has(msg);
+      mm.addMessageListener(msg, this, listenWhenClosed);
+    });
 
     // Load the frame script after registering listeners.
     mm.loadFrameScript("chrome://browser/content/content-sessionStore.js", true);
 
     // and create its data object
     this._windows[aWindow.__SSi] = { tabs: [], selected: 0, _closedTabs: [], busy: false };
 
     let isPrivateWindow = false;
@@ -1123,17 +1121,17 @@ let SessionStoreInternal = {
     for (let i = 0; i < tabbrowser.tabs.length; i++) {
       this.onTabRemove(aWindow, tabbrowser.tabs[i], true);
     }
 
     // Cache the window state until it is completely gone.
     DyingWindowCache.set(aWindow, winData);
 
     let mm = aWindow.getGroupMessageManager("browsers");
-    FMM_MESSAGES.forEach(msg => mm.removeMessageListener(msg, this));
+    MESSAGES.forEach(msg => mm.removeMessageListener(msg, this));
 
     delete aWindow.__SSi;
   },
 
   /**
    * On quit application requested
    */
   onQuitApplicationRequested: function ssi_onQuitApplicationRequested() {
--- a/browser/components/sessionstore/content/content-sessionStore.js
+++ b/browser/components/sessionstore/content/content-sessionStore.js
@@ -744,22 +744,18 @@ function handleRevivedTab() {
     if (Services.appinfo.processType != Services.appinfo.PROCESS_TYPE_DEFAULT) {
       // Sanity check - we'd better be loading this in a non-remote browser.
       throw new Error("We seem to be navigating away from about:tabcrashed in " +
                       "a non-remote browser. This should really never happen.");
     }
 
     removeEventListener("pagehide", handleRevivedTab);
 
-    // We can't send a message using the frame message manager because by
-    // the time we reach the unload event handler, it's "too late", and messages
-    // won't be sent or received. The child-process message manager works though,
-    // despite the fact that we're really running in the parent process.
-    let browser = docShell.chromeEventHandler;
-    cpmm.sendAsyncMessage("SessionStore:RemoteTabRevived", null, {browser: browser});
+    // Notify the parent.
+    sendAsyncMessage("SessionStore:crashedTabRevived");
   }
 }
 
 // If we're browsing from the tab crashed UI to a blacklisted URI that keeps
 // this browser non-remote, we'll handle that in a pagehide event.
 addEventListener("pagehide", handleRevivedTab);
 
 addEventListener("unload", () => {
--- a/browser/devtools/framework/gDevTools.jsm
+++ b/browser/devtools/framework/gDevTools.jsm
@@ -26,32 +26,33 @@ XPCOMUtils.defineLazyModuleGetter(this, 
 XPCOMUtils.defineLazyModuleGetter(this, "DebuggerClient",
                                   "resource://gre/modules/devtools/dbg-client.jsm");
 
 const EventEmitter = devtools.require("devtools/toolkit/event-emitter");
 const Telemetry = devtools.require("devtools/shared/telemetry");
 
 const TABS_OPEN_PEAK_HISTOGRAM = "DEVTOOLS_TABS_OPEN_PEAK_LINEAR";
 const TABS_OPEN_AVG_HISTOGRAM = "DEVTOOLS_TABS_OPEN_AVERAGE_LINEAR";
-const TABS_PINNED_PEAK_HISTOGRAM = "DEVTOOLS_TABS_PINNED_PEAK_EXPONENTIAL";
-const TABS_PINNED_AVG_HISTOGRAM = "DEVTOOLS_TABS_PINNED_AVERAGE_EXPONENTIAL";
+const TABS_PINNED_PEAK_HISTOGRAM = "DEVTOOLS_TABS_PINNED_PEAK_LINEAR";
+const TABS_PINNED_AVG_HISTOGRAM = "DEVTOOLS_TABS_PINNED_AVERAGE_LINEAR";
 
 const FORBIDDEN_IDS = new Set(["toolbox", ""]);
 const MAX_ORDINAL = 99;
 
 const bundle = Services.strings.createBundle("chrome://browser/locale/devtools/toolbox.properties");
 
 /**
  * DevTools is a class that represents a set of developer tools, it holds a
  * set of tools and keeps track of open toolboxes in the browser.
  */
 this.DevTools = function DevTools() {
   this._tools = new Map();     // Map<toolId, tool>
   this._themes = new Map();    // Map<themeId, theme>
   this._toolboxes = new Map(); // Map<target, toolbox>
+  this._telemetry = new Telemetry();
 
   // destroy() is an observer's handler so we need to preserve context.
   this.destroy = this.destroy.bind(this);
   this._teardown = this._teardown.bind(this);
 
   this._testing = false;
 
   EventEmitter.decorate(this);
--- a/browser/devtools/shared/devices.js
+++ b/browser/devtools/shared/devices.js
@@ -1,603 +1,74 @@
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 "use strict";
 
 const { Ci, Cc } = require("chrome");
+const { getJSON } = require("devtools/shared/getjson");
 const { Services } = require("resource://gre/modules/Services.jsm");
+const promise = require("promise");
+
+const DEVICES_URL = "devtools.devices.url";
 const Strings = Services.strings.createBundle("chrome://browser/locale/devtools/device.properties");
 
-/* `Devices` is a catalog of existing devices and their properties, intended
- * for (mobile) device emulation tools and features.
+/* This is a catalog of common web-enabled devices and their properties,
+ * intended for (mobile) device emulation.
  *
  * The properties of a device are:
- * - name: Device brand and model(s).
- * - width: Viewport width.
- * - height: Viewport height.
- * - pixelRatio: Screen pixel ratio to viewport.
- * - userAgent: Device UserAgent string.
- * - touch: Whether the screen is touch-enabled.
+ * - name: brand and model(s).
+ * - width: viewport width.
+ * - height: viewport height.
+ * - pixelRatio: ratio from viewport to physical screen pixels.
+ * - userAgent: UA string of the device's browser.
+ * - touch: whether it has a touch screen.
+ * - firefoxOS: whether Firefox OS is supported.
  *
- * To add more devices to this catalog, either patch this file, or push new
- * device descriptions from your own code (e.g. an addon) like so:
+ * The device types are:
+ *   ["phones", "tablets", "laptops", "televisions", "consoles", "watches"].
+ *
+ * You can easily add more devices to this catalog from your own code (e.g. an
+ * addon) like so:
  *
  *   var myPhone = { name: "My Phone", ... };
- *   require("devtools/shared/devices").Devices.Others.phones.push(myPhone);
+ *   require("devtools/shared/devices").AddDevice(myPhone, "phones");
  */
 
-let Devices = {
-  Types: ["phones", "tablets", "notebooks", "televisions", "watches"],
-
-  // Get the localized string of a device type.
-  GetString(deviceType) {
-    return Strings.GetStringFromName("device." + deviceType);
-  },
-};
-exports.Devices = Devices;
-
+// Local devices catalog that addons can add to.
+let localDevices = {};
 
-// The `Devices.FirefoxOS` list was put together from various sources online.
-Devices.FirefoxOS = {
-  phones: [
-    {
-      name: "Firefox OS Flame",
-      width: 320,
-      height: 570,
-      pixelRatio: 1.5,
-      userAgent: "Mozilla/5.0 (Mobile; rv:28.0) Gecko/28.0 Firefox/28.0",
-      touch: true,
-    },
-    {
-      name: "Alcatel One Touch Fire, Fire C",
-      width: 320,
-      height: 480,
-      pixelRatio: 1,
-      userAgent: "Mozilla/5.0 (Mobile; ALCATELOneTouch4012X; rv:28.0) Gecko/28.0 Firefox/28.0",
-      touch: true,
-    },
-    {
-      name: "Alcatel Fire E",
-      width: 320,
-      height: 480,
-      pixelRatio: 2,
-      userAgent: "Mozilla/5.0 (Mobile; ALCATELOneTouch4012X; rv:28.0) Gecko/28.0 Firefox/28.0",
-      touch: true,
-    },
-    {
-      name: "Geeksphone Keon",
-      width: 320,
-      height: 480,
-      pixelRatio: 1,
-      userAgent: "Mozilla/5.0 (Mobile; rv:28.0) Gecko/28.0 Firefox/28.0",
-      touch: true,
-    },
-    {
-      name: "Geeksphone Peak, Revolution",
-      width: 360,
-      height: 640,
-      pixelRatio: 1.5,
-      userAgent: "Mozilla/5.0 (Mobile; rv:28.0) Gecko/28.0 Firefox/28.0",
-      touch: true,
-    },
-    {
-      name: "Intex Cloud Fx",
-      width: 320,
-      height: 480,
-      pixelRatio: 1,
-      userAgent: "Mozilla/5.0 (Mobile; rv:28.0) Gecko/28.0 Firefox/28.0",
-      touch: true,
-    },
-    {
-      name: "LG Fireweb",
-      width: 320,
-      height: 480,
-      pixelRatio: 1,
-      userAgent: "Mozilla/5.0 (Mobile; LG-D300; rv:28.0) Gecko/28.0 Firefox/28.0",
-      touch: true,
-    },
-    {
-      name: "Spice Fire One Mi-FX1",
-      width: 320,
-      height: 480,
-      pixelRatio: 1,
-      userAgent: "Mozilla/5.0 (Mobile; rv:28.0) Gecko/28.0 Firefox/28.0",
-      touch: true,
-    },
-    {
-      name: "Symphony GoFox F15",
-      width: 320,
-      height: 480,
-      pixelRatio: 1,
-      userAgent: "Mozilla/5.0 (Mobile; rv:28.0) Gecko/28.0 Firefox/28.0",
-      touch: true,
-    },
-    {
-      name: "Zen Fire 105",
-      width: 320,
-      height: 480,
-      pixelRatio: 1,
-      userAgent: "Mozilla/5.0 (Mobile; rv:28.0) Gecko/28.0 Firefox/28.0",
-      touch: true,
-    },
-    {
-      name: "ZTE Open",
-      width: 320,
-      height: 480,
-      pixelRatio: 1,
-      userAgent: "Mozilla/5.0 (Mobile; ZTEOPEN; rv:28.0) Gecko/28.0 Firefox/28.0",
-      touch: true,
-    },
-    {
-      name: "ZTE Open C",
-      width: 320,
-      height: 450,
-      pixelRatio: 1.5,
-      userAgent: "Mozilla/5.0 (Mobile; OPENC; rv:28.0) Gecko/28.0 Firefox/28.0",
-      touch: true,
-    },
-  ],
-  tablets: [
-    {
-      name: "Foxconn InFocus",
-      width: 1280,
-      height: 800,
-      pixelRatio: 1,
-      userAgent: "Mozilla/5.0 (Mobile; rv:28.0) Gecko/28.0 Firefox/28.0",
-      touch: true,
-    },
-    {
-      name: "VIA Vixen",
-      width: 1024,
-      height: 600,
-      pixelRatio: 1,
-      userAgent: "Mozilla/5.0 (Mobile; rv:28.0) Gecko/28.0 Firefox/28.0",
-      touch: true,
-    },
-  ],
-  notebooks: [
-  ],
-  televisions: [
-    {
-      name: "720p HD Television",
-      width: 1280,
-      height: 720,
-      pixelRatio: 1,
-      userAgent: "",
-      touch: false,
-    },
-    {
-      name: "1080p Full HD Television",
-      width: 1920,
-      height: 1080,
-      pixelRatio: 1,
-      userAgent: "",
-      touch: false,
-    },
-    {
-      name: "4K Ultra HD Television",
-      width: 3840,
-      height: 2160,
-      pixelRatio: 1,
-      userAgent: "",
-      touch: false,
-    },
-  ],
-  watches: [
-    {
-      name: "LG G Watch",
-      width: 280,
-      height: 280,
-      pixelRatio: 1,
-      userAgent: "",
-      touch: true,
-    },
-    {
-      name: "LG G Watch R",
-      width: 320,
-      height: 320,
-      pixelRatio: 1,
-      userAgent: "",
-      touch: true,
-    },
-    {
-      name: "Moto 360",
-      width: 320,
-      height: 290,
-      pixelRatio: 1,
-      userAgent: "",
-      touch: true,
-    },
-    {
-      name: "Samsung Gear Live",
-      width: 320,
-      height: 320,
-      pixelRatio: 1,
-      userAgent: "",
-      touch: true,
-    },
-  ],
-};
+// Add a device to the local catalog.
+function AddDevice(device, type = "phones") {
+  let list = localDevices[type];
+  if (!list) {
+    list = localDevices[type] = [];
+  }
+  list.push(device);
+}
+exports.AddDevice = AddDevice;
+
+// Get the complete devices catalog.
+function GetDevices(bypassCache = false) {
+  let deferred = promise.defer();
 
-// `Devices.Others` was derived from the Chromium source code:
-// - chromium/src/third_party/WebKit/Source/devtools/front_end/toolbox/OverridesUI.js
-Devices.Others = {
-  phones: [
-    {
-      name: "Apple iPhone 3GS",
-      width: 320,
-      height: 480,
-      pixelRatio: 1,
-      userAgent: "Mozilla/5.0 (iPhone; U; CPU iPhone OS 4_2_1 like Mac OS X; en-us) AppleWebKit/533.17.9 (KHTML, like Gecko) Version/5.0.2 Mobile/8C148 Safari/6533.18.5",
-      touch: true,
-    },
-    {
-      name: "Apple iPhone 4",
-      width: 320,
-      height: 480,
-      pixelRatio: 2,
-      userAgent: "Mozilla/5.0 (iPhone; U; CPU iPhone OS 4_2_1 like Mac OS X; en-us) AppleWebKit/533.17.9 (KHTML, like Gecko) Version/5.0.2 Mobile/8C148 Safari/6533.18.5",
-      touch: true,
-    },
-    {
-      name: "Apple iPhone 5",
-      width: 320,
-      height: 568,
-      pixelRatio: 2,
-      userAgent: "Mozilla/5.0 (iPhone; CPU iPhone OS 7_0 like Mac OS X; en-us) AppleWebKit/537.51.1 (KHTML, like Gecko) Version/7.0 Mobile/11A465 Safari/9537.53",
-      touch: true,
-    },
-    {
-      name: "Apple iPhone 6",
-      width: 375,
-      height: 667,
-      pixelRatio: 2,
-      userAgent: "Mozilla/5.0 (iPhone; CPU iPhone OS 8_0 like Mac OS X) AppleWebKit/600.1.3 (KHTML, like Gecko) Version/8.0 Mobile/12A4345d Safari/600.1.4",
-      touch: true,
-    },
-    {
-      name: "Apple iPhone 6 Plus",
-      width: 414,
-      height: 736,
-      pixelRatio: 3,
-      userAgent: "Mozilla/5.0 (iPhone; CPU iPhone OS 8_0 like Mac OS X) AppleWebKit/600.1.3 (KHTML, like Gecko) Version/8.0 Mobile/12A4345d Safari/600.1.4",
-      touch: true,
-    },
-    {
-      name: "BlackBerry Z10",
-      width: 384,
-      height: 640,
-      pixelRatio: 2,
-      userAgent: "Mozilla/5.0 (BB10; Touch) AppleWebKit/537.10+ (KHTML, like Gecko) Version/10.0.9.2372 Mobile Safari/537.10+",
-      touch: true,
-    },
-    {
-      name: "BlackBerry Z30",
-      width: 360,
-      height: 640,
-      pixelRatio: 2,
-      userAgent: "Mozilla/5.0 (BB10; Touch) AppleWebKit/537.10+ (KHTML, like Gecko) Version/10.0.9.2372 Mobile Safari/537.10+",
-      touch: true,
-    },
-    {
-      name: "Google Nexus 4",
-      width: 384,
-      height: 640,
-      pixelRatio: 2,
-      userAgent: "Mozilla/5.0 (Linux; Android 4.2.1; en-us; Nexus 4 Build/JOP40D) AppleWebKit/535.19 (KHTML, like Gecko) Chrome/18.0.1025.166 Mobile Safari/535.19",
-      touch: true,
-    },
-    {
-      name: "Google Nexus 5",
-      width: 360,
-      height: 640,
-      pixelRatio: 3,
-      userAgent: "Mozilla/5.0 (Linux; Android 4.2.1; en-us; Nexus 5 Build/JOP40D) AppleWebKit/535.19 (KHTML, like Gecko) Chrome/18.0.1025.166 Mobile Safari/535.19",
-      touch: true,
-    },
-    {
-      name: "Google Nexus S",
-      width: 320,
-      height: 533,
-      pixelRatio: 1.5,
-      userAgent: "Mozilla/5.0 (Linux; U; Android 2.3.4; en-us; Nexus S Build/GRJ22) AppleWebKit/533.1 (KHTML, like Gecko) Version/4.0 Mobile Safari/533.1",
-      touch: true,
-    },
-    {
-      name: "HTC Evo, Touch HD, Desire HD, Desire",
-      width: 320,
-      height: 533,
-      pixelRatio: 1.5,
-      userAgent: "Mozilla/5.0 (Linux; U; Android 2.2; en-us; Sprint APA9292KT Build/FRF91) AppleWebKit/533.1 (KHTML, like Gecko) Version/4.0 Mobile Safari/533.1",
-      touch: true,
-    },
-    {
-      name: "HTC One X, EVO LTE",
-      width: 360,
-      height: 640,
-      pixelRatio: 2,
-      userAgent: "Mozilla/5.0 (Linux; Android 4.0.3; HTC One X Build/IML74K) AppleWebKit/535.19 (KHTML, like Gecko) Chrome/18.0.1025.133 Mobile Safari/535.19",
-      touch: true,
-    },
-    {
-      name: "HTC Sensation, Evo 3D",
-      width: 360,
-      height: 640,
-      pixelRatio: 1.5,
-      userAgent: "Mozilla/5.0 (Linux; U; Android 4.0.3; en-us; HTC Sensation Build/IML74K) AppleWebKit/534.30 (KHTML, like Gecko) Version/4.0 Mobile Safari/534.30",
-      touch: true,
-    },
-    {
-      name: "LG Optimus 2X, Optimus 3D, Optimus Black",
-      width: 320,
-      height: 533,
-      pixelRatio: 1.5,
-      userAgent: "Mozilla/5.0 (Linux; U; Android 2.2; en-us; LG-P990/V08c Build/FRG83) AppleWebKit/533.1 (KHTML, like Gecko) Version/4.0 Mobile Safari/533.1 MMS/LG-Android-MMS-V1.0/1.2",
-      touch: true,
-    },
-    {
-      name: "LG Optimus G",
-      width: 384,
-      height: 640,
-      pixelRatio: 2,
-      userAgent: "Mozilla/5.0 (Linux; Android 4.0; LG-E975 Build/IMM76L) AppleWebKit/535.19 (KHTML, like Gecko) Chrome/18.0.1025.166 Mobile Safari/535.19",
-      touch: true,
-    },
-    {
-      name: "LG Optimus LTE, Optimus 4X HD",
-      width: 424,
-      height: 753,
-      pixelRatio: 1.7,
-      userAgent: "Mozilla/5.0 (Linux; U; Android 2.3; en-us; LG-P930 Build/GRJ90) AppleWebKit/533.1 (KHTML, like Gecko) Version/4.0 Mobile Safari/533.1",
-      touch: true,
-    },
-    {
-      name: "LG Optimus One",
-      width: 213,
-      height: 320,
-      pixelRatio: 1.5,
-      userAgent: "Mozilla/5.0 (Linux; U; Android 2.2.1; en-us; LG-MS690 Build/FRG83) AppleWebKit/533.1 (KHTML, like Gecko) Version/4.0 Mobile Safari/533.1",
-      touch: true,
-    },
-    {
-      name: "Motorola Defy, Droid, Droid X, Milestone",
-      width: 320,
-      height: 569,
-      pixelRatio: 1.5,
-      userAgent: "Mozilla/5.0 (Linux; U; Android 2.0; en-us; Milestone Build/ SHOLS_U2_01.03.1) AppleWebKit/530.17 (KHTML, like Gecko) Version/4.0 Mobile Safari/530.17",
-      touch: true,
-    },
-    {
-      name: "Motorola Droid 3, Droid 4, Droid Razr, Atrix 4G, Atrix 2",
-      width: 540,
-      height: 960,
-      pixelRatio: 1,
-      userAgent: "Mozilla/5.0 (Linux; U; Android 2.2; en-us; Droid Build/FRG22D) AppleWebKit/533.1 (KHTML, like Gecko) Version/4.0 Mobile Safari/533.1",
-      touch: true,
-    },
-    {
-      name: "Motorola Droid Razr HD",
-      width: 720,
-      height: 1280,
-      pixelRatio: 1,
-      userAgent: "Mozilla/5.0 (Linux; U; Android 2.3; en-us; DROID RAZR 4G Build/6.5.1-73_DHD-11_M1-29) AppleWebKit/533.1 (KHTML, like Gecko) Version/4.0 Mobile Safari/533.1",
-      touch: true,
-    },
-    {
-      name: "Nokia C5, C6, C7, N97, N8, X7",
-      width: 360,
-      height: 640,
-      pixelRatio: 1,
-      userAgent: "NokiaN97/21.1.107 (SymbianOS/9.4; Series60/5.0 Mozilla/5.0; Profile/MIDP-2.1 Configuration/CLDC-1.1) AppleWebkit/525 (KHTML, like Gecko) BrowserNG/7.1.4",
-      touch: true,
-    },
-    {
-      name: "Nokia Lumia 7X0, Lumia 8XX, Lumia 900, N800, N810, N900",
-      width: 320,
-      height: 533,
-      pixelRatio: 1.5,
-      userAgent: "Mozilla/5.0 (compatible; MSIE 10.0; Windows Phone 8.0; Trident/6.0; IEMobile/10.0; ARM; Touch; NOKIA; Lumia 820)",
-      touch: true,
-    },
-    {
-      name: "Samsung Galaxy Note 3",
-      width: 360,
-      height: 640,
-      pixelRatio: 3,
-      userAgent: "Mozilla/5.0 (Linux; U; Android 4.3; en-us; SM-N900T Build/JSS15J) AppleWebKit/534.30 (KHTML, like Gecko) Version/4.0 Mobile Safari/534.30",
-      touch: true,
-    },
-    {
-      name: "Samsung Galaxy Note II",
-      width: 360,
-      height: 640,
-      pixelRatio: 2,
-      userAgent: "Mozilla/5.0 (Linux; U; Android 4.1; en-us; GT-N7100 Build/JRO03C) AppleWebKit/534.30 (KHTML, like Gecko) Version/4.0 Mobile Safari/534.30",
-      touch: true,
-    },
-    {
-      name: "Samsung Galaxy Note",
-      width: 400,
-      height: 640,
-      pixelRatio: 2,
-      userAgent: "Mozilla/5.0 (Linux; U; Android 2.3; en-us; SAMSUNG-SGH-I717 Build/GINGERBREAD) AppleWebKit/533.1 (KHTML, like Gecko) Version/4.0 Mobile Safari/533.1",
-      touch: true,
-    },
-    {
-      name: "Samsung Galaxy S III, Galaxy Nexus",
-      width: 360,
-      height: 640,
-      pixelRatio: 2,
-      userAgent: "Mozilla/5.0 (Linux; U; Android 4.0; en-us; GT-I9300 Build/IMM76D) AppleWebKit/534.30 (KHTML, like Gecko) Version/4.0 Mobile Safari/534.30",
-      touch: true,
-    },
-    {
-      name: "Samsung Galaxy S, S II, W",
-      width: 320,
-      height: 533,
-      pixelRatio: 1.5,
-      userAgent: "Mozilla/5.0 (Linux; U; Android 2.1; en-us; GT-I9000 Build/ECLAIR) AppleWebKit/525.10+ (KHTML, like Gecko) Version/3.0.4 Mobile Safari/523.12.2",
-      touch: true,
-    },
-    {
-      name: "Samsung Galaxy S4",
-      width: 360,
-      height: 640,
-      pixelRatio: 3,
-      userAgent: "Mozilla/5.0 (Linux; Android 4.2.2; GT-I9505 Build/JDQ39) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/31.0.1650.59 Mobile Safari/537.36",
-      touch: true,
-    },
-    {
-      name: "Sony Xperia S, Ion",
-      width: 360,
-      height: 640,
-      pixelRatio: 2,
-      userAgent: "Mozilla/5.0 (Linux; U; Android 4.0; en-us; LT28at Build/6.1.C.1.111) AppleWebKit/534.30 (KHTML, like Gecko) Version/4.0 Mobile Safari/534.30",
-      touch: true,
-    },
-    {
-      name: "Sony Xperia Sola, U",
-      width: 480,
-      height: 854,
-      pixelRatio: 1,
-      userAgent: "Mozilla/5.0 (Linux; U; Android 2.3; en-us; SonyEricssonST25i Build/6.0.B.1.564) AppleWebKit/533.1 (KHTML, like Gecko) Version/4.0 Mobile Safari/533.1",
-      touch: true,
-    },
-    {
-      name: "Sony Xperia Z, Z1",
-      width: 360,
-      height: 640,
-      pixelRatio: 3,
-      userAgent: "Mozilla/5.0 (Linux; U; Android 4.2; en-us; SonyC6903 Build/14.1.G.1.518) AppleWebKit/534.30 (KHTML, like Gecko) Version/4.0 Mobile Safari/534.30",
-      touch: true,
-    },
-  ],
-  tablets: [
-    {
-      name: "Amazon Kindle Fire HDX 7″",
-      width: 1920,
-      height: 1200,
-      pixelRatio: 2,
-      userAgent: "Mozilla/5.0 (Linux; U; en-us; KFTHWI Build/JDQ39) AppleWebKit/535.19 (KHTML, like Gecko) Silk/3.13 Safari/535.19 Silk-Accelerated=true",
-      touch: true,
-    },
-    {
-      name: "Amazon Kindle Fire HDX 8.9″",
-      width: 2560,
-      height: 1600,
-      pixelRatio: 2,
-      userAgent: "Mozilla/5.0 (Linux; U; en-us; KFAPWI Build/JDQ39) AppleWebKit/535.19 (KHTML, like Gecko) Silk/3.13 Safari/535.19 Silk-Accelerated=true",
-      touch: true,
-    },
-    {
-      name: "Amazon Kindle Fire (First Generation)",
-      width: 1024,
-      height: 600,
-      pixelRatio: 1,
-      userAgent: "Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10_6_3; en-us; Silk/1.0.141.16-Gen4_11004310) AppleWebkit/533.16 (KHTML, like Gecko) Version/5.0 Safari/533.16 Silk-Accelerated=true",
-      touch: true,
-    },
-    {
-      name: "Apple iPad 1 / 2 / iPad Mini",
-      width: 1024,
-      height: 768,
-      pixelRatio: 1,
-      userAgent: "Mozilla/5.0 (iPad; CPU OS 4_3_5 like Mac OS X; en-us) AppleWebKit/533.17.9 (KHTML, like Gecko) Version/5.0.2 Mobile/8L1 Safari/6533.18.5",
-      touch: true,
-    },
-    {
-      name: "Apple iPad 3 / 4",
-      width: 1024,
-      height: 768,
-      pixelRatio: 2,
-      userAgent: "Mozilla/5.0 (iPad; CPU OS 7_0 like Mac OS X) AppleWebKit/537.51.1 (KHTML, like Gecko) Version/7.0 Mobile/11A465 Safari/9537.53",
-      touch: true,
-    },
-    {
-      name: "BlackBerry PlayBook",
-      width: 1024,
-      height: 600,
-      pixelRatio: 1,
-      userAgent: "Mozilla/5.0 (PlayBook; U; RIM Tablet OS 2.1.0; en-US) AppleWebKit/536.2+ (KHTML like Gecko) Version/7.2.1.0 Safari/536.2+",
-      touch: true,
-    },
-    {
-      name: "Google Nexus 10",
-      width: 1280,
-      height: 800,
-      pixelRatio: 2,
-      userAgent: "Mozilla/5.0 (Linux; Android 4.3; Nexus 10 Build/JSS15Q) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/29.0.1547.72 Safari/537.36",
-      touch: true,
-    },
-    {
-      name: "Google Nexus 7 2",
-      width: 960,
-      height: 600,
-      pixelRatio: 2,
-      userAgent: "Mozilla/5.0 (Linux; Android 4.3; Nexus 7 Build/JSS15Q) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/29.0.1547.72 Safari/537.36",
-      touch: true,
-    },
-    {
-      name: "Google Nexus 7",
-      width: 966,
-      height: 604,
-      pixelRatio: 1.325,
-      userAgent: "Mozilla/5.0 (Linux; Android 4.3; Nexus 7 Build/JSS15Q) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/29.0.1547.72 Safari/537.36",
-      touch: true,
-    },
-    {
-      name: "Motorola Xoom, Xyboard",
-      width: 1280,
-      height: 800,
-      pixelRatio: 1,
-      userAgent: "Mozilla/5.0 (Linux; U; Android 3.0; en-us; Xoom Build/HRI39) AppleWebKit/525.10 (KHTML, like Gecko) Version/3.0.4 Mobile Safari/523.12.2",
-      touch: true,
-    },
-    {
-      name: "Samsung Galaxy Tab 7.7, 8.9, 10.1",
-      width: 1280,
-      height: 800,
-      pixelRatio: 1,
-      userAgent: "Mozilla/5.0 (Linux; U; Android 2.2; en-us; SCH-I800 Build/FROYO) AppleWebKit/533.1 (KHTML, like Gecko) Version/4.0 Mobile Safari/533.1",
-      touch: true,
-    },
-    {
-      name: "Samsung Galaxy Tab",
-      width: 1024,
-      height: 600,
-      pixelRatio: 1,
-      userAgent: "Mozilla/5.0 (Linux; U; Android 2.2; en-us; SCH-I800 Build/FROYO) AppleWebKit/533.1 (KHTML, like Gecko) Version/4.0 Mobile Safari/533.1",
-      touch: true,
-    },
-  ],
-  notebooks: [
-    {
-      name: "Notebook with touch",
-      width: 1280,
-      height: 950,
-      pixelRatio: 1,
-      userAgent: "",
-      touch: true,
-    },
-    {
-      name: "Notebook with HiDPI screen",
-      width: 1440,
-      height: 900,
-      pixelRatio: 2,
-      userAgent: "",
-      touch: false,
-    },
-    {
-      name: "Generic notebook",
-      width: 1280,
-      height: 800,
-      pixelRatio: 1,
-      userAgent: "",
-      touch: false,
-    },
-  ],
-  televisions: [
-  ],
-  watches: [
-  ],
-};
+  // Fetch common devices from Mozilla's CDN.
+  getJSON(DEVICES_URL, bypassCache).then(devices => {
+    for (let type in localDevices) {
+      if (!devices[type]) {
+        devices.TYPES.push(type);
+        devices[type] = [];
+      }
+      devices[type] = localDevices[type].concat(devices[type]);
+    }
+    deferred.resolve(devices);
+  });
+
+  return deferred.promise;
+}
+exports.GetDevices = GetDevices;
+
+// Get the localized string for a device type.
+function GetDeviceString(deviceType) {
+  return Strings.GetStringFromName("device." + deviceType);
+}
+exports.GetDeviceString = GetDeviceString;
rename from browser/devtools/webide/modules/remote-resources.js
rename to browser/devtools/shared/getjson.js
--- a/browser/devtools/webide/modules/remote-resources.js
+++ b/browser/devtools/shared/getjson.js
@@ -1,54 +1,43 @@
 /* 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 {Cu, CC} = require("chrome");
-const {Promise: promise} = Cu.import("resource://gre/modules/Promise.jsm", {});
+const promise = require("promise");
 const {Services} = Cu.import("resource://gre/modules/Services.jsm");
 
 const XMLHttpRequest = CC("@mozilla.org/xmlextras/xmlhttprequest;1");
 
-function getJSON(bypassCache, pref) {
+// Downloads and caches a JSON file from a URL given by the pref.
+exports.getJSON = function (prefName, bypassCache) {
   if (!bypassCache) {
     try {
-      let str = Services.prefs.getCharPref(pref + "_cache");
+      let str = Services.prefs.getCharPref(prefName + "_cache");
       let json = JSON.parse(str);
       return promise.resolve(json);
     } catch(e) {/* no pref or invalid json. Let's continue */}
   }
 
-
   let deferred = promise.defer();
-
   let xhr = new XMLHttpRequest();
 
   xhr.onload = () => {
     let json;
     try {
       json = JSON.parse(xhr.responseText);
     } catch(e) {
-      return deferred.reject("Not valid JSON");
+      return deferred.reject("Invalid JSON");
     }
-    Services.prefs.setCharPref(pref + "_cache", xhr.responseText);
+    Services.prefs.setCharPref(prefName + "_cache", xhr.responseText);
     deferred.resolve(json);
   }
 
   xhr.onerror = (e) => {
     deferred.reject("Network error");
   }
 
-  xhr.open("get", Services.prefs.getCharPref(pref));
+  xhr.open("get", Services.prefs.getCharPref(prefName));
   xhr.send();
 
   return deferred.promise;
 }
-
-
-
-exports.GetTemplatesJSON = function(bypassCache) {
-  return getJSON(bypassCache, "devtools.webide.templatesURL");
-}
-
-exports.GetAddonsJSON = function(bypassCache) {
-  return getJSON(bypassCache, "devtools.webide.addonsURL");
-}
--- a/browser/devtools/shared/moz.build
+++ b/browser/devtools/shared/moz.build
@@ -46,25 +46,27 @@ EXTRA_JS_MODULES.devtools.shared.timelin
 ]
 
 EXTRA_JS_MODULES.devtools.shared += [
     'autocomplete-popup.js',
     'd3.js',
     'devices.js',
     'doorhanger.js',
     'frame-script-utils.js',
+    'getjson.js',
     'inplace-editor.js',
     'observable-object.js',
     'options-view.js',
     'telemetry.js',
     'theme-switching.js',
     'theme.js',
     'undo.js',
 ]
 
 EXTRA_JS_MODULES.devtools.shared.widgets += [
+    'widgets/CubicBezierPresets.js',
     'widgets/CubicBezierWidget.js',
     'widgets/FastListWidget.js',
     'widgets/Spectrum.js',
     'widgets/TableWidget.js',
     'widgets/Tooltip.js',
     'widgets/TreeWidget.js',
 ]
--- a/browser/devtools/shared/test/browser.ini
+++ b/browser/devtools/shared/test/browser.ini
@@ -2,24 +2,28 @@
 subsuite = devtools
 support-files =
   browser_layoutHelpers.html
   browser_layoutHelpers-getBoxQuads.html
   browser_layoutHelpers_iframe.html
   browser_templater_basic.html
   browser_toolbar_basic.html
   browser_toolbar_webconsole_errors_count.html
+  browser_devices.json
   doc_options-view.xul
   head.js
   leakhunt.js
 
 [browser_css_color.js]
 [browser_cubic-bezier-01.js]
 [browser_cubic-bezier-02.js]
 [browser_cubic-bezier-03.js]
+[browser_cubic-bezier-04.js]
+[browser_cubic-bezier-05.js]
+[browser_cubic-bezier-06.js]
 [browser_flame-graph-01.js]
 [browser_flame-graph-02.js]
 [browser_flame-graph-03a.js]
 [browser_flame-graph-03b.js]
 [browser_flame-graph-03c.js]
 [browser_flame-graph-04.js]
 [browser_flame-graph-utils-01.js]
 [browser_flame-graph-utils-02.js]
@@ -93,8 +97,9 @@ skip-if = e10s # Bug 1086492 - Disable t
 [browser_templater_basic.js]
 [browser_toolbar_basic.js]
 [browser_toolbar_tooltip.js]
 [browser_toolbar_webconsole_errors_count.js]
 skip-if = buildapp == 'mulet' || e10s # The developertoolbar error count isn't correct with e10s
 [browser_treeWidget_basic.js]
 [browser_treeWidget_keyboard_interaction.js]
 [browser_treeWidget_mouse_interaction.js]
+[browser_devices.js]
--- a/browser/devtools/shared/test/browser_cubic-bezier-01.js
+++ b/browser/devtools/shared/test/browser_cubic-bezier-01.js
@@ -2,26 +2,30 @@
 /* Any copyright is dedicated to the Public Domain.
    http://creativecommons.org/publicdomain/zero/1.0/ */
 
 "use strict";
 
 // Tests that the CubicBezierWidget generates content in a given parent node
 
 const TEST_URI = "chrome://browser/content/devtools/cubic-bezier-frame.xhtml";
-const {CubicBezierWidget} = devtools.require("devtools/shared/widgets/CubicBezierWidget");
+const {CubicBezierWidget} =
+  devtools.require("devtools/shared/widgets/CubicBezierWidget");
 
 add_task(function*() {
   yield promiseTab("about:blank");
   let [host, win, doc] = yield createHost("bottom", TEST_URI);
 
-  info("Checking that the markup is created in the parent");
+  info("Checking that the graph markup is created in the parent");
   let container = doc.querySelector("#container");
   let w = new CubicBezierWidget(container);
 
+  ok(container.querySelector(".display-wrap"),
+    "The display has been added");
+
   ok(container.querySelector(".coordinate-plane"),
     "The coordinate plane has been added");
   let buttons = container.querySelectorAll("button");
   is(buttons.length, 2,
     "The 2 control points have been added");
   is(buttons[0].className, "control-point");
   is(buttons[0].id, "P1");
   is(buttons[1].className, "control-point");
--- a/browser/devtools/shared/test/browser_cubic-bezier-02.js
+++ b/browser/devtools/shared/test/browser_cubic-bezier-02.js
@@ -2,145 +2,192 @@
 /* Any copyright is dedicated to the Public Domain.
    http://creativecommons.org/publicdomain/zero/1.0/ */
 
 "use strict";
 
 // Tests the CubicBezierWidget events
 
 const TEST_URI = "chrome://browser/content/devtools/cubic-bezier-frame.xhtml";
-const {CubicBezierWidget, PREDEFINED} =
+const {CubicBezierWidget} =
   devtools.require("devtools/shared/widgets/CubicBezierWidget");
+const {PREDEFINED} = require("devtools/shared/widgets/CubicBezierPresets");
 
 add_task(function*() {
   yield promiseTab("about:blank");
   let [host, win, doc] = yield createHost("bottom", TEST_URI);
 
+  // Required or widget will be clipped inside of 'bottom'
+  // host by -14. Setting `fixed` zeroes this which is needed for
+  // calculating offsets. Occurs in test env only.
+  doc.body.setAttribute("style", "position: fixed");
+
   let container = doc.querySelector("#container");
   let w = new CubicBezierWidget(container, PREDEFINED.linear);
 
-  yield pointsCanBeDragged(w, win, doc);
-  yield curveCanBeClicked(w, win, doc);
-  yield pointsCanBeMovedWithKeyboard(w, win, doc);
+  let rect = w.curve.getBoundingClientRect();
+  rect.graphTop = rect.height * w.bezierCanvas.padding[0];
+  rect.graphBottom = rect.height - rect.graphTop;
+  rect.graphHeight = rect.graphBottom - rect.graphTop;
+
+  yield pointsCanBeDragged(w, win, doc, rect);
+  yield curveCanBeClicked(w, win, doc, rect);
+  yield pointsCanBeMovedWithKeyboard(w, win, doc, rect);
 
   w.destroy();
   host.destroy();
   gBrowser.removeCurrentTab();
 });
 
-function* pointsCanBeDragged(widget, win, doc) {
+function* pointsCanBeDragged(widget, win, doc, offsets) {
   info("Checking that the control points can be dragged with the mouse");
 
   info("Listening for the update event");
   let onUpdated = widget.once("updated");
 
   info("Generating a mousedown/move/up on P1");
   widget._onPointMouseDown({target: widget.p1});
-  doc.onmousemove({pageX: 0, pageY: 100});
+  doc.onmousemove({pageX: offsets.left, pageY: offsets.graphTop});
   doc.onmouseup();
 
   let bezier = yield onUpdated;
   ok(true, "The widget fired the updated event");
   ok(bezier, "The updated event contains a bezier argument");
   is(bezier.P1[0], 0, "The new P1 time coordinate is correct");
   is(bezier.P1[1], 1, "The new P1 progress coordinate is correct");
 
   info("Listening for the update event");
   onUpdated = widget.once("updated");
 
   info("Generating a mousedown/move/up on P2");
   widget._onPointMouseDown({target: widget.p2});
-  doc.onmousemove({pageX: 200, pageY: 300});
+  doc.onmousemove({pageX: offsets.right, pageY: offsets.graphBottom});
   doc.onmouseup();
 
   bezier = yield onUpdated;
   is(bezier.P2[0], 1, "The new P2 time coordinate is correct");
   is(bezier.P2[1], 0, "The new P2 progress coordinate is correct");
 }
 
-function* curveCanBeClicked(widget, win, doc) {
+function* curveCanBeClicked(widget, win, doc, offsets) {
   info("Checking that clicking on the curve moves the closest control point");
 
   info("Listening for the update event");
   let onUpdated = widget.once("updated");
 
   info("Click close to P1");
-  widget._onCurveClick({pageX: 50, pageY: 150});
+  let x = offsets.left + (offsets.width / 4.0);
+  let y = offsets.graphTop + (offsets.graphHeight / 4.0);
+  widget._onCurveClick({pageX: x, pageY: y});
 
   let bezier = yield onUpdated;
   ok(true, "The widget fired the updated event");
   is(bezier.P1[0], 0.25, "The new P1 time coordinate is correct");
   is(bezier.P1[1], 0.75, "The new P1 progress coordinate is correct");
   is(bezier.P2[0], 1, "P2 time coordinate remained unchanged");
   is(bezier.P2[1], 0, "P2 progress coordinate remained unchanged");
 
   info("Listening for the update event");
   onUpdated = widget.once("updated");
 
   info("Click close to P2");
-  widget._onCurveClick({pageX: 150, pageY: 250});
+  x = offsets.right - (offsets.width / 4);
+  y = offsets.graphBottom - (offsets.graphHeight / 4);
+  widget._onCurveClick({pageX: x, pageY: y});
 
   bezier = yield onUpdated;
   is(bezier.P2[0], 0.75, "The new P2 time coordinate is correct");
   is(bezier.P2[1], 0.25, "The new P2 progress coordinate is correct");
   is(bezier.P1[0], 0.25, "P1 time coordinate remained unchanged");
   is(bezier.P1[1], 0.75, "P1 progress coordinate remained unchanged");
 }
 
-function* pointsCanBeMovedWithKeyboard(widget, win, doc) {
+function* pointsCanBeMovedWithKeyboard(widget, win, doc, offsets) {
   info("Checking that points respond to keyboard events");
 
+  let singleStep = 3;
+  let shiftStep = 30;
+
   info("Moving P1 to the left");
+  let newOffset = parseInt(widget.p1.style.left) - singleStep;
+  let x = widget.bezierCanvas.
+          offsetsToCoordinates({style: {left: newOffset}})[0];
+
   let onUpdated = widget.once("updated");
   widget._onPointKeyDown(getKeyEvent(widget.p1, 37));
   let bezier = yield onUpdated;
-  is(bezier.P1[0], 0.235, "The new P1 time coordinate is correct");
+
+  is(bezier.P1[0], x, "The new P1 time coordinate is correct");
   is(bezier.P1[1], 0.75, "The new P1 progress coordinate is correct");
 
   info("Moving P1 to the left, fast");
+  newOffset = parseInt(widget.p1.style.left) - shiftStep;
+  x = widget.bezierCanvas.
+      offsetsToCoordinates({style: {left: newOffset}})[0];
+
   onUpdated = widget.once("updated");
   widget._onPointKeyDown(getKeyEvent(widget.p1, 37, true));
   bezier = yield onUpdated;
-  is(bezier.P1[0], 0.085, "The new P1 time coordinate is correct");
+  is(bezier.P1[0], x, "The new P1 time coordinate is correct");
   is(bezier.P1[1], 0.75, "The new P1 progress coordinate is correct");
 
   info("Moving P1 to the right, fast");
+  newOffset = parseInt(widget.p1.style.left) + shiftStep;
+  x = widget.bezierCanvas.
+    offsetsToCoordinates({style: {left: newOffset}})[0];
+
   onUpdated = widget.once("updated");
   widget._onPointKeyDown(getKeyEvent(widget.p1, 39, true));
   bezier = yield onUpdated;
-  is(bezier.P1[0], 0.235, "The new P1 time coordinate is correct");
+  is(bezier.P1[0], x, "The new P1 time coordinate is correct");
   is(bezier.P1[1], 0.75, "The new P1 progress coordinate is correct");
 
   info("Moving P1 to the bottom");
+  newOffset = parseInt(widget.p1.style.top) + singleStep;
+  let y = widget.bezierCanvas.
+    offsetsToCoordinates({style: {top: newOffset}})[1];
+
   onUpdated = widget.once("updated");
   widget._onPointKeyDown(getKeyEvent(widget.p1, 40));
   bezier = yield onUpdated;
-  is(bezier.P1[0], 0.235, "The new P1 time coordinate is correct");
-  is(bezier.P1[1], 0.735, "The new P1 progress coordinate is correct");
+  is(bezier.P1[0], x, "The new P1 time coordinate is correct");
+  is(bezier.P1[1], y, "The new P1 progress coordinate is correct");
 
   info("Moving P1 to the bottom, fast");
+  newOffset = parseInt(widget.p1.style.top) + shiftStep;
+  y = widget.bezierCanvas.
+    offsetsToCoordinates({style: {top: newOffset}})[1];
+
   onUpdated = widget.once("updated");
   widget._onPointKeyDown(getKeyEvent(widget.p1, 40, true));
   bezier = yield onUpdated;
-  is(bezier.P1[0], 0.235, "The new P1 time coordinate is correct");
-  is(bezier.P1[1], 0.585, "The new P1 progress coordinate is correct");
+  is(bezier.P1[0], x, "The new P1 time coordinate is correct");
+  is(bezier.P1[1], y, "The new P1 progress coordinate is correct");
 
   info("Moving P1 to the top, fast");
+  newOffset = parseInt(widget.p1.style.top) - shiftStep;
+  y = widget.bezierCanvas.
+    offsetsToCoordinates({style: {top: newOffset}})[1];
+
   onUpdated = widget.once("updated");
   widget._onPointKeyDown(getKeyEvent(widget.p1, 38, true));
   bezier = yield onUpdated;
-  is(bezier.P1[0], 0.235, "The new P1 time coordinate is correct");
-  is(bezier.P1[1], 0.735, "The new P1 progress coordinate is correct");
+  is(bezier.P1[0], x, "The new P1 time coordinate is correct");
+  is(bezier.P1[1], y, "The new P1 progress coordinate is correct");
 
   info("Checking that keyboard events also work with P2");
   info("Moving P2 to the left");
+  newOffset = parseInt(widget.p2.style.left) - singleStep;
+  x = widget.bezierCanvas.
+    offsetsToCoordinates({style: {left: newOffset}})[0];
+
   onUpdated = widget.once("updated");
   widget._onPointKeyDown(getKeyEvent(widget.p2, 37));
   bezier = yield onUpdated;
-  is(bezier.P2[0], 0.735, "The new P2 time coordinate is correct");
+  is(bezier.P2[0], x, "The new P2 time coordinate is correct");
   is(bezier.P2[1], 0.25, "The new P2 progress coordinate is correct");
 }
 
 function getKeyEvent(target, keyCode, shift=false) {
   return {
     target: target,
     keyCode: keyCode,
     shiftKey: shift,
--- a/browser/devtools/shared/test/browser_cubic-bezier-03.js
+++ b/browser/devtools/shared/test/browser_cubic-bezier-03.js
@@ -2,18 +2,19 @@
 /* Any copyright is dedicated to the Public Domain.
    http://creativecommons.org/publicdomain/zero/1.0/ */
 
 "use strict";
 
 // Tests that coordinates can be changed programatically in the CubicBezierWidget
 
 const TEST_URI = "chrome://browser/content/devtools/cubic-bezier-frame.xhtml";
-const {CubicBezierWidget, PREDEFINED} =
+const {CubicBezierWidget} =
   devtools.require("devtools/shared/widgets/CubicBezierWidget");
+const {PREDEFINED} = require("devtools/shared/widgets/CubicBezierPresets");
 
 add_task(function*() {
   yield promiseTab("about:blank");
   let [host, win, doc] = yield createHost("bottom", TEST_URI);
 
   let container = doc.querySelector("#container");
   let w = new CubicBezierWidget(container, PREDEFINED.linear);
 
new file mode 100644
--- /dev/null
+++ b/browser/devtools/shared/test/browser_cubic-bezier-04.js
@@ -0,0 +1,51 @@
+/* vim: set ts=2 et sw=2 tw=80: */
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Tests that the CubicBezierPresetWidget generates markup.
+
+const TEST_URI = "chrome://browser/content/devtools/cubic-bezier-frame.xhtml";
+const {CubicBezierPresetWidget} =
+  devtools.require("devtools/shared/widgets/CubicBezierWidget");
+const {PRESETS} = require("devtools/shared/widgets/CubicBezierPresets");
+
+add_task(function*() {
+  yield promiseTab("about:blank");
+  let [host, win, doc] = yield createHost("bottom", TEST_URI);
+
+  let container = doc.querySelector("#container");
+  let w = new CubicBezierPresetWidget(container);
+
+  info("Checking that the presets are created in the parent");
+  ok(container.querySelector(".preset-pane"),
+     "The preset pane has been added");
+
+  ok(container.querySelector("#preset-categories"),
+     "The preset categories have been added");
+  let categories = container.querySelectorAll(".category");
+  is(categories.length, Object.keys(PRESETS).length,
+     "The preset categories have been added");
+  Object.keys(PRESETS).forEach(category => {
+    ok(container.querySelector("#" + category), `${category} has been added`);
+    ok(container.querySelector("#preset-category-" + category),
+       `The preset list for ${category} has been added.`);
+  });
+
+  info("Checking that each of the presets and its preview have been added");
+  Object.keys(PRESETS).forEach(category => {
+    Object.keys(PRESETS[category]).forEach(presetLabel => {
+      let preset = container.querySelector("#" + presetLabel);
+      ok(preset, `${presetLabel} has been added`);
+      ok(preset.querySelector("canvas"),
+         `${presetLabel}'s canvas preview has been added`);
+      ok(preset.querySelector("p"),
+         `${presetLabel}'s label has been added`);
+    });
+  });
+
+  w.destroy();
+  host.destroy();
+  gBrowser.removeCurrentTab();
+});
new file mode 100644
--- /dev/null
+++ b/browser/devtools/shared/test/browser_cubic-bezier-05.js
@@ -0,0 +1,49 @@
+/* vim: set ts=2 et sw=2 tw=80: */
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Tests that the CubicBezierPresetWidget cycles menus
+
+const TEST_URI = "chrome://browser/content/devtools/cubic-bezier-frame.xhtml";
+const {CubicBezierPresetWidget} =
+  devtools.require("devtools/shared/widgets/CubicBezierWidget");
+const {PREDEFINED, PRESETS, DEFAULT_PRESET_CATEGORY} =
+  require("devtools/shared/widgets/CubicBezierPresets");
+
+add_task(function*() {
+  yield promiseTab("about:blank");
+  let [host, win, doc] = yield createHost("bottom", TEST_URI);
+
+  let container = doc.querySelector("#container");
+  let w = new CubicBezierPresetWidget(container);
+
+  info("Checking that preset is selected if coordinates are known");
+
+  w.refreshMenu([0, 0, 0, 0]);
+  is(w.activeCategory, container.querySelector(`#${DEFAULT_PRESET_CATEGORY}`),
+    "The default category is selected");
+  is(w._activePreset, null, "There is no selected category");
+
+  w.refreshMenu(PREDEFINED["linear"]);
+  is(w.activeCategory, container.querySelector("#ease-in-out"),
+     "The ease-in-out category is active");
+  is(w._activePreset, container.querySelector("#ease-in-out-linear"),
+     "The ease-in-out-linear preset is active");
+
+  w.refreshMenu(PRESETS["ease-out"]["ease-out-sine"]);
+  is(w.activeCategory, container.querySelector("#ease-out"),
+     "The ease-out category is active");
+  is(w._activePreset, container.querySelector("#ease-out-sine"),
+     "The ease-out-sine preset is active");
+
+  w.refreshMenu([0, 0, 0, 0]);
+  is(w.activeCategory, container.querySelector("#ease-out"),
+     "The ease-out category is still active");
+  is(w._activePreset, null, "No preset is active");
+
+  w.destroy();
+  host.destroy();
+  gBrowser.removeCurrentTab();
+});
new file mode 100644
--- /dev/null
+++ b/browser/devtools/shared/test/browser_cubic-bezier-06.js
@@ -0,0 +1,80 @@
+
+/* vim: set ts=2 et sw=2 tw=80: */
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Tests the integration between CubicBezierWidget and CubicBezierPresets
+
+const TEST_URI = "chrome://browser/content/devtools/cubic-bezier-frame.xhtml";
+const {CubicBezierWidget} =
+  devtools.require("devtools/shared/widgets/CubicBezierWidget");
+const {PRESETS} = require("devtools/shared/widgets/CubicBezierPresets");
+
+add_task(function*() {
+  yield promiseTab("about:blank");
+  let [host, win, doc] = yield createHost("bottom", TEST_URI);
+
+  let container = doc.querySelector("#container");
+  let w = new CubicBezierWidget(container,
+              PRESETS["ease-in"]["ease-in-sine"]);
+  w.presets.refreshMenu(PRESETS["ease-in"]["ease-in-sine"]);
+
+  let rect = w.curve.getBoundingClientRect();
+  rect.graphTop = rect.height * w.bezierCanvas.padding[0];
+
+  yield adjustingBezierUpdatesPreset(w, win, doc, rect);
+  yield selectingPresetUpdatesBezier(w, win, doc, rect);
+
+  w.destroy();
+  host.destroy();
+  gBrowser.removeCurrentTab();
+});
+
+function* adjustingBezierUpdatesPreset(widget, win, doc, rect) {
+  info("Checking that changing the bezier refreshes the preset menu");
+
+  is(widget.presets.activeCategory,
+     doc.querySelector("#ease-in"),
+     "The selected category is ease-in");
+
+  is(widget.presets._activePreset,
+     doc.querySelector("#ease-in-sine"),
+     "The selected preset is ease-in-sine");
+
+  info("Generating custom bezier curve by dragging");
+  widget._onPointMouseDown({target: widget.p1});
+  doc.onmousemove({pageX: rect.left, pageY: rect.graphTop});
+  doc.onmouseup();
+
+  is(widget.presets.activeCategory,
+     doc.querySelector("#ease-in"),
+     "The selected category is still ease-in");
+
+  is(widget.presets._activePreset, null,
+     "There is no active preset");
+ }
+
+function* selectingPresetUpdatesBezier(widget, win, doc, rect) {
+  info("Checking that selecting a preset updates bezier curve");
+
+  info("Listening for the new coordinates event");
+  let onNewCoordinates = widget.presets.once("new-coordinates");
+  let onUpdated = widget.once("updated");
+
+  info("Click a preset");
+  let preset = doc.querySelector("#ease-in-sine");
+  widget.presets._onPresetClick({currentTarget: preset});
+
+  yield onNewCoordinates;
+  ok(true, "The preset widget fired the new-coordinates event");
+
+  let bezier = yield onUpdated;
+  ok(true, "The bezier canvas fired the updated event");
+
+  is(bezier.P1[0], preset.coordinates[0], "The new P1 time coordinate is correct");
+  is(bezier.P1[1], preset.coordinates[1], "The new P1 progress coordinate is correct");
+  is(bezier.P2[0], preset.coordinates[2], "P2 time coordinate is correct ");
+  is(bezier.P2[1], preset.coordinates[3], "P2 progress coordinate is correct");
+}
new file mode 100644
--- /dev/null
+++ b/browser/devtools/shared/test/browser_devices.js
@@ -0,0 +1,50 @@
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+
+let { GetDevices, GetDeviceString, AddDevice } = devtools.require("devtools/shared/devices");
+
+add_task(function*() {
+  Services.prefs.setCharPref("devtools.devices.url", TEST_URI_ROOT + "browser_devices.json");
+
+  let devices = yield GetDevices();
+
+  is(devices.TYPES.length, 1, "Found 1 device type.");
+
+  let type1 = devices.TYPES[0];
+
+  is(devices[type1].length, 2, "Found 2 devices of type #1.");
+
+  let string = GetDeviceString(type1);
+  ok(typeof string === "string" && string.length > 0, "Able to localize type #1.");
+
+  let device1 = {
+    name: "SquarePhone",
+    width: 320,
+    height: 320,
+    pixelRatio: 2,
+    userAgent: "Mozilla/5.0 (Mobile; rv:42.0)",
+    touch: true,
+    firefoxOS: true
+  };
+  AddDevice(device1, type1);
+  devices = yield GetDevices();
+
+  is(devices[type1].length, 3, "Added new device of type #1.");
+  ok(devices[type1].filter(d => d.name === device1.name), "Found the new device.");
+
+  let type2 = "appliances";
+  let device2 = {
+    name: "Mr Freezer",
+    width: 800,
+    height: 600,
+    pixelRatio: 5,
+    userAgent: "Mozilla/5.0 (Appliance; rv:42.0)",
+    touch: true,
+    firefoxOS: true
+  };
+  AddDevice(device2, type2);
+  devices = yield GetDevices();
+
+  is(devices.TYPES.length, 2, "Added device type #2.");
+  is(devices[type2].length, 1, "Added new device of type #2.");
+});
new file mode 100644
--- /dev/null
+++ b/browser/devtools/shared/test/browser_devices.json
@@ -0,0 +1,23 @@
+{
+  "TYPES": [ "phones" ],
+  "phones": [
+    {
+      "name": "Small Phone",
+      "width": 320,
+      "height": 480,
+      "pixelRatio": 1,
+      "userAgent": "Mozilla/5.0 (Mobile; rv:39.0) Gecko/39.0 Firefox/39.0",
+      "touch": true,
+      "firefoxOS": true
+    },
+    {
+      "name": "Big Phone",
+      "width": 360,
+      "height": 640,
+      "pixelRatio": 3,
+      "userAgent": "Mozilla/5.0 (Mobile; rv:39.0) Gecko/39.0 Firefox/39.0",
+      "touch": true,
+      "firefoxOS": true
+    }
+  ]
+}
--- a/browser/devtools/shared/test/browser_graphs-07a.js
+++ b/browser/devtools/shared/test/browser_graphs-07a.js
@@ -12,24 +12,28 @@ add_task(function*() {
   yield performTest();
   gBrowser.removeCurrentTab();
 });
 
 function* performTest() {
   let [host, win, doc] = yield createHost();
   let graph = new LineGraphWidget(doc.body, "fps");
   yield graph.once("ready");
+  testGraph(graph, normalDragStop);
+  yield graph.destroy();
 
-  testGraph(graph);
+  let graph2 = new LineGraphWidget(doc.body, "fps");
+  yield graph2.once("ready");
+  testGraph(graph2, buggyDragStop);
+  yield graph2.destroy();
 
-  yield graph.destroy();
   host.destroy();
 }
 
-function testGraph(graph) {
+function testGraph(graph, dragStop) {
   graph.setData(TEST_DATA);
 
   info("Making a selection.");
 
   dragStart(graph, 300);
   ok(graph.hasSelectionInProgress(),
     "The selection should start (1).");
   is(graph.getSelection().start, 300,
@@ -181,21 +185,32 @@ function click(graph, x, y = 1) {
 
 function dragStart(graph, x, y = 1) {
   x /= window.devicePixelRatio;
   y /= window.devicePixelRatio;
   graph._onMouseMove({ clientX: x, clientY: y });
   graph._onMouseDown({ clientX: x, clientY: y });
 }
 
-function dragStop(graph, x, y = 1) {
+function normalDragStop(graph, x, y = 1) {
   x /= window.devicePixelRatio;
   y /= window.devicePixelRatio;
   graph._onMouseMove({ clientX: x, clientY: y });
   graph._onMouseUp({ clientX: x, clientY: y });
 }
 
+function buggyDragStop(graph, x, y = 1) {
+  x /= window.devicePixelRatio;
+  y /= window.devicePixelRatio;
+
+  // Only fire a mousemove instead of a mouseup.
+  // This happens when the mouseup happens outside of the toolbox,
+  // see Bug 1066504.
+  graph._onMouseMove({ clientX: x, clientY: y });
+  graph._onMouseMove({ clientX: x, clientY: y, buttons: 0 });
+}
+
 function scroll(graph, wheel, x, y = 1) {
   x /= window.devicePixelRatio;
   y /= window.devicePixelRatio;
   graph._onMouseMove({ clientX: x, clientY: y });
   graph._onMouseWheel({ clientX: x, clientY: y, detail: wheel });
 }
--- a/browser/devtools/shared/test/unit/test_bezierCanvas.js
+++ b/browser/devtools/shared/test/unit/test_bezierCanvas.js
@@ -99,15 +99,18 @@ function getCanvasMock(w=200, h=400) {
         clearRect: () => {},
         beginPath: () => {},
         closePath: () => {},
         moveTo: () => {},
         lineTo: () => {},
         stroke: () => {},
         arc: () => {},
         fill: () => {},
-        bezierCurveTo: () => {}
+        bezierCurveTo: () => {},
+        save: () => {},
+        restore: () => {},
+        setTransform: () => {}
       };
     },
     width: w,
     height: h
   };
 }
--- a/browser/devtools/shared/test/unit/test_cubicBezier.js
+++ b/browser/devtools/shared/test/unit/test_cubicBezier.js
@@ -14,16 +14,17 @@ let {CubicBezier} = require("devtools/sh
 
 function run_test() {
   throwsWhenMissingCoordinates();
   throwsWhenIncorrectCoordinates();
   convertsStringCoordinates();
   coordinatesToStringOutputsAString();
   pointGettersReturnPointCoordinatesArrays();
   toStringOutputsCubicBezierValue();
+  toStringOutputsCssPresetValues();
 }
 
 function throwsWhenMissingCoordinates() {
   do_check_throws(() => {
     new CubicBezier();
   }, "Throws an exception when coordinates are missing");
 }
 
@@ -79,18 +80,37 @@ function pointGettersReturnPointCoordina
   do_check_eq(c.P1[1], .2);
   do_check_eq(c.P2[0], .5);
   do_check_eq(c.P2[1], 1);
 }
 
 function toStringOutputsCubicBezierValue() {
   do_print("toString() outputs the cubic-bezier() value");
 
+  let c = new CubicBezier([0, 1, 1, 0]);
+  do_check_eq(c.toString(), "cubic-bezier(0,1,1,0)");
+}
+
+function toStringOutputsCssPresetValues() {
+  do_print("toString() outputs the css predefined values");
+
   let c = new CubicBezier([0, 0, 1, 1]);
-  do_check_eq(c.toString(), "cubic-bezier(0,0,1,1)");
+  do_check_eq(c.toString(), "linear");
+
+  c = new CubicBezier([0.25, 0.1, 0.25, 1]);
+  do_check_eq(c.toString(), "ease");
+
+  c = new CubicBezier([0.42, 0, 1, 1]);
+  do_check_eq(c.toString(), "ease-in");
+
+  c = new CubicBezier([0, 0, 0.58, 1]);
+  do_check_eq(c.toString(), "ease-out");
+
+  c = new CubicBezier([0.42, 0, 0.58, 1]);
+  do_check_eq(c.toString(), "ease-in-out");
 }
 
 function do_check_throws(cb, info) {
   do_print(info);
 
   let hasThrown = false;
   try {
     cb();
new file mode 100644
--- /dev/null
+++ b/browser/devtools/shared/widgets/CubicBezierPresets.js
@@ -0,0 +1,64 @@
+/**
+ * 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/.
+ */
+
+// Set of preset definitions for use with CubicBezierWidget
+// Credit: http://easings.net
+
+"use strict";
+
+const PREDEFINED = {
+  "ease": [0.25, 0.1, 0.25, 1],
+  "linear": [0, 0, 1, 1],
+  "ease-in": [0.42, 0, 1, 1],
+  "ease-out": [0, 0, 0.58, 1],
+  "ease-in-out": [0.42, 0, 0.58, 1]
+};
+
+const PRESETS = {
+  "ease-in": {
+    "ease-in-linear": [0, 0, 1, 1],
+    "ease-in-ease-in": [0.42, 0, 1, 1],
+    "ease-in-sine": [0.47, 0, 0.74, 0.71],
+    "ease-in-quadratic": [0.55, 0.09, 0.68, 0.53],
+    "ease-in-cubic": [0.55, 0.06, 0.68, 0.19],
+    "ease-in-quartic": [0.9, 0.03, 0.69, 0.22],
+    "ease-in-quintic": [0.76, 0.05, 0.86, 0.06],
+    "ease-in-exponential": [0.95, 0.05, 0.8, 0.04],
+    "ease-in-circular": [0.6, 0.04, 0.98, 0.34],
+    "ease-in-backward": [0.6, -0.28, 0.74, 0.05]
+  },
+  "ease-out": {
+    "ease-out-linear": [0, 0, 1, 1],
+    "ease-out-ease-out": [0, 0, 0.58, 1],
+    "ease-out-sine": [0.39, 0.58, 0.57, 1],
+    "ease-out-quadratic": [0.25, 0.46, 0.45, 0.94],
+    "ease-out-cubic": [0.22, 0.61, 0.36, 1],
+    "ease-out-quartic": [0.17, 0.84, 0.44, 1],
+    "ease-out-quintic": [0.23, 1, 0.32, 1],
+    "ease-out-exponential": [0.19, 1, 0.22, 1],
+    "ease-out-circular": [0.08, 0.82, 0.17, 1],
+    "ease-out-backward": [0.18, 0.89, 0.32, 1.28]
+  },
+  "ease-in-out": {
+    "ease-in-out-linear": [0, 0, 1, 1],
+    "ease-in-out-ease": [0.25, 0.1, 0.25, 1],
+    "ease-in-out-ease-in-out": [0.42, 0, 0.58, 1],
+    "ease-in-out-sine": [0.45, 0.05, 0.55, 0.95],
+    "ease-in-out-quadratic": [0.46, 0.03, 0.52, 0.96],
+    "ease-in-out-cubic": [0.65, 0.05, 0.36, 1],
+    "ease-in-out-quartic": [0.77, 0, 0.18, 1],
+    "ease-in-out-quintic": [0.86, 0, 0.07, 1],
+    "ease-in-out-exponential": [1, 0, 0, 1],
+    "ease-in-out-circular": [0.79, 0.14, 0.15, 0.86],
+    "ease-in-out-backward": [0.68, -0.55, 0.27, 1.55]
+  }
+};
+
+const DEFAULT_PRESET_CATEGORY = Object.keys(PRESETS)[0];
+
+exports.PRESETS = PRESETS;
+exports.PREDEFINED = PREDEFINED;
+exports.DEFAULT_PRESET_CATEGORY = DEFAULT_PRESET_CATEGORY;
--- a/browser/devtools/shared/widgets/CubicBezierWidget.js
+++ b/browser/devtools/shared/widgets/CubicBezierWidget.js
@@ -22,24 +22,17 @@
 
 // Based on www.cubic-bezier.com by Lea Verou
 // See https://github.com/LeaVerou/cubic-bezier
 
 "use strict";
 
 const EventEmitter = require("devtools/toolkit/event-emitter");
 const {setTimeout, clearTimeout} = require("sdk/timers");
-
-const PREDEFINED = exports.PREDEFINED = {
-  "ease": [.25, .1, .25, 1],
-  "linear": [0, 0, 1, 1],
-  "ease-in": [.42, 0, 1, 1],
-  "ease-out": [0, 0, .58, 1],
-  "ease-in-out": [.42, 0, .58, 1]
-};
+const {PREDEFINED, PRESETS, DEFAULT_PRESET_CATEGORY} = require("devtools/shared/widgets/CubicBezierPresets");
 
 /**
  * CubicBezier data structure helper
  * Accepts an array of coordinates and exposes a few useful getters
  * @param {Array} coordinates i.e. [.42, 0, .58, 1]
  */
 function CubicBezier(coordinates) {
   if (!coordinates) {
@@ -54,32 +47,36 @@ function CubicBezier(coordinates) {
       throw "Wrong coordinate at " + i + "(" + xy + ")";
     }
   }
 
   this.coordinates.toString = function() {
     return this.map(n => {
       return (Math.round(n * 100)/100 + '').replace(/^0\./, '.');
     }) + "";
-  }
+  };
 }
 
 exports.CubicBezier = CubicBezier;
 
 CubicBezier.prototype = {
   get P1() {
     return this.coordinates.slice(0, 2);
   },
 
   get P2() {
     return this.coordinates.slice(2);
   },
 
   toString: function() {
-    return 'cubic-bezier(' + this.coordinates + ')';
+    // Check first if current coords are one of css predefined functions
+    let predefName = Object.keys(PREDEFINED)
+                           .find(key => coordsAreEqual(PREDEFINED[key], this.coordinates));
+
+    return predefName || 'cubic-bezier(' + this.coordinates + ')';
   }
 };
 
 /**
  * Bezier curve canvas plotting class
  * @param {DOMNode} canvas
  * @param {CubicBezier} bezier
  * @param {Array} padding Amount of horizontal,vertical padding around the graph
@@ -92,17 +89,17 @@ function BezierCanvas(canvas, bezier, pa
   // Convert to a cartesian coordinate system with axes from 0 to 1
   this.ctx = this.canvas.getContext('2d');
   let p = this.padding;
 
   this.ctx.scale(canvas.width * (1 - p[1] - p[3]),
                  -canvas.height * (1 - p[0] - p[2]));
   this.ctx.translate(p[3] / (1 - p[1] - p[3]),
                      -1 - p[0] / (1 - p[0] - p[2]));
-};
+}
 
 exports.BezierCanvas = BezierCanvas;
 
 BezierCanvas.prototype = {
   /**
    * Get P1 and P2 current top/left offsets so they can be positioned
    * @return {Array} Returns an array of 2 {top:String,left:String} objects
    */
@@ -110,78 +107,86 @@ BezierCanvas.prototype = {
     let p = this.padding, w = this.canvas.width, h = this.canvas.height;
 
     return [{
       left: w * (this.bezier.coordinates[0] * (1 - p[3] - p[1]) - p[3]) + 'px',
       top: h * (1 - this.bezier.coordinates[1] * (1 - p[0] - p[2]) - p[0]) + 'px'
     }, {
       left: w * (this.bezier.coordinates[2] * (1 - p[3] - p[1]) - p[3]) + 'px',
       top: h * (1 - this.bezier.coordinates[3] * (1 - p[0] - p[2]) - p[0]) + 'px'
-    }]
+    }];
   },
 
   /**
    * Convert an element's left/top offsets into coordinates
    */
   offsetsToCoordinates: function(element) {
     let p = this.padding, w = this.canvas.width, h = this.canvas.height;
 
     // Convert padding percentage to actual padding
     p = p.map(function(a, i) { return a * (i % 2? w : h)});
 
     return [
-      (parseInt(element.style.left) - p[3]) / (w + p[1] + p[3]),
-      (h - parseInt(element.style.top) - p[2]) / (h - p[0] - p[2])
+      (parseFloat(element.style.left) - p[3]) / (w + p[1] + p[3]),
+      (h - parseFloat(element.style.top) - p[2]) / (h - p[0] - p[2])
     ];
   },
 
   /**
    * Draw the cubic bezier curve for the current coordinates
    */
   plot: function(settings={}) {
     let xy = this.bezier.coordinates;
 
     let defaultSettings = {
       handleColor: '#666',
       handleThickness: .008,
       bezierColor: '#4C9ED9',
-      bezierThickness: .015
+      bezierThickness: .015,
+      drawHandles: true
     };
 
     for (let setting in settings) {
       defaultSettings[setting] = settings[setting];
     }
 
-    this.ctx.clearRect(-.5,-.5, 2, 2);
+    // Clear the canvas –making sure to clear the
+    // whole area by resetting the transform first.
+    this.ctx.save();
+    this.ctx.setTransform(1, 0, 0, 1, 0, 0);
+    this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);
+    this.ctx.restore();
 
-    // Draw control handles
-    this.ctx.beginPath();
-    this.ctx.fillStyle = defaultSettings.handleColor;
-    this.ctx.lineWidth = defaultSettings.handleThickness;
-    this.ctx.strokeStyle = defaultSettings.handleColor;
-
-    this.ctx.moveTo(0, 0);
-    this.ctx.lineTo(xy[0], xy[1]);
-    this.ctx.moveTo(1,1);
-    this.ctx.lineTo(xy[2], xy[3]);
+    if (defaultSettings.drawHandles) {
+      // Draw control handles
+      this.ctx.beginPath();
+      this.ctx.fillStyle = defaultSettings.handleColor;
+      this.ctx.lineWidth = defaultSettings.handleThickness;
+      this.ctx.strokeStyle = defaultSettings.handleColor;
 
-    this.ctx.stroke();
-    this.ctx.closePath();
+      this.ctx.moveTo(0, 0);
+      this.ctx.lineTo(xy[0], xy[1]);
+      this.ctx.moveTo(1,1);
+      this.ctx.lineTo(xy[2], xy[3]);
+
+      this.ctx.stroke();
+      this.ctx.closePath();
 
-    function circle(ctx, cx, cy, r) {
-      return ctx.beginPath();
-      ctx.arc(cx, cy, r, 0, 2*Math.PI, !1);
-      ctx.closePath();
+      var circle = function(ctx, cx, cy, r) {
+        return ctx.beginPath();
+        ctx.arc(cx, cy, r, 0, 2*Math.PI, !1);
+        ctx.closePath();
+      };
+
+      circle(this.ctx, xy[0], xy[1], 1.5 * defaultSettings.handleThickness);
+      this.ctx.fill();
+      circle(this.ctx, xy[2], xy[3], 1.5 * defaultSettings.handleThickness);
+      this.ctx.fill();
     }
 
-    circle(this.ctx, xy[0], xy[1], 1.5 * defaultSettings.handleThickness);
-    this.ctx.fill();
-    circle(this.ctx, xy[2], xy[3], 1.5 * defaultSettings.handleThickness);
-    this.ctx.fill();
-
     // Draw bezier curve
     this.ctx.beginPath();
     this.ctx.lineWidth = defaultSettings.bezierThickness;
     this.ctx.strokeStyle = defaultSettings.bezierColor;
     this.ctx.moveTo(0,0);
     this.ctx.bezierCurveTo(xy[0], xy[1], xy[2], xy[3], 1,1);
     this.ctx.stroke();
     this.ctx.closePath();
@@ -192,104 +197,118 @@ BezierCanvas.prototype = {
  * Cubic-bezier widget. Uses the BezierCanvas class to draw the curve and
  * adds the control points and user interaction
  * @param {DOMNode} parent The container where the graph should be created
  * @param {Array} coordinates Coordinates of the curve to be drawn
  *
  * Emits "updated" events whenever the curve is changed. Along with the event is
  * sent a CubicBezier object
  */
-function CubicBezierWidget(parent, coordinates=PREDEFINED["ease-in-out"]) {
+function CubicBezierWidget(parent, coordinates=PRESETS["ease-in"]["ease-in-sine"]) {
+  EventEmitter.decorate(this);
+
   this.parent = parent;
   let {curve, p1, p2} = this._initMarkup();
 
+  this.curveBoundingBox = curve.getBoundingClientRect();
   this.curve = curve;
-  this.curveBoundingBox = curve.getBoundingClientRect();
   this.p1 = p1;
   this.p2 = p2;
 
   // Create and plot the bezier curve
   this.bezierCanvas = new BezierCanvas(this.curve,
-    new CubicBezier(coordinates), [.25, 0]);
+    new CubicBezier(coordinates), [0.30, 0]);
   this.bezierCanvas.plot();
 
   // Place the control points
   let offsets = this.bezierCanvas.offsets;
   this.p1.style.left = offsets[0].left;
   this.p1.style.top = offsets[0].top;
   this.p2.style.left = offsets[1].left;
   this.p2.style.top = offsets[1].top;
 
   this._onPointMouseDown = this._onPointMouseDown.bind(this);
   this._onPointKeyDown = this._onPointKeyDown.bind(this);
   this._onCurveClick = this._onCurveClick.bind(this);
-  this._initEvents();
+  this._onNewCoordinates = this._onNewCoordinates.bind(this);
+
+  // Add preset preview menu
+  this.presets = new CubicBezierPresetWidget(parent);
 
   // Add the timing function previewer
   this.timingPreview = new TimingFunctionPreviewWidget(parent);
 
-  EventEmitter.decorate(this);
+  this._initEvents();
 }
 
 exports.CubicBezierWidget = CubicBezierWidget;
 
 CubicBezierWidget.prototype = {
   _initMarkup: function() {
     let doc = this.parent.ownerDocument;
 
+    let wrap = doc.createElement("div");
+    wrap.className = "display-wrap";
+
     let plane = doc.createElement("div");
     plane.className = "coordinate-plane";
 
     let p1 = doc.createElement("button");
     p1.className = "control-point";
     p1.id = "P1";
     plane.appendChild(p1);
 
     let p2 = doc.createElement("button");
     p2.className = "control-point";
     p2.id = "P2";
     plane.appendChild(p2);
 
     let curve = doc.createElement("canvas");
-    curve.setAttribute("height", "400");
-    curve.setAttribute("width", "200");
+    curve.setAttribute("width", 150);
+    curve.setAttribute("height", 370);
     curve.id = "curve";
-    plane.appendChild(curve);
 
-    this.parent.appendChild(plane);
+    plane.appendChild(curve);
+    wrap.appendChild(plane);
+
+    this.parent.appendChild(wrap);
 
     return {
       p1: p1,
       p2: p2,
       curve: curve
-    }
+    };
   },
 
   _removeMarkup: function() {
-    this.parent.ownerDocument.querySelector(".coordinate-plane").remove();
+    this.parent.ownerDocument.querySelector(".display-wrap").remove();
   },
 
   _initEvents: function() {
     this.p1.addEventListener("mousedown", this._onPointMouseDown);
     this.p2.addEventListener("mousedown", this._onPointMouseDown);
 
     this.p1.addEventListener("keydown", this._onPointKeyDown);
     this.p2.addEventListener("keydown", this._onPointKeyDown);
 
     this.curve.addEventListener("click", this._onCurveClick);
+
+    this.presets.on("new-coordinates", this._onNewCoordinates);
   },
 
   _removeEvents: function() {
     this.p1.removeEventListener("mousedown", this._onPointMouseDown);
     this.p2.removeEventListener("mousedown", this._onPointMouseDown);
 
     this.p1.removeEventListener("keydown", this._onPointKeyDown);
     this.p2.removeEventListener("keydown", this._onPointKeyDown);
 
     this.curve.removeEventListener("click", this._onCurveClick);
+
+    this.presets.off("new-coordinates", this._onNewCoordinates);
   },
 
   _onPointMouseDown: function(event) {
     // Updating the boundingbox in case it has changed
     this.curveBoundingBox = this.curve.getBoundingClientRect();
 
     let point = event.target;
     let doc = point.ownerDocument;
@@ -312,17 +331,17 @@ CubicBezierWidget.prototype = {
       point.style.top = y - top + "px";
 
       self._updateFromPoints();
     };
 
     doc.onmouseup = function () {
       point.focus();
       doc.onmousemove = doc.onmouseup = null;
-    }
+    };
   },
 
   _onPointKeyDown: function(event) {
     let point = event.target;
     let code = event.keyCode;
 
     if (code >= 37 && code <= 40) {
       event.preventDefault();
@@ -339,16 +358,18 @@ CubicBezierWidget.prototype = {
         case 40: point.style.top = top + offset + 'px'; break;
       }
 
       this._updateFromPoints();
     }
   },
 
   _onCurveClick: function(event) {
+    this.curveBoundingBox = this.curve.getBoundingClientRect();
+
     let left = this.curveBoundingBox.left;
     let top = this.curveBoundingBox.top;
     let x = event.pageX - left;
     let y = event.pageY - top;
 
     // Find which point is closer
     let distP1 = distance(x, y,
       parseInt(this.p1.style.left), parseInt(this.p1.style.top));
@@ -357,24 +378,29 @@ CubicBezierWidget.prototype = {
 
     let point = distP1 < distP2 ? this.p1 : this.p2;
     point.style.left = x + "px";
     point.style.top = y + "px";
 
     this._updateFromPoints();
   },
 
+  _onNewCoordinates: function(event, coordinates) {
+    this.coordinates = coordinates;
+  },
+
   /**
    * Get the current point coordinates and redraw the curve to match
    */
   _updateFromPoints: function() {
     // Get the new coordinates from the point's offsets
-    let coordinates = this.bezierCanvas.offsetsToCoordinates(this.p1)
+    let coordinates = this.bezierCanvas.offsetsToCoordinates(this.p1);
     coordinates = coordinates.concat(this.bezierCanvas.offsetsToCoordinates(this.p2));
 
+    this.presets.refreshMenu(coordinates);
     this._redraw(coordinates);
   },
 
   /**
    * Redraw the curve
    * @param {Array} coordinates The array of control point coordinates
    */
   _redraw: function(coordinates) {
@@ -386,17 +412,17 @@ CubicBezierWidget.prototype = {
     this.timingPreview.preview(this.bezierCanvas.bezier + "");
   },
 
   /**
    * Set new coordinates for the control points and redraw the curve
    * @param {Array} coordinates
    */
   set coordinates(coordinates) {
-    this._redraw(coordinates)
+    this._redraw(coordinates);
 
     // Move the points
     let offsets = this.bezierCanvas.offsets;
     this.p1.style.left = offsets[0].left;
     this.p1.style.top = offsets[0].top;
     this.p2.style.left = offsets[1].left;
     this.p2.style.top = offsets[1].top;
   },
@@ -415,30 +441,282 @@ CubicBezierWidget.prototype = {
     // Try with one of the predefined values
     let coordinates = PREDEFINED[value];
 
     // Otherwise parse the coordinates from the cubic-bezier function
     if (!coordinates && value.startsWith("cubic-bezier")) {
       coordinates = value.replace(/cubic-bezier|\(|\)/g, "").split(",").map(parseFloat);
     }
 
+    this.presets.refreshMenu(coordinates);
     this.coordinates = coordinates;
   },
 
   destroy: function() {
     this._removeEvents();
     this._removeMarkup();
 
     this.timingPreview.destroy();
+    this.presets.destroy();
 
     this.curve = this.p1 = this.p2 = null;
   }
 };
 
 /**
+ * CubicBezierPreset widget.
+ * Builds a menu of presets from CubicBezierPresets
+ * @param {DOMNode} parent The container where the preset panel should be created
+ *
+ * Emits "new-coordinate" event along with the coordinates
+ * whenever a preset is selected.
+ */
+function CubicBezierPresetWidget(parent) {
+  this.parent = parent;
+
+  let {presetPane, presets, categories} = this._initMarkup();
+  this.presetPane = presetPane;
+  this.presets = presets;
+  this.categories = categories;
+
+  this._activeCategory = null;
+  this._activePresetList = null;
+  this._activePreset = null;
+
+  this._onCategoryClick = this._onCategoryClick.bind(this);
+  this._onPresetClick = this._onPresetClick.bind(this);
+
+  EventEmitter.decorate(this);
+  this._initEvents();
+}
+
+exports.CubicBezierPresetWidget = CubicBezierPresetWidget;
+
+CubicBezierPresetWidget.prototype = {
+  /*
+   * Constructs a list of all preset categories and a list
+   * of presets for each category.
+   *
+   * High level markup:
+   *  div .preset-pane
+   *    div .preset-categories
+   *      div .category
+   *      div .category
+   *      ...
+   *    div .preset-container
+   *      div .presetList
+   *        div .preset
+   *        ...
+   *      div .presetList
+   *        div .preset
+   *        ...
+   */
+  _initMarkup: function() {
+    let doc = this.parent.ownerDocument;
+
+    let presetPane = doc.createElement("div");
+    presetPane.className = "preset-pane";
+
+    let categoryList = doc.createElement("div");
+    categoryList.id = "preset-categories";
+
+    let presetContainer = doc.createElement("div");
+    presetContainer.id = "preset-container";
+
+    Object.keys(PRESETS).forEach(categoryLabel => {
+      let category = this._createCategory(categoryLabel);
+      categoryList.appendChild(category);
+
+      let presetList = this._createPresetList(categoryLabel);
+      presetContainer.appendChild(presetList);
+    });
+
+    presetPane.appendChild(categoryList);
+    presetPane.appendChild(presetContainer);
+
+    this.parent.appendChild(presetPane);
+
+    let allCategories = presetPane.querySelectorAll(".category");
+    let allPresets = presetPane.querySelectorAll(".preset");
+
+    return {
+      presetPane: presetPane,
+      presets: allPresets,
+      categories: allCategories
+    };
+  },
+
+  _createCategory: function(categoryLabel) {
+    let doc = this.parent.ownerDocument;
+
+    let category = doc.createElement("div");
+    category.id = categoryLabel;
+    category.classList.add("category");
+
+    let categoryDisplayLabel = this._normalizeCategoryLabel(categoryLabel);
+    category.textContent = categoryDisplayLabel;
+
+    return category;
+  },
+
+  _normalizeCategoryLabel: function(categoryLabel) {
+    return categoryLabel.replace("/-/g", " ");
+  },
+
+  _createPresetList: function(categoryLabel) {
+    let doc = this.parent.ownerDocument;
+
+    let presetList = doc.createElement("div");
+    presetList.id = "preset-category-" + categoryLabel;
+    presetList.classList.add("preset-list");
+
+    Object.keys(PRESETS[categoryLabel]).forEach(presetLabel => {
+      let preset = this._createPreset(categoryLabel, presetLabel);
+      presetList.appendChild(preset);
+    });
+
+    return presetList;
+  },
+
+  _createPreset: function(categoryLabel, presetLabel) {
+    let doc = this.parent.ownerDocument;
+
+    let preset = doc.createElement("div");
+    preset.classList.add("preset");
+    preset.id = presetLabel;
+    preset.coordinates = PRESETS[categoryLabel][presetLabel];
+
+    // Create preset preview
+    let curve = doc.createElement("canvas");
+    let bezier = new CubicBezier(preset.coordinates);
+
+    curve.setAttribute("height", 55);
+    curve.setAttribute("width", 55);
+
+    preset.bezierCanvas = new BezierCanvas(curve, bezier, [0.15, 0]);
+    preset.bezierCanvas.plot({
+      drawHandles: false,
+      bezierThickness: 0.025
+    });
+
+    preset.appendChild(curve);
+
+    // Create preset label
+    let presetLabelElem = doc.createElement("p");
+    let presetDisplayLabel = this._normalizePresetLabel(categoryLabel, presetLabel);
+    presetLabelElem.textContent = presetDisplayLabel;
+    preset.appendChild(presetLabelElem);
+
+    return preset;
+  },
+
+  _normalizePresetLabel: function(categoryLabel, presetLabel) {
+    return presetLabel.replace(categoryLabel + "-", "").replace("/-/g", " ");
+  },
+
+  _initEvents: function() {
+    for (let category of this.categories) {
+      category.addEventListener("click", this._onCategoryClick);
+    }
+
+    for (let preset of this.presets) {
+      preset.addEventListener("click", this._onPresetClick);
+    }
+  },
+
+  _removeEvents: function() {
+    for (let category of this.categories) {
+      category.removeEventListener("click", this._onCategoryClick);
+    }
+
+    for (let preset of this.presets) {
+      preset.removeEventListener("click", this._onPresetClick);
+    }
+  },
+
+  _onPresetClick: function(event) {
+    this.emit("new-coordinates", event.currentTarget.coordinates);
+    this.activePreset = event.currentTarget;
+  },
+
+  _onCategoryClick: function(event) {
+    this.activeCategory = event.target;
+  },
+
+  _setActivePresetList: function(presetListId) {
+    let presetList = this.presetPane.querySelector("#" + presetListId);
+    swapClassName("active-preset-list", this._activePresetList, presetList);
+    this._activePresetList = presetList;
+  },
+
+  set activeCategory(category) {
+    swapClassName("active-category", this._activeCategory, category);
+    this._activeCategory = category;
+    this._setActivePresetList("preset-category-" + category.id);
+  },
+
+  get activeCategory() {
+    return this._activeCategory;
+  },
+
+  set activePreset(preset) {
+    swapClassName("active-preset", this._activePreset, preset);
+    this._activePreset = preset;
+  },
+
+  get activePreset() {
+    return this._activePreset;
+  },
+
+  /**
+   * Called by CubicBezierWidget onload and when
+   * the curve is modified via the canvas.
+   * Attempts to match the new user setting with an
+   * existing preset.
+   * @param {Array} coordinates new coords [i, j, k, l]
+   */
+  refreshMenu: function(coordinates) {
+    // If we cannot find a matching preset, keep
+    // menu on last known preset category.
+    let category = this._activeCategory;
+
+    // If we cannot find a matching preset
+    // deselect any selected preset.
+    let preset = null;
+
+    // If a category has never been viewed before
+    // show the default category.
+    if (!category) {
+      category = this.parent.querySelector("#" + DEFAULT_PRESET_CATEGORY);
+    }
+
+    // If the new coordinates do match a preset,
+    // set its category and preset button as active.
+    Object.keys(PRESETS).forEach(categoryLabel => {
+
+      Object.keys(PRESETS[categoryLabel]).forEach(presetLabel => {
+        if (coordsAreEqual(PRESETS[categoryLabel][presetLabel], coordinates)) {
+          category = this.parent.querySelector("#" + categoryLabel);
+          preset = this.parent.querySelector("#" + presetLabel);
+        }
+      });
+
+    });
+
+    this.activeCategory = category;
+    this.activePreset = preset;
+  },
+
+  destroy: function() {
+    this._removeEvents();
+    this.parent.querySelector(".preset-pane").remove();
+  }
+};
+
+/**
  * The TimingFunctionPreviewWidget animates a dot on a scale with a given
  * timing-function
  * @param {DOMNode} parent The container where this widget should go
  */
 function TimingFunctionPreviewWidget(parent) {
   this.previousValue = null;
   this.autoRestartAnimation = null;
 
@@ -549,8 +827,34 @@ function isValidTimingFunction(value) {
 
   // Or it has to match a cubic-bezier expression
   if (value.match(/^cubic-bezier\(([0-9.\- ]+,){3}[0-9.\- ]+\)/)) {
     return true;
   }
 
   return false;
 }
+
+/**
+ * Removes a class from a node and adds it to another.
+ * @param {String} className the class to swap
+ * @param {DOMNode} from the node to remove the class from
+ * @param {DOMNode} to the node to add the class to
+ */
+function swapClassName(className, from, to) {
+  if (from !== null) {
+    from.classList.remove(className);
+  }
+
+  if (to !== null) {
+    to.classList.add(className);
+  }
+}
+
+/**
+ * Compares two arrays of coordinates [i, j, k, l]
+ * @param {Array} c1 first coordinate array to compare
+ * @param {Array} c2 second coordinate array to compare
+ * @return {Boolean}
+ */
+function coordsAreEqual(c1, c2) {
+  return c1.reduce((prev, curr, index) => prev && (curr === c2[index]), true);
+}
--- a/browser/devtools/shared/widgets/Graphs.jsm
+++ b/browser/devtools/shared/widgets/Graphs.jsm
@@ -176,16 +176,17 @@ this.AbstractCanvasGraph = function(pare
     this._height = canvas.height = bounds.height * this._pixelRatio;
     this._ctx = canvas.getContext("2d");
     this._ctx.mozImageSmoothingEnabled = false;
 
     this._cursor = new GraphCursor();
     this._selection = new GraphArea();
     this._selectionDragger = new GraphAreaDragger();
     this._selectionResizer = new GraphAreaResizer();
+    this._isMouseActive = false;
 
     this._onAnimationFrame = this._onAnimationFrame.bind(this);
     this._onMouseMove = this._onMouseMove.bind(this);
     this._onMouseDown = this._onMouseDown.bind(this);
     this._onMouseUp = this._onMouseUp.bind(this);
     this._onMouseWheel = this._onMouseWheel.bind(this);
     this._onMouseOut = this._onMouseOut.bind(this);
     this._onResize = this._onResize.bind(this);
@@ -947,31 +948,40 @@ AbstractCanvasGraph.prototype = {
 
     return { left: x, top: y };
   },
 
   /**
    * Listener for the "mousemove" event on the graph's container.
    */
   _onMouseMove: function(e) {
+    let resizer = this._selectionResizer;
+    let dragger = this._selectionDragger;
+
+    // If a mouseup happened outside the toolbox and the current operation
+    // is causing the selection changed, then end it.
+    if (e.buttons == 0 && (this.hasSelectionInProgress() ||
+                           resizer.margin != null ||
+                           dragger.origin != null)) {
+      return this._onMouseUp(e);
+    }
+
     let offset = this._getContainerOffset();
     let mouseX = (e.clientX - offset.left) * this._pixelRatio;
     let mouseY = (e.clientY - offset.top) * this._pixelRatio;
     this._cursor.x = mouseX;
     this._cursor.y = mouseY;
 
-    let resizer = this._selectionResizer;
     if (resizer.margin != null) {
       this._selection[resizer.margin] = mouseX;
       this._shouldRedraw = true;
       this.emit("selecting");
       return;
     }
 
-    let dragger = this._selectionDragger;
     if (dragger.origin != null) {
       this._selection.start = dragger.anchor.start - dragger.origin + mouseX;
       this._selection.end = dragger.anchor.end - dragger.origin + mouseX;
       this._shouldRedraw = true;
       this.emit("selecting");
       return;
     }
 
@@ -1008,16 +1018,17 @@ AbstractCanvasGraph.prototype = {
 
     this._shouldRedraw = true;
   },
 
   /**
    * Listener for the "mousedown" event on the graph's container.
    */
   _onMouseDown: function(e) {
+    this._isMouseActive = true;
     let offset = this._getContainerOffset();
     let mouseX = (e.clientX - offset.left) * this._pixelRatio;
 
     switch (this._canvas.getAttribute("input")) {
       case "hovering-background":
       case "hovering-region":
         if (!this.selectionEnabled) {
           break;
@@ -1046,16 +1057,17 @@ AbstractCanvasGraph.prototype = {
     this._shouldRedraw = true;
     this.emit("mousedown");
   },
 
   /**
    * Listener for the "mouseup" event on the graph's container.
    */
   _onMouseUp: function(e) {
+    this._isMouseActive = false;
     let offset = this._getContainerOffset();
     let mouseX = (e.clientX - offset.left) * this._pixelRatio;
 
     switch (this._canvas.getAttribute("input")) {
       case "hovering-background":
       case "hovering-region":
         if (!this.selectionEnabled) {
           break;
@@ -1156,31 +1168,27 @@ AbstractCanvasGraph.prototype = {
       selection.end = midPoint + GRAPH_WHEEL_MIN_SELECTION_WIDTH / 2;
     }
 
     this._shouldRedraw = true;
     this.emit("selecting");
     this.emit("scroll");
   },
 
-  /**
+   /**
    * Listener for the "mouseout" event on the graph's container.
+   * Clear any active cursors if a drag isn't happening.
    */
-  _onMouseOut: function() {
-    if (this.hasSelectionInProgress()) {
-      this.dropSelection();
+  _onMouseOut: function(e) {
+    if (!this._isMouseActive) {
+      this._cursor.x = null;
+      this._cursor.y = null;
+      this._canvas.removeAttribute("input");
+      this._shouldRedraw = true;
     }
-
-    this._cursor.x = null;
-    this._cursor.y = null;
-    this._selectionResizer.margin = null;
-    this._selectionDragger.origin = null;
-
-    this._canvas.removeAttribute("input");
-    this._shouldRedraw = true;
   },
 
   /**
    * Listener for the "resize" event on the graph's parent node.
    */
   _onResize: function() {
     if (this.hasData()) {
       setNamedTimeout(this._uid, GRAPH_RESIZE_EVENTS_DRAIN, this.refresh);
--- a/browser/devtools/shared/widgets/Tooltip.js
+++ b/browser/devtools/shared/widgets/Tooltip.js
@@ -790,18 +790,18 @@ Tooltip.prototype = {
    * the instance of the widget
    */
   setCubicBezierContent: function(bezier) {
     let def = promise.defer();
 
     // Create an iframe to host the cubic-bezier widget
     let iframe = this.doc.createElementNS(XHTML_NS, "iframe");
     iframe.setAttribute("transparent", true);
-    iframe.setAttribute("width", "200");
-    iframe.setAttribute("height", "415");
+    iframe.setAttribute("width", "410");
+    iframe.setAttribute("height", "360");
     iframe.setAttribute("flex", "1");
     iframe.setAttribute("class", "devtools-tooltip-iframe");
 
     let panel = this.panel;
     let xulWin = this.doc.ownerGlobal;
 
     // Wait for the load to initialize the widget
     function onLoad() {
--- a/browser/devtools/shared/widgets/cubic-bezier-frame.xhtml
+++ b/browser/devtools/shared/widgets/cubic-bezier-frame.xhtml
@@ -3,23 +3,24 @@
    - 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/. -->
 <!DOCTYPE html>
 
 <html xmlns="http://www.w3.org/1999/xhtml">
 <head>
   <meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/>
   <link rel="stylesheet" href="chrome://browser/skin/devtools/common.css" type="text/css"/>
-  <link rel="stylesheet" href="chrome://browser/content/devtools/cubic-bezier.css" ype="text/css"/>
+  <link rel="stylesheet" href="chrome://browser/content/devtools/cubic-bezier.css" type="text/css"/>
   <script type="application/javascript;version=1.8" src="theme-switching.js"/>
   <style>
-    body {
+    html, body {
       margin: 0;
       padding: 0;
-      width: 200px;
-      height: 415px;
+      overflow: hidden;
+      width: 410px;
+      height: 370px;
     }
   </style>
 </head>
 <body role="application">
   <div id="container"></div>
 </body>
 </html>
--- a/browser/devtools/shared/widgets/cubic-bezier.css
+++ b/browser/devtools/shared/widgets/cubic-bezier.css
@@ -1,41 +1,37 @@
 /* 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/. */
 
 /* Based on Lea Verou www.cubic-bezier.com
    See https://github.com/LeaVerou/cubic-bezier */
 
-.coordinate-plane {
-  position: absolute;
-  line-height: 0;
-  height: 400px;
-  width: 200px;
+#container {
+  display: flex;
+  width: 410px;
+  height: 370px;
+  flex-direction: row-reverse;
+  overflow: hidden;
 }
 
-.coordinate-plane:before,
-.coordinate-plane:after {
-  position: absolute;
-  bottom: 25%;
-  left: 0;
-  width: 100%;
+.display-wrap {
+  width: 50%;
+  height: 100%;
+  text-align: center;
+  overflow: hidden;
 }
 
-.coordinate-plane:before {
-  content: "";
-  border-bottom: 2px solid;
-  transform: rotate(-90deg) translateY(2px);
-  transform-origin: bottom left;
-}
+/* Coordinate Plane */
 
-.coordinate-plane:after {
-  content: "";
-  border-top: 2px solid;
-  margin-bottom: -2px;
+.coordinate-plane {
+  width: 150px;
+  height: 370px;
+  margin: 0 auto;
+  position: relative;
 }
 
 .theme-dark .coordinate-plane:before,
 .theme-dark .coordinate-plane:after {
   border-color: #eee;
 }
 
 .control-point {
@@ -45,64 +41,60 @@
   width: 10px;
   border: 0;
   background: #666;
   display: block;
   margin: -5px 0 0 -5px;
   outline: none;
   border-radius: 5px;
   padding: 0;
-
   cursor: pointer;
 }
 
-#P1x, #P1y {
-  color: #f08;
-}
-
-#P2x, #P2y {
-  color: #0ab;
-}
-
-canvas#curve {
-  background:
-    linear-gradient(-45deg, transparent 49.7%, rgba(0,0,0,.2) 49.7%, rgba(0,0,0,.2) 50.3%, transparent 50.3%) center no-repeat,
-    repeating-linear-gradient(transparent, #eee 0, #eee .5%, transparent .5%, transparent 10%) no-repeat,
-    repeating-linear-gradient(-90deg, transparent, #eee 0, #eee .5%, transparent .5%, transparent 10%) no-repeat;
-
-  background-size: 100% 50%, 100% 50%, 100% 50%;
-  background-position: 25%, 0, 0;
+.display-wrap {
+  background: repeating-linear-gradient(0deg, transparent, rgba(0, 0, 0, 0.05) 0, rgba(0, 0, 0, 0.05) 1px, transparent 1px, transparent 15px) no-repeat, repeating-linear-gradient(90deg, transparent, rgba(0, 0, 0, 0.05) 0, rgba(0, 0, 0, 0.05) 1px, transparent 1px, transparent 15px) no-repeat;
+  background-size: 100% 100%, 100% 100%;
+  background-position: -2px 5px, -2px 5px;
 
   -moz-user-select: none;
 }
 
-.theme-dark canvas#curve {
-  background:
-    linear-gradient(-45deg, transparent 49.7%, #eee 49.7%, #eee 50.3%, transparent 50.3%) center no-repeat,
-    repeating-linear-gradient(transparent, rgba(0,0,0,.2) 0, rgba(0,0,0,.2) .5%, transparent .5%, transparent 10%) no-repeat,
-    repeating-linear-gradient(-90deg, transparent, rgba(0,0,0,.2) 0, rgba(0,0,0,.2) .5%, transparent .5%, transparent 10%) no-repeat;
+.theme-dark .display-wrap {
+  background: repeating-linear-gradient(0deg, transparent, rgba(0, 0, 0, 0.2) 0, rgba(0, 0, 0, 0.2) 1px, transparent 1px, transparent 15px) no-repeat, repeating-linear-gradient(90deg, transparent, rgba(0, 0, 0, 0.2) 0, rgba(0, 0, 0, 0.2) 1px, transparent 1px, transparent 15px) no-repeat;
+  background-size: 100% 100%, 100% 100%;
+  background-position: -2px 5px, -2px 5px;
 
-  background-size: 100% 50%, 100% 50%, 100% 50%;
-  background-position: 25%, 0, 0;
+  -moz-user-select: none;
+}
+canvas#curve {
+  background: linear-gradient(-45deg, transparent 49.7%, rgba(0,0,0,.2) 49.7%, rgba(0,0,0,.2) 50.3%, transparent 50.3%) center no-repeat;
+  background-size: 100% 100%;
+  background-position: 0 0;
 }
 
-/* Timing function preview widget */
+.theme-dark canvas#curve {
+  background: linear-gradient(-45deg, transparent 49.7%, #eee 49.7%, #eee 50.3%, transparent 50.3%) center no-repeat;
+}
+
+/* Timing Function Preview Widget */
 
 .timing-function-preview {
   position: absolute;
-  top: 400px;
+  bottom: 20px;
+  right: 27px;
+  width: 150px;
 }
 
 .timing-function-preview .scale {
   position: absolute;
   top: 6px;
   left: 0;
   z-index: 1;
 
-  width: 200px;
+  width: 150px;
   height: 1px;
 
   background: #ccc;
 }
 
 .timing-function-preview .dot {
   position: absolute;
   top: 0;
@@ -123,20 +115,126 @@ canvas#curve {
   animation-name: timing-function-preview;
 }
 
 @keyframes timing-function-preview {
   0% {
     left: -7px;
   }
   33% {
-    left: 193px;
+    left: 143px;
   }
   50% {
-    left: 193px;
+    left: 143px;
   }
   83% {
     left: -7px;
   }
   100% {
     left: -7px;
   }
 }
+
+/* Preset Widget */
+
+.preset-pane {
+  width:50%;
+  height: 100%;
+  border-right: 1px solid var(--theme-splitter-color);
+}
+
+#preset-categories {
+  display: flex;
+  width: 94%;
+  border: 1px solid var(--theme-splitter-color);
+  border-radius: 2px;
+  background-color: var(--theme-toolbar-background);
+  margin-left: 4px;
+  margin-top: 3px;
+}
+
+#preset-categories .category:last-child {
+  border-right: none;
+}
+
+.category {
+  flex: 1 1 auto;
+  padding: 5px;
+  width: 33.33%;
+  text-align: center;
+  text-transform: capitalize;
+  border-right: 1px solid var(--theme-splitter-color);
+  cursor: default;
+  color: var(--theme-body-color);
+}
+
+.category:hover {
+  background-color: var(--theme-tab-toolbar-background);
+}
+
+.active-category {
+  background-color: var(--theme-selection-background);
+  color: var(--theme-selection-color);
+}
+
+.active-category:hover {
+  background-color: var(--theme-selection-background);
+}
+
+#preset-container {
+  padding: 0px;
+  width: 100%;
+  height: 331px;
+  overflow-y: scroll;
+}
+
+.preset-list {
+  display: none;
+}
+
+.active-preset-list {
+  display: flex;
+  flex-wrap: wrap;
+  justify-content: left;
+  padding-left: 4px;
+  padding-top: 3px;
+}
+
+.preset {
+  cursor: pointer;
+  width: 55px;
+  margin: 5px 11px 0px 0px;
+  text-transform: capitalize;
+  text-align: center;
+}
+
+.preset canvas {
+  display: block;
+  border: 1px solid #ccc;
+  border-radius: 3px;
+  background-color: var(--theme-body-background);
+}
+
+.theme-dark .preset canvas {
+  border-color: #444e58;
+}
+
+.preset p {
+  text-align: center;
+  font-size: 0.9em;
+  line-height: 0px;
+  margin: 2px 0px 0px 0p;
+  color: var(--theme-body-color-alt);
+}
+
+.active-preset p, .active-preset:hover p {
+  color: var(--theme-body-color);
+}
+
+.preset:hover canvas {
+  border-color: var(--theme-selection-background);
+}
+
+.active-preset canvas, .active-preset:hover canvas,
+.theme-dark .active-preset canvas, .theme-dark .preset:hover canvas {
+  background-color: var(--theme-selection-background-semitransparent);
+  border-color: var(--theme-selection-background);
+}
--- a/browser/devtools/webide/content/newapp.js
+++ b/browser/devtools/webide/content/newapp.js
@@ -10,36 +10,37 @@ Cu.import("resource://gre/modules/Servic
 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
 
 XPCOMUtils.defineLazyModuleGetter(this, "ZipUtils", "resource://gre/modules/ZipUtils.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "Downloads", "resource://gre/modules/Downloads.jsm");
 
 const {require} = Cu.import("resource://gre/modules/devtools/Loader.jsm", {}).devtools;
 const {FileUtils} = Cu.import("resource://gre/modules/FileUtils.jsm");
 const {AppProjects} = require("devtools/app-manager/app-projects");
-const APP_CREATOR_LIST = "devtools.webide.templatesURL";
 const {AppManager} = require("devtools/webide/app-manager");
-const {GetTemplatesJSON} = require("devtools/webide/remote-resources");
+const {getJSON} = require("devtools/shared/getjson");
+
+const TEMPLATES_URL = "devtools.webide.templatesURL";
 
 let gTemplateList = null;
 
 // See bug 989619
 console.log = console.log.bind(console);
 console.warn = console.warn.bind(console);
 console.error = console.error.bind(console);
 
 window.addEventListener("load", function onLoad() {
   window.removeEventListener("load", onLoad);
   let projectNameNode = document.querySelector("#project-name");
   projectNameNode.addEventListener("input", canValidate, true);
-  getJSON();
+  getTemplatesJSON();
 }, true);
 
-function getJSON() {
-  GetTemplatesJSON().then(list => {
+function getTemplatesJSON() {
+  getJSON(TEMPLATES_URL).then(list => {
     if (!Array.isArray(list)) {
       throw new Error("JSON response not an array");
     }
     if (list.length == 0) {
       throw new Error("JSON response is an empty array");
     }
     gTemplateList = list;
     let templatelistNode = document.querySelector("#templatelist");
--- a/browser/devtools/webide/content/webide.js
+++ b/browser/devtools/webide/content/webide.js
@@ -14,33 +14,34 @@ const {require} = devtools;
 const {Services} = Cu.import("resource://gre/modules/Services.jsm");
 const {AppProjects} = require("devtools/app-manager/app-projects");
 const {Connection} = require("devtools/client/connection-manager");
 const {AppManager} = require("devtools/webide/app-manager");
 const {Promise: promise} = Cu.import("resource://gre/modules/Promise.jsm", {});
 const ProjectEditor = require("projecteditor/projecteditor");
 const {Devices} = Cu.import("resource://gre/modules/devtools/Devices.jsm");
 const {GetAvailableAddons} = require("devtools/webide/addons");
-const {GetTemplatesJSON, GetAddonsJSON} = require("devtools/webide/remote-resources");
+const {getJSON} = require("devtools/shared/getjson");
 const utils = require("devtools/webide/utils");
 const Telemetry = require("devtools/shared/telemetry");
 const {RuntimeScanners, WiFiScanner} = require("devtools/webide/runtimes");
 const {showDoorhanger} = require("devtools/shared/doorhanger");
 const ProjectList = require("devtools/webide/project-list");
 
 const Strings = Services.strings.createBundle("chrome://browser/locale/devtools/webide.properties");
 
 const HTML = "http://www.w3.org/1999/xhtml";
 const HELP_URL = "https://developer.mozilla.org/docs/Tools/WebIDE/Troubleshooting";
 
 const MAX_ZOOM = 1.4;
 const MIN_ZOOM = 0.6;
 
-// download template index early
-GetTemplatesJSON(true);
+// Download remote resources early
+getJSON("devtools.webide.addonsURL", true);
+getJSON("devtools.webide.templatesURL", true);
 
 // See bug 989619
 console.log = console.log.bind(console);
 console.warn = console.warn.bind(console);
 console.error = console.error.bind(console);
 
 window.addEventListener("load", function onLoad() {
   window.removeEventListener("load", onLoad);
--- a/browser/devtools/webide/modules/addons.js
+++ b/browser/devtools/webide/modules/addons.js
@@ -1,18 +1,20 @@
 /* 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 {Cu} = require("chrome");
 const {Promise: promise} = Cu.import("resource://gre/modules/Promise.jsm");
 const {AddonManager} = Cu.import("resource://gre/modules/AddonManager.jsm");
-const {EventEmitter} = Cu.import("resource://gre/modules/devtools/event-emitter.js");
 const {Services} = Cu.import("resource://gre/modules/Services.jsm");
-const {GetAddonsJSON} = require("devtools/webide/remote-resources");
+const {getJSON} = require("devtools/shared/getjson");
+const EventEmitter = require("devtools/toolkit/event-emitter");
+
+const ADDONS_URL = "devtools.webide.addonsURL";
 
 let SIMULATOR_LINK = Services.prefs.getCharPref("devtools.webide.simulatorAddonsURL");
 let ADB_LINK = Services.prefs.getCharPref("devtools.webide.adbAddonURL");
 let ADAPTERS_LINK = Services.prefs.getCharPref("devtools.webide.adaptersAddonURL");
 let SIMULATOR_ADDON_ID = Services.prefs.getCharPref("devtools.webide.simulatorAddonID");
 let ADB_ADDON_ID = Services.prefs.getCharPref("devtools.webide.adbAddonID");
 let ADAPTERS_ADDON_ID = Services.prefs.getCharPref("devtools.webide.adaptersAddonID");
 
@@ -49,17 +51,17 @@ let GetAvailableAddons_promise = null;
 let GetAvailableAddons = exports.GetAvailableAddons = function() {
   if (!GetAvailableAddons_promise) {
     let deferred = promise.defer();
     GetAvailableAddons_promise = deferred.promise;
     let addons = {
       simulators: [],
       adb: null
     }
-    GetAddonsJSON(true).then(json => {
+    getJSON(ADDONS_URL, true).then(json => {
       for (let stability in json) {
         for (let version of json[stability]) {
           addons.simulators.push(new SimulatorAddon(stability, version));
         }
       }
       addons.adb = new ADBAddon();
       addons.adapters = new AdaptersAddon();
       deferred.resolve(addons);
--- a/browser/devtools/webide/moz.build
+++ b/browser/devtools/webide/moz.build
@@ -20,17 +20,16 @@ MOCHITEST_CHROME_MANIFESTS += [
 ]
 
 EXTRA_JS_MODULES.devtools.webide += [
     'modules/addons.js',
     'modules/app-manager.js',
     'modules/build.js',
     'modules/config-view.js',
     'modules/project-list.js',
-    'modules/remote-resources.js',
     'modules/runtimes.js',
     'modules/simulator-process.js',
     'modules/simulators.js',
     'modules/tab-store.js',
     'modules/utils.js'
 ]
 
 JS_PREFERENCE_FILES += [
--- a/browser/locales/en-US/chrome/browser/devtools/device.properties
+++ b/browser/locales/en-US/chrome/browser/devtools/device.properties
@@ -9,11 +9,12 @@
 # language in which you'd find the best documentation on web development on the
 # web.
 
 # LOCALIZATION NOTE:
 # These strings are category names in a list of devices that a user can choose
 # to simulate (e.g. "ZTE Open C", "VIA Vixen", "720p HD Television", etc).
 device.phones=Phones
 device.tablets=Tablets
-device.notebooks=Notebooks
+device.laptops=Laptops
 device.televisions=TVs
+device.consoles=Gaming consoles
 device.watches=Watches
--- a/browser/metro/profile/metro.js
+++ b/browser/metro/profile/metro.js
@@ -539,17 +539,17 @@ pref("editor.singleLine.pasteNewlines", 
 
 #ifdef MOZ_SERVICES_SYNC
 // sync service
 pref("services.sync.registerEngines", "Tab,Bookmarks,Form,History,Password,Prefs");
 
 // prefs to sync by default
 pref("services.sync.prefs.sync.browser.tabs.warnOnClose", true);
 pref("services.sync.prefs.sync.devtools.errorconsole.enabled", true);
-pref("services.sync.prefs.sync.lightweightThemes.isThemeSelected", true);
+pref("services.sync.prefs.sync.lightweightThemes.selectedThemeID", true);
 pref("services.sync.prefs.sync.lightweightThemes.usedThemes", true);
 pref("services.sync.prefs.sync.privacy.donottrackheader.enabled", true);
 pref("services.sync.prefs.sync.privacy.donottrackheader.value", true);
 pref("services.sync.prefs.sync.signon.rememberSignons", true);
 #endif
 
 // threshold where a tap becomes a drag, in 1/240" reference pixels
 // The names of the preferences are to be in sync with EventStateManager.cpp
--- a/browser/modules/ReaderParent.jsm
+++ b/browser/modules/ReaderParent.jsm
@@ -8,16 +8,17 @@
 const { classes: Cc, interfaces: Ci, utils: Cu } = Components;
 
 this.EXPORTED_SYMBOLS = [ "ReaderParent" ];
 
 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
 Cu.import("resource://gre/modules/Services.jsm");
 Cu.import("resource://gre/modules/Task.jsm");
 
+XPCOMUtils.defineLazyModuleGetter(this, "PlacesUtils","resource://gre/modules/PlacesUtils.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "ReaderMode", "resource://gre/modules/ReaderMode.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "ReadingList", "resource:///modules/readinglist/ReadingList.jsm");
 
 const gStringBundle = Services.strings.createBundle("chrome://global/locale/aboutReader.properties");
 
 let ReaderParent = {
 
   MESSAGES: [
@@ -51,17 +52,28 @@ let ReaderParent = {
           // Make sure the target browser is still alive before trying to send data back.
           if (message.target.messageManager) {
             message.target.messageManager.sendAsyncMessage("Reader:ArticleData", { article: article });
           }
         });
         break;
 
       case "Reader:FaviconRequest": {
-        // XXX: To implement.
+        if (message.target.messageManager) {
+          let faviconUrl = PlacesUtils.promiseFaviconLinkUrl(message.data.url);
+          faviconUrl.then(function onResolution(favicon) {
+            message.target.messageManager.sendAsyncMessage("Reader:FaviconReturn", {
+              url: message.data.url,
+              faviconUrl: favicon.path.replace(/^favicon:/, "")
+            })
+          },
+          function onRejection(reason) {
+            Cu.reportError("Error requesting favicon URL for about:reader content: " + reason);
+          }).catch(Cu.reportError);
+        }
         break;
       }
       case "Reader:ListStatusRequest":
         ReadingList.hasItemForURL(message.data.url).then(inList => {
           let mm = message.target.messageManager
           // Make sure the target browser is still alive before trying to send data back.
           if (mm) {
             mm.sendAsyncMessage("Reader:ListStatusData",
--- a/browser/themes/shared/readinglist/sidebar.inc.css
+++ b/browser/themes/shared/readinglist/sidebar.inc.css
@@ -45,23 +45,28 @@ body {
 .item-thumb-container {
   min-width: 64px;
   max-width: 64px;
   min-height: 40px;
   max-height: 40px;
   border: 1px solid white;
   box-shadow: 0px 1px 2px rgba(0,0,0,.35);
   margin: 5px;
-  background-color: #fff;
-  background-size: cover;
+  background-color: #ebebeb;
+  background-size: contain;
   background-repeat: no-repeat;
   background-position: center;
   background-image: url("chrome://branding/content/silhouette-40.svg");
 }
 
+.item-thumb-container.preview-available {
+  background-color: #fff;
+  background-size: cover;
+}
+
 .item-summary-container {
   display: flex;
   flex-flow: column;
   -moz-padding-start: 4px;
   overflow: hidden;
   flex-grow: 1;
 }
 
@@ -84,17 +89,17 @@ body {
   color: #0095DD;
 }
 
 .item:hover .item-domain {
   color: #008ACB;
 }
 
 .item:not(:hover):not(.selected) .remove-button {
-  display: none;
+  visibility: hidden;
 }
 
 .remove-button {
   padding: 0;
   width: 16px;
   height: 16px;
   background-size: contain;
   background-color: transparent;
--- a/mobile/android/base/TextSelection.java
+++ b/mobile/android/base/TextSelection.java
@@ -38,17 +38,17 @@ class TextSelection extends Layer implem
 
     private final TextSelectionHandle anchorHandle;
     private final TextSelectionHandle caretHandle;
     private final TextSelectionHandle focusHandle;
 
     private final DrawListener mDrawListener;
     private boolean mDraggingHandles;
 
-    private int selectionID; // Unique ID provided for each selection action.
+    private String selectionID; // Unique ID provided for each selection action.
     private float mViewLeft;
     private float mViewTop;
     private float mViewZoom;
 
     private String mCurrentItems;
 
     private TextSelectionActionModeCallback mCallback;
 
@@ -127,17 +127,17 @@ class TextSelection extends Layer implem
             return;
         }
 
         ThreadUtils.postToUiThread(new Runnable() {
             @Override
             public void run() {
                 try {
                     if (event.equals("TextSelection:ShowHandles")) {
-                        selectionID = message.getInt("selectionID");
+                        selectionID = message.getString("selectionID");
                         final JSONArray handles = message.getJSONArray("handles");
                         for (int i=0; i < handles.length(); i++) {
                             String handle = handles.getString(i);
                             getHandle(handle).setVisibility(View.VISIBLE);
                         }
 
                         mViewLeft = 0.0f;
                         mViewTop = 0.0f;
--- a/mobile/android/chrome/content/InputWidgetHelper.js
+++ b/mobile/android/chrome/content/InputWidgetHelper.js
@@ -8,17 +8,17 @@ var InputWidgetHelper = {
 
   handleEvent: function(aEvent) {
     this.handleClick(aEvent.target);
   },
 
   handleClick: function(aTarget) {
     // if we're busy looking at a InputWidget we want to eat any clicks that
     // come to us, but not to process them
-    if (this._uiBusy || !this.hasInputWidget(aTarget))
+    if (this._uiBusy || !this.hasInputWidget(aTarget) || this._isDisabledElement(aTarget))
       return;
 
     this._uiBusy = true;
     this.show(aTarget);
     this._uiBusy = false;
   },
 
   show: function(aElement) {
@@ -76,10 +76,21 @@ var InputWidgetHelper = {
   fireOnChange: function(aElement) {
     let evt = aElement.ownerDocument.createEvent("Events");
     evt.initEvent("change", true, true, aElement.defaultView, 0,
                   false, false,
                   false, false, null);
     setTimeout(function() {
       aElement.dispatchEvent(evt);
     }, 0);
+  },
+
+  _isDisabledElement : function(aElement) {
+    let currentElement = aElement;
+    while (currentElement) {
+      if (currentElement.disabled)
+	return true;
+
+      currentElement = currentElement.parentElement;
+    }
+    return false;
   }
 };
--- a/mobile/android/chrome/content/SelectionHandler.js
+++ b/mobile/android/chrome/content/SelectionHandler.js
@@ -40,17 +40,17 @@ var SelectionHandler = {
   // stored here are relative to the _contentWindow window.
   _cache: { anchorPt: {}, focusPt: {} },
   _targetIsRTL: false,
   _anchorIsRTL: false,
   _focusIsRTL: false,
 
   _activeType: 0, // TYPE_NONE
   _selectionPrivate: null, // private selection reference
-  _selectionID: 0, // Unique Selection ID
+  _selectionID: null, // Unique Selection ID
 
   _draggingHandles: false, // True while user drags text selection handles
   _dragStartAnchorOffset: null, // Editables need initial pos during HandleMove events
   _dragStartFocusOffset: null, // Editables need initial pos during HandleMove events
 
   _ignoreCompositionChanges: false, // Persist caret during IME composition updates
   _prevHandlePositions: [], // Avoid issuing duplicate "TextSelection:Position" messages
   _deferCloseTimer: null, // Used to defer _closeSelection() actions during programmatic changes
@@ -79,16 +79,23 @@ var SelectionHandler = {
     this._targetElementRef = Cu.getWeakReference(aTargetElement);
   },
 
   get _domWinUtils() {
     return BrowserApp.selectedBrowser.contentWindow.QueryInterface(Ci.nsIInterfaceRequestor).
                                                     getInterface(Ci.nsIDOMWindowUtils);
   },
 
+  // Provides UUID service for selection ID's.
+  get _idService() {
+    delete this._idService;
+    return this._idService = Cc["@mozilla.org/uuid-generator;1"].
+      getService(Ci.nsIUUIDGenerator);
+  },
+
   _addObservers: function sh_addObservers() {
     Services.obs.addObserver(this, "Gesture:SingleTap", false);
     Services.obs.addObserver(this, "Tab:Selected", false);
     Services.obs.addObserver(this, "TextSelection:Move", false);
     Services.obs.addObserver(this, "TextSelection:Position", false);
     Services.obs.addObserver(this, "TextSelection:End", false);
     Services.obs.addObserver(this, "TextSelection:Action", false);
     Services.obs.addObserver(this, "TextSelection:LayerReflow", false);
@@ -823,17 +830,17 @@ var SelectionHandler = {
         // Blur the targetElement to force IME code to undo previous style compositions
         // (visible underlines / etc generated by autoCorrection, autoSuggestion)
         aElement.blur();
       }
       // Ensure targetElement is now focused normally
       aElement.focus();
     }
 
-    this._selectionID++;
+    this._selectionID = this._idService.generateUUID().toString();
     this._stopDraggingHandles();
     this._contentWindow = aElement.ownerDocument.defaultView;
     this._targetIsRTL = (this._contentWindow.getComputedStyle(aElement, "").direction == "rtl");
 
     this._addObservers();
   },
 
   _getSelection: function sh_getSelection() {
--- a/mobile/android/chrome/content/browser.js
+++ b/mobile/android/chrome/content/browser.js
@@ -7743,17 +7743,17 @@ var Distribution = {
           case "undefined":
             defaults.setCharPref(key, value);
             break;
         }
       } catch (e) { /* ignore bad prefs and move on */ }
     }
 
     // Apply a lightweight theme if necessary
-    if (prefs && prefs["lightweightThemes.isThemeSelected"]) {
+    if (prefs && prefs["lightweightThemes.selectedThemeID"]) {
       Services.obs.notifyObservers(null, "lightweight-theme-apply", "");
     }
 
     let localizedString = Cc["@mozilla.org/pref-localizedstring;1"].createInstance(Ci.nsIPrefLocalizedString);
     let localizeablePrefs = aData["LocalizablePreferences"];
     for (let key in localizeablePrefs) {
       try {
         let value = localizeablePrefs[key];
--- a/modules/libpref/init/all.js
+++ b/modules/libpref/init/all.js
@@ -832,16 +832,19 @@ pref("devtools.discovery.log", false);
 pref("devtools.remote.wifi.scan", false);
 // Hide UI options for controlling device visibility over WiFi
 // N.B.: This does not set whether the device can be discovered via WiFi, only
 // whether the UI control to make such a choice is shown to the user
 pref("devtools.remote.wifi.visible", false);
 // Client must complete TLS handshake within this window (ms)
 pref("devtools.remote.tls-handshake-timeout", 10000);
 
+// URL of the remote JSON catalog used for device simulation
+pref("devtools.devices.url", "https://code.cdn.mozilla.net/devices/devices.json");
+
 // view source
 pref("view_source.syntax_highlight", true);
 pref("view_source.wrap_long_lines", false);
 pref("view_source.editor.external", false);
 pref("view_source.editor.path", "");
 // allows to add further arguments to the editor; use the %LINE% placeholder
 // for jumping to a specific line (e.g. "/line:%LINE%" or "--goto %LINE%")
 pref("view_source.editor.args", "");
@@ -4524,17 +4527,17 @@ pref("dom.mozSettings.SettingsService.ve
 pref("dom.mozSettings.allowForceReadOnly", false);
 
 // The interval at which to check for slow running addons
 #ifdef NIGHTLY_BUILD
 pref("browser.addon-watch.interval", 15000);
 #else
 pref("browser.addon-watch.interval", -1);
 #endif
-pref("browser.addon-watch.ignore", "[\"mochikit@mozilla.org\",\"special-powers@mozilla.org\"]");
+pref("browser.addon-watch.ignore", "[\"mochikit@mozilla.org\",\"special-powers@mozilla.org\",\"fxdevtools-adapters@mozilla.org\",\"fx-devtools\"]");
 // the percentage of time addons are allowed to use without being labeled slow
 pref("browser.addon-watch.percentage-limit", 5);
 
 // RequestSync API is disabled by default.
 pref("dom.requestSync.enabled", false);
 
 // Search service settings
 pref("browser.search.log", false);
--- a/services/sync/modules/engines/prefs.js
+++ b/services/sync/modules/engines/prefs.js
@@ -105,19 +105,18 @@ PrefStore.prototype = {
         // Missing prefs get the null value.
         values[pref] = this._prefs.get(pref, null);
       }
     }
     return values;
   },
 
   _setAllPrefs: function PrefStore__setAllPrefs(values) {
-    let enabledPref = "lightweightThemes.isThemeSelected";
-    let enabledBefore = this._prefs.get(enabledPref, false);
-    let prevTheme = LightweightThemeManager.currentTheme;
+    let selectedThemeIDPref = "lightweightThemes.selectedThemeID";
+    let selectedThemeIDBefore = this._prefs.get(selectedThemeIDPref, null);
 
     for (let [pref, value] in Iterator(values)) {
       if (!this._isSynced(pref))
         continue;
 
       // Pref has gone missing, best we can do is reset it.
       if (value == null) {
         this._prefs.reset(pref);
@@ -126,23 +125,24 @@ PrefStore.prototype = {
 
       try {
         this._prefs.set(pref, value);
       } catch(ex) {
         this._log.trace("Failed to set pref: " + pref + ": " + ex);
       } 
     }
 
-    // Notify the lightweight theme manager of all the new values
-    let enabledNow = this._prefs.get(enabledPref, false);
-    if (enabledBefore && !enabledNow) {
+    // Notify the lightweight theme manager if the selected theme has changed.
+    let selectedThemeIDAfter = this._prefs.get(selectedThemeIDPref, null);
+    if (selectedThemeIDBefore != selectedThemeIDAfter) {
+      // The currentTheme getter will reflect the theme with the new
+      // selectedThemeID (if there is one).  Just reset it to itself
+      let currentTheme = LightweightThemeManager.currentTheme;
       LightweightThemeManager.currentTheme = null;
-    } else if (enabledNow && LightweightThemeManager.usedThemes[0] != prevTheme) {
-      LightweightThemeManager.currentTheme = null;
-      LightweightThemeManager.currentTheme = LightweightThemeManager.usedThemes[0];
+      LightweightThemeManager.currentTheme = currentTheme;
     }
   },
 
   getAllIDs: function PrefStore_getAllIDs() {
     /* We store all prefs in just one WBO, with just one GUID */
     let allprefs = {};
     allprefs[PREFS_GUID] = true;
     return allprefs;
--- a/services/sync/tests/unit/test_prefs_store.js
+++ b/services/sync/tests/unit/test_prefs_store.js
@@ -90,38 +90,38 @@ function run_test() {
     do_check_eq(prefs.get("testing.deleteme"), undefined);
     do_check_eq(prefs.get("testing.dont.change"), "Please don't change me.");
     do_check_eq(Svc.Prefs.get("prefs.sync.testing.somepref"), true);
 
     _("Enable persona");
     // Ensure we don't go to the network to fetch personas and end up leaking
     // stuff.
     Services.io.offline = true;
-    do_check_false(!!prefs.get("lightweightThemes.isThemeSelected"));
+    do_check_false(!!prefs.get("lightweightThemes.selectedThemeID"));
     do_check_eq(LightweightThemeManager.currentTheme, null);
 
     let persona1 = makePersona();
     let persona2 = makePersona();
     let usedThemes = JSON.stringify([persona1, persona2]);
     record.value = {
-      "lightweightThemes.isThemeSelected": true,
+      "lightweightThemes.selectedThemeID": persona1.id,
       "lightweightThemes.usedThemes": usedThemes
     };
     store.update(record);
-    do_check_true(prefs.get("lightweightThemes.isThemeSelected"));
+    do_check_eq(prefs.get("lightweightThemes.selectedThemeID"), persona1.id);
     do_check_true(Utils.deepEquals(LightweightThemeManager.currentTheme,
                   persona1));
 
     _("Disable persona");
     record.value = {
-      "lightweightThemes.isThemeSelected": false,
+      "lightweightThemes.selectedThemeID": null,
       "lightweightThemes.usedThemes": usedThemes
     };
     store.update(record);
-    do_check_false(prefs.get("lightweightThemes.isThemeSelected"));
+    do_check_false(!!prefs.get("lightweightThemes.selectedThemeID"));
     do_check_eq(LightweightThemeManager.currentTheme, null);
 
     _("Only the current app's preferences are applied.");
     record = new PrefRec("prefs", "some-fake-app");
     record.value = {
       "testing.int": 98
     };
     store.update(record);
--- a/storage/src/mozStorageAsyncStatementParams.h
+++ b/storage/src/mozStorageAsyncStatementParams.h
@@ -11,16 +11,18 @@
 #include "nsIXPCScriptable.h"
 #include "mozilla/Attributes.h"
 
 class mozIStorageAsyncStatement;
 
 namespace mozilla {
 namespace storage {
 
+class AsyncStatement;
+
 /*
  * Since mozIStorageStatementParams is just a tagging interface we do not have
  * an async variant.
  */
 class AsyncStatementParams final : public mozIStorageStatementParams
                                      , public nsIXPCScriptable
 {
 public:
--- a/storage/src/mozStorageStatementJSHelper.h
+++ b/storage/src/mozStorageStatementJSHelper.h
@@ -3,16 +3,17 @@
  * 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/. */
 
 #ifndef MOZSTORAGESTATEMENTJSHELPER_H
 #define MOZSTORAGESTATEMENTJSHELPER_H
 
 #include "nsIXPCScriptable.h"
+#include "nsIXPConnect.h"
 
 class Statement;
 
 namespace mozilla {
 namespace storage {
 
 class StatementJSHelper : public nsIXPCScriptable
 {
--- a/storage/src/mozStorageStatementRow.h
+++ b/storage/src/mozStorageStatementRow.h
@@ -9,16 +9,18 @@
 
 #include "mozIStorageStatementRow.h"
 #include "nsIXPCScriptable.h"
 #include "mozilla/Attributes.h"
 
 namespace mozilla {
 namespace storage {
 
+class Statement;
+
 class StatementRow final : public mozIStorageStatementRow
                              , public nsIXPCScriptable
 {
 public:
   NS_DECL_ISUPPORTS
   NS_DECL_MOZISTORAGESTATEMENTROW
   NS_DECL_NSIXPCSCRIPTABLE
 
--- a/toolkit/components/autocomplete/nsAutoCompleteController.cpp
+++ b/toolkit/components/autocomplete/nsAutoCompleteController.cpp
@@ -727,35 +727,70 @@ nsAutoCompleteController::SetSearchStrin
 
 NS_IMETHODIMP
 nsAutoCompleteController::GetSearchString(nsAString &aSearchString)
 {
   aSearchString = mSearchString;
   return NS_OK;
 }
 
+void
+nsAutoCompleteController::HandleSearchResult(nsIAutoCompleteSearch *aSearch,
+                                             nsIAutoCompleteResult *aResult)
+{
+  // Look up the index of the search which is returning.
+  for (uint32_t i = 0; i < mSearches.Length(); ++i) {
+    if (mSearches[i] == aSearch) {
+      ProcessResult(i, aResult);
+    }
+  }
+}
+
 
 ////////////////////////////////////////////////////////////////////////
 //// nsIAutoCompleteObserver
 
 NS_IMETHODIMP
 nsAutoCompleteController::OnUpdateSearchResult(nsIAutoCompleteSearch *aSearch, nsIAutoCompleteResult* aResult)
 {
+  MOZ_ASSERT(mSearches.Contains(aSearch));
+
   ClearResults();
-  return OnSearchResult(aSearch, aResult);
+  HandleSearchResult(aSearch, aResult);
+  return NS_OK;
 }
 
 NS_IMETHODIMP
 nsAutoCompleteController::OnSearchResult(nsIAutoCompleteSearch *aSearch, nsIAutoCompleteResult* aResult)
 {
-  // look up the index of the search which is returning
-  for (uint32_t i = 0; i < mSearches.Length(); ++i) {
-    if (mSearches[i] == aSearch) {
-      ProcessResult(i, aResult);
-    }
+  MOZ_ASSERT(mSearchesOngoing > 0 && mSearches.Contains(aSearch));
+
+  // If this is the first search result we are processing
+  // we should clear out the previously cached results.
+  if (mFirstSearchResult) {
+    ClearResults();
+    mFirstSearchResult = false;
+  }
+
+  uint16_t result = 0;
+  if (aResult) {
+    aResult->GetSearchResult(&result);
+  }
+
+  // If our results are incremental, the search is still ongoing.
+  if (result != nsIAutoCompleteResult::RESULT_SUCCESS_ONGOING &&
+      result != nsIAutoCompleteResult::RESULT_NOMATCH_ONGOING) {
+    --mSearchesOngoing;
+  }
+
+  HandleSearchResult(aSearch, aResult);
+
+  if (mSearchesOngoing == 0) {
+    // If this is the last search to return, cleanup.
+    PostSearchCleanup();
   }
 
   return NS_OK;
 }
 
 ////////////////////////////////////////////////////////////////////////
 //// nsITimerCallback
 
@@ -1070,18 +1105,23 @@ nsAutoCompleteController::BeforeSearches
 }
 
 nsresult
 nsAutoCompleteController::StartSearch(uint16_t aSearchType)
 {
   NS_ENSURE_STATE(mInput);
   nsCOMPtr<nsIAutoCompleteInput> input = mInput;
 
-  for (uint32_t i = 0; i < mSearches.Length(); ++i) {
-    nsCOMPtr<nsIAutoCompleteSearch> search = mSearches[i];
+  // Iterate a copy of |mSearches| so that we don't run into trouble if the
+  // array is mutated while we're still in the loop. An nsIAutoCompleteSearch
+  // implementation could synchronously start a new search when StartSearch()
+  // is called and that would lead to assertions down the way.
+  nsCOMArray<nsIAutoCompleteSearch> searchesCopy(mSearches);
+  for (uint32_t i = 0; i < searchesCopy.Length(); ++i) {
+    nsCOMPtr<nsIAutoCompleteSearch> search = searchesCopy[i];
 
     // Filter on search type.  Not all the searches implement this interface,
     // in such a case just consider them delayed.
     uint16_t searchType = nsIAutoCompleteSearchDescriptor::SEARCH_TYPE_DELAYED;
     nsCOMPtr<nsIAutoCompleteSearchDescriptor> searchDesc =
       do_QueryInterface(search);
     if (searchDesc)
       searchDesc->GetSearchType(&searchType);
@@ -1102,16 +1142,17 @@ nsAutoCompleteController::StartSearch(ui
     nsAutoString searchParam;
     nsresult rv = input->GetSearchParam(searchParam);
     if (NS_FAILED(rv))
         return rv;
 
     rv = search->StartSearch(mSearchString, searchParam, result, static_cast<nsIAutoCompleteObserver *>(this));
     if (NS_FAILED(rv)) {
       ++mSearchesFailed;
+      MOZ_ASSERT(mSearchesOngoing > 0);
       --mSearchesOngoing;
     }
     // Because of the joy of nested event loops (which can easily happen when some
     // code uses a generator for an asynchronous AutoComplete search),
     // nsIAutoCompleteSearch::StartSearch might cause us to be detached from our input
     // field.  The next time we iterate, we'd be touching something that we shouldn't
     // be, and result in a crash.
     if (!mInput) {
@@ -1424,33 +1465,20 @@ nsAutoCompleteController::RevertTextValu
 }
 
 nsresult
 nsAutoCompleteController::ProcessResult(int32_t aSearchIndex, nsIAutoCompleteResult *aResult)
 {
   NS_ENSURE_STATE(mInput);
   nsCOMPtr<nsIAutoCompleteInput> input(mInput);
 
-  // If this is the first search result we are processing
-  // we should clear out the previously cached results
-  if (mFirstSearchResult) {
-    ClearResults();
-    mFirstSearchResult = false;
-  }
-
   uint16_t result = 0;
   if (aResult)
     aResult->GetSearchResult(&result);
 
-  // if our results are incremental, the search is still ongoing
-  if (result != nsIAutoCompleteResult::RESULT_SUCCESS_ONGOING &&
-      result != nsIAutoCompleteResult::RESULT_NOMATCH_ONGOING) {
-    --mSearchesOngoing;
-  }
-
   uint32_t oldMatchCount = 0;
   uint32_t matchCount = 0;
   if (aResult)
     aResult->GetMatchCount(&matchCount);
 
   int32_t resultIndex = mResults.IndexOf(aResult);
   if (resultIndex == -1) {
     // cache the result
@@ -1500,32 +1528,27 @@ nsAutoCompleteController::ProcessResult(
     uint32_t minResults;
     input->GetMinResultsForPopup(&minResults);
 
     // Make sure the popup is open, if necessary, since we now have at least one
     // search result ready to display. Don't force the popup closed if we might
     // get results in the future to avoid unnecessarily canceling searches.
     if (mRowCount || !minResults) {
       OpenPopup();
-    } else if (result != nsIAutoCompleteResult::RESULT_NOMATCH_ONGOING) {
+    } else if (mSearchesOngoing == 0) {
       ClosePopup();
     }
   }
 
   if (result == nsIAutoCompleteResult::RESULT_SUCCESS ||
       result == nsIAutoCompleteResult::RESULT_SUCCESS_ONGOING) {
     // Try to autocomplete the default index for this search.
     CompleteDefaultIndex(resultIndex);
   }
 
-  if (mSearchesOngoing == 0) {
-    // If this is the last search to return, cleanup.
-    PostSearchCleanup();
-  }
-
   return NS_OK;
 }
 
 nsresult
 nsAutoCompleteController::PostSearchCleanup()
 {
   NS_ENSURE_STATE(mInput);
   nsCOMPtr<nsIAutoCompleteInput> input(mInput);
--- a/toolkit/components/autocomplete/nsAutoCompleteController.h
+++ b/toolkit/components/autocomplete/nsAutoCompleteController.h
@@ -45,16 +45,18 @@ protected:
   nsresult StartSearch(uint16_t aSearchType);
 
   nsresult BeforeSearches();
   nsresult StartSearches();
   void AfterSearches();
   nsresult ClearSearchTimer();
   void MaybeCompletePlaceholder();
 
+  void HandleSearchResult(nsIAutoCompleteSearch *aSearch,
+                          nsIAutoCompleteResult *aResult);
   nsresult ProcessResult(int32_t aSearchIndex, nsIAutoCompleteResult *aResult);
   nsresult PostSearchCleanup();
 
   nsresult EnterMatch(bool aIsPopupSelection);
   nsresult RevertTextValue();
 
   nsresult CompleteDefaultIndex(int32_t aResultIndex);
   nsresult CompleteValue(nsString &aValue);
--- a/toolkit/components/passwordmgr/moz.build
+++ b/toolkit/components/passwordmgr/moz.build
@@ -38,17 +38,16 @@ EXTRA_PP_COMPONENTS += [
 EXTRA_PP_JS_MODULES += [
     'LoginManagerParent.jsm',
 ]
 
 EXTRA_JS_MODULES += [
     'InsecurePasswordUtils.jsm',
     'LoginHelper.jsm',
     'LoginManagerContent.jsm',
-    'LoginManagerParent.jsm',
     'LoginRecipes.jsm',
 ]
 
 if CONFIG['OS_TARGET'] == 'Android':
     EXTRA_COMPONENTS += [
         'storage-mozStorage.js',
     ]
 else:
--- a/toolkit/components/passwordmgr/nsLoginManagerPrompter.js
+++ b/toolkit/components/passwordmgr/nsLoginManagerPrompter.js
@@ -9,22 +9,20 @@ Cu.import("resource://gre/modules/XPCOMU
 Cu.import("resource://gre/modules/Services.jsm");
 Cu.import("resource://gre/modules/PrivateBrowsingUtils.jsm");
 Cu.import("resource://gre/modules/SharedPromptUtils.jsm");
 
 /* Constants for password prompt telemetry.
  * Mirrored in mobile/android/components/LoginManagerPrompter.js */
 const PROMPT_DISPLAYED = 0;
 
-const PROMPT_ADD = 1;
+const PROMPT_ADD_OR_UPDATE = 1;
 const PROMPT_NOTNOW = 2;
 const PROMPT_NEVER = 3;
 
-const PROMPT_UPDATE = 1;
-
 /*
  * LoginManagerPromptFactory
  *
  * Implements nsIPromptFactory
  *
  * Invoked by [toolkit/components/prompts/src/nsPrompter.js]
  */
 function LoginManagerPromptFactory() {
@@ -737,18 +735,16 @@ LoginManagerPrompter.prototype = {
 
 
   /*
    * promptToSavePassword
    *
    */
   promptToSavePassword : function (aLogin) {
     var notifyObj = this._getPopupNote() || this._getNotifyBox();
-    Services.telemetry.getHistogramById("PWMGR_PROMPT_REMEMBER_ACTION").add(PROMPT_DISPLAYED);
-
     if (notifyObj)
       this._showSaveLoginNotification(notifyObj, aLogin);
     else
       this._showSaveLoginDialog(aLogin);
   },
 
 
   /*
@@ -778,16 +774,110 @@ LoginManagerPrompter.prototype = {
     newBar.timeout = Date.now() + 20000; // 20 seconds
 
     if (oldBar) {
       this.log("(...and removing old " + aName + " notification bar)");
       aNotifyBox.removeNotification(oldBar);
     }
   },
 
+  /**
+   * Displays the PopupNotifications.jsm doorhanger for password save or change.
+   *
+   * @param {nsILoginInfo} login
+   *        Login to save or change. For changes, this login should contain the
+   *        new password.
+   * @param {string} type
+   *        This is "password-save" or "password-change" depending on the
+   *        original notification type. This is used for telemetry and tests.
+   */
+  _showLoginCaptureDoorhanger(login, type) {
+    let { browser } = this._getNotifyWindow();
+
+    let msgNames = type == "password-save" ? {
+      prompt: "rememberPasswordMsgNoUsername",
+      buttonLabel: "notifyBarRememberPasswordButtonText",
+      buttonAccessKey: "notifyBarRememberPasswordButtonAccessKey",
+    } : {
+      // We reuse the existing message, even if it expects a username, until we
+      // switch to the final terminology in bug 1144856.
+      prompt: "updatePasswordMsg",
+      buttonLabel: "notifyBarUpdateButtonText",
+      buttonAccessKey: "notifyBarUpdateButtonAccessKey",
+    };
+
+    let histogramName = type == "password-save" ? "PWMGR_PROMPT_REMEMBER_ACTION"
+                                                : "PWMGR_PROMPT_UPDATE_ACTION";
+    let histogram = Services.telemetry.getHistogramById(histogramName);
+    histogram.add(PROMPT_DISPLAYED);
+
+    // The main action is the "Remember" or "Update" button.
+    let mainAction = {
+      label: this._getLocalizedString(msgNames.buttonLabel),
+      accessKey: this._getLocalizedString(msgNames.buttonAccessKey),
+      callback: () => {
+        histogram.add(PROMPT_ADD_OR_UPDATE);
+        let foundLogins = Services.logins.findLogins({}, login.hostname,
+                                                     login.formSubmitURL,
+                                                     login.httpRealm);
+        let logins = foundLogins.filter(l => l.username == login.username);
+        if (logins.length == 0) {
+          Services.logins.addLogin(login);
+        } else if (logins.length == 1) {
+          this._updateLogin(logins[0], login.password);
+        } else {
+          Cu.reportError("Unexpected match of multiple logins.");
+        }
+        browser.focus();
+      }
+    };
+
+    // Include a "Never for this site" button when saving a new password.
+    let secondaryActions = type == "password-save" ? [{
+      label: this._getLocalizedString("notifyBarNeverRememberButtonText"),
+      accessKey: this._getLocalizedString("notifyBarNeverRememberButtonAccessKey"),
+      callback: () => {
+        histogram.add(PROMPT_NEVER);
+        Services.logins.setLoginSavingEnabled(login.hostname, false);
+        browser.focus();
+      }
+    }] : null;
+
+    let usernamePlaceholder = this._getLocalizedString("noUsernamePlaceholder");
+    let displayHost = this._getShortDisplayHost(login.hostname);
+
+    this._getPopupNote().show(
+      browser,
+      "password",
+      this._getLocalizedString(msgNames.prompt, [displayHost]),
+      "password-notification-icon",
+      mainAction,
+      secondaryActions,
+      {
+        timeout: Date.now() + 10000,
+        persistWhileVisible: true,
+        passwordNotificationType: type,
+        eventCallback: function (topic) {
+          if (topic != "showing") {
+            return false;
+          }
+
+          let chromeDoc = this.browser.ownerDocument;
+
+          chromeDoc.getElementById("password-notification-username")
+                   .setAttribute("placeholder", usernamePlaceholder);
+          chromeDoc.getElementById("password-notification-username")
+                   .setAttribute("value", login.username);
+          chromeDoc.getElementById("password-notification-password")
+                   .setAttribute("value", login.password);
+        },
+      }
+    );
+  },
+
   /*
    * _showSaveLoginNotification
    *
    * Displays a notification bar or a popup notification, to allow the user
    * to save the specified login. This allows the user to see the results of
    * their login, and only save a login which they know worked.
    *
    * @param aNotifyObj
@@ -801,80 +891,30 @@ LoginManagerPrompter.prototype = {
     var neverButtonText =
           this._getLocalizedString("notifyBarNeverRememberButtonText");
     var neverButtonAccessKey =
           this._getLocalizedString("notifyBarNeverRememberButtonAccessKey");
     var rememberButtonText =
           this._getLocalizedString("notifyBarRememberPasswordButtonText");
     var rememberButtonAccessKey =
           this._getLocalizedString("notifyBarRememberPasswordButtonAccessKey");
-    var usernamePlaceholder =
-          this._getLocalizedString("noUsernamePlaceholder");
 
     var displayHost = this._getShortDisplayHost(aLogin.hostname);
     var notificationText = this._getLocalizedString(
                                   "rememberPasswordMsgNoUsername",
                                   [displayHost]);
 
     // The callbacks in |buttons| have a closure to access the variables
     // in scope here; set one to |this._pwmgr| so we can get back to pwmgr
     // without a getService() call.
     var pwmgr = this._pwmgr;
-    let promptHistogram = Services.telemetry.getHistogramById("PWMGR_PROMPT_REMEMBER_ACTION");
 
     // Notification is a PopupNotification
     if (aNotifyObj == this._getPopupNote()) {
-      // "Remember" button
-      var mainAction = {
-        label:     rememberButtonText,
-        accessKey: rememberButtonAccessKey,
-        callback: function(aNotifyObj, aButton) {
-          promptHistogram.add(PROMPT_ADD);
-          pwmgr.addLogin(aLogin);
-          browser.focus();
-        }
-      };
-
-      var secondaryActions = [
-        // "Never for this site" button
-        {
-          label:     neverButtonText,
-          accessKey: neverButtonAccessKey,
-          callback: function(aNotifyObj, aButton) {
-            promptHistogram.add(PROMPT_NEVER);
-            pwmgr.setLoginSavingEnabled(aLogin.hostname, false);
-            browser.focus();
-          }
-        }
-      ];
-
-      var { browser } = this._getNotifyWindow();
-
-      let eventCallback = function (topic) {
-        if (topic != "showing") {
-          return false;
-        }
-
-        let chromeDoc = this.browser.ownerDocument;
-
-        chromeDoc.getElementById("password-notification-username")
-                 .setAttribute("placeholder", usernamePlaceholder);
-        chromeDoc.getElementById("password-notification-username")
-                 .setAttribute("value", aLogin.username);
-        chromeDoc.getElementById("password-notification-password")
-                 .setAttribute("value", aLogin.password);
-      };
-
-      aNotifyObj.show(browser, "password", notificationText,
-                      "password-notification-icon", mainAction,
-                      secondaryActions,
-                      { timeout: Date.now() + 10000,
-                        persistWhileVisible: true,
-                        passwordNotificationType: "password-save",
-                        eventCallback });
+      this._showLoginCaptureDoorhanger(aLogin, "password-save");
     } else {
       var notNowButtonText =
             this._getLocalizedString("notifyBarNotNowButtonText");
       var notNowButtonAccessKey =
             this._getLocalizedString("notifyBarNotNowButtonAccessKey");
       var buttons = [
         // "Remember" button
         {
@@ -1025,68 +1065,32 @@ LoginManagerPrompter.prototype = {
    * @param aNotifyObj
    *        A notification box or a popup notification.
    */
   _showChangeLoginNotification : function (aNotifyObj, aOldLogin, aNewPassword) {
     var changeButtonText =
           this._getLocalizedString("notifyBarUpdateButtonText");
     var changeButtonAccessKey =
           this._getLocalizedString("notifyBarUpdateButtonAccessKey");
-    var usernamePlaceholder =
-          this._getLocalizedString("noUsernamePlaceholder");
 
     // We reuse the existing message, even if it expects a username, until we
     // switch to the final terminology in bug 1144856.
     var displayHost = this._getShortDisplayHost(aOldLogin.hostname);
     var notificationText = this._getLocalizedString("updatePasswordMsg",
                                                     [displayHost]);
 
     // The callbacks in |buttons| have a closure to access the variables
     // in scope here; set one to |this._pwmgr| so we can get back to pwmgr
     // without a getService() call.
     var self = this;
 
-    let promptHistogram = Services.telemetry.getHistogramById("PWMGR_PROMPT_UPDATE_ACTION");
     // Notification is a PopupNotification
     if (aNotifyObj == this._getPopupNote()) {
-      // "Yes" button
-      var mainAction = {
-        label:     changeButtonText,
-        accessKey: changeButtonAccessKey,
-        popup:     null,
-        callback:  function(aNotifyObj, aButton) {
-          self._updateLogin(aOldLogin, aNewPassword);
-          promptHistogram.add(PROMPT_UPDATE);
-        }
-      };
-
-      var { browser } = this._getNotifyWindow();
-
-      let eventCallback = function (topic) {
-        if (topic != "showing") {
-          return false;
-        }
-
-        let chromeDoc = this.browser.ownerDocument;
-
-        chromeDoc.getElementById("password-notification-username")
-                 .setAttribute("placeholder", usernamePlaceholder);
-        chromeDoc.getElementById("password-notification-username")
-                 .setAttribute("value", aOldLogin.username);
-        chromeDoc.getElementById("password-notification-password")
-                 .setAttribute("value", aNewPassword);
-      };
-
-      Services.telemetry.getHistogramById("PWMGR_PROMPT_UPDATE_ACTION").add(PROMPT_DISPLAYED);
-      aNotifyObj.show(browser, "password", notificationText,
-                      "password-notification-icon", mainAction,
-                      null, { timeout: Date.now() + 10000,
-                              persistWhileVisible: true,
-                              passwordNotificationType: "password-change",
-                              eventCallback });
+      aOldLogin.password = aNewPassword;
+      this._showLoginCaptureDoorhanger(aOldLogin, "password-change");
     } else {
       var dontChangeButtonText =
             this._getLocalizedString("notifyBarDontChangeButtonText");
       var dontChangeButtonAccessKey =
             this._getLocalizedString("notifyBarDontChangeButtonAccessKey");
       var buttons = [
         // "Yes" button
         {
--- a/toolkit/components/places/PlacesUtils.jsm
+++ b/toolkit/components/places/PlacesUtils.jsm
@@ -1557,17 +1557,17 @@ this.PlacesUtils = {
     if (!(aPageUrl instanceof Ci.nsIURI))
       aPageUrl = NetUtil.newURI(aPageUrl);
 
     PlacesUtils.favicons.getFaviconURLForPage(aPageUrl, uri => {
       if (uri) {
         uri = PlacesUtils.favicons.getFaviconLinkForIcon(uri);
         deferred.resolve(uri);
       } else {
-        deferred.reject();
+        deferred.reject("favicon not found for uri");
       }
     });
     return deferred.promise;
   },
 
   /**
    * Returns the passed URL with a #-moz-resolution fragment
    * for the specified dimensions and devicePixelRatio.
--- a/toolkit/components/reader/Readability.js
+++ b/toolkit/components/reader/Readability.js
@@ -98,17 +98,17 @@ Readability.prototype = {
     unlikelyCandidates: /combx|comment|community|disqus|extra|foot|header|menu|remark|rss|shoutbox|sidebar|sponsor|ad-break|agegate|pagination|pager|popup|tweet|twitter/i,
     okMaybeItsACandidate: /and|article|body|column|main|shadow/i,
     positive: /article|body|content|entry|hentry|main|page|pagination|post|text|blog|story/i,
     negative: /hidden|combx|comment|com-|contact|foot|footer|footnote|masthead|media|meta|outbrain|promo|related|scroll|shoutbox|sidebar|sponsor|shopping|tags|tool|widget/i,
     extraneous: /print|archive|comment|discuss|e[\-]?mail|share|reply|all|login|sign|single|utility/i,
     byline: /byline|author|dateline|writtenby/i,
     replaceFonts: /<(\/?)font[^>]*>/gi,
     normalize: /\s{2,}/g,
-    videos: /https?:\/\/(www\.)?(youtube|vimeo)\.com/i,
+    videos: /https?:\/\/(www\.)?(youtube|youtube-nocookie|player\.vimeo)\.com/i,
     nextLink: /(next|weiter|continue|>([^\|]|$)|»([^\|]|$))/i,
     prevLink: /(prev|earl|old|new|<|«)/i,
     whitespace: /^\s*$/,
     hasContent: /\S$/,
   },
 
   DIV_TO_P_ELEMS: [ "A", "BLOCKQUOTE", "DL", "DIV", "IMG", "OL", "P", "PRE", "TABLE", "UL", "SELECT" ],
 
@@ -121,16 +121,46 @@ Readability.prototype = {
    * @return void
   **/
   _postProcessContent: function(articleContent) {
     // Readability cannot open relative uris so we convert them to absolute uris.
     this._fixRelativeUris(articleContent);
   },
 
   /**
+   * Iterate over a NodeList, which doesn't natively fully implement the Array
+   * interface.
+   *
+   * For convenience, the current object context is applied to the provided
+   * iterate function.
+   *
+   * @param  NodeList nodeList The NodeList.
+   * @param  Function fn       The iterate function.
+   * @return void
+   */
+  _forEachNode: function(nodeList, fn) {
+    return Array.prototype.forEach.call(nodeList, fn, this);
+  },
+
+  /**
+   * Iterate over a NodeList, return true if any of the provided iterate
+   * function calls returns true, false otherwise.
+   *
+   * For convenience, the current object context is applied to the
+   * provided iterate function.
+   *
+   * @param  NodeList nodeList The NodeList.
+   * @param  Function fn       The iterate function.
+   * @return Boolean
+   */
+  _someNode: function(nodeList, fn) {
+    return Array.prototype.some.call(nodeList, fn, this);
+  },
+
+  /**
    * Converts each <a> and <img> uri in the given element to an absolute URI.
    *
    * @param Element
    * @return void
    */
   _fixRelativeUris: function(articleContent) {
     var scheme = this._uri.scheme;
     var prePath = this._uri.prePath;
@@ -144,36 +174,39 @@ Readability.prototype = {
       // Scheme-rooted relative URI.
       if (uri.substr(0, 2) == "//")
         return scheme + "://" + uri.substr(2);
 
       // Prepath-rooted relative URI.
       if (uri[0] == "/")
         return prePath + uri;
 
+      // Dotslash relative URI.
+      if (uri.indexOf("./") === 0)
+        return pathBase + uri.slice(2);
+
       // Standard relative URI; add entire path. pathBase already includes a
       // trailing "/".
       return pathBase + uri;
     }
 
     function convertRelativeURIs(tagName, propName) {
       var elems = articleContent.getElementsByTagName(tagName);
-      for (var i = elems.length; --i >= 0;) {
-        var elem = elems[i];
+      this._forEachNode(elems, function(elem) {
         var relativeURI = elem.getAttribute(propName);
         if (relativeURI != null)
-          elems[i].setAttribute(propName, toAbsoluteURI(relativeURI));
-      }
+          elem.setAttribute(propName, toAbsoluteURI(relativeURI));
+      });
     }
 
      // Fix links.
-    convertRelativeURIs("a", "href");
+    convertRelativeURIs.call(this, "a", "href");
 
      // Fix images.
-    convertRelativeURIs("img", "src");
+    convertRelativeURIs.call(this, "img", "src");
   },
 
   /**
    * Get the article title as an H1.
    *
    * @return void
    **/
   _getArticleTitle: function() {
@@ -219,29 +252,27 @@ Readability.prototype = {
    * This includes things like stripping javascript, CSS, and handling terrible markup.
    *
    * @return void
    **/
   _prepDocument: function() {
     var doc = this._doc;
 
     // Remove all style tags in head
-    var styleTags = doc.getElementsByTagName("style");
-    for (var st = styleTags.length - 1; st >= 0; st -= 1) {
-      styleTags[st].parentNode.removeChild(styleTags[st]);
-    }
+    this._forEachNode(doc.getElementsByTagName("style"), function(styleNode) {
+      styleNode.parentNode.removeChild(styleNode);
+    });
 
     if (doc.body) {
       this._replaceBrs(doc.body);
     }
 
-    var fonts = doc.getElementsByTagName("FONT");
-    for (var i = fonts.length; --i >=0;) {
-      this._setNodeTag(fonts[i], "SPAN");
-    }
+    this._forEachNode(doc.getElementsByTagName("font"), function(fontNode) {
+      this._setNodeTag(fontNode, "SPAN");
+    });
   },
 
   /**
    * Finds the next element, starting from the given node, and ignoring
    * whitespace in between. If the given node is an element, the same node is
    * returned.
    */
   _nextElement: function (node) {
@@ -257,19 +288,17 @@ Readability.prototype = {
   /**
    * Replaces 2 or more successive <br> elements with a single <p>.
    * Whitespace between <br> elements are ignored. For example:
    *   <div>foo<br>bar<br> <br><br>abc</div>
    * will become:
    *   <div>foo<br>bar<p>abc</p></div>
    */
   _replaceBrs: function (elem) {
-    var brs = elem.getElementsByTagName("br");
-    for (var i = 0; i < brs.length; i++) {
-      var br = brs[i];
+    this._forEachNode(elem.getElementsByTagName("br"), function(br) {
       var next = br.nextSibling;
 
       // Whether 2 or more <br> elements have been found and replaced with a
       // <p> block.
       var replaced = false;
 
       // If we find a <br> chain, remove the <br>s until we hit another element
       // or non-whitespace. This leaves behind the first <br> in the chain
@@ -298,17 +327,17 @@ Readability.prototype = {
           }
 
           // Otherwise, make this node a child of the new <p>.
           var sibling = next.nextSibling;
           p.appendChild(next);
           next = sibling;
         }
       }
-    }
+    });
   },
 
   _setNodeTag: function (node, tag) {
     // FIXME this doesn't work on anything but JSDOMParser (ie the node's tag
     // won't actually be set).
     node.localName = tag.toLowerCase();
     node.tagName = tag.toUpperCase();
   },
@@ -321,16 +350,17 @@ Readability.prototype = {
    * @return void
    **/
   _prepArticle: function(articleContent) {
     this._cleanStyles(articleContent);
 
     // Clean out junk from the article content
     this._cleanConditionally(articleContent, "form");
     this._clean(articleContent, "object");
+    this._clean(articleContent, "embed");
     this._clean(articleContent, "h1");
 
     // If there is only one h2, they are probably using it as a header
     // and not a subheader, so remove it since we already have a header.
     if (articleContent.getElementsByTagName('h2').length === 1)
       this._clean(articleContent, "h2");
 
     this._clean(articleContent, "iframe");
@@ -338,36 +368,33 @@ Readability.prototype = {
 
     // Do these last as the previous stuff may have removed junk
     // that will affect these
     this._cleanConditionally(articleContent, "table");
     this._cleanConditionally(articleContent, "ul");
     this._cleanConditionally(articleContent, "div");
 
     // Remove extra paragraphs
-    var articleParagraphs = articleContent.getElementsByTagName('p');
-    for (var i = articleParagraphs.length - 1; i >= 0; i -= 1) {
-      var imgCount = articleParagraphs[i].getElementsByTagName('img').length;
-      var embedCount = articleParagraphs[i].getElementsByTagName('embed').length;
-      var objectCount = articleParagraphs[i].getElementsByTagName('object').length;
+    this._forEachNode(articleContent.getElementsByTagName('p'), function(paragraph) {
+      var imgCount = paragraph.getElementsByTagName('img').length;
+      var embedCount = paragraph.getElementsByTagName('embed').length;
+      var objectCount = paragraph.getElementsByTagName('object').length;
+      // At this point, nasty iframes have been removed, only remain embedded video ones.
+      var iframeCount = paragraph.getElementsByTagName('iframe').length;
+      var totalCount = imgCount + embedCount + objectCount + iframeCount;
 
-      if (imgCount === 0 &&
-        embedCount === 0 &&
-        objectCount === 0 &&
-        this._getInnerText(articleParagraphs[i], false) === '')
-        articleParagraphs[i].parentNode.removeChild(articleParagraphs[i]);
-    }
+      if (totalCount === 0 && !this._getInnerText(paragraph, false))
+        paragraph.parentNode.removeChild(paragraph);
+    });
 
-    var brs = articleContent.getElementsByTagName("BR");
-    for (var i = brs.length; --i >= 0;) {
-      var br = brs[i];
+    this._forEachNode(articleContent.getElementsByTagName("br"), function(br) {
       var next = this._nextElement(br.nextSibling);
       if (next && next.tagName == "P")
         br.parentNode.removeChild(br);
-    }
+    });
   },
 
   /**
    * Initialize a node with the readability object. Also checks the
    * className/id for special names to add to its score.
    *
    * @param Element
    * @return void
@@ -524,49 +551,48 @@ Readability.prototype = {
             var newNode = node.firstElementChild;
             node.parentNode.replaceChild(newNode, node);
             node = newNode;
           } else if (!this._hasChildBlockElement(node)) {
             this._setNodeTag(node, "P");
             elementsToScore.push(node);
           } else {
             // EXPERIMENTAL
-            for (var i = 0, il = node.childNodes.length; i < il; i += 1) {
-              var childNode = node.childNodes[i];
+            this._forEachNode(node.childNodes, function(childNode) {
               if (childNode.nodeType === Node.TEXT_NODE) {
                 var p = doc.createElement('p');
                 p.textContent = childNode.textContent;
                 p.style.display = 'inline';
                 p.className = 'readability-styled';
                 node.replaceChild(p, childNode);
               }
-            }
+            });
           }
         }
         node = this._getNextNode(node);
       }
 
       /**
        * Loop through all paragraphs, and assign a score to them based on how content-y they look.
        * Then add their score to their parent node.
        *
        * A score is determined by things like number of commas, class names, etc. Maybe eventually link density.
       **/
       var candidates = [];
-      for (var pt = 0; pt < elementsToScore.length; pt += 1) {
-        var parentNode = elementsToScore[pt].parentNode;
+      this._forEachNode(elementsToScore, function(elementToScore) {
+        var parentNode = elementToScore.parentNode;
         var grandParentNode = parentNode ? parentNode.parentNode : null;
-        var innerText = this._getInnerText(elementsToScore[pt]);
+        var innerText = this._getInnerText(elementToScore);
 
         if (!parentNode || typeof(parentNode.tagName) === 'undefined')
-          continue;
+          return;
 
         // If this paragraph is less than 25 characters, don't even count it.
         if (innerText.length < 25)
-          continue;
+          return;
 
         // Initialize readability data for the parent.
         if (typeof parentNode.readability === 'undefined') {
           this._initializeNode(parentNode);
           candidates.push(parentNode);
         }
 
         // Initialize readability data for the grandparent.
@@ -588,17 +614,17 @@ Readability.prototype = {
         // For every 100 characters in this paragraph, add another point. Up to 3 points.
         contentScore += Math.min(Math.floor(innerText.length / 100), 3);
 
         // Add the score to the parent. The grandparent gets half.
         parentNode.readability.contentScore += contentScore;
 
         if (grandParentNode)
           grandParentNode.readability.contentScore += contentScore / 2;
-      }
+      });
 
       // After we've calculated scores, loop through all of the possible
       // candidate nodes we found and find the one with the highest score.
       var topCandidates = [];
       for (var c = 0, cl = candidates.length; c < cl; c += 1) {
         var candidate = candidates[c];
 
         // Scale the final candidates score based on link density. Good content
@@ -645,28 +671,29 @@ Readability.prototype = {
         // Because of our bonus system, parents of candidates might have scores
         // themselves. They get half of the node. There won't be nodes with higher
         // scores than our topCandidate, but if we see the score going *up* in the first
         // few steps up the tree, that's a decent sign that there might be more content
         // lurking in other places that we want to unify in. The sibling stuff
         // below does some of that - but only if we've looked high enough up the DOM
         // tree.
         var parentOfTopCandidate = topCandidate.parentNode;
+        var lastScore = topCandidate.readability.contentScore;
         // The scores shouldn't get too low.
-        var scoreThreshold = topCandidate.readability.contentScore / 3;
-        var lastScore = parentOfTopCandidate.readability.contentScore;
+        var scoreThreshold = lastScore / 3;
         while (parentOfTopCandidate && parentOfTopCandidate.readability) {
           var parentScore = parentOfTopCandidate.readability.contentScore;
           if (parentScore < scoreThreshold)
             break;
           if (parentScore > lastScore) {
             // Alright! We found a better parent to use.
             topCandidate = parentOfTopCandidate;
             break;
           }
+          lastScore = parentOfTopCandidate.readability.contentScore;
           parentOfTopCandidate = parentOfTopCandidate.parentNode;
         }
       }
 
       // Now that we have the top candidate, look through its siblings for content
       // that might also be related. Things like preambles, content split by ads
       // that we removed, etc.
       var articleContent = doc.createElement("DIV");
@@ -799,40 +826,39 @@ Readability.prototype = {
       byline = byline.trim();
       return (byline.length > 0) && (byline.length < 100);
     }
     return false;
   },
 
   /**
    * Attempts to get excerpt and byline metadata for the article.
-   * 
+   *
    * @return Object with optional "excerpt" and "byline" properties
    */
   _getArticleMetadata: function() {
     var metadata = {};
     var values = {};
     var metaElements = this._doc.getElementsByTagName("meta");
 
     // Match "description", or Twitter's "twitter:description" (Cards)
     // in name attribute.
     var namePattern = /^\s*((twitter)\s*:\s*)?description\s*$/gi;
 
     // Match Facebook's og:description (Open Graph) in property attribute.
     var propertyPattern = /^\s*og\s*:\s*description\s*$/gi;
 
     // Find description tags.
-    for (var i = 0; i < metaElements.length; i++) {
-      var element = metaElements[i];
+    this._forEachNode(metaElements, function(element) {
       var elementName = element.getAttribute("name");
       var elementProperty = element.getAttribute("property");
 
       if (elementName === "author") {
         metadata.byline = element.getAttribute("content");
-        continue;
+        return;
       }
 
       var name = null;
       if (namePattern.test(elementName)) {
         name = elementName;
       } else if (propertyPattern.test(elementProperty)) {
         name = elementProperty;
       }
@@ -841,17 +867,17 @@ Readability.prototype = {
         var content = element.getAttribute("content");
         if (content) {
           // Convert to lowercase and remove any whitespace
           // so we can match below.
           name = name.toLowerCase().replace(/\s/g, '');
           values[name] = content.trim();
         }
       }
-    }
+    });
 
     if ("description" in values) {
       metadata.excerpt = values["description"];
     } else if ("og:description" in values) {
       // Use facebook open graph description.
       metadata.excerpt = values["og:description"];
     } else if ("twitter:description" in values) {
       // Use twitter cards description.
@@ -862,76 +888,68 @@ Readability.prototype = {
   },
 
   /**
    * Removes script tags from the document.
    *
    * @param Element
   **/
   _removeScripts: function(doc) {
-    var scripts = doc.getElementsByTagName('script');
-    for (var i = scripts.length - 1; i >= 0; i -= 1) {
-      scripts[i].nodeValue="";
-      scripts[i].removeAttribute('src');
+    this._forEachNode(doc.getElementsByTagName('script'), function(scriptNode) {
+      scriptNode.nodeValue = "";
+      scriptNode.removeAttribute('src');
 
-      if (scripts[i].parentNode)
-          scripts[i].parentNode.removeChild(scripts[i]);
-    }
+      if (scriptNode.parentNode)
+        scriptNode.parentNode.removeChild(scriptNode);
+    });
   },
 
   /**
    * Check if this node has only whitespace and a single P element
    * Returns false if the DIV node contains non-empty text nodes
    * or if it contains no P or more than 1 element.
    *
    * @param Element
   **/
-  _hasSinglePInsideElement: function(e) {
+  _hasSinglePInsideElement: function(element) {
     // There should be exactly 1 element child which is a P:
-    if (e.children.length != 1 || e.firstElementChild.tagName !== "P") {
+    if (element.children.length != 1 || element.firstElementChild.tagName !== "P") {
       return false;
     }
+
     // And there should be no text nodes with real content
-    var childNodes = e.childNodes;
-    for (var i = childNodes.length; --i >= 0;) {
-      var node = childNodes[i];
-      if (node.nodeType == Node.TEXT_NODE &&
-          this.REGEXPS.hasContent.test(node.textContent)) {
-        return false;
-      }
-    }
-
-    return true;
+    return !this._someNode(element.childNodes, function(node) {
+      return node.nodeType === Node.TEXT_NODE &&
+             this.REGEXPS.hasContent.test(node.textContent);
+    });
   },
 
   /**
    * Determine whether element has any children block level elements.
    *
    * @param Element
    */
-  _hasChildBlockElement: function (e) {
-    var length = e.children.length;
-    for (var i = 0; i < length; i++) {
-      var child = e.children[i];
-      if (this.DIV_TO_P_ELEMS.indexOf(child.tagName) !== -1 || this._hasChildBlockElement(child))
-        return true;
-    }
-    return false;
+  _hasChildBlockElement: function (element) {
+    return this._someNode(element.childNodes, function(node) {
+      return this.DIV_TO_P_ELEMS.indexOf(node.tagName) !== -1 ||
+             this._hasChildBlockElement(node);
+    });
   },
 
   /**
    * Get the inner text of a node - cross browser compatibly.
    * This also strips out any excess whitespace to be found.
    *
    * @param Element
+   * @param Boolean normalizeSpaces (default: true)
    * @return string
   **/
   _getInnerText: function(e, normalizeSpaces) {
+    normalizeSpaces = (typeof normalizeSpaces === 'undefined') ? true : normalizeSpaces;
     var textContent = e.textContent.trim();
-    normalizeSpaces = (typeof normalizeSpaces === 'undefined') ? true : normalizeSpaces;
 
     if (normalizeSpaces) {
       return textContent.replace(this.REGEXPS.normalize, " ");
     } else {
       return textContent;
     }
   },
 
@@ -980,24 +998,27 @@ Readability.prototype = {
 
   /**
    * Get the density of links as a percentage of the content
    * This is the amount of text that is inside a link divided by the total text in the node.
    *
    * @param Element
    * @return number (float)
   **/
-  _getLinkDensity: function(e) {
-    var links = e.getElementsByTagName("a");
-    var textLength = this._getInnerText(e).length;
+  _getLinkDensity: function(element) {
+    var textLength = this._getInnerText(element).length;
+    if (textLength === 0)
+      return;
+
     var linkLength = 0;
 
-    for (var i = 0, il = links.length; i < il; i += 1) {
-      linkLength += this._getInnerText(links[i]).length;
-    }
+    // XXX implement _reduceNodeList?
+    this._forEachNode(element.getElementsByTagName("a"), function(linkNode) {
+      linkLength += this._getInnerText(linkNode).length;
+    });
 
     return linkLength / textLength;
   },
 
   /**
    * Find a cleaned up version of the current URL, to use for comparing links for possible next-pageyness.
    *
    * @author Dan Lacy
@@ -1400,38 +1421,36 @@ Readability.prototype = {
    * Clean a node of all elements of type "tag".
    * (Unless it's a youtube/vimeo video. People love movies.)
    *
    * @param Element
    * @param string tag to clean
    * @return void
    **/
   _clean: function(e, tag) {
-    var targetList = e.getElementsByTagName(tag);
-    var isEmbed = (tag === 'object' || tag === 'embed');
+    var isEmbed = ["object", "embed", "iframe"].indexOf(tag) !== -1;
 
-    for (var y = targetList.length - 1; y >= 0; y -= 1) {
+    this._forEachNode(e.getElementsByTagName(tag), function(element) {
       // Allow youtube and vimeo videos through as people usually want to see those.
       if (isEmbed) {
-        var attributeValues = "";
-        for (var i = 0, il = targetList[y].attributes.length; i < il; i += 1) {
-          attributeValues += targetList[y].attributes[i].value + '|';
-        }
+        var attributeValues = [].map.call(element.attributes, function(attr) {
+          return attr.value;
+        }).join("|");
 
         // First, check the elements attributes to see if any of them contain youtube or vimeo
         if (this.REGEXPS.videos.test(attributeValues))
-          continue;
+          return;
 
         // Then check the elements inside this element for the same.
-        if (this.REGEXPS.videos.test(targetList[y].innerHTML))
-          continue;
+        if (this.REGEXPS.videos.test(element.innerHTML))
+          return;
       }
 
-      targetList[y].parentNode.removeChild(targetList[y]);
-    }
+      element.parentNode.removeChild(element);
+    });
   },
 
   /**
    * Clean an element of all tags of type "tag" if they look fishy.
    * "Fishy" is an algorithm based on content length, classnames, link density, number of images & embeds, etc.
    *
    * @return void
    **/
@@ -1573,17 +1592,17 @@ Readability.prototype = {
     // }
 
     // If we haven't found an excerpt in the article's metadata, use the article's
     // first paragraph as the excerpt. This is used for displaying a preview of
     // the article's content.
     if (!metadata.excerpt) {
       var paragraphs = articleContent.getElementsByTagName("p");
       if (paragraphs.length > 0) {
-        metadata.excerpt = paragraphs[0].textContent;
+        metadata.excerpt = paragraphs[0].textContent.trim();
       }
     }
 
     return { uri: this._uri,
              title: articleTitle,
              byline: metadata.byline || this._articleByline,
              dir: this._articleDir,
              content: articleContent.innerHTML,
--- a/toolkit/components/satchel/nsFormAutoComplete.js
+++ b/toolkit/components/satchel/nsFormAutoComplete.js
@@ -269,24 +269,30 @@ FormAutoComplete.prototype = {
         let processResults = {
           handleResult: aResult => {
             results.push(aResult);
           },
           handleError: aError => {
             this.log("getAutocompleteValues failed: " + aError.message);
           },
           handleCompletion: aReason => {
-            this._pendingQuery = null;
-            if (!aReason) {
-              callback(results);
+            // Check that the current query is still the one we created. Our
+            // query might have been canceled shortly before completing, in
+            // that case we don't want to call the callback anymore.
+            if (query == this._pendingQuery) {
+              this._pendingQuery = null;
+              if (!aReason) {
+                callback(results);
+              }
             }
           }
         };
 
-        this._pendingQuery = FormHistory.getAutoCompleteResults(searchString, params, processResults);
+        let query = FormHistory.getAutoCompleteResults(searchString, params, processResults);
+        this._pendingQuery = query;
     },
 
     /*
      * _calculateScore
      *
      * entry    -- an nsIAutoCompleteResult entry
      * aSearchString -- current value of the input (lowercase)
      * searchTokens -- array of tokens of the search string
@@ -324,16 +330,17 @@ function FormAutoCompleteChild() {
 }
 
 FormAutoCompleteChild.prototype = {
     classID          : Components.ID("{c11c21b2-71c9-4f87-a0f8-5e13f50495fd}"),
     QueryInterface   : XPCOMUtils.generateQI([Ci.nsIFormAutoComplete, Ci.nsISupportsWeakReference]),
 
     _debug: false,
     _enabled: true,
+    _pendingSearch: null,
 
     /*
      * init
      *
      * Initializes the content-process side of the FormAutoComplete component,
      * and add a listener for the message that the parent process sends when
      * a result is produced.
      */
@@ -356,17 +363,19 @@ FormAutoCompleteChild.prototype = {
 
     autoCompleteSearch : function (aInputName, aUntrimmedSearchString, aField, aPreviousResult) {
       // This function is deprecated
     },
 
     autoCompleteSearchAsync : function (aInputName, aUntrimmedSearchString, aField, aPreviousResult, aListener) {
       this.log("autoCompleteSearchAsync");
 
-      this._pendingListener = aListener;
+      if (this._pendingSearch) {
+        this.stopAutoCompleteSearch();
+      }
 
       let rect = BrowserUtils.getElementBoundingScreenRect(aField);
 
       let window = aField.ownerDocument.defaultView;
       let topLevelDocshell = window.QueryInterface(Ci.nsIInterfaceRequestor)
                                    .getInterface(Ci.nsIDocShell)
                                    .sameTypeRootTreeItem
                                    .QueryInterface(Ci.nsIDocShell);
@@ -378,36 +387,45 @@ FormAutoCompleteChild.prototype = {
         inputName: aInputName,
         untrimmedSearchString: aUntrimmedSearchString,
         left: rect.left,
         top: rect.top,
         width: rect.width,
         height: rect.height
       });
 
-      mm.addMessageListener("FormAutoComplete:AutoCompleteSearchAsyncResult",
-        function searchFinished(message) {
-          mm.removeMessageListener("FormAutoComplete:AutoCompleteSearchAsyncResult", searchFinished);
-          let result = new FormAutoCompleteResult(
-            null,
-            [{text: res} for (res of message.data.results)],
-            null,
-            null
-          );
-          if (aListener) {
-            aListener.onSearchCompletion(result);
-          }
+      let search = this._pendingSearch = {};
+      let searchFinished = message => {
+        mm.removeMessageListener("FormAutoComplete:AutoCompleteSearchAsyncResult", searchFinished);
+
+        // Check whether stopAutoCompleteSearch() was called, i.e. the search
+        // was cancelled, while waiting for a result.
+        if (search != this._pendingSearch) {
+          return;
         }
-      );
+        this._pendingSearch = null;
 
+        let result = new FormAutoCompleteResult(
+          null,
+          [for (res of message.data.results) {text: res}],
+          null,
+          null
+        );
+        if (aListener) {
+          aListener.onSearchCompletion(result);
+        }
+      }
+
+      mm.addMessageListener("FormAutoComplete:AutoCompleteSearchAsyncResult", searchFinished);
       this.log("autoCompleteSearchAsync message was sent");
     },
 
     stopAutoCompleteSearch : function () {
-       this.log("stopAutoCompleteSearch");
+      this.log("stopAutoCompleteSearch");
+      this._pendingSearch = null;
     },
 }; // end of FormAutoCompleteChild implementation
 
 // nsIAutoCompleteResult implementation
 function FormAutoCompleteResult (formHistory, entries, fieldName, searchString) {
     this.formHistory = formHistory;
     this.entries = entries;
     this.fieldName = fieldName;
--- a/toolkit/components/telemetry/Histograms.json
+++ b/toolkit/components/telemetry/Histograms.json
@@ -6566,26 +6566,26 @@
   },
   "DEVTOOLS_TABS_OPEN_PEAK_LINEAR": {
     "expires_in_version": "never",
     "kind": "linear",
     "high": "101",
     "n_buckets": 100,
     "description": "The peak number of open tabs in all windows for a session for devtools users."
   },
-  "DEVTOOLS_TABS_OPEN_AVERAGE_EXPONENTIAL": {
-    "expires_in_version": "never",
-    "kind": "exponential",
+  "DEVTOOLS_TABS_OPEN_AVERAGE_LINEAR": {
+    "expires_in_version": "never",
+    "kind": "linear",
     "high": "101",
     "n_buckets": "100",
     "description": "The mean number of open tabs in all windows for a session for devtools users."
   },
-  "DEVTOOLS_TABS_PINNED_PEAK_EXPONENTIAL": {
-    "expires_in_version": "never",
-    "kind": "exponential",
+  "DEVTOOLS_TABS_PINNED_PEAK_LINEAR": {
+    "expires_in_version": "never",
+    "kind": "linear",
     "high": "101",
     "n_buckets": "100",
     "description": "The peak number of pinned tabs (app tabs) in all windows for a session for devtools users."
   },
   "DEVTOOLS_TABS_PINNED_AVERAGE_LINEAR": {
     "expires_in_version": "never",
     "kind": "linear",
     "high": "101",
@@ -7282,16 +7282,24 @@
   },
   "LOOP_TWO_WAY_MEDIA_CONN_LENGTH": {
     "expires_in_version": "43",
     "kind": "count",
     "keyed": true,
     "releaseChannelCollection": "opt-out",
     "description": "Connection length for bi-directionally connected media"
   },
+  "LOOP_SHARING_STATE_CHANGE": {
+    "alert_emails": ["firefox-dev@mozilla.org", "mdeboer@mozilla.com"],
+    "expires_in_version": "43",
+    "kind": "count",
+    "keyed": true,
+    "releaseChannelCollection": "opt-in",
+    "description": "Number of times the sharing feature has been enabled and disabled"
+  },
   "E10S_AUTOSTART": {
     "expires_in_version": "never",
     "kind": "boolean",
     "description": "Whether a session is set to autostart e10s windows"
   },
   "E10S_WINDOW": {
     "expires_in_version": "never",
     "kind": "boolean",
--- a/toolkit/mozapps/extensions/LightweightThemeManager.jsm
+++ b/toolkit/mozapps/extensions/LightweightThemeManager.jsm
@@ -64,35 +64,68 @@ this.__defineSetter__("_maxUsedThemes", 
 });
 
 // Holds the ID of the theme being enabled or disabled while sending out the
 // events so cached AddonWrapper instances can return correct values for
 // permissions and pendingOperations
 var _themeIDBeingEnabled = null;
 var _themeIDBeingDisabled = null;
 
+// Convert from the old storage format (in which the order of usedThemes
+// was combined with isThemeSelected to determine which theme was selected)
+// to the new one (where a selectedThemeID determines which theme is selected).
+(function migrateToNewStorageFormat() {
+  let wasThemeSelected = false;
+  try {
+    wasThemeSelected = _prefs.getBoolPref("isThemeSelected");
+  } catch(e) { }
+
+  if (wasThemeSelected) {
+    _prefs.clearUserPref("isThemeSelected");
+    let themes = [];
+    try {
+      themes = JSON.parse(_prefs.getComplexValue("usedThemes",
+                                                 Ci.nsISupportsString).data);
+    } catch (e) { }
+
+    if (Array.isArray(themes) && themes[0]) {
+      _prefs.setCharPref("selectedThemeID", themes[0].id);
+    }
+  }
+})();
+
 this.LightweightThemeManager = {
   get name() "LightweightThemeManager",
 
+  // Themes that can be added for an application.  They can't be removed, and
+  // will always show up at the top of the list.
+  _builtInThemes: new Map(),
+
   get usedThemes () {
+    let themes = [];
     try {
-      return JSON.parse(_prefs.getComplexValue("usedThemes",
-                                               Ci.nsISupportsString).data);
-    } catch (e) {
-      return [];
-    }
+      themes = JSON.parse(_prefs.getComplexValue("usedThemes",
+                                                 Ci.nsISupportsString).data);
+    } catch (e) { }
+
+    themes.push(...this._builtInThemes.values());
+    return themes;
   },
 
   get currentTheme () {
+    let selectedThemeID = null;
     try {
-      if (_prefs.getBoolPref("isThemeSelected"))
-        var data = this.usedThemes[0];
+      selectedThemeID = _prefs.getCharPref("selectedThemeID");
     } catch (e) {}
 
-    return data || null;
+    let data = null;
+    if (selectedThemeID) {
+      data = this.getUsedTheme(selectedThemeID);
+    }
+    return data;
   },
 
   get currentThemeForDisplay () {
     var data = this.currentTheme;
 
     if (data && PERSIST_ENABLED) {
       for (let key in PERSIST_FILES) {
         try {
@@ -120,32 +153,56 @@ this.LightweightThemeManager = {
       if (usedTheme.id == aId)
         return usedTheme;
     }
     return null;
   },
 
   forgetUsedTheme: function LightweightThemeManager_forgetUsedTheme(aId) {
     let theme = this.getUsedTheme(aId);
-    if (!theme)
+    if (!theme || LightweightThemeManager._builtInThemes.has(theme.id))
       return;
 
     let wrapper = new AddonWrapper(theme);
     AddonManagerPrivate.callAddonListeners("onUninstalling", wrapper, false);
 
     var currentTheme = this.currentTheme;
     if (currentTheme && currentTheme.id == aId) {
       this.themeChanged(null);
       AddonManagerPrivate.notifyAddonChanged(null, ADDON_TYPE, false);
     }
 
     _updateUsedThemes(_usedThemesExceptId(aId));
     AddonManagerPrivate.callAddonListeners("onUninstalled", wrapper);
   },
 
+  addBuiltInTheme: function LightweightThemeManager_addBuiltInTheme(theme) {
+    if (!theme || !theme.id || this.usedThemes.some(t => t.id == theme.id)) {
+      throw new Error("Trying to add invalid builtIn theme");
+    }
+
+    this._builtInThemes.set(theme.id, theme);
+  },
+
+  forgetBuiltInTheme: function LightweightThemeManager_forgetBuiltInTheme(id) {
+    if (!this._builtInThemes.has(id)) {
+      let currentTheme = this.currentTheme;
+      if (currentTheme && currentTheme.id == id) {
+        this.currentTheme = null;
+      }
+    }
+    return this._builtInThemes.delete(id);
+  },
+
+  clearBuiltInThemes: function LightweightThemeManager_clearBuiltInThemes() {
+    for (let id of this._builtInThemes.keys()) {
+      this.forgetBuiltInTheme(id);
+    }
+  },
+
   previewTheme: function LightweightThemeManager_previewTheme(aData) {
     let cancel = Cc["@mozilla.org/supports-PRBool;1"].createInstance(Ci.nsISupportsPRBool);
     cancel.data = false;
     Services.obs.notifyObservers(cancel, "lightweight-theme-preview-requested",
                                  JSON.stringify(aData));
     if (cancel.data)
       return;
 
@@ -237,17 +294,21 @@ this.LightweightThemeManager = {
       if (PERSIST_ENABLED) {
         LightweightThemeImageOptimizer.purge();
         _persistImages(aData, function themeChanged_persistImages() {
           _notifyWindows(this.currentThemeForDisplay);
         }.bind(this));
       }
     }
 
-    _prefs.setBoolPref("isThemeSelected", aData != null);
+    if (aData)
+      _prefs.setCharPref("selectedThemeID", aData.id);
+    else
+      _prefs.clearUserPref("selectedThemeID");
+
     _notifyWindows(aData);
     Services.obs.notifyObservers(null, "lightweight-theme-changed", null);
   },
 
   /**
    * Starts the Addons provider and enables the new lightweight theme if
    * necessary.
    */
@@ -457,17 +518,21 @@ function AddonWrapper(aTheme) {
 
   this.__defineGetter__("size", function AddonWrapper_sizeGetter() {
     // The size changes depending on whether the theme is in use or not, this is
     // probably not worth exposing.
     return null;
   });
 
   this.__defineGetter__("permissions", function AddonWrapper_permissionsGetter() {
-    let permissions = AddonManager.PERM_CAN_UNINSTALL;
+    let permissions = 0;
+
+    // Do not allow uninstall of builtIn themes.
+    if (!LightweightThemeManager._builtInThemes.has(aTheme.id))
+      permissions = AddonManager.PERM_CAN_UNINSTALL;
     if (this.userDisabled)
       permissions |= AddonManager.PERM_CAN_ENABLE;
     else
       permissions |= AddonManager.PERM_CAN_DISABLE;
     return permissions;
   });
 
   this.__defineGetter__("userDisabled", function AddonWrapper_userDisabledGetter() {
@@ -674,16 +739,19 @@ function _usedThemesExceptId(aId)
 
 function _version(aThemeData)
   aThemeData.version || "";
 
 function _makeURI(aURL, aBaseURI)
   Services.io.newURI(aURL, null, aBaseURI);
 
 function _updateUsedThemes(aList) {
+  // Remove app-specific themes before saving them to the usedThemes pref.
+  aList = aList.filter(theme => !LightweightThemeManager._builtInThemes.has(theme.id));
+
   // Send uninstall events for all themes that need to be removed.
   while (aList.length > _maxUsedThemes) {
     let wrapper = new AddonWrapper(aList[aList.length - 1]);
     AddonManagerPrivate.callAddonListeners("onUninstalling", wrapper, false);
     aList.pop();
     AddonManagerPrivate.callAddonListeners("onUninstalled", wrapper);
   }
 
--- a/toolkit/mozapps/extensions/test/xpcshell/test_LightweightThemeManager.js
+++ b/toolkit/mozapps/extensions/test/xpcshell/test_LightweightThemeManager.js
@@ -14,28 +14,30 @@ function dummy(id) {
     name: Math.random().toString(),
     headerURL: "http://lwttest.invalid/a.png",
     footerURL: "http://lwttest.invalid/b.png",
     textcolor: Math.random().toString(),
     accentcolor: Math.random().toString()
   };
 }
 
+function hasPermission(aAddon, aPerm) {
+  var perm = AddonManager["PERM_CAN_" + aPerm.toUpperCase()];
+  return !!(aAddon.permissions & perm);
+}
+
 function run_test() {
   createAppInfo("xpcshell@tests.mozilla.org", "XPCShell", "1", "1.9");
   startupManager();
 
   Services.prefs.setIntPref("lightweightThemes.maxUsedThemes", 8);
 
-  var temp = {};
-  Components.utils.import("resource://gre/modules/LightweightThemeManager.jsm", temp);
-  do_check_eq(typeof temp.LightweightThemeManager, "object");
+  let {LightweightThemeManager: ltm} = Components.utils.import("resource://gre/modules/LightweightThemeManager.jsm", {});
 
-  var ltm = temp.LightweightThemeManager;
-
+  do_check_eq(typeof ltm, "object");
   do_check_eq(typeof ltm.usedThemes, "object");
   do_check_eq(ltm.usedThemes.length, 0);
   do_check_eq(ltm.currentTheme, null);
 
   ltm.previewTheme(dummy("preview0"));
   do_check_eq(ltm.usedThemes.length, 0);
   do_check_eq(ltm.currentTheme, null);
 
@@ -506,9 +508,91 @@ function run_test() {
 
   ltm.currentTheme = dummy("x33");
 
   do_check_eq(ltm.usedThemes.length, 32);
 
   Services.prefs.clearUserPref("lightweightThemes.maxUsedThemes");
 
   do_check_eq(ltm.usedThemes.length, 30);
+
+  let usedThemes = ltm.usedThemes;
+  for (let theme of usedThemes) {
+    ltm.forgetUsedTheme(theme.id);
+  }
+
+  // Check builtInTheme functionality for Bug 1094821
+  do_check_eq(ltm._builtInThemes.toString(), "[object Map]");
+  do_check_eq([...ltm._builtInThemes.entries()].length, 0);
+  do_check_eq(ltm.usedThemes.length, 0);
+
+  ltm.addBuiltInTheme(dummy("builtInTheme0"));
+  do_check_eq([...ltm._builtInThemes].length, 1);
+  do_check_eq(ltm.usedThemes.length, 1);
+  do_check_eq(ltm.usedThemes[0].id, "builtInTheme0");
+
+  ltm.addBuiltInTheme(dummy("builtInTheme1"));
+  do_check_eq([...ltm._builtInThemes].length, 2);
+  do_check_eq(ltm.usedThemes.length, 2);
+  do_check_eq(ltm.usedThemes[1].id, "builtInTheme1");
+
+  // Clear all and then re-add
+  ltm.clearBuiltInThemes();
+  do_check_eq([...ltm._builtInThemes].length, 0);
+  do_check_eq(ltm.usedThemes.length, 0);
+
+  ltm.addBuiltInTheme(dummy("builtInTheme0"));
+  ltm.addBuiltInTheme(dummy("builtInTheme1"));
+  do_check_eq([...ltm._builtInThemes].length, 2);
+  do_check_eq(ltm.usedThemes.length, 2);
+
+  do_test_pending();
+
+  AddonManager.getAddonByID("builtInTheme0@personas.mozilla.org", aAddon => {
+    // App specific theme can't be uninstalled or disabled,
+    // but can be enabled (since it isn't already applied).
+    do_check_eq(hasPermission(aAddon, "uninstall"), false);
+    do_check_eq(hasPermission(aAddon, "disable"), false);
+    do_check_eq(hasPermission(aAddon, "enable"), true);
+
+    ltm.currentTheme = dummy("x0");
+    do_check_eq([...ltm._builtInThemes].length, 2);
+    do_check_eq(ltm.usedThemes.length, 3);
+    do_check_eq(ltm.usedThemes[0].id, "x0");
+    do_check_eq(ltm.currentTheme.id, "x0");
+    do_check_eq(ltm.usedThemes[1].id, "builtInTheme0");
+    do_check_eq(ltm.usedThemes[2].id, "builtInTheme1");
+
+    Assert.throws(() => { ltm.addBuiltInTheme(dummy("builtInTheme0")) },
+      "Exception is thrown adding a duplicate theme");
+    Assert.throws(() => { ltm.addBuiltInTheme("not a theme object") },
+      "Exception is thrown adding an invalid theme");
+
+    AddonManager.getAddonByID("x0@personas.mozilla.org", aAddon => {
+      // Currently applied (non-app-specific) can be uninstalled or disabled,
+      // but can't be enabled (since it's already applied).
+      do_check_eq(hasPermission(aAddon, "uninstall"), true);
+      do_check_eq(hasPermission(aAddon, "disable"), true);
+      do_check_eq(hasPermission(aAddon, "enable"), false);
+
+      ltm.forgetUsedTheme("x0");
+      do_check_eq(ltm.currentTheme, null);
+
+      // Removing the currently applied app specific theme should unapply it
+      ltm.currentTheme = ltm.getUsedTheme("builtInTheme0");
+      do_check_eq(ltm.currentTheme.id, "builtInTheme0");
+      do_check_true(ltm.forgetBuiltInTheme("builtInTheme0"));
+      do_check_eq(ltm.currentTheme, null);
+
+      do_check_eq([...ltm._builtInThemes].length, 1);
+      do_check_eq(ltm.usedThemes.length, 1);
+
+      do_check_true(ltm.forgetBuiltInTheme("builtInTheme1"));
+      do_check_false(ltm.forgetBuiltInTheme("not-an-existing-theme-id"));
+
+      do_check_eq([...ltm._builtInThemes].length, 0);
+      do_check_eq(ltm.usedThemes.length, 0);
+      do_check_eq(ltm.currentTheme, null);
+
+      do_test_finished();
+    });
+  });
 }
--- a/toolkit/mozapps/extensions/test/xpcshell/test_migrate3.js
+++ b/toolkit/mozapps/extensions/test/xpcshell/test_migrate3.js
@@ -113,17 +113,17 @@ function run_test() {
     description: "A test theme",
     author: "Mozilla",
     homepageURL: "http://localhost/data/index.html",
     headerURL: "http://localhost/data/header.png",
     footerURL: "http://localhost/data/footer.png",
     previewURL: "http://localhost/data/preview.png",
     iconURL: "http://localhost/data/icon.png"
   }]));
-  Services.prefs.setBoolPref("lightweightThemes.isThemeSelected", true);
+  Services.prefs.setCharPref("lightweightThemes.selectedThemeID", "1");
 
   let stagedXPIs = profileDir.clone();
   stagedXPIs.append("staged-xpis");
   stagedXPIs.append("addon6@tests.mozilla.org");
   stagedXPIs.create(AM_Ci.nsIFile.DIRECTORY_TYPE, 0755);
 
   let addon6 = do_get_addon("test_migrate6");
   addon6.copyTo(stagedXPIs, "tmp.xpi");
--- a/toolkit/themes/windows/global/aboutReader.css
+++ b/toolkit/themes/windows/global/aboutReader.css
@@ -252,16 +252,17 @@ body {
   background-repeat: no-repeat;
   background-color: transparent;
   height: 40px;
   width: 40px;
   border-top: 0;
   border-left: 0;
   border-right: 0;
   border-bottom: 1px solid #c1c1c1;
+  padding: 0;
 }
 
 .button[hidden] {
   display: none;
 }
 
 .dropdown {
   text-align: center;