Merge m-c to inbound.
authorRyan VanderMeulen <ryanvm@gmail.com>
Fri, 07 Mar 2014 15:45:57 -0500
changeset 190826 a3bf4be14ce0f61d13b8d7144343220081a5b5e4
parent 190825 f952ac387d75e168dac87fb82a66ceea3d49d04e (current diff)
parent 190806 cd67334b34c574f789234f6a97fcc7c31ac83df4 (diff)
child 190827 aab169fa66bea2e1a701ea6460fe7e9f6b5d39e1
push id474
push userasasaki@mozilla.com
push dateMon, 02 Jun 2014 21:01:02 +0000
treeherdermozilla-release@967f4cf1b31c [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
milestone30.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/b2g/config/emulator-ics/sources.xml
+++ b/b2g/config/emulator-ics/sources.xml
@@ -14,23 +14,23 @@
   <!--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="1ad48c4be51b279f7f63c1a13025b52fe087d231">
     <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="b3758a90b8888e9d95128846b2833b4d9444ef7f"/>
+  <project name="gaia.git" path="gaia" remote="mozillaorg" revision="6c109df47006b08c082761c0ddd6ba53a864983a"/>
   <project name="gonk-misc" path="gonk-misc" remote="b2g" revision="15e8982284c4560f9c74c2b9fe8bb361ebfe0cb6"/>
   <project name="rilproxy" path="rilproxy" remote="b2g" revision="827214fcf38d6569aeb5c6d6f31cb296d1f09272"/>
   <project name="platform_hardware_ril" path="hardware/ril" remote="b2g" revision="d11f524d00cacf5ba0dfbf25e4aa2158b1c3a036"/>
   <project name="platform_external_qemu" path="external/qemu" remote="b2g" revision="456499c44d1ef39b602ea02e9ed460b6aab85b44"/>
-  <project name="moztt" path="external/moztt" remote="b2g" revision="3d5c964015967ca8c86abe6dbbebee3cb82b1609"/>
-  <project name="apitrace" path="external/apitrace" remote="apitrace" revision="b2f773d8320d30648b89767dfe5b25ef94bc7e62"/>
+  <project name="moztt" path="external/moztt" remote="b2g" revision="cf1dcc0704c0c1845f8a0a0b44838f7e0c0362c9"/>
+  <project name="apitrace" path="external/apitrace" remote="apitrace" revision="707630df1b4270eae3dd49b7344c645f32c1b5f4"/>
   <!-- Stock Android things -->
   <project name="platform/abi/cpp" path="abi/cpp" revision="dd924f92906085b831bf1cbbc7484d3c043d613c"/>
   <project name="platform/bionic" path="bionic" revision="c72b8f6359de7ed17c11ddc9dfdde3f615d188a9"/>
   <project name="platform/bootable/recovery" path="bootable/recovery" revision="425f8b5fadf5889834c5acd27d23c9e0b2129c28"/>
   <project name="device/common" path="device/common" revision="42b808b7e93d0619286ae8e59110b176b7732389"/>
   <project name="device/sample" path="device/sample" revision="237bd668d0f114d801a8d6455ef5e02cc3577587"/>
   <project name="platform_external_apriori" path="external/apriori" remote="b2g" revision="11816ad0406744f963537b23d68ed9c2afb412bd"/>
   <project name="platform/external/bluetooth/bluez" path="external/bluetooth/bluez" revision="52a1a862a8bac319652b8f82d9541ba40bfa45ce"/>
--- a/b2g/config/emulator-jb/sources.xml
+++ b/b2g/config/emulator-jb/sources.xml
@@ -12,20 +12,20 @@
   <!--original fetch url was https://git.mozilla.org/releases-->
   <remote fetch="https://git.mozilla.org/releases" name="mozillaorg"/>
   <!-- B2G specific things. -->
   <project name="platform_build" path="build" remote="b2g" revision="97a5b461686757dbb8ecab2aac5903e41d2e1afe">
     <copyfile dest="Makefile" src="core/root.mk"/>
   </project>
   <project name="rilproxy" path="rilproxy" remote="b2g" revision="827214fcf38d6569aeb5c6d6f31cb296d1f09272"/>
   <project name="fake-libdvm" path="dalvik" remote="b2g" revision="d50ae982b19f42f0b66d08b9eb306be81687869f"/>
-  <project name="gaia" path="gaia" remote="mozillaorg" revision="b3758a90b8888e9d95128846b2833b4d9444ef7f"/>
+  <project name="gaia" path="gaia" remote="mozillaorg" revision="6c109df47006b08c082761c0ddd6ba53a864983a"/>
   <project name="gonk-misc" path="gonk-misc" remote="b2g" revision="15e8982284c4560f9c74c2b9fe8bb361ebfe0cb6"/>
-  <project name="moztt" path="external/moztt" remote="b2g" revision="3d5c964015967ca8c86abe6dbbebee3cb82b1609"/>
-  <project name="apitrace" path="external/apitrace" remote="apitrace" revision="b2f773d8320d30648b89767dfe5b25ef94bc7e62"/>
+  <project name="moztt" path="external/moztt" remote="b2g" revision="cf1dcc0704c0c1845f8a0a0b44838f7e0c0362c9"/>
+  <project name="apitrace" path="external/apitrace" remote="apitrace" revision="707630df1b4270eae3dd49b7344c645f32c1b5f4"/>
   <project name="valgrind" path="external/valgrind" remote="b2g" revision="daa61633c32b9606f58799a3186395fd2bbb8d8c"/>
   <project name="vex" path="external/VEX" remote="b2g" revision="47f031c320888fe9f3e656602588565b52d43010"/>
   <!-- Stock Android things -->
   <project groups="linux" name="platform/prebuilts/clang/linux-x86/3.1" path="prebuilts/clang/linux-x86/3.1" revision="5c45f43419d5582949284eee9cef0c43d866e03b"/>
   <project groups="linux" name="platform/prebuilts/clang/linux-x86/3.2" path="prebuilts/clang/linux-x86/3.2" revision="3748b4168e7bd8d46457d4b6786003bc6a5223ce"/>
   <project groups="linux" name="platform/prebuilts/gcc/linux-x86/host/i686-linux-glibc2.7-4.6" path="prebuilts/gcc/linux-x86/host/i686-linux-glibc2.7-4.6" revision="9025e50b9d29b3cabbbb21e1dd94d0d13121a17e"/>
   <project groups="linux" name="platform/prebuilts/gcc/linux-x86/host/x86_64-linux-glibc2.7-4.6" path="prebuilts/gcc/linux-x86/host/x86_64-linux-glibc2.7-4.6" revision="b89fda71fcd0fa0cf969310e75be3ea33e048b44"/>
   <project groups="linux,arm" name="platform/prebuilts/gcc/linux-x86/arm/arm-eabi-4.7" path="prebuilts/gcc/linux-x86/arm/arm-eabi-4.7" revision="2e7d5348f35575870b3c7e567a9a9f6d66f8d6c5"/>
--- a/b2g/config/emulator-kk/sources.xml
+++ b/b2g/config/emulator-kk/sources.xml
@@ -10,24 +10,24 @@
   <!--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="a9e08b91e9cd1f0930f16cfc49ec72f63575d5fe">
     <copyfile dest="Makefile" src="core/root.mk"/>
   </project>
-  <project name="gaia" path="gaia" remote="mozillaorg" revision="b3758a90b8888e9d95128846b2833b4d9444ef7f"/>
+  <project name="gaia" path="gaia" remote="mozillaorg" revision="6c109df47006b08c082761c0ddd6ba53a864983a"/>
   <project name="fake-libdvm" path="dalvik" remote="b2g" revision="d50ae982b19f42f0b66d08b9eb306be81687869f"/>
   <project name="gonk-misc" path="gonk-misc" remote="b2g" revision="15e8982284c4560f9c74c2b9fe8bb361ebfe0cb6"/>
-  <project name="moztt" path="external/moztt" remote="b2g" revision="3d5c964015967ca8c86abe6dbbebee3cb82b1609"/>
+  <project name="moztt" path="external/moztt" remote="b2g" revision="cf1dcc0704c0c1845f8a0a0b44838f7e0c0362c9"/>
   <project name="rilproxy" path="rilproxy" remote="b2g" revision="827214fcf38d6569aeb5c6d6f31cb296d1f09272"/>
   <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="b2f773d8320d30648b89767dfe5b25ef94bc7e62"/>
+  <project name="apitrace" path="external/apitrace" remote="apitrace" revision="707630df1b4270eae3dd49b7344c645f32c1b5f4"/>
   <!-- Stock Android things -->
   <project groups="linux" name="platform/prebuilts/gcc/linux-x86/host/i686-linux-glibc2.7-4.6" path="prebuilts/gcc/linux-x86/host/i686-linux-glibc2.7-4.6" revision="f92a936f2aa97526d4593386754bdbf02db07a12"/>
   <project groups="linux" name="platform/prebuilts/gcc/linux-x86/host/x86_64-linux-glibc2.7-4.6" path="prebuilts/gcc/linux-x86/host/x86_64-linux-glibc2.7-4.6" revision="6e47ff2790f5656b5b074407829ceecf3e6188c4"/>
   <project groups="linux,arm" name="platform/prebuilts/gcc/linux-x86/arm/arm-eabi-4.7" path="prebuilts/gcc/linux-x86/arm/arm-eabi-4.7" revision="1950e4760fa14688b83cdbb5acaa1af9f82ef434"/>
   <project groups="linux,arm" name="platform/prebuilts/gcc/linux-x86/arm/arm-linux-androideabi-4.7" path="prebuilts/gcc/linux-x86/arm/arm-linux-androideabi-4.7" revision="ac6eb97a37035c09fb5ede0852f0881e9aadf9ad"/>
   <project groups="linux,x86" name="platform/prebuilts/gcc/linux-x86/x86/i686-linux-android-4.7" path="prebuilts/gcc/linux-x86/x86/i686-linux-android-4.7" revision="737f591c5f95477148d26602c7be56cbea0cdeb9"/>
   <project name="device/common" path="device/common" revision="798a3664597e6041985feab9aef42e98d458bc3d"/>
   <project name="device/sample" path="device/sample" revision="9aebb4ac5b6c1b2552599651a0e51b01e720eea6"/>
--- a/b2g/config/emulator/sources.xml
+++ b/b2g/config/emulator/sources.xml
@@ -14,23 +14,23 @@
   <!--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="1ad48c4be51b279f7f63c1a13025b52fe087d231">
     <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="b3758a90b8888e9d95128846b2833b4d9444ef7f"/>
+  <project name="gaia.git" path="gaia" remote="mozillaorg" revision="6c109df47006b08c082761c0ddd6ba53a864983a"/>
   <project name="gonk-misc" path="gonk-misc" remote="b2g" revision="15e8982284c4560f9c74c2b9fe8bb361ebfe0cb6"/>
   <project name="rilproxy" path="rilproxy" remote="b2g" revision="827214fcf38d6569aeb5c6d6f31cb296d1f09272"/>
   <project name="platform_hardware_ril" path="hardware/ril" remote="b2g" revision="d11f524d00cacf5ba0dfbf25e4aa2158b1c3a036"/>
   <project name="platform_external_qemu" path="external/qemu" remote="b2g" revision="456499c44d1ef39b602ea02e9ed460b6aab85b44"/>
-  <project name="moztt" path="external/moztt" remote="b2g" revision="3d5c964015967ca8c86abe6dbbebee3cb82b1609"/>
-  <project name="apitrace" path="external/apitrace" remote="apitrace" revision="b2f773d8320d30648b89767dfe5b25ef94bc7e62"/>
+  <project name="moztt" path="external/moztt" remote="b2g" revision="cf1dcc0704c0c1845f8a0a0b44838f7e0c0362c9"/>
+  <project name="apitrace" path="external/apitrace" remote="apitrace" revision="707630df1b4270eae3dd49b7344c645f32c1b5f4"/>
   <!-- Stock Android things -->
   <project name="platform/abi/cpp" path="abi/cpp" revision="dd924f92906085b831bf1cbbc7484d3c043d613c"/>
   <project name="platform/bionic" path="bionic" revision="c72b8f6359de7ed17c11ddc9dfdde3f615d188a9"/>
   <project name="platform/bootable/recovery" path="bootable/recovery" revision="425f8b5fadf5889834c5acd27d23c9e0b2129c28"/>
   <project name="device/common" path="device/common" revision="42b808b7e93d0619286ae8e59110b176b7732389"/>
   <project name="device/sample" path="device/sample" revision="237bd668d0f114d801a8d6455ef5e02cc3577587"/>
   <project name="platform_external_apriori" path="external/apriori" remote="b2g" revision="11816ad0406744f963537b23d68ed9c2afb412bd"/>
   <project name="platform/external/bluetooth/bluez" path="external/bluetooth/bluez" revision="52a1a862a8bac319652b8f82d9541ba40bfa45ce"/>
--- a/b2g/config/gaia.json
+++ b/b2g/config/gaia.json
@@ -1,9 +1,9 @@
 {
     "git": {
         "remote": "", 
         "branch": "", 
         "revision": ""
     }, 
-    "revision": "1ad040b490a7eca701eab481a3716c5404041c1b", 
+    "revision": "2d53c8213843d04a5fa3421ff8263fbe67fd5e49", 
     "repo_path": "/integration/gaia-central"
 }
--- a/b2g/config/hamachi/sources.xml
+++ b/b2g/config/hamachi/sources.xml
@@ -12,22 +12,22 @@
   <!--original fetch url was git://github.com/apitrace/-->
   <remote fetch="https://git.mozilla.org/external/apitrace" name="apitrace"/>
   <default remote="caf" revision="b2g/ics_strawberry" sync-j="4"/>
   <!-- Gonk specific things and forks -->
   <project name="platform_build" path="build" remote="b2g" revision="1ad48c4be51b279f7f63c1a13025b52fe087d231">
     <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="b3758a90b8888e9d95128846b2833b4d9444ef7f"/>
+  <project name="gaia.git" path="gaia" remote="mozillaorg" revision="6c109df47006b08c082761c0ddd6ba53a864983a"/>
   <project name="gonk-misc" path="gonk-misc" remote="b2g" revision="15e8982284c4560f9c74c2b9fe8bb361ebfe0cb6"/>
   <project name="rilproxy" path="rilproxy" remote="b2g" revision="827214fcf38d6569aeb5c6d6f31cb296d1f09272"/>
   <project name="librecovery" path="librecovery" remote="b2g" revision="1f6a1fe07f81c5bc5e1d079c9b60f7f78ca2bf4f"/>
-  <project name="moztt" path="external/moztt" remote="b2g" revision="3d5c964015967ca8c86abe6dbbebee3cb82b1609"/>
-  <project name="apitrace" path="external/apitrace" remote="apitrace" revision="b2f773d8320d30648b89767dfe5b25ef94bc7e62"/>
+  <project name="moztt" path="external/moztt" remote="b2g" revision="cf1dcc0704c0c1845f8a0a0b44838f7e0c0362c9"/>
+  <project name="apitrace" path="external/apitrace" remote="apitrace" revision="707630df1b4270eae3dd49b7344c645f32c1b5f4"/>
   <!-- Stock Android things -->
   <project name="platform/abi/cpp" path="abi/cpp" revision="6426040f1be4a844082c9769171ce7f5341a5528"/>
   <project name="platform/bionic" path="bionic" revision="d2eb6c7b6e1bc7643c17df2d9d9bcb1704d0b9ab"/>
   <project name="platform/bootable/recovery" path="bootable/recovery" revision="746bc48f34f5060f90801925dcdd964030c1ab6d"/>
   <project name="platform/development" path="development" revision="2460485184bc8535440bb63876d4e63ec1b4770c"/>
   <project name="device/common" path="device/common" revision="0dcc1e03659db33b77392529466f9eb685cdd3c7"/>
   <project name="device/sample" path="device/sample" revision="68b1cb978a20806176123b959cb05d4fa8adaea4"/>
   <project name="platform_external_apriori" path="external/apriori" remote="b2g" revision="11816ad0406744f963537b23d68ed9c2afb412bd"/>
--- a/b2g/config/helix/sources.xml
+++ b/b2g/config/helix/sources.xml
@@ -10,21 +10,21 @@
   <!--original fetch url was https://git.mozilla.org/releases-->
   <remote fetch="https://git.mozilla.org/releases" name="mozillaorg"/>
   <default remote="caf" revision="b2g/ics_strawberry" sync-j="4"/>
   <!-- Gonk specific things and forks -->
   <project name="platform_build" path="build" remote="b2g" revision="1ad48c4be51b279f7f63c1a13025b52fe087d231">
     <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="b3758a90b8888e9d95128846b2833b4d9444ef7f"/>
+  <project name="gaia.git" path="gaia" remote="mozillaorg" revision="6c109df47006b08c082761c0ddd6ba53a864983a"/>
   <project name="gonk-misc" path="gonk-misc" remote="b2g" revision="15e8982284c4560f9c74c2b9fe8bb361ebfe0cb6"/>
   <project name="rilproxy" path="rilproxy" remote="b2g" revision="827214fcf38d6569aeb5c6d6f31cb296d1f09272"/>
   <project name="librecovery" path="librecovery" remote="b2g" revision="1f6a1fe07f81c5bc5e1d079c9b60f7f78ca2bf4f"/>
-  <project name="moztt" path="external/moztt" remote="b2g" revision="3d5c964015967ca8c86abe6dbbebee3cb82b1609"/>
+  <project name="moztt" path="external/moztt" remote="b2g" revision="cf1dcc0704c0c1845f8a0a0b44838f7e0c0362c9"/>
   <project name="gonk-patches" path="patches" remote="b2g" revision="223a2421006e8f5da33f516f6891c87cae86b0f6"/>
   <!-- Stock Android things -->
   <project name="platform/abi/cpp" path="abi/cpp" revision="6426040f1be4a844082c9769171ce7f5341a5528"/>
   <project name="platform/bionic" path="bionic" revision="d2eb6c7b6e1bc7643c17df2d9d9bcb1704d0b9ab"/>
   <project name="platform/bootable/recovery" path="bootable/recovery" revision="575fdbf046e966a5915b1f1e800e5d6ad0ea14c0"/>
   <project name="platform/development" path="development" revision="b1025ec93beeb480caaf3049d171283c3846461d"/>
   <project name="device/common" path="device/common" revision="0dcc1e03659db33b77392529466f9eb685cdd3c7"/>
   <project name="device/sample" path="device/sample" revision="68b1cb978a20806176123b959cb05d4fa8adaea4"/>
--- a/b2g/config/inari/sources.xml
+++ b/b2g/config/inari/sources.xml
@@ -14,22 +14,22 @@
   <!--original fetch url was git://github.com/apitrace/-->
   <remote fetch="https://git.mozilla.org/external/apitrace" name="apitrace"/>
   <default remote="caf" revision="ics_chocolate_rb4.2" sync-j="4"/>
   <!-- Gonk specific things and forks -->
   <project name="platform_build" path="build" remote="b2g" revision="1ad48c4be51b279f7f63c1a13025b52fe087d231">
     <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="b3758a90b8888e9d95128846b2833b4d9444ef7f"/>
+  <project name="gaia.git" path="gaia" remote="mozillaorg" revision="6c109df47006b08c082761c0ddd6ba53a864983a"/>
   <project name="gonk-misc" path="gonk-misc" remote="b2g" revision="15e8982284c4560f9c74c2b9fe8bb361ebfe0cb6"/>
   <project name="rilproxy" path="rilproxy" remote="b2g" revision="827214fcf38d6569aeb5c6d6f31cb296d1f09272"/>
   <project name="librecovery" path="librecovery" remote="b2g" revision="1f6a1fe07f81c5bc5e1d079c9b60f7f78ca2bf4f"/>
-  <project name="moztt" path="external/moztt" remote="b2g" revision="3d5c964015967ca8c86abe6dbbebee3cb82b1609"/>
-  <project name="apitrace" path="external/apitrace" remote="apitrace" revision="b2f773d8320d30648b89767dfe5b25ef94bc7e62"/>
+  <project name="moztt" path="external/moztt" remote="b2g" revision="cf1dcc0704c0c1845f8a0a0b44838f7e0c0362c9"/>
+  <project name="apitrace" path="external/apitrace" remote="apitrace" revision="707630df1b4270eae3dd49b7344c645f32c1b5f4"/>
   <!-- Stock Android things -->
   <project name="platform/abi/cpp" path="abi/cpp" revision="6426040f1be4a844082c9769171ce7f5341a5528"/>
   <project name="platform/bionic" path="bionic" revision="cd5dfce80bc3f0139a56b58aca633202ccaee7f8"/>
   <project name="platform/bootable/recovery" path="bootable/recovery" revision="e0a9ac010df3afaa47ba107192c05ac8b5516435"/>
   <project name="platform/development" path="development" revision="a384622f5fcb1d2bebb9102591ff7ae91fe8ed2d"/>
   <project name="device/common" path="device/common" revision="7c65ea240157763b8ded6154a17d3c033167afb7"/>
   <project name="device/sample" path="device/sample" revision="c328f3d4409db801628861baa8d279fb8855892f"/>
   <project name="platform_external_apriori" path="external/apriori" remote="b2g" revision="11816ad0406744f963537b23d68ed9c2afb412bd"/>
--- a/b2g/config/leo/sources.xml
+++ b/b2g/config/leo/sources.xml
@@ -12,22 +12,22 @@
   <!--original fetch url was git://github.com/apitrace/-->
   <remote fetch="https://git.mozilla.org/external/apitrace" name="apitrace"/>
   <default remote="caf" revision="b2g/ics_strawberry" sync-j="4"/>
   <!-- Gonk specific things and forks -->
   <project name="platform_build" path="build" remote="b2g" revision="1ad48c4be51b279f7f63c1a13025b52fe087d231">
     <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="b3758a90b8888e9d95128846b2833b4d9444ef7f"/>
+  <project name="gaia.git" path="gaia" remote="mozillaorg" revision="6c109df47006b08c082761c0ddd6ba53a864983a"/>
   <project name="gonk-misc" path="gonk-misc" remote="b2g" revision="15e8982284c4560f9c74c2b9fe8bb361ebfe0cb6"/>
   <project name="rilproxy" path="rilproxy" remote="b2g" revision="827214fcf38d6569aeb5c6d6f31cb296d1f09272"/>
   <project name="librecovery" path="librecovery" remote="b2g" revision="1f6a1fe07f81c5bc5e1d079c9b60f7f78ca2bf4f"/>
-  <project name="moztt" path="external/moztt" remote="b2g" revision="3d5c964015967ca8c86abe6dbbebee3cb82b1609"/>
-  <project name="apitrace" path="external/apitrace" remote="apitrace" revision="b2f773d8320d30648b89767dfe5b25ef94bc7e62"/>
+  <project name="moztt" path="external/moztt" remote="b2g" revision="cf1dcc0704c0c1845f8a0a0b44838f7e0c0362c9"/>
+  <project name="apitrace" path="external/apitrace" remote="apitrace" revision="707630df1b4270eae3dd49b7344c645f32c1b5f4"/>
   <project name="gonk-patches" path="patches" remote="b2g" revision="223a2421006e8f5da33f516f6891c87cae86b0f6"/>
   <!-- Stock Android things -->
   <project name="platform/abi/cpp" path="abi/cpp" revision="6426040f1be4a844082c9769171ce7f5341a5528"/>
   <project name="platform/bionic" path="bionic" revision="d2eb6c7b6e1bc7643c17df2d9d9bcb1704d0b9ab"/>
   <project name="platform/bootable/recovery" path="bootable/recovery" revision="575fdbf046e966a5915b1f1e800e5d6ad0ea14c0"/>
   <project name="platform/development" path="development" revision="b1025ec93beeb480caaf3049d171283c3846461d"/>
   <project name="device/common" path="device/common" revision="0dcc1e03659db33b77392529466f9eb685cdd3c7"/>
   <project name="device/sample" path="device/sample" revision="68b1cb978a20806176123b959cb05d4fa8adaea4"/>
--- a/b2g/config/mako/sources.xml
+++ b/b2g/config/mako/sources.xml
@@ -12,20 +12,20 @@
   <!--original fetch url was https://git.mozilla.org/releases-->
   <remote fetch="https://git.mozilla.org/releases" name="mozillaorg"/>
   <!-- B2G specific things. -->
   <project name="platform_build" path="build" remote="b2g" revision="97a5b461686757dbb8ecab2aac5903e41d2e1afe">
     <copyfile dest="Makefile" src="core/root.mk"/>
   </project>
   <project name="rilproxy" path="rilproxy" remote="b2g" revision="827214fcf38d6569aeb5c6d6f31cb296d1f09272"/>
   <project name="fake-libdvm" path="dalvik" remote="b2g" revision="d50ae982b19f42f0b66d08b9eb306be81687869f"/>
-  <project name="gaia" path="gaia" remote="mozillaorg" revision="b3758a90b8888e9d95128846b2833b4d9444ef7f"/>
+  <project name="gaia" path="gaia" remote="mozillaorg" revision="6c109df47006b08c082761c0ddd6ba53a864983a"/>
   <project name="gonk-misc" path="gonk-misc" remote="b2g" revision="15e8982284c4560f9c74c2b9fe8bb361ebfe0cb6"/>
-  <project name="moztt" path="external/moztt" remote="b2g" revision="3d5c964015967ca8c86abe6dbbebee3cb82b1609"/>
-  <project name="apitrace" path="external/apitrace" remote="apitrace" revision="b2f773d8320d30648b89767dfe5b25ef94bc7e62"/>
+  <project name="moztt" path="external/moztt" remote="b2g" revision="cf1dcc0704c0c1845f8a0a0b44838f7e0c0362c9"/>
+  <project name="apitrace" path="external/apitrace" remote="apitrace" revision="707630df1b4270eae3dd49b7344c645f32c1b5f4"/>
   <project name="valgrind" path="external/valgrind" remote="b2g" revision="daa61633c32b9606f58799a3186395fd2bbb8d8c"/>
   <project name="vex" path="external/VEX" remote="b2g" revision="47f031c320888fe9f3e656602588565b52d43010"/>
   <!-- Stock Android things -->
   <project groups="linux" name="platform/prebuilts/clang/linux-x86/3.1" path="prebuilts/clang/linux-x86/3.1" revision="5c45f43419d5582949284eee9cef0c43d866e03b"/>
   <project groups="linux" name="platform/prebuilts/clang/linux-x86/3.2" path="prebuilts/clang/linux-x86/3.2" revision="3748b4168e7bd8d46457d4b6786003bc6a5223ce"/>
   <project groups="linux" name="platform/prebuilts/gcc/linux-x86/host/i686-linux-glibc2.7-4.6" path="prebuilts/gcc/linux-x86/host/i686-linux-glibc2.7-4.6" revision="9025e50b9d29b3cabbbb21e1dd94d0d13121a17e"/>
   <project groups="linux" name="platform/prebuilts/gcc/linux-x86/host/x86_64-linux-glibc2.7-4.6" path="prebuilts/gcc/linux-x86/host/x86_64-linux-glibc2.7-4.6" revision="b89fda71fcd0fa0cf969310e75be3ea33e048b44"/>
   <project groups="linux,arm" name="platform/prebuilts/gcc/linux-x86/arm/arm-eabi-4.7" path="prebuilts/gcc/linux-x86/arm/arm-eabi-4.7" revision="2e7d5348f35575870b3c7e567a9a9f6d66f8d6c5"/>
--- a/b2g/config/wasabi/sources.xml
+++ b/b2g/config/wasabi/sources.xml
@@ -12,22 +12,22 @@
   <!--original fetch url was git://github.com/apitrace/-->
   <remote fetch="https://git.mozilla.org/external/apitrace" name="apitrace"/>
   <default remote="caf" revision="ics_chocolate_rb4.2" sync-j="4"/>
   <!-- Gonk specific things and forks -->
   <project name="platform_build" path="build" remote="b2g" revision="1ad48c4be51b279f7f63c1a13025b52fe087d231">
     <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="b3758a90b8888e9d95128846b2833b4d9444ef7f"/>
+  <project name="gaia.git" path="gaia" remote="mozillaorg" revision="6c109df47006b08c082761c0ddd6ba53a864983a"/>
   <project name="gonk-misc" path="gonk-misc" remote="b2g" revision="15e8982284c4560f9c74c2b9fe8bb361ebfe0cb6"/>
   <project name="rilproxy" path="rilproxy" remote="b2g" revision="827214fcf38d6569aeb5c6d6f31cb296d1f09272"/>
   <project name="librecovery" path="librecovery" remote="b2g" revision="1f6a1fe07f81c5bc5e1d079c9b60f7f78ca2bf4f"/>
-  <project name="moztt" path="external/moztt" remote="b2g" revision="3d5c964015967ca8c86abe6dbbebee3cb82b1609"/>
-  <project name="apitrace" path="external/apitrace" remote="apitrace" revision="b2f773d8320d30648b89767dfe5b25ef94bc7e62"/>
+  <project name="moztt" path="external/moztt" remote="b2g" revision="cf1dcc0704c0c1845f8a0a0b44838f7e0c0362c9"/>
+  <project name="apitrace" path="external/apitrace" remote="apitrace" revision="707630df1b4270eae3dd49b7344c645f32c1b5f4"/>
   <project name="gonk-patches" path="patches" remote="b2g" revision="223a2421006e8f5da33f516f6891c87cae86b0f6"/>
   <!-- Stock Android things -->
   <project name="platform/abi/cpp" path="abi/cpp" revision="6426040f1be4a844082c9769171ce7f5341a5528"/>
   <project name="platform/bionic" path="bionic" revision="cd5dfce80bc3f0139a56b58aca633202ccaee7f8"/>
   <project name="platform/bootable/recovery" path="bootable/recovery" revision="e0a9ac010df3afaa47ba107192c05ac8b5516435"/>
   <project name="platform/development" path="development" revision="a384622f5fcb1d2bebb9102591ff7ae91fe8ed2d"/>
   <project name="device/common" path="device/common" revision="7c65ea240157763b8ded6154a17d3c033167afb7"/>
   <project name="device/sample" path="device/sample" revision="c328f3d4409db801628861baa8d279fb8855892f"/>
--- a/browser/base/content/browser-fxaccounts.js
+++ b/browser/base/content/browser-fxaccounts.js
@@ -51,18 +51,20 @@ let gFxAccounts = {
     // Referencing Weave.Service will implicitly initialize sync, and we don't
     // want to force that - so first check if it is ready.
     let service = Cc["@mozilla.org/weave/service;1"]
                   .getService(Components.interfaces.nsISupports)
                   .wrappedJSObject;
     if (!service.ready) {
       return false;
     }
-    return Weave.Service.identity.readyToAuthenticate &&
-           Weave.Status.login != Weave.LOGIN_SUCCEEDED;
+    // LOGIN_FAILED_LOGIN_REJECTED explicitly means "you must log back in".
+    // All other login failures are assumed to be transient and should go
+    // away by themselves, so aren't reflected here.
+    return Weave.Status.login == Weave.LOGIN_FAILED_LOGIN_REJECTED;
   },
 
   get isActiveWindow() {
     let mostRecentNonPopupWindow =
       RecentWindow.getMostRecentBrowserWindow({allowPopups: false});
     return window == mostRecentNonPopupWindow;
   },
 
--- a/browser/base/content/browser-menubar.inc
+++ b/browser/base/content/browser-menubar.inc
@@ -464,16 +464,21 @@
                         accesskey="&syncSetup.accesskey;"
                         observes="sync-setup-state"
                         oncommand="gSyncUI.openSetup()"/>
               <menuitem id="sync-syncnowitem"
                         label="&syncSyncNowItem.label;"
                         accesskey="&syncSyncNowItem.accesskey;"
                         observes="sync-syncnow-state"
                         oncommand="gSyncUI.doSync(event);"/>
+              <menuitem id="sync-reauthitem"
+                        label="&syncReAuthItem.label;"
+                        accesskey="&syncReAuthItem.accesskey;"
+                        observes="sync-reauth-state"
+                        oncommand="gSyncUI.openSignInAgainPage();"/>
 #endif
               <menuseparator id="devToolsSeparator"/>
               <menu id="webDeveloperMenu"
                     label="&webDeveloperMenu.label;"
                     accesskey="&webDeveloperMenu.accesskey;">
                 <menupopup id="menuWebDeveloperPopup">
                   <menuitem id="menu_devToolbox"
                             observes="devtoolsMenuBroadcaster_DevToolbox"
--- a/browser/base/content/browser-sets.inc
+++ b/browser/base/content/browser-sets.inc
@@ -172,16 +172,17 @@
     <broadcaster id="isImage"/>
     <broadcaster id="isFrameImage"/>
     <broadcaster id="singleFeedMenuitemState" disabled="true"/>
     <broadcaster id="multipleFeedsMenuState" hidden="true"/>
     <broadcaster id="tabviewGroupsNumber" groups="1"/>
 #ifdef MOZ_SERVICES_SYNC
     <broadcaster id="sync-setup-state"/>
     <broadcaster id="sync-syncnow-state"/>
+    <broadcaster id="sync-reauth-state"/>
 #endif
     <broadcaster id="workOfflineMenuitemState"/>
     <broadcaster id="socialSidebarBroadcaster" hidden="true"/>
 
     <!-- DevTools broadcasters -->
     <broadcaster id="devtoolsMenuBroadcaster_DevToolbox"
                  label="&devToolboxMenuItem.label;"
                  type="checkbox" autocheck="false"
--- a/browser/base/content/browser-syncui.js
+++ b/browser/base/content/browser-syncui.js
@@ -91,20 +91,34 @@ let gSyncUI = {
     let firstSync = "";
     try {
       firstSync = Services.prefs.getCharPref("services.sync.firstSync");
     } catch (e) { }
     return Weave.Status.checkSetup() == Weave.CLIENT_NOT_CONFIGURED ||
            firstSync == "notReady";
   },
 
+  _loginFailed: function () {
+    // Referencing Weave.Service will implicitly initialize sync, and we don't
+    // want to force that - so first check if it is ready.
+    let service = Cc["@mozilla.org/weave/service;1"]
+                  .getService(Components.interfaces.nsISupports)
+                  .wrappedJSObject;
+    if (!service.ready) {
+      return false;
+    }
+    return Weave.Status.login == Weave.LOGIN_FAILED_LOGIN_REJECTED;
+  },
+
   updateUI: function SUI_updateUI() {
     let needsSetup = this._needsSetup();
-    document.getElementById("sync-setup-state").hidden = !needsSetup;
-    document.getElementById("sync-syncnow-state").hidden = needsSetup;
+    let loginFailed = this._loginFailed();
+    document.getElementById("sync-setup-state").hidden = loginFailed || !needsSetup;
+    document.getElementById("sync-syncnow-state").hidden = loginFailed || needsSetup;
+    document.getElementById("sync-reauth-state").hidden = !loginFailed;
 
     if (!gBrowser)
       return;
 
     let syncButton = document.getElementById("sync-button");
     let panelHorizontalButton = document.getElementById("PanelUI-fxa-status");
     [syncButton, panelHorizontalButton].forEach(function(button) {
       if (!button)
@@ -333,16 +347,19 @@ let gSyncUI = {
         "chrome://browser/content/sync/quota.xul", "",
         "centerscreen,chrome,dialog,modal");
   },
 
   openPrefs: function SUI_openPrefs() {
     openPreferences("paneSync");
   },
 
+  openSignInAgainPage: function () {
+    switchToTabHavingURI("about:accounts?action=reauth", true);
+  },
 
   // Helpers
   _updateLastSyncTime: function SUI__updateLastSyncTime() {
     if (!gBrowser)
       return;
 
     let syncButton = document.getElementById("sync-button");
     if (!syncButton)
--- a/browser/base/content/test/general/browser_get_user_media.js
+++ b/browser/base/content/test/general/browser_get_user_media.js
@@ -17,17 +17,17 @@ XPCOMUtils.defineLazyServiceGetter(this,
 var gObservedTopics = {};
 function observer(aSubject, aTopic, aData) {
   if (!(aTopic in gObservedTopics))
     gObservedTopics[aTopic] = 1;
   else
     ++gObservedTopics[aTopic];
 }
 
-function promiseNotification(aTopic, aAction) {
+function promiseObserverCalled(aTopic, aAction) {
   let deferred = Promise.defer();
 
   Services.obs.addObserver(function observer() {
     ok(true, "got " + aTopic + " notification");
     Services.obs.removeObserver(observer, aTopic);
 
     if (kObservedTopics.indexOf(aTopic) != -1) {
       if (!(aTopic in gObservedTopics))
@@ -40,23 +40,23 @@ function promiseNotification(aTopic, aAc
   }, aTopic, false);
 
   if (aAction)
     aAction();
 
   return deferred.promise;
 }
 
-function expectNotification(aTopic) {
+function expectObserverCalled(aTopic) {
   is(gObservedTopics[aTopic], 1, "expected notification " + aTopic);
   if (aTopic in gObservedTopics)
     --gObservedTopics[aTopic];
 }
 
-function expectNoNotifications() {
+function expectNoObserverCalled() {
   for (let topic in gObservedTopics) {
     if (gObservedTopics[topic])
       is(gObservedTopics[topic], 0, topic + " notification unexpected");
   }
   gObservedTopics = {}
 }
 
 function promiseMessage(aMessage, aAction) {
@@ -72,45 +72,43 @@ function promiseMessage(aMessage, aActio
   });
 
   if (aAction)
     aAction();
 
   return deferred.promise;
 }
 
+function promisePopupNotificationShown(aName, aAction) {
+  let deferred = Promise.defer();
 
-function promisePopupNotification(aName, aShown) {
+  PopupNotifications.panel.addEventListener("popupshown", function popupNotifShown() {
+    PopupNotifications.panel.removeEventListener("popupshown", popupNotifShown);
+
+    ok(!!PopupNotifications.getNotification(aName), aName + " notification shown");
+    ok(!!PopupNotifications.panel.firstChild, "notification panel populated");
+
+    deferred.resolve();
+  });
+
+  if (aAction)
+    aAction();
+
+  return deferred.promise;
+}
+
+function promisePopupNotification(aName) {
   let deferred = Promise.defer();
 
   waitForCondition(() => PopupNotifications.getNotification(aName),
                    () => {
-    let notification = PopupNotifications.getNotification(aName);
-    ok(!!notification, aName + " notification appeared");
-
-    if (!notification || !aShown) {
-      deferred.resolve();
-      return;
-    }
+    ok(!!PopupNotifications.getNotification(aName),
+       aName + " notification appeared");
 
-    // If aShown is true, the notification is expected to be opened by
-    // default, so we check that the panel has been populated.
-    if (PopupNotifications.panel.firstChild) {
-      ok(true, "notification panel populated");
-      deferred.resolve();
-    }
-    else {
-      todo(false, "We shouldn't have to force re-open the panel, see bug 976544");
-      notification.reshow();
-      waitForCondition(() => PopupNotifications.panel.firstChild,
-                       () => {
-        ok(PopupNotifications.panel.firstChild, "notification panel populated");
-        deferred.resolve();
-      }, "timeout waiting for notification to be reshown");
-    }
+    deferred.resolve();
   }, "timeout waiting for popup notification " + aName);
 
   return deferred.promise;
 }
 
 function promiseNoPopupNotification(aName) {
   let deferred = Promise.defer();
 
@@ -165,27 +163,27 @@ function getMediaCaptureState() {
   if (hasVideo.value)
     return "Camera";
   if (hasAudio.value)
     return "Microphone";
   return "none";
 }
 
 function closeStream(aAlreadyClosed) {
-  expectNoNotifications();
+  expectNoObserverCalled();
 
   info("closing the stream");
   content.wrappedJSObject.closeStream();
 
   if (!aAlreadyClosed)
-    yield promiseNotification("recording-device-events");
+    yield promiseObserverCalled("recording-device-events");
 
   yield promiseNoPopupNotification("webRTC-sharingDevices");
   if (!aAlreadyClosed)
-    expectNotification("recording-window-ended");
+    expectObserverCalled("recording-window-ended");
 
   let statusButton = document.getElementById("webrtc-status-button");
   ok(statusButton.hidden, "WebRTC status button hidden");
 }
 
 function checkDeviceSelectors(aAudio, aVideo) {
   let micSelector = document.getElementById("webRTC-selectMicrophone");
   if (aAudio)
@@ -216,261 +214,255 @@ function checkNotSharing() {
   ok(statusButton.hidden, "WebRTC status button hidden");
 }
 
 let gTests = [
 
 {
   desc: "getUserMedia audio+video",
   run: function checkAudioVideo() {
-    yield promiseNotification("getUserMedia:request", () => {
+    yield promisePopupNotificationShown("webRTC-shareDevices", () => {
       info("requesting devices");
       content.wrappedJSObject.requestDevice(true, true);
     });
+    expectObserverCalled("getUserMedia:request");
 
-    yield promisePopupNotification("webRTC-shareDevices", true);
     is(PopupNotifications.getNotification("webRTC-shareDevices").anchorID,
        "webRTC-shareDevices-notification-icon", "anchored to device icon");
     checkDeviceSelectors(true, true);
     is(PopupNotifications.panel.firstChild.getAttribute("popupid"),
        "webRTC-shareDevices", "panel using devices icon");
 
     yield promiseMessage("ok", () => {
       PopupNotifications.panel.firstChild.button.click();
     });
-    expectNotification("getUserMedia:response:allow");
-    expectNotification("recording-device-events");
+    expectObserverCalled("getUserMedia:response:allow");
+    expectObserverCalled("recording-device-events");
     is(getMediaCaptureState(), "CameraAndMicrophone",
        "expected camera and microphone to be shared");
 
     yield checkSharingUI();
     yield closeStream();
   }
 },
 
 {
   desc: "getUserMedia audio only",
   run: function checkAudioOnly() {
-    yield promiseNotification("getUserMedia:request", () => {
+    yield promisePopupNotificationShown("webRTC-shareDevices", () => {
       info("requesting devices");
       content.wrappedJSObject.requestDevice(true);
     });
+    expectObserverCalled("getUserMedia:request");
 
-    yield promisePopupNotification("webRTC-shareDevices", true);
     is(PopupNotifications.getNotification("webRTC-shareDevices").anchorID,
        "webRTC-shareMicrophone-notification-icon", "anchored to mic icon");
     checkDeviceSelectors(true);
     is(PopupNotifications.panel.firstChild.getAttribute("popupid"),
        "webRTC-shareMicrophone", "panel using microphone icon");
 
     yield promiseMessage("ok", () => {
       PopupNotifications.panel.firstChild.button.click();
     });
-    expectNotification("getUserMedia:response:allow");
-    expectNotification("recording-device-events");
+    expectObserverCalled("getUserMedia:response:allow");
+    expectObserverCalled("recording-device-events");
     is(getMediaCaptureState(), "Microphone", "expected microphone to be shared");
 
     yield checkSharingUI();
     yield closeStream();
   }
 },
 
 {
   desc: "getUserMedia video only",
   run: function checkVideoOnly() {
-    yield promiseNotification("getUserMedia:request", () => {
+    yield promisePopupNotificationShown("webRTC-shareDevices", () => {
       info("requesting devices");
       content.wrappedJSObject.requestDevice(false, true);
     });
+    expectObserverCalled("getUserMedia:request");
 
-    yield promisePopupNotification("webRTC-shareDevices", true);
     is(PopupNotifications.getNotification("webRTC-shareDevices").anchorID,
        "webRTC-shareDevices-notification-icon", "anchored to device icon");
     checkDeviceSelectors(false, true);
     is(PopupNotifications.panel.firstChild.getAttribute("popupid"),
        "webRTC-shareDevices", "panel using devices icon");
 
     yield promiseMessage("ok", () => {
       PopupNotifications.panel.firstChild.button.click();
     });
-    expectNotification("getUserMedia:response:allow");
-    expectNotification("recording-device-events");
+    expectObserverCalled("getUserMedia:response:allow");
+    expectObserverCalled("recording-device-events");
     is(getMediaCaptureState(), "Camera", "expected camera to be shared");
 
     yield checkSharingUI();
     yield closeStream();
   }
 },
 
 {
   desc: "getUserMedia audio+video, user disables video",
   run: function checkDisableVideo() {
-    yield promiseNotification("getUserMedia:request", () => {
+    yield promisePopupNotificationShown("webRTC-shareDevices", () => {
       info("requesting devices");
       content.wrappedJSObject.requestDevice(true, true);
     });
-
-    yield promisePopupNotification("webRTC-shareDevices", true);
+    expectObserverCalled("getUserMedia:request");
     checkDeviceSelectors(true, true);
 
     // disable the camera
     document.getElementById("webRTC-selectCamera-menulist").value = -1;
 
     yield promiseMessage("ok", () => {
       PopupNotifications.panel.firstChild.button.click();
     });
 
     // reset the menuitem to have no impact on the following tests.
     document.getElementById("webRTC-selectCamera-menulist").value = 0;
 
-    expectNotification("getUserMedia:response:allow");
-    expectNotification("recording-device-events");
+    expectObserverCalled("getUserMedia:response:allow");
+    expectObserverCalled("recording-device-events");
     is(getMediaCaptureState(), "Microphone",
        "expected microphone to be shared");
 
     yield checkSharingUI();
     yield closeStream();
   }
 },
 
 {
   desc: "getUserMedia audio+video, user disables audio",
   run: function checkDisableAudio() {
-    yield promiseNotification("getUserMedia:request", () => {
+    yield promisePopupNotificationShown("webRTC-shareDevices", () => {
       info("requesting devices");
       content.wrappedJSObject.requestDevice(true, true);
     });
-
-    yield promisePopupNotification("webRTC-shareDevices", true);
+    expectObserverCalled("getUserMedia:request");
     checkDeviceSelectors(true, true);
 
     // disable the microphone
     document.getElementById("webRTC-selectMicrophone-menulist").value = -1;
 
     yield promiseMessage("ok", () => {
       PopupNotifications.panel.firstChild.button.click();
     });
 
     // reset the menuitem to have no impact on the following tests.
     document.getElementById("webRTC-selectMicrophone-menulist").value = 0;
 
-    expectNotification("getUserMedia:response:allow");
-    expectNotification("recording-device-events");
+    expectObserverCalled("getUserMedia:response:allow");
+    expectObserverCalled("recording-device-events");
     is(getMediaCaptureState(), "Camera",
        "expected microphone to be shared");
 
     yield checkSharingUI();
     yield closeStream();
   }
 },
 
 {
   desc: "getUserMedia audio+video, user disables both audio and video",
   run: function checkDisableAudioVideo() {
-    yield promiseNotification("getUserMedia:request", () => {
+    yield promisePopupNotificationShown("webRTC-shareDevices", () => {
       info("requesting devices");
       content.wrappedJSObject.requestDevice(true, true);
     });
-
-    yield promisePopupNotification("webRTC-shareDevices", true);
+    expectObserverCalled("getUserMedia:request");
     checkDeviceSelectors(true, true);
 
     // disable the camera and microphone
     document.getElementById("webRTC-selectCamera-menulist").value = -1;
     document.getElementById("webRTC-selectMicrophone-menulist").value = -1;
 
     yield promiseMessage("error: PERMISSION_DENIED", () => {
       PopupNotifications.panel.firstChild.button.click();
     });
 
     // reset the menuitems to have no impact on the following tests.
     document.getElementById("webRTC-selectCamera-menulist").value = 0;
     document.getElementById("webRTC-selectMicrophone-menulist").value = 0;
 
-    expectNotification("getUserMedia:response:deny");
-    expectNotification("recording-window-ended");
+    expectObserverCalled("getUserMedia:response:deny");
+    expectObserverCalled("recording-window-ended");
     checkNotSharing();
   }
 },
 
 {
   desc: "getUserMedia audio+video, user clicks \"Don't Share\"",
   run: function checkDontShare() {
-    yield promiseNotification("getUserMedia:request", () => {
+    yield promisePopupNotificationShown("webRTC-shareDevices", () => {
       info("requesting devices");
       content.wrappedJSObject.requestDevice(true, true);
     });
-
-    yield promisePopupNotification("webRTC-shareDevices", true);
+    expectObserverCalled("getUserMedia:request");
     checkDeviceSelectors(true, true);
 
     yield promiseMessage("error: PERMISSION_DENIED", () => {
       activateSecondaryAction(kActionDeny);
     });
 
-    expectNotification("getUserMedia:response:deny");
-    expectNotification("recording-window-ended");
+    expectObserverCalled("getUserMedia:response:deny");
+    expectObserverCalled("recording-window-ended");
     checkNotSharing();
   }
 },
 
 {
   desc: "getUserMedia audio+video: stop sharing",
   run: function checkStopSharing() {
-    yield promiseNotification("getUserMedia:request", () => {
+    yield promisePopupNotificationShown("webRTC-shareDevices", () => {
       info("requesting devices");
       content.wrappedJSObject.requestDevice(true, true);
     });
-
-    yield promisePopupNotification("webRTC-shareDevices", true);
+    expectObserverCalled("getUserMedia:request");
     checkDeviceSelectors(true, true);
 
     yield promiseMessage("ok", () => {
       PopupNotifications.panel.firstChild.button.click();
     });
-    expectNotification("getUserMedia:response:allow");
-    expectNotification("recording-device-events");
+    expectObserverCalled("getUserMedia:response:allow");
+    expectObserverCalled("recording-device-events");
     is(getMediaCaptureState(), "CameraAndMicrophone",
        "expected camera and microphone to be shared");
 
     yield checkSharingUI();
 
     PopupNotifications.getNotification("webRTC-sharingDevices").reshow();
     activateSecondaryAction(kActionDeny);
 
-    yield promiseNotification("recording-device-events");
-    expectNotification("getUserMedia:revoke");
+    yield promiseObserverCalled("recording-device-events");
+    expectObserverCalled("getUserMedia:revoke");
 
     yield promiseNoPopupNotification("webRTC-sharingDevices");
 
     if (gObservedTopics["recording-device-events"] == 1) {
       todo(false, "Got the 'recording-device-events' notification twice, likely because of bug 962719");
       gObservedTopics["recording-device-events"] = 0;
     }
 
-    expectNoNotifications();
+    expectNoObserverCalled();
     checkNotSharing();
 
     // the stream is already closed, but this will do some cleanup anyway
     yield closeStream(true);
   }
 },
 
 {
   desc: "getUserMedia prompt: Always/Never Share",
   run: function checkRememberCheckbox() {
     let elt = id => document.getElementById(id);
 
     function checkPerm(aRequestAudio, aRequestVideo, aAllowAudio, aAllowVideo,
                        aExpectedAudioPerm, aExpectedVideoPerm, aNever) {
-      yield promiseNotification("getUserMedia:request", () => {
+      yield promisePopupNotificationShown("webRTC-shareDevices", () => {
         content.wrappedJSObject.requestDevice(aRequestAudio, aRequestVideo);
       });
-
-      yield promisePopupNotification("webRTC-shareDevices", true);
+      expectObserverCalled("getUserMedia:request");
 
       let noAudio = aAllowAudio === undefined;
       is(elt("webRTC-selectMicrophone").hidden, noAudio,
          "microphone selector expected to be " + (noAudio ? "hidden" : "visible"));
       if (!noAudio)
         elt("webRTC-selectMicrophone-menulist").value = (aAllowAudio || aNever) ? 0 : -1;
 
       let noVideo = aAllowVideo === undefined;
@@ -481,27 +473,27 @@ let gTests = [
 
       let expectedMessage =
         (aAllowVideo || aAllowAudio) ? "ok" : "error: PERMISSION_DENIED";
       yield promiseMessage(expectedMessage, () => {
         activateSecondaryAction(aNever ? kActionNever : kActionAlways);
       });
       let expected = [];
       if (expectedMessage == "ok") {
-        expectNotification("getUserMedia:response:allow");
-        expectNotification("recording-device-events");
+        expectObserverCalled("getUserMedia:response:allow");
+        expectObserverCalled("recording-device-events");
         if (aAllowVideo)
           expected.push("Camera");
         if (aAllowAudio)
           expected.push("Microphone");
         expected = expected.join("And");
       }
       else {
-        expectNotification("getUserMedia:response:deny");
-        expectNotification("recording-window-ended");
+        expectObserverCalled("getUserMedia:response:deny");
+        expectObserverCalled("recording-window-ended");
         expected = "none";
       }
       is(getMediaCaptureState(), expected,
          "expected " + expected + " to be shared");
 
       function checkDevicePermissions(aDevice, aExpected) {
         let Perms = Services.perms;
         let uri = content.document.documentURIObject;
@@ -576,48 +568,48 @@ let gTests = [
       }
 
       let gum = function() {
         content.wrappedJSObject.requestDevice(aRequestAudio, aRequestVideo);
       };
 
       if (aExpectStream === undefined) {
         // Check that we get a prompt.
-        yield promiseNotification("getUserMedia:request", gum);
-        yield promisePopupNotification("webRTC-shareDevices", true);
+        yield promisePopupNotificationShown("webRTC-shareDevices", gum);
+        expectObserverCalled("getUserMedia:request");
 
         // Deny the request to cleanup...
         yield promiseMessage("error: PERMISSION_DENIED", () => {
           activateSecondaryAction(kActionDeny);
         });
-        expectNotification("getUserMedia:response:deny");
-        expectNotification("recording-window-ended");
+        expectObserverCalled("getUserMedia:response:deny");
+        expectObserverCalled("recording-window-ended");
       }
       else {
         let allow = (aAllowVideo && aRequestVideo) || (aAllowAudio && aRequestAudio);
         let expectedMessage = allow ? "ok" : "error: PERMISSION_DENIED";
         yield promiseMessage(expectedMessage, gum);
 
         if (expectedMessage == "ok") {
-          expectNotification("recording-device-events");
+          expectObserverCalled("recording-device-events");
 
           // Check what's actually shared.
           let expected = [];
           if (aAllowVideo && aRequestVideo)
             expected.push("Camera");
           if (aAllowAudio && aRequestAudio)
             expected.push("Microphone");
           expected = expected.join("And");
           is(getMediaCaptureState(), expected,
              "expected " + expected + " to be shared");
 
           yield closeStream();
         }
         else {
-          expectNotification("recording-window-ended");
+          expectObserverCalled("recording-window-ended");
         }
       }
 
       Perms.remove(uri.host, "camera");
       Perms.remove(uri.host, "microphone");
     }
 
     // Set both permissions identically
@@ -686,33 +678,33 @@ let gTests = [
       // Initially set both permissions to 'allow'.
       Perms.add(uri, "microphone", Perms.ALLOW_ACTION);
       Perms.add(uri, "camera", Perms.ALLOW_ACTION);
 
       // Start sharing what's been requested.
       yield promiseMessage("ok", () => {
         content.wrappedJSObject.requestDevice(aRequestAudio, aRequestVideo);
       });
-      expectNotification("recording-device-events");
+      expectObserverCalled("recording-device-events");
       yield checkSharingUI();
 
       PopupNotifications.getNotification("webRTC-sharingDevices").reshow();
       let expectedIcon = "webRTC-sharingDevices";
       if (aRequestAudio && !aRequestVideo)
         expectedIcon = "webRTC-sharingMicrophone";
       is(PopupNotifications.getNotification("webRTC-sharingDevices").anchorID,
          expectedIcon + "-notification-icon", "anchored to correct icon");
       is(PopupNotifications.panel.firstChild.getAttribute("popupid"), expectedIcon,
          "panel using correct icon");
 
       // Stop sharing.
       activateSecondaryAction(kActionDeny);
 
-      yield promiseNotification("recording-device-events");
-      expectNotification("getUserMedia:revoke");
+      yield promiseObserverCalled("recording-device-events");
+      expectObserverCalled("getUserMedia:revoke");
 
       yield promiseNoPopupNotification("webRTC-sharingDevices");
 
       if (gObservedTopics["recording-device-events"] == 1) {
         todo(false, "Got the 'recording-device-events' notification twice, likely because of bug 962719");
         gObservedTopics["recording-device-events"] = 0;
       }
 
@@ -761,17 +753,17 @@ function test() {
     Services.prefs.setBoolPref(PREF_PERMISSION_FAKE, true);
 
     Task.spawn(function () {
       for (let test of gTests) {
         info(test.desc);
         yield test.run();
 
         // Cleanup before the next test
-        expectNoNotifications();
+        expectNoObserverCalled();
       }
     }).then(finish, ex => {
      ok(false, "Unexpected Exception: " + ex);
      finish();
     });
   }, true);
   let rootDir = getRootDirectory(gTestPath)
   rootDir = rootDir.replace("chrome://mochitests/content/",
--- a/browser/components/customizableui/content/panelUI.xml
+++ b/browser/components/customizableui/content/panelUI.xml
@@ -261,28 +261,35 @@
 
       <method name="handleEvent">
         <parameter name="aEvent"/>
         <body><![CDATA[
           if (aEvent.type.startsWith("popup") && aEvent.target != this._panel) {
             // Shouldn't act on e.g. context menus being shown from within the panel.
             return;
           }
-          switch(aEvent.type) {
+          switch (aEvent.type) {
             case "click":
               if (aEvent.originalTarget == this._clickCapturer) {
                 this.showMainView();
               }
               break;
             case "overflow":
-              // Resize the right view on the next tick.
-              if (this.showingSubView) {
-                setTimeout(this._syncContainerWithSubView.bind(this), 0);
-              } else if (!this.transitioning) {
-                setTimeout(this._syncContainerWithMainView.bind(this), 0);
+              switch (aEvent.target.localName) {
+                case "vbox":
+                  // Resize the right view on the next tick.
+                  if (this.showingSubView) {
+                    setTimeout(this._syncContainerWithSubView.bind(this), 0);
+                  } else if (!this.transitioning) {
+                    setTimeout(this._syncContainerWithMainView.bind(this), 0);
+                  }
+                  break;
+                case "toolbarbutton":
+                  aEvent.target.setAttribute("fadelabel", "true");
+                  break;
               }
               break;
             case "popupshowing":
               this.setAttribute("panelopen", "true");
               // Bug 941196 - The panel can get taller when opening a subview. Disabling
               // autoPositioning means that the panel won't jump around if an opened
               // subview causes the panel to exceed the dimensions of the screen in the
               // direction that the panel originally opened in. This property resets
--- a/browser/components/downloads/content/contentAreaDownloadsView.xul
+++ b/browser/components/downloads/content/contentAreaDownloadsView.xul
@@ -33,13 +33,14 @@
 #ifdef XP_MACOSX
     <key id="key_delete2" keycode="VK_BACK" command="cmd_delete"/>
 #endif
   </keyset>
 
   <stack flex="1">
     <richlistbox id="downloadsRichListBox"/>
     <description id="downloadsListEmptyDescription"
-                 value="&downloadsListEmpty.label;"/>
+                 value="&downloadsListEmpty.label;"
+                 mousethrough="always"/>
   </stack>
   <commandset id="downloadCommands"/>
   <menupopup id="downloadsContextMenu"/>
 </window>
--- a/browser/components/preferences/sync.js
+++ b/browser/components/preferences/sync.js
@@ -129,21 +129,24 @@ let gSyncPane = {
         let enginesListDisabled;
         // Not Verfied implies login error state, so check that first.
         if (!data.verified) {
           fxaLoginStatus.selectedIndex = FXA_LOGIN_UNVERIFIED;
           enginesListDisabled = true;
         // So we think we are logged in, so login problems are next.
         // (Although if the Sync identity manager is still initializing, we
         // ignore login errors and assume all will eventually be good.)
-        } else if (Weave.Service.identity.readyToAuthenticate &&
-                   Weave.Status.login != Weave.LOGIN_SUCCEEDED) {
+        // LOGIN_FAILED_LOGIN_REJECTED explicitly means "you must log back in".
+        // All other login failures are assumed to be transient and should go
+        // away by themselves, so aren't reflected here.
+        } else if (Weave.Status.login == Weave.LOGIN_FAILED_LOGIN_REJECTED) {
           fxaLoginStatus.selectedIndex = FXA_LOGIN_FAILED;
           enginesListDisabled = true;
-        // Else we must be golden!
+        // Else we must be golden (or in an error state we expect to magically
+        // resolve itself)
         } else {
           fxaLoginStatus.selectedIndex = FXA_LOGIN_VERIFIED;
           enginesListDisabled = false;
         }
         document.getElementById("fxaEmailAddress1").textContent = data.email;
         document.getElementById("fxaEmailAddress2").textContent = data.email;
         document.getElementById("fxaEmailAddress3").textContent = data.email;
         document.getElementById("fxaSyncComputerName").value = Weave.Service.clientsEngine.localName;
--- a/browser/locales/en-US/chrome/browser/browser.dtd
+++ b/browser/locales/en-US/chrome/browser/browser.dtd
@@ -655,16 +655,18 @@ just addresses the organization to follo
 <!ENTITY syncTabsMenu2.label     "Tabs From Other Devices">
 
 <!ENTITY syncBrand.shortName.label    "Sync">
 
 <!ENTITY syncSetup.label              "Set Up &syncBrand.shortName.label;…">
 <!ENTITY syncSetup.accesskey          "Y">
 <!ENTITY syncSyncNowItem.label        "Sync Now">
 <!ENTITY syncSyncNowItem.accesskey    "S">
+<!ENTITY syncReAuthItem.label         "Reconnect to &syncBrand.shortName.label;…">
+<!ENTITY syncReAuthItem.accesskey     "R">
 <!ENTITY syncToolbarButton.label      "Sync">
 
 <!ENTITY socialToolbar.title        "Social Toolbar Button">
 
 <!ENTITY social.ok.label       "OK">
 <!ENTITY social.ok.accesskey   "O">
 
 <!ENTITY social.toggleSidebar.label "Show sidebar">
--- a/browser/modules/UITour.jsm
+++ b/browser/modules/UITour.jsm
@@ -50,16 +50,17 @@ this.UITour = {
   originTabs: new WeakMap(),
   /* Map from browser windows to a set of pinned tabs opened by (a) tour(s) */
   pinnedTabs: new WeakMap(),
   urlbarCapture: new WeakMap(),
   appMenuOpenForAnnotation: new Set(),
   availableTargetsCache: new WeakMap(),
 
   _detachingTab: false,
+  _annotationPanelMutationObservers: new WeakMap(),
   _queuedEvents: [],
   _pendingDoc: null,
 
   highlightEffects: ["random", "wobble", "zoom", "color"],
   targets: new Map([
     ["accountStatus", {
       query: (aDocument) => {
         let statusButton = aDocument.getElementById("PanelUI-fxa-status");
@@ -319,17 +320,22 @@ this.UITour = {
                 buttons.push(button);
 
                 if (buttons.length == MAX_BUTTONS)
                   break;
               }
             }
           }
 
-          this.showInfo(contentDocument, target, data.title, data.text, iconURL, buttons);
+          let infoOptions = {};
+
+          if (typeof data.closeButtonCallbackID == "string")
+            infoOptions.closeButtonCallbackID = data.closeButtonCallbackID;
+
+          this.showInfo(contentDocument, target, data.title, data.text, iconURL, buttons, infoOptions);
         }).then(null, Cu.reportError);
         break;
       }
 
       case "hideInfo": {
         this.hideInfo(window);
         break;
       }
@@ -502,24 +508,16 @@ this.UITour = {
 
       case "input": {
         if (aEvent.target.id == "urlbar") {
           let window = aEvent.target.ownerDocument.defaultView;
           this.handleUrlbarInput(window);
         }
         break;
       }
-
-      case "command": {
-        if (aEvent.target.id == "UITourTooltipClose") {
-          let window = aEvent.target.ownerDocument.defaultView;
-          this.hideInfo(window);
-        }
-        break;
-      }
     }
   },
 
   setTelemetryBucket: function(aPageID) {
     let bucket = BUCKET_NAME + BrowserUITelemetry.BUCKET_SEPARATOR + aPageID;
     BrowserUITelemetry.setBucket(bucket);
   },
 
@@ -837,16 +835,18 @@ this.UITour = {
       let paddingLeftPx = 0 - parseFloat(containerStyle.paddingLeft);
       let highlightStyle = highlightWindow.getComputedStyle(highlighter);
       let highlightHeightWithMin = Math.max(highlightHeight, parseFloat(highlightStyle.minHeight));
       let highlightWidthWithMin = Math.max(highlightWidth, parseFloat(highlightStyle.minWidth));
       let offsetX = paddingTopPx
                       - (Math.max(0, highlightWidthWithMin - targetRect.width) / 2);
       let offsetY = paddingLeftPx
                       - (Math.max(0, highlightHeightWithMin - targetRect.height) / 2);
+
+      this._addAnnotationPanelMutationObserver(highlighter.parentElement);
       highlighter.parentElement.openPopup(aTargetEl, "overlap", offsetX, offsetY);
     }
 
     // Prevent showing a panel at an undefined position.
     if (!this.isElementVisible(aTarget.node))
       return;
 
     this._setAppMenuStateForAnnotation(aTarget.node.ownerDocument.defaultView, "highlight",
@@ -855,23 +855,37 @@ this.UITour = {
   },
 
   hideHighlight: function(aWindow) {
     let tabData = this.pinnedTabs.get(aWindow);
     if (tabData && !tabData.sticky)
       this.removePinnedTab(aWindow);
 
     let highlighter = aWindow.document.getElementById("UITourHighlight");
+    this._removeAnnotationPanelMutationObserver(highlighter.parentElement);
     highlighter.parentElement.hidePopup();
     highlighter.removeAttribute("active");
 
     this._setAppMenuStateForAnnotation(aWindow, "highlight", false);
   },
 
-  showInfo: function(aContentDocument, aAnchor, aTitle = "", aDescription = "", aIconURL = "", aButtons = []) {
+  /**
+   * Show an info panel.
+   *
+   * @param {Document} aContentDocument
+   * @param {Node}     aAnchor
+   * @param {String}   [aTitle=""]
+   * @param {String}   [aDescription=""]
+   * @param {String}   [aIconURL=""]
+   * @param {Object[]} [aButtons=[]]
+   * @param {Object}   [aOptions={}]
+   * @param {String}   [aOptions.closeButtonCallbackID]
+   */
+  showInfo: function(aContentDocument, aAnchor, aTitle = "", aDescription = "", aIconURL = "",
+                     aButtons = [], aOptions = {}) {
     function showInfoPanel(aAnchorEl) {
       aAnchorEl.focus();
 
       let document = aAnchorEl.ownerDocument;
       let tooltip = document.getElementById("UITourTooltip");
       let tooltipTitle = document.getElementById("UITourTooltipTitle");
       let tooltipDesc = document.getElementById("UITourTooltipDescription");
       let tooltipIcon = document.getElementById("UITourTooltipIcon");
@@ -905,37 +919,48 @@ this.UITour = {
         });
 
         tooltipButtons.appendChild(el);
       }
 
       tooltipButtons.hidden = !aButtons.length;
 
       let tooltipClose = document.getElementById("UITourTooltipClose");
-      tooltipClose.addEventListener("command", this);
+      let closeButtonCallback = (event) => {
+        this.hideInfo(document.defaultView);
+        if (aOptions && aOptions.closeButtonCallbackID)
+          this.sendPageCallback(aContentDocument, aOptions.closeButtonCallbackID);
+      };
+      tooltipClose.addEventListener("command", closeButtonCallback);
+      tooltip.addEventListener("popuphiding", function tooltipHiding(event) {
+        tooltip.removeEventListener("popuphiding", tooltipHiding);
+        tooltipClose.removeEventListener("command", closeButtonCallback);
+      });
 
       tooltip.setAttribute("targetName", aAnchor.targetName);
       tooltip.hidden = false;
       let alignment = "bottomcenter topright";
+      this._addAnnotationPanelMutationObserver(tooltip);
       tooltip.openPopup(aAnchorEl, alignment);
     }
 
     // Prevent showing a panel at an undefined position.
     if (!this.isElementVisible(aAnchor.node))
       return;
 
     this._setAppMenuStateForAnnotation(aAnchor.node.ownerDocument.defaultView, "info",
                                        this.targetIsInAppMenu(aAnchor),
                                        showInfoPanel.bind(this, aAnchor.node));
   },
 
   hideInfo: function(aWindow) {
     let document = aWindow.document;
 
     let tooltip = document.getElementById("UITourTooltip");
+    this._removeAnnotationPanelMutationObserver(tooltip);
     tooltip.hidePopup();
     this._setAppMenuStateForAnnotation(aWindow, "info", false);
 
     let tooltipButtons = document.getElementById("UITourTooltipButtons");
     while (tooltipButtons.firstChild)
       tooltipButtons.firstChild.remove();
   },
 
@@ -1105,11 +1130,52 @@ this.UITour = {
       this.sendPageCallback(aContentDocument, aCallbackID, data);
     }, (err) => {
       Cu.reportError(err);
       this.sendPageCallback(aContentDocument, aCallbackID, {
         targets: [],
       });
     });
   },
+
+  _addAnnotationPanelMutationObserver: function(aPanelEl) {
+#ifdef XP_LINUX
+    let observer = this._annotationPanelMutationObservers.get(aPanelEl);
+    if (observer) {
+      return;
+    }
+    let win = aPanelEl.ownerDocument.defaultView;
+    observer = new win.MutationObserver(this._annotationMutationCallback);
+    this._annotationPanelMutationObservers.set(aPanelEl, observer);
+    let observerOptions = {
+      attributeFilter: ["height", "width"],
+      attributes: true,
+    };
+    observer.observe(aPanelEl, observerOptions);
+#endif
+  },
+
+  _removeAnnotationPanelMutationObserver: function(aPanelEl) {
+#ifdef XP_LINUX
+    let observer = this._annotationPanelMutationObservers.get(aPanelEl);
+    if (observer) {
+      observer.disconnect();
+      this._annotationPanelMutationObservers.delete(aPanelEl);
+    }
+#endif
+  },
+
+/**
+ * Workaround for Ubuntu panel craziness in bug 970788 where incorrect sizes get passed to
+ * nsXULPopupManager::PopupResized and lead to incorrect width and height attributes getting
+ * set on the panel.
+ */
+  _annotationMutationCallback: function(aMutations) {
+    for (let mutation of aMutations) {
+      // Remove both attributes at once and ignore remaining mutations to be proccessed.
+      mutation.target.removeAttribute("width");
+      mutation.target.removeAttribute("height");
+      return;
+    }
+  },
 };
 
 this.UITour.init();
--- a/browser/modules/moz.build
+++ b/browser/modules/moz.build
@@ -15,26 +15,26 @@ EXTRA_JS_MODULES += [
     'Feeds.jsm',
     'NetworkPrioritizer.jsm',
     'offlineAppCache.jsm',
     'SharedFrame.jsm',
     'SignInToWebsite.jsm',
     'SitePermissions.jsm',
     'Social.jsm',
     'TabCrashReporter.jsm',
-    'UITour.jsm',
     'webappsUI.jsm',
     'webrtcUI.jsm',
 ]
 
 if CONFIG['MOZ_WIDGET_TOOLKIT'] == 'windows':
     EXTRA_JS_MODULES += [
         'WindowsJumpLists.jsm',
         'WindowsPreviewPerTab.jsm',
     ]
 
 EXTRA_PP_JS_MODULES += [
     'AboutHome.jsm',
     'RecentWindow.jsm',
+    'UITour.jsm',
 ]
 
 if CONFIG['MOZILLA_OFFICIAL']:
     DEFINES['MOZILLA_OFFICIAL'] = 1
--- a/browser/modules/test/browser.ini
+++ b/browser/modules/test/browser.ini
@@ -7,14 +7,15 @@ support-files =
 [browser_BrowserUITelemetry_buckets.js]
 [browser_NetworkPrioritizer.js]
 [browser_SignInToWebsite.js]
 [browser_UITour.js]
 skip-if = os == "linux" # Intermittent failures, bug 951965
 [browser_UITour2.js]
 [browser_UITour3.js]
 [browser_UITour_availableTargets.js]
+[browser_UITour_detach_tab.js]
+[browser_UITour_annotation_size_attributes.js]
 [browser_UITour_panel_close_annotation.js]
-[browser_UITour_detach_tab.js]
 [browser_UITour_registerPageID.js]
 [browser_UITour_sync.js]
 [browser_taskbar_preview.js]
 run-if = os == "win"
--- a/browser/modules/test/browser_UITour3.js
+++ b/browser/modules/test/browser_UITour3.js
@@ -117,9 +117,26 @@ let tests = [
       });
 
       EventUtils.synthesizeMouseAtCenter(buttons.childNodes[1], {}, window);
     });
 
     let buttons = gContentWindow.makeButtons();
     gContentAPI.showInfo("urlbar", "another title", "moar text", "./image.png", buttons);
   },
+
+  function test_info_close_button(done) {
+    let popup = document.getElementById("UITourTooltip");
+    let closeButton = document.getElementById("UITourTooltipClose");
+
+    popup.addEventListener("popupshown", function onPopupShown() {
+      popup.removeEventListener("popupshown", onPopupShown);
+      EventUtils.synthesizeMouseAtCenter(closeButton, {}, window);
+      executeSoon(function() {
+        is(gContentWindow.callbackResult, "closeButton", "Close button callback called");
+        done();
+      });
+    });
+
+    let infoOptions = gContentWindow.makeInfoOptions();
+    gContentAPI.showInfo("urlbar", "Close me", "X marks the spot", null, null, infoOptions);
+  }
 ];
new file mode 100644
--- /dev/null
+++ b/browser/modules/test/browser_UITour_annotation_size_attributes.js
@@ -0,0 +1,51 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Test that width and height attributes don't get set by widget code on the highlight panel.
+ */
+
+"use strict";
+
+let gTestTab;
+let gContentAPI;
+let gContentWindow;
+let highlight = document.getElementById("UITourHighlightContainer");
+let tooltip = document.getElementById("UITourTooltip");
+
+Components.utils.import("resource:///modules/UITour.jsm");
+
+function test() {
+  UITourTest();
+}
+
+let tests = [
+  function test_highlight_size_attributes(done) {
+    gContentAPI.showHighlight("appMenu");
+    waitForElementToBeVisible(highlight, function moveTheHighlight() {
+      gContentAPI.showHighlight("urlbar");
+      waitForElementToBeVisible(highlight, function checkPanelAttributes() {
+        SimpleTest.executeSoon(() => {
+          ise(highlight.height, "", "Highlight panel should have no explicit height set");
+          ise(highlight.width, "", "Highlight panel should have no explicit width set");
+          done();
+        });
+      }, "Highlight should be moved to the urlbar");
+    }, "Highlight should be shown after showHighlight() for the appMenu");
+  },
+
+  function test_info_size_attributes(done) {
+    gContentAPI.showInfo("appMenu", "test title", "test text");
+    waitForElementToBeVisible(tooltip, function moveTheTooltip() {
+      gContentAPI.showInfo("urlbar", "new title", "new text");
+      waitForElementToBeVisible(tooltip, function checkPanelAttributes() {
+        SimpleTest.executeSoon(() => {
+          ise(tooltip.height, "", "Info panel should have no explicit height set");
+          ise(tooltip.width, "", "Info panel should have no explicit width set");
+          done();
+        });
+      }, "Tooltip should be moved to the urlbar");
+    }, "Tooltip should be shown after showInfo() for the appMenu");
+  },
+
+];
--- a/browser/modules/test/browser_UITour_detach_tab.js
+++ b/browser/modules/test/browser_UITour_detach_tab.js
@@ -1,13 +1,13 @@
 /* Any copyright is dedicated to the Public Domain.
  * http://creativecommons.org/publicdomain/zero/1.0/ */
 
 /**
- * Tests that annotations disappear when their target is hidden.
+ * Detaching a tab to a new window shouldn't break the menu panel.
  */
 
 "use strict";
 
 let gTestTab;
 let gContentAPI;
 let gContentWindow;
 let gContentDoc;
--- a/browser/modules/test/uitour.html
+++ b/browser/modules/test/uitour.html
@@ -15,16 +15,22 @@
 
       // Defined in content to avoid weird issues when crossing between chrome/content.
       function makeButtons() {
         return [
           {label: "Button 1", callback: makeCallback("button1")},
           {label: "Button 2", callback: makeCallback("button2"), icon: "image.png"}
         ];
       }
+
+      function makeInfoOptions() {
+        return {
+          closeButtonCallback: makeCallback("closeButton")
+        };
+      }
     </script>
   </head>
   <body>
     <h1>UITour tests</h1>
     <p>Because Firefox is...</p>
     <p>Never gonna let you down</p>
     <p>Never gonna give you up</p>
   </body>
--- a/browser/modules/test/uitour.js
+++ b/browser/modules/test/uitour.js
@@ -72,35 +72,40 @@ if (typeof Mozilla == 'undefined') {
 			effect: effect
 		});
 	};
 
 	Mozilla.UITour.hideHighlight = function() {
 		_sendEvent('hideHighlight');
 	};
 
-	Mozilla.UITour.showInfo = function(target, title, text, icon, buttons) {
+	Mozilla.UITour.showInfo = function(target, title, text, icon, buttons, options) {
 		var buttonData = [];
 		if (Array.isArray(buttons)) {
 			for (var i = 0; i < buttons.length; i++) {
 				buttonData.push({
 					label: buttons[i].label,
 					icon: buttons[i].icon,
 					style: buttons[i].style,
 					callbackID: _waitForCallback(buttons[i].callback)
 			});
 			}
 		}
 
+		var closeButtonCallbackID;
+		if (options && options.closeButtonCallback)
+			closeButtonCallbackID = _waitForCallback(options.closeButtonCallback);
+
 		_sendEvent('showInfo', {
 			target: target,
 			title: title,
 			text: text,
 			icon: icon,
-			buttons: buttonData
+			buttons: buttonData,
+			closeButtonCallbackID: closeButtonCallbackID
 		});
 	};
 
 	Mozilla.UITour.hideInfo = function() {
 		_sendEvent('hideInfo');
 	};
 
 	Mozilla.UITour.previewTheme = function(theme) {
--- a/browser/themes/linux/customizableui/panelUIOverlay.css
+++ b/browser/themes/linux/customizableui/panelUIOverlay.css
@@ -12,14 +12,26 @@
   -moz-appearance: none;
   border: 0;
 }
 
 .widget-overflow-list .toolbarbutton-1 > .toolbarbutton-menubutton-dropmarker {
   -moz-margin-start: 0;
 }
 
+.subviewbutton > .toolbarbutton-text {
+  -moz-padding-start: 16px;
+}
+
+.subviewbutton:-moz-any([image],[targetURI],.cui-withicon, .restoreallitem, .bookmark-item) > .toolbarbutton-text {
+  -moz-padding-start: 0;
+}
+
+.subviewbutton.bookmark-item > .toolbarbutton-icon {
+  -moz-margin-start: 3px;
+}
+
 .PanelUI-subView toolbarseparator,
 .PanelUI-subView menuseparator,
 .cui-widget-panelview menuseparator,
 #PanelUI-footer-inner > toolbarseparator {
   -moz-appearance: none !important;
 }
--- a/browser/themes/osx/customizableui/panelUIOverlay.css
+++ b/browser/themes/osx/customizableui/panelUIOverlay.css
@@ -53,24 +53,54 @@
     -moz-image-region: rect(0, 64px, 32px, 32px);
   }
 
   #PanelUI-customize:hover:active,
   #PanelUI-help:not([disabled]):hover:active,
   #PanelUI-quit:not([disabled]):hover:active {
     -moz-image-region: rect(0, 96px, 32px, 64px);
   }
+
+  .subviewbutton[checked="true"] {
+    background-image: url("chrome://global/skin/menu/menu-check@2x.png");
+  }
+
 }
 
 .panelUI-grid .toolbarbutton-1 {
   margin-right: 0;
   margin-left: 0;
   margin-bottom: 0;
 }
 
+.subviewbutton > .toolbarbutton-text,
+.subviewbutton > .menu-iconic-text {
+  margin: 2px 0px;
+}
+
+.subviewbutton:-moz-any([image],[targetURI],.cui-withicon, .bookmark-item) > .toolbarbutton-text {
+  margin: 2px 6px;
+}
+
+.restoreallitem > .toolbarbutton-icon {
+  display: none;
+}
+
+.subviewbutton {
+  -moz-padding-start: 18px;
+}
+
+.subviewbutton[checked="true"] {
+  background-position: top 5px left 4px;
+}
+
+.subviewbutton:not(:-moz-any([image],[targetURI],.cui-withicon, .bookmark-item)) > .menu-iconic-left {
+  display: none;
+}
+
 #BMB_bookmarksPopup > menu,
 #BMB_bookmarksPopup > menuitem:not(.panel-subview-footer) {
   padding-top: 5px;
   padding-bottom: 5px;
 }
 
 /* Override OSX-specific toolkit styles for the bookmarks panel */
 #BMB_bookmarksPopup > menu > .menu-right {
@@ -89,13 +119,8 @@
 .widget-overflow-list .toolbarbutton-1 > .toolbarbutton-menubutton-dropmarker {
   -moz-margin-start: 4px;
 }
 
 .PanelUI-subView menuseparator,
 .cui-widget-panelview menuseparator {
   padding: 0 !important;
 }
-
-.PanelUI-subView menuitem[checked="true"]::before,
-.PanelUI-subView toolbarbutton[checked="true"]::before {
-  margin: 0 5px;
-}
--- a/browser/themes/shared/UITour.inc.css
+++ b/browser/themes/shared/UITour.inc.css
@@ -46,17 +46,16 @@
 #UITourTooltipDescription {
   -moz-margin-start: 0;
   -moz-margin-end: 0;
   font-size: 1.15rem;
   line-height: 1.8rem;
 }
 
 #UITourTooltipClose {
-  visibility: hidden; /* XXX Temporarily disabled by bug 966913 */
   -moz-appearance: none;
   border: none;
   background-color: transparent;
   min-width: 0;
   -moz-margin-start: 4px;
   -moz-margin-end: -10px;
   margin-top: -8px;
 }
--- a/browser/themes/shared/customizableui/panelUIOverlay.inc.css
+++ b/browser/themes/shared/customizableui/panelUIOverlay.inc.css
@@ -7,20 +7,20 @@
 %define menuPanelWidth 22.35em
 % XXXgijs This is the ugliest bit of code I think I've ever written for Mozilla.
 % Basically, the 0.1px is there to avoid CSS rounding errors causing buttons to wrap.
 % For gory details, refer to https://bugzilla.mozilla.org/show_bug.cgi?id=963365#c11
 % There's no calc() here (and therefore lots of calc() where this is used) because
 % we don't support nested calc(): https://bugzilla.mozilla.org/show_bug.cgi?id=968761
 %define menuPanelButtonWidth (@menuPanelWidth@ / 3 - 0.1px)
 %define exitSubviewGutterWidth 38px
-%define buttonStateHover :not(:-moz-any([disabled],[open],[checked="true"],:active)):hover
-%define menuStateHover :not(:-moz-any([disabled],[checked="true"],:active))[_moz-menuactive]
-%define buttonStateActive :not([disabled]):-moz-any([open],[checked="true"],:hover:active)
-%define menuStateActive :not([disabled]):-moz-any([checked="true"],[_moz-menuactive]:active)
+%define buttonStateHover :not(:-moz-any([disabled],[open],:active)):hover
+%define menuStateHover :not(:-moz-any([disabled],:active))[_moz-menuactive]
+%define buttonStateActive :not([disabled]):-moz-any([open],:hover:active)
+%define menuStateActive :not([disabled])[_moz-menuactive]:active
 
 %include ../browser.inc
 
 #PanelUI-button {
   background-image: linear-gradient(to bottom, hsla(0,0%,100%,0), hsla(0,0%,100%,.3) 30%, hsla(0,0%,100%,.3) 70%, hsla(0,0%,100%,0)),
                     linear-gradient(to bottom, hsla(210,54%,20%,0), hsla(210,54%,20%,.3) 30%, hsla(210,54%,20%,.3) 70%, hsla(210,54%,20%,0)),
                     linear-gradient(to bottom, hsla(0,0%,100%,0), hsla(0,0%,100%,.3) 30%, hsla(0,0%,100%,.3) 70%, hsla(0,0%,100%,0));
   background-size: 1px calc(100% - 1px), 1px calc(100% - 1px), 1px  calc(100% - 1px) !important;
@@ -50,20 +50,16 @@
 .panel-viewstack[viewtype="main"] > .panel-subviews:-moz-locale-dir(rtl) {
   transform: translateX(-@menuPanelWidth@);
 }
 
 .panel-viewstack:not([viewtype="main"]) > .panel-mainview > #PanelUI-mainView {
   -moz-box-flex: 1;
 }
 
-.subviewbutton:not(:-moz-any([image],[targetURI],.cui-withicon)) > .toolbarbutton-text {
-  -moz-margin-start: 0;
-}
-
 .panel-subview-body {
   overflow-y: auto;
   overflow-x: hidden;
   -moz-box-flex: 1;
 }
 
 #PanelUI-popup .panel-subview-body {
   margin: -4px;
@@ -117,34 +113,33 @@
   overflow: hidden;
 }
 
 #PanelUI-popup > .panel-arrowcontainer > .panel-arrowcontent,
 .cui-widget-panel > .panel-arrowcontainer > .panel-arrowcontent > .popup-internal-box {
   padding: 0;
 }
 
-.panelUI-grid .toolbarbutton-menubutton-button > .toolbarbutton-multiline-text,
+.panelUI-grid .toolbarbutton-1 > .toolbarbutton-menubutton-button > .toolbarbutton-multiline-text,
 .panelUI-grid .toolbarbutton-1 > .toolbarbutton-multiline-text {
   -moz-hyphens: auto;
-  min-height: 3.5em;
 }
 
-.panelUI-grid:not([customize-transitioning]) .toolbarbutton-menubutton-button > .toolbarbutton-multiline-text,
-.panelUI-grid:not([customize-transitioning]) .toolbarbutton-1 > .toolbarbutton-multiline-text {
+.panelUI-grid:not([customize-transitioning]) .toolbarbutton-1[fadelabel] > .toolbarbutton-menubutton-button > .toolbarbutton-multiline-text,
+.panelUI-grid:not([customize-transitioning]) .toolbarbutton-1[fadelabel] > .toolbarbutton-multiline-text {
   mask: url(chrome://browser/content/browser.xul#menuPanelButtonTextFadeOutMask);
 }
 
 .panelUI-grid .toolbarbutton-1 > .toolbarbutton-text,
 .panelUI-grid .toolbarbutton-1 > .toolbarbutton-multiline-text {
   text-align: center;
   margin: 2px 0 0;
 }
 
-.panelUI-grid .toolbarbutton-menubutton-button > .toolbarbutton-multiline-text {
+.panelUI-grid .toolbarbutton-1 > .toolbarbutton-menubutton-button > .toolbarbutton-multiline-text {
   text-align: center;
   margin: -1px 0 0;
 }
 
 #wrapper-edit-controls:-moz-any([place="palette"],[place="panel"]) > #edit-controls,
 #wrapper-zoom-controls:-moz-any([place="palette"],[place="panel"]) > #zoom-controls {
   -moz-margin-start: 0;
 }
@@ -684,20 +679,16 @@ menuitem.subviewbutton@menuStateActive@,
   -moz-margin-end: auto;
   color: hsl(0,0%,50%);
 }
 
 #PanelUI-historyItems > toolbarbutton {
   list-style-image: url("chrome://mozapps/skin/places/defaultFavicon.png");
 }
 
-.restoreallitem.subviewbutton > .toolbarbutton-icon {
-  display: none;
-}
-
 #PanelUI-recentlyClosedWindows > toolbarbutton > .toolbarbutton-icon,
 #PanelUI-recentlyClosedTabs > toolbarbutton > .toolbarbutton-icon,
 #PanelUI-historyItems > toolbarbutton > .toolbarbutton-icon {
   width: 16px;
   height: 16px;
 }
 
 #PanelUI-footer > #PanelUI-footer-inner[panel-multiview-anchor=true],
@@ -815,24 +806,19 @@ toolbarpaletteitem[place="palette"] > #s
                     linear-gradient(to bottom, hsla(210,54%,20%,0), hsla(210,54%,20%,.15) 40%, hsla(210,54%,20%,.15) 60%, hsla(210,54%,20%,0)),
                     linear-gradient(to bottom, hsla(0,0%,100%,0), hsla(0,0%,100%,.3) 40%, hsla(0,0%,100%,.3) 60%, hsla(0,0%,100%,0));
   background-size: 1px, 1px, 1px;
   background-position: 0 0, 1px 0, 2px 0;
   background-repeat: no-repeat;
 }
 
 .toolbaritem-combined-buttons@inAnyPanel@ > separator {
-  /**
-   * The calculation below is a layout hack. Without it, when hovering over
-   * a .toolbaritem-combined-buttons element in the menu panel, the disappearance
-   * of the separator margins causes things in the menu panel to shift by a few
-   * pixels on Linux. See bug 978767.
-   */
-  margin: calc(0.5em - 1px) 0;
+  margin: .5em 0;
   width: 1px;
+  height: auto;
   background: hsla(210,4%,10%,.15);
   transition-property: margin;
   transition-duration: 10ms;
   transition-timing-function: ease;
 }
 
 .toolbaritem-combined-buttons@inAnyPanel@:hover > separator {
   margin: 0;
@@ -894,27 +880,21 @@ toolbaritem[overflowedItem=true],
   background-image: linear-gradient(hsla(210,54%,20%,.2) 0, hsla(210,54%,20%,.2) 18px);
   background-clip: padding-box;
   background-position: center;
   background-repeat: no-repeat;
   background-size: 1px 18px;
   box-shadow: 0 0 0 1px hsla(0,0%,100%,.2);
 }
 
-.PanelUI-subView toolbarbutton[checked="true"] {
-  -moz-padding-start: 4px;
-}
-
-.PanelUI-subView toolbarbutton[checked="true"] > .toolbarbutton-text {
-  -moz-padding-start: 0px;
+.subviewbutton[checked="true"] {
+  background: url("chrome://global/skin/menu/menu-check.png") top 7px left 7px / 11px 11px no-repeat transparent;
 }
 
-.PanelUI-subView menuitem[checked="true"]::before,
-.PanelUI-subView toolbarbutton[checked="true"]::before {
-  content: "✓";
-  display: -moz-box;
-  width: 12px;
-  margin: 0 2px;
+.PanelUI-subView > menu > .menu-iconic-left,
+.PanelUI-subView > menuitem > .menu-iconic-left {
+  -moz-appearance: none;
+  -moz-margin-end: 3px;
 }
 
-#BMB_bookmarksPopup > menuitem[checked="true"] > .menu-iconic-left {
-  display: none;
+.PanelUI-subView > menuitem[checked="true"] > .menu-iconic-left {
+  visibility: hidden;
 }
--- a/browser/themes/windows/customizableui/panelUIOverlay.css
+++ b/browser/themes/windows/customizableui/panelUIOverlay.css
@@ -30,19 +30,26 @@
   -moz-padding-start: 0;
   height: 18px;
 }
 
 .widget-overflow-list .toolbarbutton-1 > .toolbarbutton-menubutton-dropmarker > .dropmarker-icon {
   padding: 0 6px;
 }
 
-#BMB_bookmarksPopup menuitem[checked="true"]::before,
-#BMB_bookmarksPopup toolbarbutton[checked="true"]::before {
-  margin: 0 9px;
+.subviewbutton > .toolbarbutton-text {
+  -moz-padding-start: 16px;
+}
+
+.subviewbutton:-moz-any([image],[targetURI],.cui-withicon, .restoreallitem, .bookmark-item) > .toolbarbutton-text {
+  -moz-padding-start: 0;
+}
+
+.subviewbutton.bookmark-item > .toolbarbutton-icon {
+  -moz-margin-start: 3px;
 }
 
 %ifdef WINDOWS_AERO
 /* Win8 and beyond. */
 @media not all and (-moz-os-version: windows-vista) {
   @media not all and (-moz-os-version: windows-win7) {
     panelview .toolbarbutton-1,
     .subviewbutton,
--- a/content/svg/content/src/nsSVGElement.cpp
+++ b/content/svg/content/src/nsSVGElement.cpp
@@ -1200,17 +1200,17 @@ MappedAttrParser::ParseMappedAttrValue(n
   if (!mDecl) {
     mDecl = new css::Declaration();
     mDecl->InitializeEmpty();
   }
 
   // Get the nsCSSProperty ID for our mapped attribute.
   nsCSSProperty propertyID =
     nsCSSProps::LookupProperty(nsDependentAtomString(aMappedAttrName),
-                               nsCSSProps::eEnabled);
+                               nsCSSProps::eEnabledForAllContent);
   if (propertyID != eCSSProperty_UNKNOWN) {
     bool changed; // outparam for ParseProperty. (ignored)
     mParser.ParseProperty(propertyID, aMappedAttrValue, mDocURI, mBaseURI,
                           mNodePrincipal, mDecl, &changed, false, true);
     return;
   }
   NS_ABORT_IF_FALSE(aMappedAttrName == nsGkAtoms::lang,
                     "Only 'lang' should be unrecognized!");
@@ -2534,17 +2534,17 @@ nsSVGElement::GetAnimatedAttr(int32_t aN
     // targeting width/height on outer-<svg> don't appear to be ignored
     // because we returned a nsISMILAttr for the corresponding
     // SVGAnimatedLength.
 
     // Mapped attributes:
     if (IsAttributeMapped(aName)) {
       nsCSSProperty prop =
         nsCSSProps::LookupProperty(nsDependentAtomString(aName),
-                                   nsCSSProps::eEnabled);
+                                   nsCSSProps::eEnabledForAllContent);
       // Check IsPropertyAnimatable to avoid attributes that...
       //  - map to explicitly unanimatable properties (e.g. 'direction')
       //  - map to unsupported attributes (e.g. 'glyph-orientation-horizontal')
       if (nsSMILCSSProperty::IsPropertyAnimatable(prop)) {
         return new nsSMILMappedAttribute(prop, this);
       }
     }
 
--- a/dom/base/nsDOMWindowUtils.cpp
+++ b/dom/base/nsDOMWindowUtils.cpp
@@ -2612,18 +2612,18 @@ nsDOMWindowUtils::ComputeAnimationDistan
   }
 
   nsresult rv;
   nsCOMPtr<nsIContent> content = do_QueryInterface(aElement, &rv);
   NS_ENSURE_SUCCESS(rv, rv);
 
   // Convert direction-dependent properties as appropriate, e.g.,
   // border-left to border-left-value.
-  nsCSSProperty property = nsCSSProps::LookupProperty(aProperty,
-                                                      nsCSSProps::eAny);
+  nsCSSProperty property =
+    nsCSSProps::LookupProperty(aProperty, nsCSSProps::eIgnoreEnabledState);
   if (property != eCSSProperty_UNKNOWN && nsCSSProps::IsShorthand(property)) {
     nsCSSProperty subprop0 = *nsCSSProps::SubpropertyEntryFor(property);
     if (nsCSSProps::PropHasFlags(subprop0, CSS_PROPERTY_REPORT_OTHER_NAME) &&
         nsCSSProps::OtherNameFor(subprop0) == property) {
       property = subprop0;
     } else {
       property = eCSSProperty_UNKNOWN;
     }
--- a/dom/bindings/GenerateCSS2PropertiesWebIDL.py
+++ b/dom/bindings/GenerateCSS2PropertiesWebIDL.py
@@ -2,19 +2,28 @@
 # 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/.
 
 import sys
 import string
 
 propList = eval(sys.stdin.read())
 props = ""
-for [prop, pref] in propList:
+for [prop, id, flags, pref] in propList:
     extendedAttrs = ["Throws", "TreatNullAs=EmptyString"]
-    if pref is not "":
+    # To limit the overhead of Func= annotations, we only generate them when
+    # necessary, which is when the
+    # CSS_PROPERTY_ALWAYS_ENABLED_IN_CHROME_OR_CERTIFIED_APP flag is set.
+    # Otherwise, we try to get by with just a Pref= annotation or no annotation
+    # at all.
+    if "CSS_PROPERTY_ALWAYS_ENABLED_IN_CHROME_OR_CERTIFIED_APP" in flags:
+        extendedAttrs.append('Func="IsCSSPropertyExposedToJS<eCSSProperty_%s>"' % id)
+    # The following is an 'elif' because it is the responsibility of
+    # IsCSSPropertyExposedToJS to handle the pref if there is one.
+    elif pref is not "":
         extendedAttrs.append('Pref="%s"' % pref)
     if not prop.startswith("Moz"):
         prop = prop[0].lower() + prop[1:]
     # Unfortunately, even some of the getters here are fallible
     # (e.g. on nsComputedDOMStyle).
     props += "  [%s] attribute DOMString %s;\n" % (", ".join(extendedAttrs),
                                                    prop)
 
--- a/dom/media/tests/mochitest/pc.js
+++ b/dom/media/tests/mochitest/pc.js
@@ -1626,20 +1626,22 @@ PeerConnectionWrapper.prototype = {
               }
               if (res.remoteId) {
                 var rem = stats[res.remoteId];
                 ok(rem.isRemote, "Remote is rtcp");
                 ok(rem.remoteId == res.id, "Remote backlink match");
                 if(res.type == "outboundrtp") {
                   ok(rem.type == "inboundrtp", "Rtcp is inbound");
                   ok(rem.packetsReceived !== undefined, "Rtcp packetsReceived");
-                  ok(rem.packetsReceived <= res.packetsSent, "No more than sent");
+                  // TODO: Re-enable once Bug 980497 is fixed
+                  // ok(rem.packetsReceived <= res.packetsSent, "No more than sent");
                   ok(rem.packetsLost !== undefined, "Rtcp packetsLost");
                   ok(rem.bytesReceived >= rem.packetsReceived * 8, "Rtcp bytesReceived");
-                  ok(rem.bytesReceived <= res.bytesSent, "No more than sent bytes");
+                  // TODO: Re-enable once Bug 980497 is fixed
+                  // ok(rem.bytesReceived <= res.bytesSent, "No more than sent bytes");
                   ok(rem.jitter !== undefined, "Rtcp jitter");
                 } else {
                   ok(rem.type == "outboundrtp", "Rtcp is outbound");
                   ok(rem.packetsSent !== undefined, "Rtcp packetsSent");
                   // We may have received more than outdated Rtcp packetsSent
                   ok(rem.bytesSent >= rem.packetsSent * 8, "Rtcp bytesSent");
                 }
                 ok(rem.ssrc == res.ssrc, "Remote ssrc match");
--- a/dom/smil/nsSMILAnimationController.cpp
+++ b/dom/smil/nsSMILAnimationController.cpp
@@ -789,17 +789,17 @@ nsSMILAnimationController::GetTargetIden
       // width/height are special as they may be attributes or for
       // outer-<svg> elements, mapped into style.
       if (attributeName == nsGkAtoms::width ||
           attributeName == nsGkAtoms::height) {
         isCSS = targetElem->GetNameSpaceID() != kNameSpaceID_SVG;
       } else {
         nsCSSProperty prop =
           nsCSSProps::LookupProperty(nsDependentAtomString(attributeName),
-                                     nsCSSProps::eEnabled);
+                                     nsCSSProps::eEnabledForAllContent);
         isCSS = nsSMILCSSProperty::IsPropertyAnimatable(prop);
       }
     }
   } else {
     isCSS = (attributeType == eSMILTargetAttrType_CSS);
   }
 
   // Construct the key
--- a/dom/smil/nsSMILCompositor.cpp
+++ b/dom/smil/nsSMILCompositor.cpp
@@ -120,17 +120,17 @@ nsSMILCompositor::ClearAnimationEffects(
 // Protected Helper Functions
 // --------------------------
 nsISMILAttr*
 nsSMILCompositor::CreateSMILAttr()
 {
   if (mKey.mIsCSS) {
     nsCSSProperty propId =
       nsCSSProps::LookupProperty(nsDependentAtomString(mKey.mAttributeName),
-                                 nsCSSProps::eEnabled);
+                                 nsCSSProps::eEnabledForAllContent);
     if (nsSMILCSSProperty::IsPropertyAnimatable(propId)) {
       return new nsSMILCSSProperty(propId, mKey.mElement.get());
     }
   } else {
     return mKey.mElement->GetAnimatedAttr(mKey.mAttributeNamespaceID,
                                           mKey.mAttributeName);
   }
   return nullptr;
--- a/dom/webidl/CSS2PropertiesProps.h
+++ b/dom/webidl/CSS2PropertiesProps.h
@@ -1,34 +1,39 @@
 /* A file meant as input to the preprocessor only */
 
 /* DO_PROP serves as an extra level of indirection to allow expansion
    of CSS_PROP_DOMPROP_PREFIXED */
 
 [
 
-#define DO_PROP(method, pref) \
-  [ #method, pref ],
+#define PROP_STRINGIFY_INTERNAL(X) #X
+#define PROP_STRINGIFY(X) PROP_STRINGIFY_INTERNAL(X)
+
+#define DO_PROP(method, id, flags, pref) \
+  [ #method, #id, PROP_STRINGIFY(flags), pref ],
 #define CSS_PROP(name, id, method, flags, pref, parsevariant, kwtable, \
 		 stylestruct, stylestructofset, animtype) \
-  DO_PROP(method, pref)
+  DO_PROP(method, id, flags, pref)
 #define CSS_PROP_SHORTHAND(name, id, method, flags, pref) \
-  DO_PROP(method, pref)
+  DO_PROP(method, id, flags, pref)
 #define CSS_PROP_PUBLIC_OR_PRIVATE(publicname_, privatename_) publicname_
 #define CSS_PROP_LIST_EXCLUDE_INTERNAL
 
 #include "nsCSSPropList.h"
 
 #undef CSS_PROP_LIST_EXCLUDE_INTERNAL
 #undef CSS_PROP_PUBLIC_OR_PRIVATE
 #undef CSS_PROP_SHORTHAND
 #undef CSS_PROP
 
 #define CSS_PROP_ALIAS(name, id, method, pref) \
-  DO_PROP(method, pref)
+  DO_PROP(method, id, 0, pref)
 
 #include "nsCSSPropAliasList.h"
 
 #undef CSS_PROP_ALIAS
 
 #undef DO_PROP
+#undef PROP_STRINGIFY
+#undef PROP_STRINGIFY_INTERNAL
 
 ]
--- a/editor/libeditor/html/nsHTMLCSSUtils.cpp
+++ b/editor/libeditor/html/nsHTMLCSSUtils.cpp
@@ -560,17 +560,17 @@ nsHTMLCSSUtils::GetCSSInlinePropertyBase
 
   MOZ_ASSERT(aStyleType == eSpecified);
   nsRefPtr<css::StyleRule> rule = element->GetInlineStyleRule();
   if (!rule) {
     return NS_OK;
   }
   nsCSSProperty prop =
     nsCSSProps::LookupProperty(nsDependentAtomString(aProperty),
-                               nsCSSProps::eEnabled);
+                               nsCSSProps::eEnabledForAllContent);
   MOZ_ASSERT(prop != eCSSProperty_UNKNOWN);
   rule->GetDeclaration()->GetValue(prop, aValue);
 
   return NS_OK;
 }
 
 already_AddRefed<nsComputedDOMStyle>
 nsHTMLCSSUtils::GetComputedStyle(nsIDOMElement* aElement)
--- a/gfx/layers/ImageContainer.h
+++ b/gfx/layers/ImageContainer.h
@@ -21,16 +21,17 @@
 #include "nsAutoRef.h"                  // for nsCountedRef
 #include "nsCOMPtr.h"                   // for already_AddRefed
 #include "nsDebug.h"                    // for NS_ASSERTION
 #include "nsISupportsImpl.h"            // for Image::Release, etc
 #include "nsRect.h"                     // for nsIntRect
 #include "nsSize.h"                     // for nsIntSize
 #include "nsTArray.h"                   // for nsTArray
 #include "mozilla/Atomics.h"
+#include "mozilla/WeakPtr.h"
 #include "nsThreadUtils.h"
 #include "mozilla/gfx/2D.h"
 #include "nsDataHashtable.h"
 
 #ifndef XPCOM_GLUE_AVOID_NSPR
 /**
  * We need to be able to hold a reference to a gfxASurface from Image
  * subclasses. This is potentially a problem since Images can be addrefed
@@ -375,17 +376,17 @@ struct RemoteImageData {
  * a layer transaction.
  * 3) Remote. Initiated by calling SetRemoteImageData on the ImageContainer
  * before any other activity.
  * The ImageContainer uses a shared memory block containing a cross-process mutex
  * to communicate with the compositor thread. SetCurrentImage synchronously
  * updates the shared state to point to the new image and the old image
  * is immediately released (not true in Normal or Asynchronous modes).
  */
-class ImageContainer {
+class ImageContainer : public SupportsWeakPtr<ImageContainer> {
   NS_INLINE_DECL_THREADSAFE_REFCOUNTING(ImageContainer)
 public:
 
   enum { DISABLE_ASYNC = 0x0, ENABLE_ASYNC = 0x01 };
 
   ImageContainer(int flag = 0);
 
   ~ImageContainer();
--- a/image/src/RasterImage.cpp
+++ b/image/src/RasterImage.cpp
@@ -917,18 +917,34 @@ RasterImage::GetFrame(uint32_t aWhichFra
 
   nsRefPtr<gfxASurface> framesurf;
 
   // If this frame covers the entire image, we can just reuse its existing
   // surface.
   nsIntRect framerect = frame->GetRect();
   if (framerect.x == 0 && framerect.y == 0 &&
       framerect.width == mSize.width &&
-      framerect.height == mSize.height)
+      framerect.height == mSize.height) {
     frame->GetSurface(getter_AddRefs(framesurf));
+    if (!framesurf && !frame->IsSinglePixel()) {
+      // No reason to be optimized away here - the OS threw out the data
+      if (!(aFlags & FLAG_SYNC_DECODE))
+        return nullptr;
+
+      // Unconditionally call ForceDiscard() here because GetSurface can only
+      // return null when we can forcibly discard and redecode. There are two
+      // other cases where GetSurface() can return null - when it is a single
+      // pixel image, which we check before getting here, or when this is an
+      // indexed image, in which case we shouldn't be in this function at all.
+      // The only remaining possibility is that SetDiscardable() was called on
+      // this imgFrame, which implies the image can be redecoded.
+      ForceDiscard();
+      return GetFrame(aWhichFrame, aFlags);
+    }
+  }
 
   // The image doesn't have a surface because it's been optimized away. Create
   // one.
   if (!framesurf) {
     nsRefPtr<gfxImageSurface> imgsurf;
     CopyFrame(aWhichFrame, aFlags, getter_AddRefs(imgsurf));
     framesurf = imgsurf;
   }
@@ -942,17 +958,25 @@ RasterImage::GetCurrentImage()
   if (!mDecoded) {
     // We can't call StartDecoding because that can synchronously notify
     // which can cause DOM modification
     RequestDecodeCore(ASYNCHRONOUS);
     return nullptr;
   }
 
   nsRefPtr<gfxASurface> imageSurface = GetFrame(FRAME_CURRENT, FLAG_NONE);
-  NS_ENSURE_TRUE(imageSurface, nullptr);
+  if (!imageSurface) {
+    // The OS threw out some or all of our buffer. Start decoding again.
+    // GetFrame will only return null in the case that the image was
+    // discarded. We already checked that the image is decoded, so other
+    // error paths are not possible.
+    ForceDiscard();
+    RequestDecodeCore(ASYNCHRONOUS);
+    return nullptr;
+  }
 
   if (!mImageContainer) {
     mImageContainer = LayerManager::CreateImageContainer();
   }
 
   CairoImage::Data cairoData;
   cairoData.mDeprecatedSurface = imageSurface;
   GetWidth(&cairoData.mSize.width);
@@ -976,30 +1000,41 @@ RasterImage::GetImageContainer(LayerMana
     *_retval = nullptr;
     return NS_OK;
   }
 
   if (IsUnlocked() && mStatusTracker) {
     mStatusTracker->OnUnlockedDraw();
   }
 
+  if (!mImageContainer) {
+    mImageContainer = mImageContainerCache;
+  }
+
   if (mImageContainer) {
     *_retval = mImageContainer;
     NS_ADDREF(*_retval);
     return NS_OK;
   }
 
   nsRefPtr<layers::Image> image = GetCurrentImage();
   if (!image) {
     return NS_ERROR_NOT_AVAILABLE;
   }
   mImageContainer->SetCurrentImageInTransaction(image);
 
   *_retval = mImageContainer;
   NS_ADDREF(*_retval);
+  // We only need to be careful about holding on to the image when it is
+  // discardable by the OS.
+  if (CanForciblyDiscardAndRedecode()) {
+    mImageContainerCache = mImageContainer->asWeakPtr();
+    mImageContainer = nullptr;
+  }
+
   return NS_OK;
 }
 
 void
 RasterImage::UpdateImageContainer()
 {
   if (!mImageContainer || IsInUpdateImageContainer()) {
     return;
@@ -1387,16 +1422,22 @@ RasterImage::DecodingComplete()
   }
 
   // If there's only 1 frame, optimize it. Optimizing animated images
   // is not supported.
   //
   // We don't optimize the frame for multipart images because we reuse
   // the frame.
   if ((GetNumFrames() == 1) && !mMultipart) {
+    // CanForciblyDiscard is used instead of CanForciblyDiscardAndRedecode
+    // because we know decoding is complete at this point and this is not
+    // an animation
+    if (DiscardingEnabled() && CanForciblyDiscard()) {
+      mFrameBlender.RawGetFrame(0)->SetDiscardable();
+    }
     rv = mFrameBlender.RawGetFrame(0)->Optimize();
     NS_ENSURE_SUCCESS(rv, rv);
   }
 
   // Double-buffer our frame in the multipart case, since we'll start decoding
   // into the first frame again immediately and this produces severe tearing.
   if (mMultipart) {
     if (GetNumFrames() == 1) {
@@ -2088,35 +2129,35 @@ RasterImage::ShutdownDecoder(eShutdownIn
                     "Invalid shutdown intent");
 
   // Ensure that the decoder is initialized
   NS_ABORT_IF_FALSE(mDecoder, "Calling ShutdownDecoder() with no active decoder!");
 
   // Figure out what kind of decode we were doing before we get rid of our decoder
   bool wasSizeDecode = mDecoder->IsSizeDecode();
 
-  // Unlock the last frame (if we have any). Our invariant is that, while we
-  // have a decoder open, the last frame is always locked.
-  if (GetNumFrames() > 0) {
-    imgFrame *curframe = mFrameBlender.RawGetFrame(GetNumFrames() - 1);
-    curframe->UnlockImageData();
-  }
-
   // Finalize the decoder
   // null out mDecoder, _then_ check for errors on the close (otherwise the
   // error routine might re-invoke ShutdownDecoder)
   nsRefPtr<Decoder> decoder = mDecoder;
   mDecoder = nullptr;
 
   mFinishing = true;
   mInDecoder = true;
   decoder->Finish(aIntent);
   mInDecoder = false;
   mFinishing = false;
 
+  // Unlock the last frame (if we have any). Our invariant is that, while we
+  // have a decoder open, the last frame is always locked.
+  if (GetNumFrames() > 0) {
+    imgFrame *curframe = mFrameBlender.RawGetFrame(GetNumFrames() - 1);
+    curframe->UnlockImageData();
+  }
+
   // Kill off our decode request, if it's pending.  (If not, this call is
   // harmless.)
   DecodePool::StopDecoding(this);
 
   nsresult decoderStatus = decoder->GetDecoderError();
   if (NS_FAILED(decoderStatus)) {
     DoError();
     return decoderStatus;
@@ -2672,16 +2713,27 @@ RasterImage::Draw(gfxContext *aContext,
 
   uint32_t frameIndex = aWhichFrame == FRAME_FIRST ? 0
                                                    : GetCurrentImgFrameIndex();
   imgFrame* frame = GetDrawableImgFrame(frameIndex);
   if (!frame) {
     return NS_OK; // Getting the frame (above) touches the image and kicks off decoding
   }
 
+  nsRefPtr<gfxASurface> surf;
+  if (!frame->IsSinglePixel()) {
+    frame->GetSurface(getter_AddRefs(surf));
+    if (!surf) {
+      // The OS threw out some or all of our buffer. Start decoding again.
+      ForceDiscard();
+      WantDecodedFrames();
+      return NS_OK;
+    }
+  }
+
   DrawWithPreDownscaleIfNeeded(frame, aContext, aFilter, aUserSpaceToImageSpace, aFill, aSubimage, aFlags);
 
   if (mDecoded && !mDrawStartTime.IsNull()) {
       TimeDuration drawLatency = TimeStamp::Now() - mDrawStartTime;
       Telemetry::Accumulate(Telemetry::IMAGE_DECODE_ON_DRAW_LATENCY, int32_t(drawLatency.ToMicroseconds()));
       // clear the value of mDrawStartTime
       mDrawStartTime = TimeStamp();
   }
--- a/image/src/RasterImage.h
+++ b/image/src/RasterImage.h
@@ -654,16 +654,19 @@ private: // data
   nsIntSize                  mRequestedResolution;
 
   // A hint for image decoder that directly scale the image to smaller buffer
   int                        mRequestedSampleSize;
 
   // Cached value for GetImageContainer.
   nsRefPtr<mozilla::layers::ImageContainer> mImageContainer;
 
+  // If not cached in mImageContainer, this might have our image container
+  WeakPtr<mozilla::layers::ImageContainer> mImageContainerCache;
+
 #ifdef DEBUG
   uint32_t                       mFramesNotified;
 #endif
 
   // Below are the pieces of data that can be accessed on more than one thread
   // at once, and hence need to be locked by mDecodingMonitor.
 
   // BEGIN LOCKED MEMBER VARIABLES
--- a/image/src/imgFrame.cpp
+++ b/image/src/imgFrame.cpp
@@ -7,16 +7,17 @@
 #include "imgFrame.h"
 #include "DiscardTracker.h"
 
 #include "prenv.h"
 
 #include "gfx2DGlue.h"
 #include "gfxPlatform.h"
 #include "gfxUtils.h"
+#include "gfxAlphaRecovery.h"
 
 static bool gDisableOptimize = false;
 
 #include "cairo.h"
 #include "GeckoProfiler.h"
 #include "mozilla/Likely.h"
 #include "mozilla/MemoryReporting.h"
 #include "nsMargin.h"
@@ -30,16 +31,58 @@ static bool gDisableOptimize = false;
 #define USE_WIN_SURFACE 1
 
 #endif
 
 using namespace mozilla;
 using namespace mozilla::gfx;
 using namespace mozilla::image;
 
+static cairo_user_data_key_t kVolatileBuffer;
+
+static void
+VolatileBufferRelease(void *vbuf)
+{
+  delete static_cast<VolatileBufferPtr<unsigned char>*>(vbuf);
+}
+
+gfxImageSurface *
+LockedImageSurface::CreateSurface(VolatileBuffer *vbuf,
+                                  const gfxIntSize& size,
+                                  gfxImageFormat format)
+{
+  VolatileBufferPtr<unsigned char> *vbufptr =
+    new VolatileBufferPtr<unsigned char>(vbuf);
+  MOZ_ASSERT(!vbufptr->WasBufferPurged(), "Expected image data!");
+
+  long stride = gfxImageSurface::ComputeStride(size, format);
+  gfxImageSurface *img = new gfxImageSurface(*vbufptr, size, stride, format);
+  if (!img || img->CairoStatus()) {
+    delete img;
+    delete vbufptr;
+    return nullptr;
+  }
+
+  img->SetData(&kVolatileBuffer, vbufptr, VolatileBufferRelease);
+  return img;
+}
+
+TemporaryRef<VolatileBuffer>
+LockedImageSurface::AllocateBuffer(const gfxIntSize& size,
+                                   gfxImageFormat format)
+{
+  long stride = gfxImageSurface::ComputeStride(size, format);
+  RefPtr<VolatileBuffer> buf = new VolatileBuffer();
+  if (buf->Init(stride * size.height,
+                1 << gfxAlphaRecovery::GoodAlignmentLog2()))
+    return buf;
+
+  return nullptr;
+}
+
 // Returns true if an image of aWidth x aHeight is allowed and legal.
 static bool AllowedImageSize(int32_t aWidth, int32_t aHeight)
 {
   // reject over-wide or over-tall images
   const int32_t k64KLimit = 0x0000FFFF;
   if (MOZ_UNLIKELY(aWidth > k64KLimit || aHeight > k64KLimit )) {
     NS_WARNING("image too big");
     return false;
@@ -103,16 +146,17 @@ imgFrame::imgFrame() :
   mTimeout(100),
   mDisposalMethod(0), /* imgIContainer::kDisposeNotSpecified */
   mLockCount(0),
   mBlendMethod(1), /* imgIContainer::kBlendOver */
   mSinglePixel(false),
   mFormatChanged(false),
   mCompositingFailed(false),
   mNonPremult(false),
+  mDiscardable(false),
   mInformedDiscardTracker(false),
   mDirty(false)
 {
   static bool hasCheckedOptimize = false;
   if (!hasCheckedOptimize) {
     if (PR_GetEnv("MOZ_DISABLE_IMAGE_OPTIMIZE")) {
       gDisableOptimize = true;
     }
@@ -169,22 +213,27 @@ nsresult imgFrame::Init(int32_t aX, int3
         // no error
         mImageSurface = mWinSurface->GetAsImageSurface();
       } else {
         mWinSurface = nullptr;
       }
     }
 #endif
 
-    // For other platforms we create the image surface first and then
-    // possibly wrap it in a device surface.  This branch is also used
-    // on Windows if we're not using device surfaces or if we couldn't
-    // create one.
-    if (!mImageSurface)
-      mImageSurface = new gfxImageSurface(gfxIntSize(mSize.width, mSize.height), mFormat);
+    // For other platforms, space for the image surface is first allocated in
+    // a volatile buffer and then wrapped by a LockedImageSurface.
+    // This branch is also used on Windows if we're not using device surfaces
+    // or if we couldn't create one.
+    if (!mImageSurface) {
+      mVBuf = LockedImageSurface::AllocateBuffer(mSize, mFormat);
+      if (!mVBuf) {
+        return NS_ERROR_OUT_OF_MEMORY;
+      }
+      mImageSurface = LockedImageSurface::CreateSurface(mVBuf, mSize, mFormat);
+    }
 
     if (!mImageSurface || mImageSurface->CairoStatus()) {
       mImageSurface = nullptr;
       // guess
       if (!mImageSurface) {
         NS_WARNING("Allocation of gfxImageSurface should succeed");
       } else if (!mImageSurface->CairoStatus()) {
         NS_WARNING("gfxImageSurface should have good CairoStatus");
@@ -246,16 +295,17 @@ nsresult imgFrame::Optimize()
         if (mFormat == gfxImageFormat::ARGB32)
           inputType = gfxRGBA::PACKED_ARGB_PREMULTIPLIED;
 
         mSinglePixelColor = gfxRGBA(firstPixel, inputType);
 
         mSinglePixel = true;
 
         // blow away the older surfaces (if they exist), to release their memory
+        mVBuf = nullptr;
         mImageSurface = nullptr;
         mOptSurface = nullptr;
 #ifdef USE_WIN_SURFACE
         mWinSurface = nullptr;
 #endif
 #ifdef XP_MACOSX
         mQuartzSurface = nullptr;
 #endif
@@ -295,16 +345,17 @@ nsresult imgFrame::Optimize()
     mQuartzSurface->Flush();
   }
 #endif
 
   if (mOptSurface == nullptr)
     mOptSurface = gfxPlatform::GetPlatform()->OptimizeImage(mImageSurface, mFormat);
 
   if (mOptSurface) {
+    mVBuf = nullptr;
     mImageSurface = nullptr;
 #ifdef USE_WIN_SURFACE
     mWinSurface = nullptr;
 #endif
 #ifdef XP_MACOSX
     mQuartzSurface = nullptr;
 #endif
   }
@@ -478,20 +529,23 @@ bool imgFrame::GetNeedsBackground() cons
   return (mFormat == gfxImageFormat::ARGB32 || !ImageComplete());
 }
 
 uint32_t imgFrame::GetImageBytesPerRow() const
 {
   if (mImageSurface)
     return mImageSurface->Stride();
 
+  if (mVBuf)
+    return gfxImageSurface::ComputeStride(mSize, mFormat);
+
   if (mPaletteDepth)
     return mSize.width;
 
-  NS_ERROR("GetImageBytesPerRow called with mImageSurface == null and mPaletteDepth == 0");
+  NS_ERROR("GetImageBytesPerRow called with mImageSurface == null, mVBuf == null and mPaletteDepth == 0");
 
   return 0;
 }
 
 uint32_t imgFrame::GetImageDataLength() const
 {
   return GetImageBytesPerRow() * mSize.height;
 }
@@ -564,50 +618,70 @@ nsresult imgFrame::LockImageData()
   if (mLockCount != 1) {
     return NS_OK;
   }
 
   // Paletted images don't have surfaces, so there's nothing to do.
   if (mPalettedImageData)
     return NS_OK;
 
-  if ((mOptSurface || mSinglePixel) && !mImageSurface) {
-    // Recover the pixels
-    mImageSurface = new gfxImageSurface(gfxIntSize(mSize.width, mSize.height),
-                                        gfxImageFormat::ARGB32);
-    if (!mImageSurface || mImageSurface->CairoStatus())
-      return NS_ERROR_OUT_OF_MEMORY;
+  if (!mImageSurface) {
+    if (mVBuf) {
+      VolatileBufferPtr<uint8_t> ref(mVBuf);
+      if (ref.WasBufferPurged())
+        return NS_ERROR_FAILURE;
+
+      mImageSurface = LockedImageSurface::CreateSurface(mVBuf, mSize, mFormat);
+      if (!mImageSurface || mImageSurface->CairoStatus())
+        return NS_ERROR_OUT_OF_MEMORY;
+    } else if (mOptSurface || mSinglePixel) {
+      // Recover the pixels
+      mVBuf = LockedImageSurface::AllocateBuffer(mSize, mFormat);
+      if (!mVBuf) {
+        return NS_ERROR_OUT_OF_MEMORY;
+      }
 
-    gfxContext context(mImageSurface);
-    context.SetOperator(gfxContext::OPERATOR_SOURCE);
-    if (mSinglePixel)
-      context.SetDeviceColor(mSinglePixelColor);
-    else
-      context.SetSource(mOptSurface);
-    context.Paint();
+      mImageSurface = LockedImageSurface::CreateSurface(mVBuf, mSize, mFormat);
+      if (!mImageSurface || mImageSurface->CairoStatus())
+        return NS_ERROR_OUT_OF_MEMORY;
 
-    mOptSurface = nullptr;
+      gfxContext context(mImageSurface);
+      context.SetOperator(gfxContext::OPERATOR_SOURCE);
+      if (mSinglePixel)
+        context.SetDeviceColor(mSinglePixelColor);
+      else
+        context.SetSource(mOptSurface);
+      context.Paint();
+
+      mOptSurface = nullptr;
 #ifdef USE_WIN_SURFACE
-    mWinSurface = nullptr;
+      mWinSurface = nullptr;
 #endif
 #ifdef XP_MACOSX
-    mQuartzSurface = nullptr;
+      mQuartzSurface = nullptr;
 #endif
+    }
   }
 
   // We might write to the bits in this image surface, so we need to make the
   // surface ready for that.
   if (mImageSurface)
     mImageSurface->Flush();
 
 #ifdef USE_WIN_SURFACE
   if (mWinSurface)
     mWinSurface->Flush();
 #endif
 
+#ifdef XP_MACOSX
+  if (!mQuartzSurface && !ShouldUseImageSurfaces()) {
+    mQuartzSurface = new gfxQuartzImageSurface(mImageSurface);
+  }
+#endif
+
   return NS_OK;
 }
 
 nsresult imgFrame::UnlockImageData()
 {
   MOZ_ASSERT(NS_IsMainThread());
 
   NS_ABORT_IF_FALSE(mLockCount != 0, "Unlocking an unlocked image!");
@@ -653,16 +727,23 @@ nsresult imgFrame::UnlockImageData()
 
 #ifdef XP_MACOSX
   // The quartz image surface (ab)uses the flush method to get the
   // cairo_image_surface data into a CGImage, so we have to call Flush() here.
   if (mQuartzSurface)
     mQuartzSurface->Flush();
 #endif
 
+  if (mVBuf && mDiscardable) {
+    mImageSurface = nullptr;
+#ifdef XP_MACOSX
+    mQuartzSurface = nullptr;
+#endif
+  }
+
   return NS_OK;
 }
 
 void imgFrame::ApplyDirtToSurfaces()
 {
   MOZ_ASSERT(NS_IsMainThread());
 
   MutexAutoLock lock(mDirtyMutex);
@@ -692,16 +773,22 @@ void imgFrame::ApplyDirtToSurfaces()
     if (mQuartzSurface)
       mQuartzSurface->Flush();
 #endif
 
     mDirty = false;
   }
 }
 
+void imgFrame::SetDiscardable()
+{
+  MOZ_ASSERT(mLockCount, "Expected to be locked when SetDiscardable is called");
+  mDiscardable = true;
+}
+
 int32_t imgFrame::GetRawTimeout() const
 {
   return mTimeout;
 }
 
 void imgFrame::SetRawTimeout(int32_t aTimeout)
 {
   mTimeout = aTimeout;
@@ -790,30 +877,35 @@ imgFrame::SizeOfExcludingThisWithCompute
 
 #ifdef USE_WIN_SURFACE
   if (mWinSurface && aLocation == mWinSurface->GetMemoryLocation()) {
     n += mWinSurface->KnownMemoryUsed();
   } else
 #endif
 #ifdef XP_MACOSX
   if (mQuartzSurface && aLocation == gfxMemoryLocation::IN_PROCESS_HEAP) {
-    n += mSize.width * mSize.height * 4;
-  } else
+    n += aMallocSizeOf(mQuartzSurface);
+  }
 #endif
   if (mImageSurface && aLocation == mImageSurface->GetMemoryLocation()) {
     size_t n2 = 0;
     if (aLocation == gfxMemoryLocation::IN_PROCESS_HEAP) { // HEAP: measure
       n2 = mImageSurface->SizeOfIncludingThis(aMallocSizeOf);
     }
     if (n2 == 0) {  // non-HEAP or computed fallback for HEAP
       n2 = mImageSurface->KnownMemoryUsed();
     }
     n += n2;
   }
 
+  if (mVBuf && aLocation == gfxMemoryLocation::IN_PROCESS_HEAP) {
+    n += aMallocSizeOf(mVBuf);
+    n += mVBuf->HeapSizeOfExcludingThis(aMallocSizeOf);
+  }
+
   if (mOptSurface && aLocation == mOptSurface->GetMemoryLocation()) {
     size_t n2 = 0;
     if (aLocation == gfxMemoryLocation::IN_PROCESS_HEAP &&
         mOptSurface->SizeOfIsMeasured()) {
       // HEAP: measure (but only if the sub-class is capable of measuring)
       n2 = mOptSurface->SizeOfIncludingThis(aMallocSizeOf);
     }
     if (n2 == 0) {  // non-HEAP or computed fallback for HEAP
--- a/image/src/imgFrame.h
+++ b/image/src/imgFrame.h
@@ -4,31 +4,47 @@
  * 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 imgFrame_h
 #define imgFrame_h
 
 #include "mozilla/MemoryReporting.h"
 #include "mozilla/Mutex.h"
+#include "mozilla/VolatileBuffer.h"
 #include "nsRect.h"
 #include "nsPoint.h"
 #include "nsSize.h"
 #include "gfxPattern.h"
 #include "gfxDrawable.h"
 #include "gfxImageSurface.h"
 #if defined(XP_WIN)
 #include "gfxWindowsSurface.h"
 #elif defined(XP_MACOSX)
 #include "gfxQuartzImageSurface.h"
 #endif
 #include "nsAutoPtr.h"
 #include "imgIContainer.h"
 #include "gfxColor.h"
 
+/*
+ * This creates a gfxImageSurface which will unlock the buffer on destruction
+ */
+
+class LockedImageSurface
+{
+public:
+  static gfxImageSurface *
+  CreateSurface(mozilla::VolatileBuffer *vbuf,
+                const gfxIntSize& size,
+                gfxImageFormat format);
+  static mozilla::TemporaryRef<mozilla::VolatileBuffer>
+  AllocateBuffer(const gfxIntSize& size, gfxImageFormat format);
+};
+
 class imgFrame
 {
 public:
   imgFrame();
   ~imgFrame();
 
   nsresult Init(int32_t aX, int32_t aY, int32_t aWidth, int32_t aHeight, gfxImageFormat aFormat, uint8_t aPaletteDepth = 0);
   nsresult Optimize();
@@ -67,45 +83,77 @@ public:
 
   bool GetCompositingFailed() const;
   void SetCompositingFailed(bool val);
 
   nsresult LockImageData();
   nsresult UnlockImageData();
   void ApplyDirtToSurfaces();
 
-  nsresult GetSurface(gfxASurface **aSurface) const
+  void SetDiscardable();
+
+  nsresult GetSurface(gfxASurface **aSurface)
   {
     *aSurface = ThebesSurface();
     NS_IF_ADDREF(*aSurface);
     return NS_OK;
   }
 
-  nsresult GetPattern(gfxPattern **aPattern) const
+  nsresult GetPattern(gfxPattern **aPattern)
   {
     if (mSinglePixel)
       *aPattern = new gfxPattern(mSinglePixelColor);
     else
       *aPattern = new gfxPattern(ThebesSurface());
     NS_ADDREF(*aPattern);
     return NS_OK;
   }
 
-  gfxASurface* ThebesSurface() const
+  bool IsSinglePixel()
+  {
+    return mSinglePixel;
+  }
+
+  gfxASurface* ThebesSurface()
   {
     if (mOptSurface)
       return mOptSurface;
 #if defined(XP_WIN)
     if (mWinSurface)
       return mWinSurface;
 #elif defined(XP_MACOSX)
     if (mQuartzSurface)
       return mQuartzSurface;
 #endif
-    return mImageSurface;
+    if (mImageSurface)
+      return mImageSurface;
+    if (mVBuf) {
+      mozilla::VolatileBufferPtr<uint8_t> ref(mVBuf);
+      if (ref.WasBufferPurged())
+        return nullptr;
+
+      gfxImageSurface *sur =
+        LockedImageSurface::CreateSurface(mVBuf, mSize, mFormat);
+#if defined(XP_MACOSX)
+      // Manually addref and release to make sure the cairo surface isn't lost
+      NS_ADDREF(sur);
+      gfxQuartzImageSurface *quartzSur = new gfxQuartzImageSurface(sur);
+      // quartzSur does not hold on to the gfxImageSurface
+      NS_RELEASE(sur);
+      return quartzSur;
+#else
+      return sur;
+#endif
+    }
+    // We can return null here if we're single pixel optimized
+    // or a paletted image. However, one has to check for paletted
+    // image data first before attempting to get a surface, so
+    // this is only valid for single pixel optimized images
+    MOZ_ASSERT(mSinglePixel, "No image surface and not a single pixel!");
+    return nullptr;
   }
 
   size_t SizeOfExcludingThisWithComputedFallbackIfHeap(
            gfxMemoryLocation aLocation,
            mozilla::MallocSizeOf aMallocSizeOf) const;
 
   uint8_t GetPaletteDepth() const { return mPaletteDepth; }
   uint32_t PaletteDataLength() const {
@@ -162,23 +210,26 @@ private: // data
   gfxRGBA      mSinglePixelColor;
 
   int32_t      mTimeout; // -1 means display forever
   int32_t      mDisposalMethod;
 
   /** Indicates how many readers currently have locked this frame */
   int32_t mLockCount;
 
+  mozilla::RefPtr<mozilla::VolatileBuffer> mVBuf;
+
   gfxImageFormat mFormat;
   uint8_t      mPaletteDepth;
   int8_t       mBlendMethod;
   bool mSinglePixel;
   bool mFormatChanged;
   bool mCompositingFailed;
   bool mNonPremult;
+  bool mDiscardable;
 
   /** Have we called DiscardTracker::InformAllocation()? */
   bool mInformedDiscardTracker;
 
   bool mDirty;
 };
 
 namespace mozilla {
--- a/layout/inspector/inCSSValueSearch.cpp
+++ b/layout/inspector/inCSSValueSearch.cpp
@@ -222,17 +222,17 @@ inCSSValueSearch::SetNormalizeChromeURLs
   return NS_OK;
 }
 
 NS_IMETHODIMP 
 inCSSValueSearch::AddPropertyCriteria(const char16_t *aPropName)
 {
   nsCSSProperty prop =
     nsCSSProps::LookupProperty(nsDependentString(aPropName),
-                               nsCSSProps::eAny);
+                               nsCSSProps::eIgnoreEnabledState);
   mProperties[mPropertyCount] = prop;
   mPropertyCount++;
   return NS_OK;
 }
 
 NS_IMETHODIMP 
 inCSSValueSearch::GetTextCriteria(char16_t** aTextCriteria)
 {
--- a/layout/inspector/inDOMUtils.cpp
+++ b/layout/inspector/inDOMUtils.cpp
@@ -378,18 +378,18 @@ inDOMUtils::SelectorMatchesElement(nsIDO
   *aMatches = nsCSSRuleProcessor::SelectorListMatches(element, matchingContext,
                                                       sel);
   return NS_OK;
 }
 
 NS_IMETHODIMP
 inDOMUtils::IsInheritedProperty(const nsAString &aPropertyName, bool *_retval)
 {
-  nsCSSProperty prop = nsCSSProps::LookupProperty(aPropertyName,
-                                                  nsCSSProps::eAny);
+  nsCSSProperty prop =
+    nsCSSProps::LookupProperty(aPropertyName, nsCSSProps::eIgnoreEnabledState);
   if (prop == eCSSProperty_UNKNOWN) {
     *_retval = false;
     return NS_OK;
   }
 
   if (prop == eCSSPropertyExtra_variable) {
     *_retval = true;
     return NS_OK;
@@ -560,17 +560,17 @@ static void GetOtherValuesForProperty(co
 }
 
 NS_IMETHODIMP
 inDOMUtils::GetCSSValuesForProperty(const nsAString& aProperty,
                                     uint32_t* aLength,
                                     char16_t*** aValues)
 {
   nsCSSProperty propertyID = nsCSSProps::LookupProperty(aProperty,
-                                                        nsCSSProps::eEnabled);
+                                                        nsCSSProps::eEnabledForAllContent);
   if (propertyID == eCSSProperty_UNKNOWN) {
     return NS_ERROR_FAILURE;
   }
 
   nsTArray<nsString> array;
   // We start collecting the values, BUT colors need to go in first, because array
   // needs to stay sorted, and the colors are sorted, so we just append them.
   if (propertyID == eCSSPropertyExtra_variable) {
--- a/layout/style/Declaration.cpp
+++ b/layout/style/Declaration.cpp
@@ -954,17 +954,18 @@ Declaration::GetValue(nsCSSProperty aPro
 }
 
 // Length of the "var-" prefix of custom property names.
 #define VAR_PREFIX_LENGTH 4
 
 bool
 Declaration::GetValueIsImportant(const nsAString& aProperty) const
 {
-  nsCSSProperty propID = nsCSSProps::LookupProperty(aProperty, nsCSSProps::eAny);
+  nsCSSProperty propID =
+    nsCSSProps::LookupProperty(aProperty, nsCSSProps::eIgnoreEnabledState);
   if (propID == eCSSProperty_UNKNOWN) {
     return false;
   }
   if (propID == eCSSPropertyExtra_variable) {
     return GetVariableValueIsImportant(Substring(aProperty, VAR_PREFIX_LENGTH));
   }
   return GetValueIsImportant(propID);
 }
--- a/layout/style/nsCSSParser.cpp
+++ b/layout/style/nsCSSParser.cpp
@@ -40,16 +40,17 @@
 #include "nsContentUtils.h"
 #include "nsAutoPtr.h"
 #include "CSSCalc.h"
 #include "nsMediaFeatures.h"
 #include "nsLayoutUtils.h"
 #include "mozilla/Preferences.h"
 #include "nsRuleData.h"
 #include "mozilla/CSSVariableValues.h"
+#include "mozilla/dom/URL.h"
 
 using namespace mozilla;
 
 typedef nsCSSProps::KTableValue KTableValue;
 
 const uint32_t
 nsCSSProps::kParserVariantTable[eCSSProperty_COUNT_no_shorthands] = {
 #define CSS_PROP(name_, id_, method_, flags_, pref_, parsevariant_, kwtable_, \
@@ -243,19 +244,27 @@ public:
                                            nsIURI* aDocURL,
                                            nsIURI* aBaseURL,
                                            nsIPrincipal* aDocPrincipal,
                                            nsCSSStyleSheet* aSheet,
                                            uint32_t aLineNumber,
                                            uint32_t aLineOffset);
 
   nsCSSProperty LookupEnabledProperty(const nsAString& aProperty) {
-    return nsCSSProps::LookupProperty(aProperty, mUnsafeRulesEnabled ?
-                                                   nsCSSProps::eEnabledInUASheets :
-                                                   nsCSSProps::eEnabled);
+    static_assert(nsCSSProps::eEnabledForAllContent == 0,
+                  "nsCSSProps::eEnabledForAllContent should be zero for "
+                  "this bitfield to work");
+    nsCSSProps::EnabledState enabledState = nsCSSProps::eEnabledForAllContent;
+    if (mUnsafeRulesEnabled) {
+      enabledState |= nsCSSProps::eEnabledInUASheets;
+    }
+    if (mIsChromeOrCertifiedApp) {
+      enabledState |= nsCSSProps::eEnabledInChromeOrCertifiedApp;
+    }
+    return nsCSSProps::LookupProperty(aProperty, enabledState);
   }
 
 protected:
   class nsAutoParseCompoundProperty;
   friend class nsAutoParseCompoundProperty;
 
   class nsAutoFailingSupportsRule;
   friend class nsAutoFailingSupportsRule;
@@ -874,16 +883,22 @@ protected:
   bool mHashlessColorQuirk : 1;
 
   // True when the unitless length quirk applies.
   bool mUnitlessLengthQuirk : 1;
 
   // True if unsafe rules should be allowed
   bool mUnsafeRulesEnabled : 1;
 
+  // True if we are in parsing rules for Chrome or Certified App content,
+  // in which case CSS properties with the
+  // CSS_PROPERTY_ALWAYS_ENABLED_IN_CHROME_OR_CERTIFIED_APP
+  // flag should be allowed.
+  bool mIsChromeOrCertifiedApp : 1;
+
   // True if viewport units should be allowed.
   bool mViewportUnitsEnabled : 1;
 
   // True for parsing media lists for HTML attributes, where we have to
   // ignore CSS comments.
   bool mHTMLMediaMode : 1;
 
   // This flag is set when parsing a non-box shorthand; it's used to not apply
@@ -968,16 +983,17 @@ CSSParserImpl::CSSParserImpl()
     mChildLoader(nullptr),
     mSection(eCSSSection_Charset),
     mNameSpaceMap(nullptr),
     mHavePushBack(false),
     mNavQuirkMode(false),
     mHashlessColorQuirk(false),
     mUnitlessLengthQuirk(false),
     mUnsafeRulesEnabled(false),
+    mIsChromeOrCertifiedApp(false),
     mViewportUnitsEnabled(true),
     mHTMLMediaMode(false),
     mParsingCompoundProperty(false),
     mInSupportsCondition(false),
     mInFailingSupportsRule(false),
     mSuppressErrors(false),
     mNextFree(nullptr)
 {
@@ -1108,16 +1124,19 @@ CSSParserImpl::ParseSheet(const nsAStrin
       }
     }
   }
   else {
     mSection = eCSSSection_Charset; // sheet is empty, any rules are fair
   }
 
   mUnsafeRulesEnabled = aAllowUnsafeRules;
+  mIsChromeOrCertifiedApp =
+    dom::IsChromeURI(aSheetURI) ||
+    aSheetPrincipal->GetAppStatus() == nsIPrincipal::APP_STATUS_CERTIFIED;
 
   nsCSSToken* tk = &mToken;
   for (;;) {
     // Get next non-whitespace token
     if (!GetToken(true)) {
       OUTPUT_ERROR();
       break;
     }
@@ -1131,16 +1150,17 @@ CSSParserImpl::ParseSheet(const nsAStrin
     UngetToken();
     if (ParseRuleSet(AppendRuleToSheet, this)) {
       mSection = eCSSSection_General;
     }
   }
   ReleaseScanner();
 
   mUnsafeRulesEnabled = false;
+  mIsChromeOrCertifiedApp = false;
 
   // XXX check for low level errors
   return NS_OK;
 }
 
 /**
  * Determines whether the identifier contained in the given string is a
  * vendor-specific identifier, as described in CSS 2.1 section 4.1.2.1.
--- a/layout/style/nsCSSPropList.h
+++ b/layout/style/nsCSSPropList.h
@@ -3786,17 +3786,18 @@ CSS_PROP_SVGRESET(
     offsetof(nsStyleSVGReset, mVectorEffect),
     eStyleAnimType_EnumU8)
 
 CSS_PROP_DISPLAY(
     will-change,
     will_change,
     WillChange,
     CSS_PROPERTY_PARSE_FUNCTION |
-        CSS_PROPERTY_VALUE_LIST_USES_COMMAS,
+        CSS_PROPERTY_VALUE_LIST_USES_COMMAS |
+        CSS_PROPERTY_ALWAYS_ENABLED_IN_CHROME_OR_CERTIFIED_APP,
     "layout.css.will-change.enabled",
     0,
     nullptr,
     CSS_PROP_NO_OFFSET,
     eStyleAnimType_None)
 
 // The shorthands below are essentially aliases, but they require different
 // parsing rules, and are therefore implemented as shorthands.
--- a/layout/style/nsCSSProps.cpp
+++ b/layout/style/nsCSSProps.cpp
@@ -390,23 +390,23 @@ nsCSSProps::LookupProperty(const nsACStr
   if (MOZ_LIKELY(res < eCSSProperty_COUNT)) {
     if (res != eCSSProperty_UNKNOWN && !IsEnabled(res, aEnabled)) {
       res = eCSSProperty_UNKNOWN;
     }
     return res;
   }
   MOZ_ASSERT(eCSSAliasCount != 0,
              "'res' must be an alias at this point so we better have some!");
-  // We intentionally don't support eEnabledInUASheets for aliases yet
-  // because it's unlikely there will be a need for it.
-  if (IsEnabled(res) || aEnabled == eAny) {
+  // We intentionally don't support eEnabledInUASheets or eEnabledInChromeOrCertifiedApp
+  // for aliases yet because it's unlikely there will be a need for it.
+  if (IsEnabled(res) || aEnabled == eIgnoreEnabledState) {
     res = gAliases[res - eCSSProperty_COUNT];
     NS_ABORT_IF_FALSE(0 <= res && res < eCSSProperty_COUNT,
                       "aliases must not point to other aliases");
-    if (IsEnabled(res) || aEnabled == eAny) {
+    if (IsEnabled(res) || aEnabled == eIgnoreEnabledState) {
       return res;
     }
   }
   return eCSSProperty_UNKNOWN;
 }
 
 nsCSSProperty
 nsCSSProps::LookupProperty(const nsAString& aProperty, EnabledState aEnabled)
@@ -426,21 +426,21 @@ nsCSSProps::LookupProperty(const nsAStri
       res = eCSSProperty_UNKNOWN;
     }
     return res;
   }
   MOZ_ASSERT(eCSSAliasCount != 0,
              "'res' must be an alias at this point so we better have some!");
   // We intentionally don't support eEnabledInUASheets for aliases yet
   // because it's unlikely there will be a need for it.
-  if (IsEnabled(res) || aEnabled == eAny) {
+  if (IsEnabled(res) || aEnabled == eIgnoreEnabledState) {
     res = gAliases[res - eCSSProperty_COUNT];
     NS_ABORT_IF_FALSE(0 <= res && res < eCSSProperty_COUNT,
                       "aliases must not point to other aliases");
-    if (IsEnabled(res) || aEnabled == eAny) {
+    if (IsEnabled(res) || aEnabled == eIgnoreEnabledState) {
       return res;
     }
   }
   return eCSSProperty_UNKNOWN;
 }
 
 nsCSSFontDesc
 nsCSSProps::LookupFontDesc(const nsACString& aFontDesc)
--- a/layout/style/nsCSSProps.h
+++ b/layout/style/nsCSSProps.h
@@ -196,16 +196,24 @@ static_assert((CSS_PROPERTY_PARSE_PROPER
 
 // This property is always enabled in UA sheets.  This is meant to be used
 // together with a pref that enables the property for non-UA sheets.
 // Note that if such a property has an alias, then any use of that alias
 // in an UA sheet will still be ignored unless the pref is enabled.
 // In other words, this bit has no effect on the use of aliases.
 #define CSS_PROPERTY_ALWAYS_ENABLED_IN_UA_SHEETS  (1<<22)
 
+// This property is always enabled in chrome and in certified apps. This is
+// meant to be used together with a pref that enables the property for
+// non-privileged content. Note that if such a property has an alias, then any
+// use of that alias in privileged content will still be ignored unless the
+// pref is enabled. In other words, this bit has no effect on the use of
+// aliases.
+#define CSS_PROPERTY_ALWAYS_ENABLED_IN_CHROME_OR_CERTIFIED_APP (1<<23)
+
 /**
  * Types of animatable values.
  */
 enum nsStyleAnimType {
   // requires a custom implementation in
   // nsStyleAnimation::ExtractComputedValue
   eStyleAnimType_Custom,
 
@@ -253,22 +261,31 @@ enum nsStyleAnimType {
 
 class nsCSSProps {
 public:
   typedef int16_t KTableValue;
 
   static void AddRefTable(void);
   static void ReleaseTable(void);
 
-  // Given a property string, return the enum value
   enum EnabledState {
-    eEnabled,
-    eEnabledInUASheets,
-    eAny
+    // The default EnabledState: only enable what's enabled for all content,
+    // given the current values of preferences.
+    eEnabledForAllContent = 0,
+    // Enable a property in UA sheets.
+    eEnabledInUASheets    = 0x01,
+    // Enable a property in privileged content, i.e. chrome or Certified Apps
+    eEnabledInChromeOrCertifiedApp = 0x02,
+    // Special value to unconditionally enable a property. This implies all the
+    // bits above, but is strictly more than just their OR-ed union.
+    // This just skips any test so a property will be enabled even if it would
+    // have been disabled with all the bits above set.
+    eIgnoreEnabledState   = 0xff
   };
+
   // Looks up the property with name aProperty and returns its corresponding
   // nsCSSProperty value.  If aProperty is the name of a custom property,
   // then eCSSPropertyExtra_variable will be returned.
   static nsCSSProperty LookupProperty(const nsAString& aProperty,
                                       EnabledState aEnabled);
   static nsCSSProperty LookupProperty(const nsACString& aProperty,
                                       EnabledState aEnabled);
   // Returns whether aProperty is a custom property name, i.e. begins with
@@ -442,21 +459,35 @@ public:
 
   static bool IsEnabled(nsCSSProperty aProperty) {
     NS_ABORT_IF_FALSE(0 <= aProperty &&
                       aProperty < eCSSProperty_COUNT_with_aliases,
                       "out of range");
     return gPropertyEnabled[aProperty];
   }
 
-  static bool IsEnabled(nsCSSProperty aProperty, EnabledState aEnabled) {
-    return IsEnabled(aProperty) ||
-      (aEnabled == eEnabledInUASheets &&
-       PropHasFlags(aProperty, CSS_PROPERTY_ALWAYS_ENABLED_IN_UA_SHEETS)) ||
-      aEnabled == eAny;
+  static bool IsEnabled(nsCSSProperty aProperty, EnabledState aEnabled)
+  {
+    if (IsEnabled(aProperty)) {
+      return true;
+    }
+    if (aEnabled == eIgnoreEnabledState) {
+      return true;
+    }
+    if ((aEnabled & eEnabledInUASheets) &&
+        PropHasFlags(aProperty, CSS_PROPERTY_ALWAYS_ENABLED_IN_UA_SHEETS))
+    {
+      return true;
+    }
+    if ((aEnabled & eEnabledInChromeOrCertifiedApp) &&
+        PropHasFlags(aProperty, CSS_PROPERTY_ALWAYS_ENABLED_IN_CHROME_OR_CERTIFIED_APP))
+    {
+      return true;
+    }
+    return false;
   }
 
 public:
 
 #define CSSPROPS_FOR_SHORTHAND_SUBPROPERTIES(iter_, prop_)                    \
   for (const nsCSSProperty* iter_ = nsCSSProps::SubpropertyEntryFor(prop_);   \
        *iter_ != eCSSProperty_UNKNOWN; ++iter_) \
     if (nsCSSProps::IsEnabled(*iter_))
@@ -602,9 +633,33 @@ public:
   static const KTableValue kWidthKTable[]; // also min-width, max-width
   static const KTableValue kWindowShadowKTable[];
   static const KTableValue kWordBreakKTable[];
   static const KTableValue kWordWrapKTable[];
   static const KTableValue kWritingModeKTable[];
   static const KTableValue kHyphensKTable[];
 };
 
+inline nsCSSProps::EnabledState operator|(nsCSSProps::EnabledState a,
+                                          nsCSSProps::EnabledState b)
+{
+  return nsCSSProps::EnabledState(int(a) | int(b));
+}
+
+inline nsCSSProps::EnabledState operator&(nsCSSProps::EnabledState a,
+                                          nsCSSProps::EnabledState b)
+{
+  return nsCSSProps::EnabledState(int(a) & int(b));
+}
+
+inline nsCSSProps::EnabledState& operator|=(nsCSSProps::EnabledState& a,
+                                            nsCSSProps::EnabledState b)
+{
+  return a = a | b;
+}
+
+inline nsCSSProps::EnabledState& operator&=(nsCSSProps::EnabledState& a,
+                                            nsCSSProps::EnabledState b)
+{
+  return a = a & b;
+}
+
 #endif /* nsCSSProps_h___ */
--- a/layout/style/nsComputedDOMStyle.cpp
+++ b/layout/style/nsComputedDOMStyle.cpp
@@ -713,17 +713,17 @@ nsComputedDOMStyle::ClearCurrentStyleSou
   // whenever a frame is not available.
   mStyleContextHolder = nullptr;
 }
 
 already_AddRefed<CSSValue>
 nsComputedDOMStyle::GetPropertyCSSValue(const nsAString& aPropertyName, ErrorResult& aRv)
 {
   nsCSSProperty prop = nsCSSProps::LookupProperty(aPropertyName,
-                                                  nsCSSProps::eEnabled);
+                                                  nsCSSProps::eEnabledForAllContent);
 
   bool needsLayoutFlush;
   nsComputedStyleMap::Entry::ComputeMethod getter;
 
   if (prop == eCSSPropertyExtra_variable) {
     needsLayoutFlush = false;
     getter = nullptr;
   } else {
--- a/layout/style/nsDOMCSSDeclaration.cpp
+++ b/layout/style/nsDOMCSSDeclaration.cpp
@@ -11,16 +11,18 @@
 #include "nsCSSStyleSheet.h"
 #include "mozilla/css/Rule.h"
 #include "mozilla/css/Declaration.h"
 #include "mozilla/dom/CSS2PropertiesBinding.h"
 #include "nsCSSProps.h"
 #include "nsCOMPtr.h"
 #include "mozAutoDocUpdate.h"
 #include "nsIURI.h"
+#include "mozilla/dom/BindingUtils.h"
+#include "nsContentUtils.h"
 
 using namespace mozilla;
 
 nsDOMCSSDeclaration::~nsDOMCSSDeclaration()
 {
 }
 
 /* virtual */ JSObject*
@@ -164,18 +166,19 @@ nsDOMCSSDeclaration::IndexedGetter(uint3
   css::Declaration* decl = GetCSSDeclaration(false);
   aFound = decl && decl->GetNthProperty(aIndex, aPropName);
 }
 
 NS_IMETHODIMP
 nsDOMCSSDeclaration::GetPropertyValue(const nsAString& aPropertyName,
                                       nsAString& aReturn)
 {
-  const nsCSSProperty propID = nsCSSProps::LookupProperty(aPropertyName,
-                                                          nsCSSProps::eEnabled);
+  const nsCSSProperty propID =
+    nsCSSProps::LookupProperty(aPropertyName,
+                               nsCSSProps::eEnabledForAllContent);
   if (propID == eCSSProperty_UNKNOWN) {
     aReturn.Truncate();
     return NS_OK;
   }
 
   if (propID == eCSSPropertyExtra_variable) {
     GetCustomPropertyValue(aPropertyName, aReturn);
     return NS_OK;
@@ -183,18 +186,19 @@ nsDOMCSSDeclaration::GetPropertyValue(co
 
   return GetPropertyValue(propID, aReturn);
 }
 
 NS_IMETHODIMP
 nsDOMCSSDeclaration::GetAuthoredPropertyValue(const nsAString& aPropertyName,
                                               nsAString& aReturn)
 {
-  const nsCSSProperty propID = nsCSSProps::LookupProperty(aPropertyName,
-                                                          nsCSSProps::eEnabled);
+  const nsCSSProperty propID =
+    nsCSSProps::LookupProperty(aPropertyName,
+                               nsCSSProps::eEnabledForAllContent);
   if (propID == eCSSProperty_UNKNOWN) {
     aReturn.Truncate();
     return NS_OK;
   }
 
   if (propID == eCSSPropertyExtra_variable) {
     GetCustomPropertyValue(aPropertyName, aReturn);
     return NS_OK;
@@ -224,18 +228,19 @@ nsDOMCSSDeclaration::GetPropertyPriority
 }
 
 NS_IMETHODIMP
 nsDOMCSSDeclaration::SetProperty(const nsAString& aPropertyName,
                                  const nsAString& aValue,
                                  const nsAString& aPriority)
 {
   // In the common (and fast) cases we can use the property id
-  nsCSSProperty propID = nsCSSProps::LookupProperty(aPropertyName,
-                                                    nsCSSProps::eEnabled);
+  nsCSSProperty propID =
+    nsCSSProps::LookupProperty(aPropertyName,
+                               nsCSSProps::eEnabledForAllContent);
   if (propID == eCSSProperty_UNKNOWN) {
     return NS_OK;
   }
 
   if (aValue.IsEmpty()) {
     // If the new value of the property is an empty string we remove the
     // property.
     // XXX this ignores the priority string, should it?
@@ -260,18 +265,19 @@ nsDOMCSSDeclaration::SetProperty(const n
   }
   return ParsePropertyValue(propID, aValue, important);
 }
 
 NS_IMETHODIMP
 nsDOMCSSDeclaration::RemoveProperty(const nsAString& aPropertyName,
                                     nsAString& aReturn)
 {
-  const nsCSSProperty propID = nsCSSProps::LookupProperty(aPropertyName,
-                                                          nsCSSProps::eEnabled);
+  const nsCSSProperty propID =
+    nsCSSProps::LookupProperty(aPropertyName,
+                               nsCSSProps::eEnabledForAllContent);
   if (propID == eCSSProperty_UNKNOWN) {
     aReturn.Truncate();
     return NS_OK;
   }
 
   if (propID == eCSSPropertyExtra_variable) {
     RemoveCustomProperty(aPropertyName);
     return NS_OK;
@@ -420,8 +426,29 @@ nsDOMCSSDeclaration::RemoveCustomPropert
   // between when we mutate the declaration and when we set the new
   // rule (see stack in bug 209575).
   mozAutoDocConditionalContentUpdateBatch autoUpdate(DocToUpdate(), true);
 
   decl = decl->EnsureMutable();
   decl->RemoveVariableDeclaration(Substring(aPropertyName, VAR_PREFIX_LENGTH));
   return SetCSSDeclaration(decl);
 }
+
+bool IsCSSPropertyExposedToJS(nsCSSProperty aProperty, JSContext* cx, JSObject* obj)
+{
+  nsCSSProps::EnabledState enabledState = nsCSSProps::eEnabledForAllContent;
+
+  // Optimization: we skip checking properties of the JSContext
+  // in the majority case where the property does not have the
+  // CSS_PROPERTY_ALWAYS_ENABLED_IN_PRIVILEGED_CONTENT flag.
+  bool isEnabledInChromeOrCertifiedApp
+    = nsCSSProps::PropHasFlags(aProperty,
+                               CSS_PROPERTY_ALWAYS_ENABLED_IN_CHROME_OR_CERTIFIED_APP);
+
+  if (isEnabledInChromeOrCertifiedApp) {
+    if (dom::IsInCertifiedApp(cx, obj) ||
+        nsContentUtils::ThreadsafeIsCallerChrome())
+    {
+      enabledState |= nsCSSProps::eEnabledInChromeOrCertifiedApp;
+    }
+  }
+  return nsCSSProps::IsEnabled(aProperty, enabledState);
+}
--- a/layout/style/nsDOMCSSDeclaration.h
+++ b/layout/style/nsDOMCSSDeclaration.h
@@ -10,16 +10,18 @@
 
 #include "nsICSSDeclaration.h"
 
 #include "mozilla/Attributes.h"
 #include "nsCOMPtr.h"
 
 class nsIPrincipal;
 class nsIDocument;
+struct JSContext;
+class JSObject;
 
 namespace mozilla {
 namespace css {
 class Declaration;
 class Loader;
 class Rule;
 }
 }
@@ -147,9 +149,17 @@ protected:
 protected:
   virtual ~nsDOMCSSDeclaration();
   nsDOMCSSDeclaration()
   {
     SetIsDOMBinding();
   }
 };
 
+bool IsCSSPropertyExposedToJS(nsCSSProperty aProperty, JSContext* cx, JSObject* obj);
+
+template <nsCSSProperty Property>
+MOZ_ALWAYS_INLINE bool IsCSSPropertyExposedToJS(JSContext* cx, JSObject* obj)
+{
+  return IsCSSPropertyExposedToJS(Property, cx, obj);
+}
+
 #endif // nsDOMCSSDeclaration_h___
--- a/layout/style/nsRuleNode.cpp
+++ b/layout/style/nsRuleNode.cpp
@@ -4853,18 +4853,19 @@ nsRuleNode::ComputeDisplayData(void* aSt
     } else if (property.unit == eCSSUnit_None) {
       transition->SetProperty(eCSSPropertyExtra_no_properties);
     } else if (property.list) {
       const nsCSSValue &val = property.list->mValue;
 
       if (val.GetUnit() == eCSSUnit_Ident) {
         nsDependentString
           propertyStr(property.list->mValue.GetStringBufferValue());
-        nsCSSProperty prop = nsCSSProps::LookupProperty(propertyStr,
-                                                        nsCSSProps::eEnabled);
+        nsCSSProperty prop =
+          nsCSSProps::LookupProperty(propertyStr,
+                                     nsCSSProps::eEnabledForAllContent);
         if (prop == eCSSProperty_UNKNOWN) {
           transition->SetUnknownProperty(propertyStr);
         } else {
           transition->SetProperty(prop);
         }
       } else {
         NS_ABORT_IF_FALSE(val.GetUnit() == eCSSUnit_All,
                           nsPrintfCString("Invalid transition property unit %d",
@@ -5497,17 +5498,18 @@ nsRuleNode::ComputeDisplayData(void* aSt
         if (buffer.EqualsLiteral("opacity")) {
           display->mWillChangeBitField |= NS_STYLE_WILL_CHANGE_OPACITY;
         }
         if (buffer.EqualsLiteral("scroll-position")) {
           display->mWillChangeBitField |= NS_STYLE_WILL_CHANGE_SCROLL;
         }
 
         nsCSSProperty prop =
-          nsCSSProps::LookupProperty(buffer, nsCSSProps::eEnabled);
+          nsCSSProps::LookupProperty(buffer,
+                                     nsCSSProps::eEnabledForAllContent);
         if (prop != eCSSProperty_UNKNOWN &&
             nsCSSProps::PropHasFlags(prop,
                                      CSS_PROPERTY_CREATES_STACKING_CONTEXT))
         {
           display->mWillChangeBitField |= NS_STYLE_WILL_CHANGE_STACKING_CONTEXT;
         }
       }
     }
--- a/layout/style/nsStyleStruct.cpp
+++ b/layout/style/nsStyleStruct.cpp
@@ -2225,17 +2225,17 @@ void nsTransition::SetInitialValues()
   mDuration = 0.0;
   mDelay = 0.0;
   mProperty = eCSSPropertyExtra_all_properties;
 }
 
 void nsTransition::SetUnknownProperty(const nsAString& aUnknownProperty)
 {
   NS_ASSERTION(nsCSSProps::LookupProperty(aUnknownProperty,
-                                          nsCSSProps::eEnabled) ==
+                                          nsCSSProps::eEnabledForAllContent) ==
                  eCSSProperty_UNKNOWN,
                "should be unknown property");
   mProperty = eCSSProperty_UNKNOWN;
   mUnknownProperty = do_GetAtom(aUnknownProperty);
 }
 
 nsAnimation::nsAnimation(const nsAnimation& aCopy)
   : mTimingFunction(aCopy.mTimingFunction)
--- a/layout/style/test/TestCSSPropertyLookup.cpp
+++ b/layout/style/test/TestCSSPropertyLookup.cpp
@@ -35,47 +35,49 @@ TestProps()
   const char*const* et = &kCSSRawProperties[0];
   const char*const* end = &kCSSRawProperties[eCSSProperty_COUNT];
   index = eCSSProperty_UNKNOWN;
   while (et < end) {
     char tagName[100];
     PL_strcpy(tagName, *et);
     index = nsCSSProperty(int32_t(index) + 1);
 
-    id = nsCSSProps::LookupProperty(nsCString(tagName), nsCSSProps::eAny);
+    id = nsCSSProps::LookupProperty(nsCString(tagName),
+                                    nsCSSProps::eIgnoreEnabledState);
     if (id == eCSSProperty_UNKNOWN) {
       printf("bug: can't find '%s'\n", tagName);
       success = false;
     }
     if (id != index) {
       printf("bug: name='%s' id=%d index=%d\n", tagName, id, index);
       success = false;
     }
 
     // fiddle with the case to make sure we can still find it
     if (('a' <= tagName[0]) && (tagName[0] <= 'z')) {
       tagName[0] = tagName[0] - 32;
     }
     id = nsCSSProps::LookupProperty(NS_ConvertASCIItoUTF16(tagName),
-                                    nsCSSProps::eAny);
+                                    nsCSSProps::eIgnoreEnabledState);
     if (id < 0) {
       printf("bug: can't find '%s'\n", tagName);
       success = false;
     }
     if (index != id) {
       printf("bug: name='%s' id=%d index=%d\n", tagName, id, index);
       success = false;
     }
     et++;
   }
 
   // Now make sure we don't find some garbage
   for (int i = 0; i < (int) (sizeof(kJunkNames) / sizeof(const char*)); i++) {
     const char* const tag = kJunkNames[i];
-    id = nsCSSProps::LookupProperty(nsAutoCString(tag), nsCSSProps::eAny);
+    id = nsCSSProps::LookupProperty(nsAutoCString(tag),
+                                    nsCSSProps::eIgnoreEnabledState);
     if (id >= 0) {
       printf("bug: found '%s'\n", tag ? tag : "(null)");
       success = false;
     }
   }
 
   nsCSSProps::ReleaseTable();
   return success;
--- a/mobile/android/base/Tab.java
+++ b/mobile/android/base/Tab.java
@@ -62,17 +62,17 @@ public class Tab {
     private int mBackgroundColor;
     private int mState;
     private Bitmap mThumbnailBitmap;
     private boolean mDesktopMode;
     private boolean mEnteringReaderMode;
     private Context mAppContext;
     private ErrorType mErrorType = ErrorType.NONE;
     private static final int MAX_HISTORY_LIST_SIZE = 50;
-    private int mLoadProgress;
+    private volatile int mLoadProgress;
 
     public static final int STATE_DELAYED = 0;
     public static final int STATE_LOADING = 1;
     public static final int STATE_SUCCESS = 2;
     public static final int STATE_ERROR = 3;
 
     public static final int LOAD_PROGRESS_INIT = 10;
     public static final int LOAD_PROGRESS_START = 20;
@@ -630,17 +630,17 @@ public class Tab {
         setHasFeeds(false);
         setHasOpenSearch(false);
         updateIdentityData(null);
         setReaderEnabled(false);
         setZoomConstraints(new ZoomConstraints(true));
         setHasTouchListeners(false);
         setBackgroundColor(DEFAULT_BACKGROUND_COLOR);
         setErrorType(ErrorType.NONE);
-        setLoadProgress(LOAD_PROGRESS_LOCATION_CHANGE);
+        setLoadProgressIfLoading(LOAD_PROGRESS_LOCATION_CHANGE);
 
         Tabs.getInstance().notifyListeners(this, Tabs.TabEvents.LOCATION_CHANGE, oldUrl);
     }
 
     private static boolean shouldShowProgress(final String url) {
         return AboutPages.isAboutHome(url) ||
                AboutPages.isAboutReader(url) ||
                AboutPages.isAboutPrivateBrowsing(url);
@@ -667,17 +667,17 @@ public class Tab {
                     return;
 
                 ThumbnailHelper.getInstance().getAndProcessThumbnailFor(tab);
             }
         }, 500);
     }
 
     void handleContentLoaded() {
-        setLoadProgress(LOAD_PROGRESS_LOADED);
+        setLoadProgressIfLoading(LOAD_PROGRESS_LOADED);
     }
 
     protected void saveThumbnailToDB() {
         try {
             String url = getURL();
             if (url == null)
                 return;
 
@@ -772,16 +772,32 @@ public class Tab {
      *
      * @param progressPercentage Percentage to set progress to (0-100)
      */
     void setLoadProgress(int progressPercentage) {
         mLoadProgress = progressPercentage;
     }
 
     /**
+     * Sets the tab load progress to the given percentage only if the tab is
+     * currently loading.
+     *
+     * about:neterror can trigger a STOP before other page load events (bug
+     * 976426), so any post-START events should make sure the page is loading
+     * before updating progress.
+     *
+     * @param progressPercentage Percentage to set progress to (0-100)
+     */
+    void setLoadProgressIfLoading(int progressPercentage) {
+        if (getState() == STATE_LOADING) {
+            setLoadProgress(progressPercentage);
+        }
+    }
+
+    /**
      * Gets the tab load progress percentage.
      *
      * @return Current progress percentage
      */
     public int getLoadProgress() {
         return mLoadProgress;
     }
 }
--- a/mobile/android/base/gfx/GeckoLayerClient.java
+++ b/mobile/android/base/gfx/GeckoLayerClient.java
@@ -882,17 +882,17 @@ public class GeckoLayerClient implements
     /** Implementation of PanZoomTarget
      * Notification that a subdocument has been scrolled by a certain amount.
      * This is used here to make sure that the margins are still accessible
      * during subdocument scrolling.
      *
      * You must hold the monitor while calling this.
      */
     @Override
-    public void onSubdocumentScrollBy(float dx, float dy) {
+    public void scrollMarginsBy(float dx, float dy) {
         ImmutableViewportMetrics newMarginsMetrics =
             mMarginsAnimator.scrollBy(mViewportMetrics, dx, dy);
         mViewportMetrics = mViewportMetrics.setMarginsFrom(newMarginsMetrics);
         viewportMetricsChanged(true);
     }
 
     /** Implementation of PanZoomTarget */
     @Override
--- a/mobile/android/base/gfx/JavaPanZoomController.java
+++ b/mobile/android/base/gfx/JavaPanZoomController.java
@@ -123,16 +123,18 @@ class JavaPanZoomController
     /* The per-frame zoom delta for the currently-running AUTONAV animation. */
     private float mAutonavZoomDelta;
     /* The user selected panning mode */
     private AxisLockMode mMode;
     /* A medium-length tap/press is happening */
     private boolean mMediumPress;
     /* Used to change the scrollY direction */
     private boolean mNegateWheelScrollY;
+    /* Whether the current event has been default-prevented. */
+    private boolean mDefaultPrevented;
 
     // Handler to be notified when overscroll occurs
     private Overscroll mOverscroll;
 
     public JavaPanZoomController(PanZoomTarget target, View view, EventDispatcher eventDispatcher) {
         mTarget = target;
         mSubscroller = new SubdocumentScrollHelper(eventDispatcher);
         mX = new AxisX(mSubscroller);
@@ -338,17 +340,19 @@ class JavaPanZoomController
     }
 
     /** This function MUST be called on the UI thread */
     @Override
     public boolean onTouchEvent(MotionEvent event) {
         return mTouchEventHandler.handleEvent(event);
     }
 
-    boolean handleEvent(MotionEvent event) {
+    boolean handleEvent(MotionEvent event, boolean defaultPrevented) {
+        mDefaultPrevented = defaultPrevented;
+
         switch (event.getAction() & MotionEvent.ACTION_MASK) {
         case MotionEvent.ACTION_DOWN:   return handleTouchStart(event);
         case MotionEvent.ACTION_MOVE:   return handleTouchMove(event);
         case MotionEvent.ACTION_UP:     return handleTouchEnd(event);
         case MotionEvent.ACTION_CANCEL: return handleTouchCancel(event);
         }
         return false;
     }
@@ -396,27 +400,16 @@ class JavaPanZoomController
         if (waitingForTouchListeners && (event.getAction() & MotionEvent.ACTION_MASK) == MotionEvent.ACTION_DOWN) {
             // this is the first touch point going down, so we enter the pending state
             // seting the state will kill any animations in progress, possibly leaving
             // the page in overscroll
             setState(PanZoomState.WAITING_LISTENERS);
         }
     }
 
-    /** This function must be called on the UI thread. */
-    public void preventedTouchFinished() {
-        checkMainThread();
-        if (mState == PanZoomState.WAITING_LISTENERS) {
-            // if we enter here, we just finished a block of events whose default actions
-            // were prevented by touch listeners. Now there are no touch points left, so
-            // we need to reset our state and re-bounce because we might be in overscroll
-            bounce();
-        }
-    }
-
     /** This must be called on the UI thread. */
     @Override
     public void pageRectUpdated() {
         if (mState == PanZoomState.NOTHING) {
             synchronized (mTarget.getLock()) {
                 ImmutableViewportMetrics validated = getValidViewportMetrics();
                 if (!getMetrics().fuzzyEquals(validated)) {
                     // page size changed such that we are now in overscroll. snap to the
@@ -519,26 +512,28 @@ class JavaPanZoomController
     }
 
     private boolean handleTouchEnd(MotionEvent event) {
 
         switch (mState) {
         case FLING:
         case AUTONAV:
         case BOUNCE:
-        case WAITING_LISTENERS:
-            // should never happen
-            Log.e(LOGTAG, "Received impossible touch end while in " + mState);
-            // fall through
         case ANIMATED_ZOOM:
         case NOTHING:
             // may happen if user double-taps and drags without lifting after the
             // second tap. ignore if this happens.
             return false;
 
+        case WAITING_LISTENERS:
+            if (!mDefaultPrevented) {
+              // should never happen
+              Log.e(LOGTAG, "Received impossible touch end while in " + mState);
+            }
+            // fall through
         case TOUCHING:
             // the switch into TOUCHING might have happened while the page was
             // snapping back after overscroll. we need to finish the snap if that
             // was the case
             bounce();
             return false;
 
         case PANNING:
@@ -557,26 +552,16 @@ class JavaPanZoomController
         }
         Log.e(LOGTAG, "Unhandled case " + mState + " in handleTouchEnd");
         return false;
     }
 
     private boolean handleTouchCancel(MotionEvent event) {
         cancelTouch();
 
-        if (mState == PanZoomState.WAITING_LISTENERS) {
-            // we might get a cancel event from the TouchEventHandler while in the
-            // WAITING_LISTENERS state if the touch listeners prevent-default the
-            // block of events. at this point being in WAITING_LISTENERS is equivalent
-            // to being in NOTHING with the exception of possibly being in overscroll.
-            // so here we don't want to do anything right now; the overscroll will be
-            // corrected in preventedTouchFinished().
-            return false;
-        }
-
         // ensure we snap back if we're overscrolled
         bounce();
         return false;
     }
 
     private boolean handlePointerScroll(MotionEvent event) {
         if (mState == PanZoomState.NOTHING || mState == PanZoomState.FLING) {
             float scrollX = event.getAxisValue(MotionEvent.AXIS_HSCROLL);
@@ -822,19 +807,19 @@ class JavaPanZoomController
 
     private void updatePosition() {
         mX.displace();
         mY.displace();
         PointF displacement = resetDisplacement();
         if (FloatUtils.fuzzyEquals(displacement.x, 0.0f) && FloatUtils.fuzzyEquals(displacement.y, 0.0f)) {
             return;
         }
-        if (mSubscroller.scrollBy(displacement)) {
+        if (mDefaultPrevented || mSubscroller.scrollBy(displacement)) {
             synchronized (mTarget.getLock()) {
-                mTarget.onSubdocumentScrollBy(displacement.x, displacement.y);
+                mTarget.scrollMarginsBy(displacement.x, displacement.y);
             }
         } else {
             synchronized (mTarget.getLock()) {
                 scrollBy(displacement.x, displacement.y);
             }
         }
     }
 
--- a/mobile/android/base/gfx/PanZoomTarget.java
+++ b/mobile/android/base/gfx/PanZoomTarget.java
@@ -14,17 +14,17 @@ public interface PanZoomTarget {
     public ImmutableViewportMetrics getViewportMetrics();
     public ZoomConstraints getZoomConstraints();
     public boolean isFullScreen();
     public RectF getMaxMargins();
 
     public void setAnimationTarget(ImmutableViewportMetrics viewport);
     public void setViewportMetrics(ImmutableViewportMetrics viewport);
     public void scrollBy(float dx, float dy);
-    public void onSubdocumentScrollBy(float dx, float dy);
+    public void scrollMarginsBy(float dx, float dy);
     public void panZoomStopped();
     /** This triggers an (asynchronous) viewport update/redraw. */
     public void forceRedraw(DisplayPortMetrics displayPort);
 
     public boolean post(Runnable action);
     public boolean postDelayed(Runnable action, long delayMillis);
     public void postRenderTask(RenderTask task);
     public void removeRenderTask(RenderTask task);
--- a/mobile/android/base/gfx/TouchEventHandler.java
+++ b/mobile/android/base/gfx/TouchEventHandler.java
@@ -69,21 +69,21 @@ final class TouchEventHandler implements
     // per-tab and is updated when we switch tabs).
     private boolean mWaitForTouchListeners;
 
     // true if we should hold incoming events in our queue. this is re-set for every
     // block of events, this is cleared once we find out if the block has been
     // default-prevented or not (or we time out waiting for that).
     private boolean mHoldInQueue;
 
-    // true if we should dispatch incoming events to the gesture detector and the pan/zoom
-    // controller. if this is false, then the current block of events has been
-    // default-prevented, and we should not dispatch these events (although we'll still send
-    // them to gecko listeners).
-    private boolean mDispatchEvents;
+    // false if the current event block has been default-prevented. In this case,
+    // we still pass the event to both Gecko and the pan/zoom controller, but the
+    // latter will not use it to scroll content. It may still use the events for
+    // other things, such as making the dynamic toolbar visible.
+    private boolean mAllowDefaultAction;
 
     // this next variable requires some explanation. strap yourself in.
     //
     // for each block of events, we do two things: (1) send the events to gecko and expect
     // exactly one default-prevented notification in return, and (2) kick off a delayed
     // ListenerTimeoutProcessor that triggers in case we don't hear from the listener in
     // a timely fashion.
     // since events are constantly coming in, we need to be able to handle more than one
@@ -123,37 +123,37 @@ final class TouchEventHandler implements
     TouchEventHandler(Context context, View view, JavaPanZoomController panZoomController) {
         mView = view;
 
         mEventQueue = new LinkedList<MotionEvent>();
         mPanZoomController = panZoomController;
         mGestureDetector = new GestureDetector(context, mPanZoomController);
         mScaleGestureDetector = new SimpleScaleGestureDetector(mPanZoomController);
         mListenerTimeoutProcessor = new ListenerTimeoutProcessor();
-        mDispatchEvents = true;
+        mAllowDefaultAction = true;
 
         mGestureDetector.setOnDoubleTapListener(mPanZoomController);
 
         Tabs.registerOnTabsChangedListener(this);
     }
 
     public void destroy() {
         Tabs.unregisterOnTabsChangedListener(this);
     }
 
     /* This function MUST be called on the UI thread */
     public boolean handleEvent(MotionEvent event) {
         if (isDownEvent(event)) {
             // this is the start of a new block of events! whee!
             mHoldInQueue = mWaitForTouchListeners;
 
-            // Set mDispatchEvents to true so that we are guaranteed to either queue these
-            // events or dispatch them. The only time we should not do either is once we've
-            // heard back from content to preventDefault this block.
-            mDispatchEvents = true;
+            // Set mAllowDefaultAction to true so that in the event we dispatch events, the
+            // PanZoomController doesn't treat them as if they've been prevent-defaulted
+            // when they haven't.
+            mAllowDefaultAction = true;
             if (mHoldInQueue) {
                 // if the new block we are starting is the current block (i.e. there are no
                 // other blocks waiting in the queue, then we should let the pan/zoom controller
                 // know we are waiting for the touch listeners to run
                 if (mEventQueue.isEmpty()) {
                     mPanZoomController.startingNewEventBlock(event, true);
                 }
             } else {
@@ -165,27 +165,22 @@ final class TouchEventHandler implements
                 mPanZoomController.startingNewEventBlock(event, false);
             }
 
             // set the timeout so that we dispatch these events and update mProcessingBalance
             // if we don't get a default-prevented notification
             mView.postDelayed(mListenerTimeoutProcessor, EVENT_LISTENER_TIMEOUT);
         }
 
-        // if we need to hold the events, add it to the queue. if we need to dispatch
-        // it directly, do that. it is possible that both mHoldInQueue and mDispatchEvents
-        // are false, in which case we are processing a block of events that we know
-        // has been default-prevented. in that case we don't keep the events as we don't
-        // need them (but we still pass them to the gecko listener).
+        // if we need to hold the events, add it to the queue, otherwise dispatch
+        // it directly.
         if (mHoldInQueue) {
             mEventQueue.add(MotionEvent.obtain(event));
-        } else if (mDispatchEvents) {
-            dispatchEvent(event);
-        } else if (touchFinished(event)) {
-            mPanZoomController.preventedTouchFinished();
+        } else {
+            dispatchEvent(event, mAllowDefaultAction);
         }
 
         return false;
     }
 
     /**
      * This function is how gecko sends us a default-prevented notification. It is called
      * once gecko knows definitively whether the block of events has had preventDefault
@@ -219,71 +214,60 @@ final class TouchEventHandler implements
     private boolean touchFinished(MotionEvent event) {
         int action = (event.getAction() & MotionEvent.ACTION_MASK);
         return (action == MotionEvent.ACTION_UP || action == MotionEvent.ACTION_CANCEL);
     }
 
     /**
      * Dispatch the event to the gesture detectors and the pan/zoom controller.
      */
-    private void dispatchEvent(MotionEvent event) {
-        if (mGestureDetector.onTouchEvent(event)) {
-            return;
+    private void dispatchEvent(MotionEvent event, boolean allowDefaultAction) {
+        if (allowDefaultAction) {
+            if (mGestureDetector.onTouchEvent(event)) {
+                return;
+            }
+            mScaleGestureDetector.onTouchEvent(event);
+            if (mScaleGestureDetector.isInProgress()) {
+                return;
+            }
         }
-        mScaleGestureDetector.onTouchEvent(event);
-        if (mScaleGestureDetector.isInProgress()) {
-            return;
-        }
-        mPanZoomController.handleEvent(event);
+        mPanZoomController.handleEvent(event, !allowDefaultAction);
     }
 
     /**
      * Process the block of events at the head of the queue now that we know
      * whether it has been default-prevented or not.
      */
     private void processEventBlock(boolean allowDefaultAction) {
-        if (!allowDefaultAction) {
-            // if the block has been default-prevented, cancel whatever stuff we had in
-            // progress in the gesture detector and pan zoom controller
-            long now = SystemClock.uptimeMillis();
-            dispatchEvent(MotionEvent.obtain(now, now, MotionEvent.ACTION_CANCEL, 0, 0, 0));
-        }
-
         if (mEventQueue.isEmpty()) {
             Log.e(LOGTAG, "Unexpected empty event queue in processEventBlock!", new Exception());
             return;
         }
 
         // the odd loop condition is because the first event in the queue will
         // always be a DOWN or POINTER_DOWN event, and we want to process all
         // the events in the queue starting at that one, up to but not including
         // the next DOWN or POINTER_DOWN event.
 
         MotionEvent event = mEventQueue.poll();
         while (true) {
             // event being null here is valid and represents a block of events
             // that has already been dispatched.
 
             if (event != null) {
-                // for each event we process, only dispatch it if the block hasn't been
-                // default-prevented.
-                if (allowDefaultAction) {
-                    dispatchEvent(event);
-                } else if (touchFinished(event)) {
-                    mPanZoomController.preventedTouchFinished();
-                }
+                dispatchEvent(event, allowDefaultAction);
             }
             if (mEventQueue.isEmpty()) {
                 // we have processed the backlog of events, and are all caught up.
                 // now we can set clear the hold flag and set the dispatch flag so
                 // that the handleEvent() function can do the right thing for all
                 // remaining events in this block (which is still ongoing) without
                 // having to put them in the queue.
                 mHoldInQueue = false;
-                mDispatchEvents = allowDefaultAction;
+                mAllowDefaultAction = allowDefaultAction;
                 break;
             }
             event = mEventQueue.peek();
             if (event == null || isDownEvent(event)) {
                 // we have finished processing the block we were interested in.
                 // now we wait for the next call to processEventBlock
                 if (event != null) {
                     mPanZoomController.startingNewEventBlock(event, true);
--- a/mobile/android/base/home/HomeConfig.java
+++ b/mobile/android/base/home/HomeConfig.java
@@ -1,28 +1,33 @@
 /* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
  * This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 package org.mozilla.gecko.home;
 
 import org.mozilla.gecko.R;
+import org.mozilla.gecko.util.ThreadUtils;
 
 import org.json.JSONArray;
 import org.json.JSONException;
 import org.json.JSONObject;
 
 import android.content.Context;
 import android.os.Parcel;
 import android.os.Parcelable;
 import android.text.TextUtils;
 
 import java.util.ArrayList;
+import java.util.Collections;
 import java.util.EnumSet;
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.LinkedHashMap;
 import java.util.List;
 
 public final class HomeConfig {
     /**
      * Used to determine what type of HomeFragment subclass to use when creating
      * a given panel. With the exception of DYNAMIC, all of these types correspond
      * to a default set of built-in panels. The DYNAMIC panel type is used by
      * third-party services to create panels with varying types of content.
@@ -260,29 +265,29 @@ public final class HomeConfig {
         public boolean isDynamic() {
             return (mType == PanelType.DYNAMIC);
         }
 
         public boolean isDefault() {
             return mFlags.contains(Flags.DEFAULT_PANEL);
         }
 
-        public void setIsDefault(boolean isDefault) {
+        private void setIsDefault(boolean isDefault) {
             if (isDefault) {
                 mFlags.add(Flags.DEFAULT_PANEL);
             } else {
                 mFlags.remove(Flags.DEFAULT_PANEL);
             }
         }
 
         public boolean isDisabled() {
             return mFlags.contains(Flags.DISABLED_PANEL);
         }
 
-        public void setIsDisabled(boolean isDisabled) {
+        private void setIsDisabled(boolean isDisabled) {
             if (isDisabled) {
                 mFlags.add(Flags.DISABLED_PANEL);
             } else {
                 mFlags.remove(Flags.DISABLED_PANEL);
             }
         }
 
         public JSONObject toJSON() throws JSONException {
@@ -687,16 +692,384 @@ public final class HomeConfig {
 
             @Override
             public ViewConfig[] newArray(final int size) {
                 return new ViewConfig[size];
             }
         };
     }
 
+    /**
+     * Immutable representation of the current state of {@code HomeConfig}.
+     * This is what HomeConfig returns from a load() call and takes as
+     * input to save a new state.
+     *
+     * Users of {@code State} should use an {@code Iterator} to iterate
+     * through the contained {@code PanelConfig} instances.
+     *
+     * {@code State} is immutable i.e. you can't add, remove, or update
+     * contained elements directly. You have to use an {@code Editor} to
+     * change the state, which can be created through the {@code edit()}
+     * method.
+     */
+    public static class State implements Iterable<PanelConfig> {
+        private final HomeConfig mHomeConfig;
+        private final List<PanelConfig> mPanelConfigs;
+
+        private State(HomeConfig homeConfig, List<PanelConfig> panelConfigs) {
+            mHomeConfig = homeConfig;
+            mPanelConfigs = Collections.unmodifiableList(panelConfigs);
+        }
+
+        @Override
+        public Iterator<PanelConfig> iterator() {
+            return mPanelConfigs.iterator();
+        }
+
+        /**
+         * Creates an {@code Editor} for this state.
+         */
+        public Editor edit() {
+            return new Editor(mHomeConfig, this);
+        }
+    }
+
+    /**
+     * {@code Editor} allows you to make changes to a {@code State}. You
+     * can create {@code Editor} by calling {@code edit()} on the target
+     * {@code State} instance.
+     *
+     * {@code Editor} works on a copy of the {@code State} that originated
+     * it. This means that adding, removing, or updating panels in an
+     * {@code Editor} will never change the {@code State} which you
+     * created the {@code Editor} from. Calling {@code commit()} or
+     * {@code apply()} will cause the new {@code State} instance to be
+     * created and saved using the {@code HomeConfig} instance that
+     * created the source {@code State}.
+     *
+     * {@code Editor} is *not* thread-safe. You can only make calls on it
+     * from the thread where it was originally created. It will throw an
+     * exception if you don't follow this invariant.
+     */
+    public static class Editor implements Iterable<PanelConfig> {
+        private final HomeConfig mHomeConfig;
+        private final HashMap<String, PanelConfig> mConfigMap;
+        private final Thread mOriginalThread;
+
+        private PanelConfig mDefaultPanel;
+        private int mEnabledCount;
+
+        private Editor(HomeConfig homeConfig, State configState) {
+            mHomeConfig = homeConfig;
+            mOriginalThread = Thread.currentThread();
+            mConfigMap = new LinkedHashMap<String, PanelConfig>();
+            mEnabledCount = 0;
+
+            initFromState(configState);
+        }
+
+        /**
+         * Initialize the initial state of the editor from the given
+         * {@sode State}. A LinkedHashMap is used to represent the list of
+         * panels as it provides fast access to specific panels from IDs
+         * while also being order-aware. We keep a reference to the
+         * default panel and the number of enabled panels to avoid iterating
+         * through the map every time we need those.
+         *
+         * @param configState The source State to load the editor from.
+         */
+        private void initFromState(State configState) {
+            for (PanelConfig panelConfig : configState) {
+                final PanelConfig panelCopy = new PanelConfig(panelConfig);
+
+                if (!panelCopy.isDisabled()) {
+                    mEnabledCount++;
+                }
+
+                if (panelCopy.isDefault()) {
+                    if (mDefaultPanel == null) {
+                        mDefaultPanel = panelCopy;
+                    } else {
+                        throw new IllegalStateException("Multiple default panels in HomeConfig state");
+                    }
+                }
+
+                mConfigMap.put(panelConfig.getId(), panelCopy);
+            }
+
+            // We should always have a defined default panel if there's
+            // at least one enabled panel around.
+            if (mEnabledCount > 0 && mDefaultPanel == null) {
+                throw new IllegalStateException("Default panel in HomeConfig state is undefined");
+            }
+        }
+
+        private PanelConfig getPanelOrThrow(String panelId) {
+            final PanelConfig panelConfig = mConfigMap.get(panelId);
+            if (panelConfig == null) {
+                throw new IllegalStateException("Tried to access non-existing panel: " + panelId);
+            }
+
+            return panelConfig;
+        }
+
+        private boolean isCurrentDefaultPanel(PanelConfig panelConfig) {
+            if (mDefaultPanel == null) {
+                return false;
+            }
+
+            return mDefaultPanel.equals(panelConfig);
+        }
+
+        private void findNewDefault() {
+            // Pick the first panel that is neither disabled nor currently
+            // set as default.
+            for (PanelConfig panelConfig : mConfigMap.values()) {
+                if (!panelConfig.isDefault() && !panelConfig.isDisabled()) {
+                    setDefault(panelConfig.getId());
+                    return;
+                }
+            }
+
+            mDefaultPanel = null;
+        }
+
+        private List<PanelConfig> makeDeepCopy() {
+            List<PanelConfig> copiedList = new ArrayList<PanelConfig>();
+            for (PanelConfig panelConfig : mConfigMap.values()) {
+                copiedList.add(new PanelConfig(panelConfig));
+            }
+
+            return copiedList;
+        }
+
+        private void setPanelIsDisabled(PanelConfig panelConfig, boolean disabled) {
+            if (panelConfig.isDisabled() == disabled) {
+                return;
+            }
+
+            panelConfig.setIsDisabled(disabled);
+            mEnabledCount += (disabled ? -1 : 1);
+        }
+
+        /**
+         * Gets the ID of the current default panel.
+         */
+        public String getDefaultPanelId() {
+            ThreadUtils.assertOnThread(mOriginalThread);
+
+            if (mDefaultPanel == null) {
+                return null;
+            }
+
+            return mDefaultPanel.getId();
+        }
+
+        /**
+         * Set a new default panel.
+         *
+         * @param panelId the ID of the new default panel.
+         */
+        public void setDefault(String panelId) {
+            ThreadUtils.assertOnThread(mOriginalThread);
+
+            final PanelConfig panelConfig = getPanelOrThrow(panelId);
+            if (isCurrentDefaultPanel(panelConfig)) {
+                return;
+            }
+
+            if (mDefaultPanel != null) {
+                mDefaultPanel.setIsDefault(false);
+            }
+
+            panelConfig.setIsDefault(true);
+            setPanelIsDisabled(panelConfig, false);
+
+            mDefaultPanel = panelConfig;
+        }
+
+        /**
+         * Toggles disabled state for a panel.
+         *
+         * @param panelId the ID of the target panel.
+         * @param disabled true to disable the panel.
+         */
+        public void setDisabled(String panelId, boolean disabled) {
+            ThreadUtils.assertOnThread(mOriginalThread);
+
+            final PanelConfig panelConfig = getPanelOrThrow(panelId);
+            if (panelConfig.isDisabled() == disabled) {
+                return;
+            }
+
+            setPanelIsDisabled(panelConfig, disabled);
+
+            if (disabled) {
+                if (isCurrentDefaultPanel(panelConfig)) {
+                    panelConfig.setIsDefault(false);
+                    findNewDefault();
+                }
+            } else if (mEnabledCount == 1) {
+                setDefault(panelId);
+            }
+        }
+
+        /**
+         * Adds a new {@code PanelConfig}. It will do nothing if the
+         * {@code Editor} already contains a panel with the same ID.
+         *
+         * @param panelConfig the {@code PanelConfig} instance to be added.
+         * @return true if the item has been added.
+         */
+        public boolean install(PanelConfig panelConfig) {
+            ThreadUtils.assertOnThread(mOriginalThread);
+
+            if (panelConfig == null) {
+                throw new IllegalStateException("Can't install a null panel");
+            }
+
+            if (!panelConfig.isDynamic()) {
+                throw new IllegalStateException("Can't install a built-in panel: " + panelConfig.getId());
+            }
+
+            if (panelConfig.isDisabled()) {
+                throw new IllegalStateException("Can't install a disabled panel: " + panelConfig.getId());
+            }
+
+            boolean installed = false;
+
+            final String id = panelConfig.getId();
+            if (!mConfigMap.containsKey(id)) {
+                mConfigMap.put(id, panelConfig);
+
+                mEnabledCount++;
+                if (mEnabledCount == 1 || panelConfig.isDefault()) {
+                    setDefault(panelConfig.getId());
+                }
+
+                installed = true;
+            }
+
+            return installed;
+        }
+
+        /**
+         * Removes an existing panel.
+         *
+         * @return true if the item has been removed.
+         */
+        public boolean uninstall(String panelId) {
+            ThreadUtils.assertOnThread(mOriginalThread);
+
+            final PanelConfig panelConfig = mConfigMap.get(panelId);
+            if (panelConfig == null) {
+                return false;
+            }
+
+            if (!panelConfig.isDynamic()) {
+                throw new IllegalStateException("Can't uninstall a built-in panel: " + panelConfig.getId());
+            }
+
+            mConfigMap.remove(panelId);
+
+            if (!panelConfig.isDisabled()) {
+                mEnabledCount--;
+            }
+
+            if (isCurrentDefaultPanel(panelConfig)) {
+                findNewDefault();
+            }
+
+            return true;
+        }
+
+        /**
+         * Replaces an existing panel with a new {@code PanelConfig} instance.
+         *
+         * @return true if the item has been updated.
+         */
+        public boolean update(PanelConfig panelConfig) {
+            ThreadUtils.assertOnThread(mOriginalThread);
+
+            if (panelConfig == null) {
+                throw new IllegalStateException("Can't update a null panel");
+            }
+
+            boolean updated = false;
+
+            final String id = panelConfig.getId();
+            if (mConfigMap.containsKey(id)) {
+                final PanelConfig oldPanelConfig = mConfigMap.put(id, panelConfig);
+
+                // The disabled and default states can't never be
+                // changed by an update operation.
+                panelConfig.setIsDefault(oldPanelConfig.isDefault());
+                panelConfig.setIsDisabled(oldPanelConfig.isDisabled());
+
+                updated = true;
+            }
+
+            return updated;
+        }
+
+        /**
+         * Saves the current {@code Editor} state asynchronously in the
+         * background thread.
+         *
+         * @return the resulting {@code State} instance.
+         */
+        public State apply() {
+            ThreadUtils.assertOnThread(mOriginalThread);
+
+            // We're about to save the current state in the background thread
+            // so we should use a deep copy of the PanelConfig instances to
+            // avoid saving corrupted state.
+            final State newConfigState = new State(mHomeConfig, makeDeepCopy());
+
+            ThreadUtils.getBackgroundHandler().post(new Runnable() {
+                @Override
+                public void run() {
+                    mHomeConfig.save(newConfigState);
+                }
+            });
+
+            return newConfigState;
+        }
+
+        /**
+         * Saves the current {@code Editor} state synchronously in the
+         * current thread.
+         *
+         * @return the resulting {@code State} instance.
+         */
+        public State commit() {
+            ThreadUtils.assertOnThread(mOriginalThread);
+
+            final State newConfigState =
+                    new State(mHomeConfig, new ArrayList<PanelConfig>(mConfigMap.values()));
+
+            // This is a synchronous blocking operation, hence no
+            // need to deep copy the current PanelConfig instances.
+            mHomeConfig.save(newConfigState);
+
+            return newConfigState;
+        }
+
+        public boolean isEmpty() {
+            return mConfigMap.isEmpty();
+        }
+
+        @Override
+        public Iterator<PanelConfig> iterator() {
+            ThreadUtils.assertOnThread(mOriginalThread);
+
+            return mConfigMap.values().iterator();
+        }
+    }
+
     public interface OnChangeListener {
         public void onChange();
     }
 
     public interface HomeConfigBackend {
         public List<PanelConfig> load();
         public void save(List<PanelConfig> entries);
         public String getLocale();
@@ -710,26 +1083,27 @@ public final class HomeConfig {
     private static final String HISTORY_PANEL_ID = "f134bf20-11f7-4867-ab8b-e8e705d7fbe8";
 
     private final HomeConfigBackend mBackend;
 
     public HomeConfig(HomeConfigBackend backend) {
         mBackend = backend;
     }
 
-    public List<PanelConfig> load() {
-        return mBackend.load();
+    public State load() {
+        final List<PanelConfig> panelConfigs = mBackend.load();
+        return new State(this, panelConfigs);
     }
 
     public String getLocale() {
         return mBackend.getLocale();
     }
 
-    public void save(List<PanelConfig> panelConfigs) {
-        mBackend.save(panelConfigs);
+    public void save(State configState) {
+        mBackend.save(configState.mPanelConfigs);
     }
 
     public void setOnChangeListener(OnChangeListener listener) {
         mBackend.setOnChangeListener(listener);
     }
 
     public static PanelConfig createBuiltinPanelConfig(Context context, PanelType panelType) {
         return createBuiltinPanelConfig(context, panelType, EnumSet.noneOf(PanelConfig.Flags.class));
--- a/mobile/android/base/home/HomeConfigInvalidator.java
+++ b/mobile/android/base/home/HomeConfigInvalidator.java
@@ -185,141 +185,104 @@ public class HomeConfigInvalidator imple
         } else {
             handler.postDelayed(mInvalidationRunnable, INVALIDATION_DELAY_MSEC);
         }
 
         Log.d(LOGTAG, "scheduleInvalidation: scheduled new invalidation: " + mode);
     }
 
     /**
-     * Replace an element if a matching PanelConfig is
-     * present in the given list.
-     */
-    private boolean replacePanelConfig(List<PanelConfig> panelConfigs, PanelConfig panelConfig) {
-        final int index = panelConfigs.indexOf(panelConfig);
-        if (index >= 0) {
-            panelConfigs.set(index, panelConfig);
-            Log.d(LOGTAG, "executePendingChanges: replaced position " + index + " with " + panelConfig.getId());
-
-            return true;
-        }
-
-        return false;
-    }
-
-    private PanelConfig findPanelConfigWithId(List<PanelConfig> panelConfigs, String panelId) {
-        for (PanelConfig panelConfig : panelConfigs) {
-            if (panelConfig.getId().equals(panelId)) {
-                return panelConfig;
-            }
-        }
-
-        return null;
-    }
-
-    /**
      * Runs in the background thread.
      */
-    private List<PanelConfig> executePendingChanges(List<PanelConfig> panelConfigs) {
+    private void executePendingChanges(HomeConfig.Editor editor) {
         boolean shouldRefresh = false;
 
         while (!mPendingChanges.isEmpty()) {
             final ConfigChange pendingChange = mPendingChanges.poll();
 
             switch (pendingChange.type) {
                 case UNINSTALL: {
                     final String panelId = (String) pendingChange.target;
-                    final PanelConfig panelConfig = findPanelConfigWithId(panelConfigs, panelId);
-                    if (panelConfig != null && panelConfigs.remove(panelConfig)) {
-                        Log.d(LOGTAG, "executePendingChanges: removed panel " + panelConfig.getId());
+                    if (editor.uninstall(panelId)) {
+                        Log.d(LOGTAG, "executePendingChanges: uninstalled panel " + panelId);
                     }
                     break;
                 }
 
                 case INSTALL: {
                     final PanelConfig panelConfig = (PanelConfig) pendingChange.target;
-                    if (!replacePanelConfig(panelConfigs, panelConfig)) {
-                        panelConfigs.add(panelConfig);
+                    if (editor.install(panelConfig)) {
                         Log.d(LOGTAG, "executePendingChanges: added panel " + panelConfig.getId());
                     }
                     break;
                 }
 
                 case UPDATE: {
                     final PanelConfig panelConfig = (PanelConfig) pendingChange.target;
-                    if (!replacePanelConfig(panelConfigs, panelConfig)) {
-                        Log.w(LOGTAG, "Tried to update non-existing panel " + panelConfig.getId());
+                    if (editor.update(panelConfig)) {
+                        Log.w(LOGTAG, "executePendingChanges: updated panel " + panelConfig.getId());
                     }
                     break;
                 }
 
                 case REFRESH: {
                     shouldRefresh = true;
                 }
             }
         }
 
         if (shouldRefresh) {
-            return executeRefresh(panelConfigs);
-        } else {
-            return panelConfigs;
+            executeRefresh(editor);
         }
     }
 
     /**
      * Runs in the background thread.
      */
-    private List<PanelConfig> refreshFromPanelInfos(List<PanelConfig> panelConfigs, List<PanelInfo> panelInfos) {
+    private void refreshFromPanelInfos(HomeConfig.Editor editor, List<PanelInfo> panelInfos) {
         Log.d(LOGTAG, "refreshFromPanelInfos");
 
-        final int count = panelConfigs.size();
-        for (int i = 0; i < count; i++) {
-            final PanelConfig panelConfig = panelConfigs.get(i);
+        for (PanelConfig panelConfig : editor) {
+            PanelConfig refreshedPanelConfig = null;
 
-            PanelConfig refreshedPanelConfig = null;
             if (panelConfig.isDynamic()) {
                 for (PanelInfo panelInfo : panelInfos) {
                     if (panelInfo.getId().equals(panelConfig.getId())) {
                         refreshedPanelConfig = panelInfo.toPanelConfig();
                         Log.d(LOGTAG, "refreshFromPanelInfos: refreshing from panel info: " + panelInfo.getId());
                         break;
                     }
                 }
             } else {
                 refreshedPanelConfig = createBuiltinPanelConfig(mContext, panelConfig.getType());
                 Log.d(LOGTAG, "refreshFromPanelInfos: refreshing built-in panel: " + panelConfig.getId());
             }
 
             if (refreshedPanelConfig == null) {
                 Log.d(LOGTAG, "refreshFromPanelInfos: no refreshed panel, falling back: " + panelConfig.getId());
-                refreshedPanelConfig = panelConfig;
+                continue;
             }
 
-            refreshedPanelConfig.setIsDefault(panelConfig.isDefault());
-            refreshedPanelConfig.setIsDisabled(panelConfig.isDisabled());
-
-            Log.d(LOGTAG, "refreshFromPanelInfos: set " + i + " with " + refreshedPanelConfig.getId());
-            panelConfigs.set(i, refreshedPanelConfig);
+            Log.d(LOGTAG, "refreshFromPanelInfos: refreshed panel " + refreshedPanelConfig.getId());
+            editor.update(refreshedPanelConfig);
         }
-
-        return panelConfigs;
     }
 
     /**
      * Runs in the background thread.
      */
-    private List<PanelConfig> executeRefresh(List<PanelConfig> panelConfigs) {
-        if (panelConfigs.isEmpty()) {
-            return panelConfigs;
+    private void executeRefresh(HomeConfig.Editor editor) {
+        if (editor.isEmpty()) {
+            return;
         }
 
         Log.d(LOGTAG, "executeRefresh");
 
         final Set<String> ids = new HashSet<String>();
-        for (PanelConfig panelConfig : panelConfigs) {
+        for (PanelConfig panelConfig : editor) {
             ids.add(panelConfig.getId());
         }
 
         final Object panelRequestLock = new Object();
         final List<PanelInfo> latestPanelInfos = new ArrayList<PanelInfo>();
 
         final PanelManager pm = new PanelManager();
         pm.requestPanelsById(ids, new RequestCallback() {
@@ -334,26 +297,27 @@ public class HomeConfigInvalidator imple
             }
         });
 
         try {
             synchronized(panelRequestLock) {
                 panelRequestLock.wait(PANEL_INFO_TIMEOUT_MSEC);
 
                 Log.d(LOGTAG, "executeRefresh: done fetching panel infos");
-                return refreshFromPanelInfos(panelConfigs, latestPanelInfos);
+                refreshFromPanelInfos(editor, latestPanelInfos);
             }
         } catch (InterruptedException e) {
             Log.e(LOGTAG, "Failed to fetch panels from gecko", e);
-            return panelConfigs;
         }
     }
 
     /**
      * Runs in the background thread.
      */
     private class InvalidationRunnable implements Runnable {
         @Override
         public void run() {
-            mHomeConfig.save(executePendingChanges(mHomeConfig.load()));
+            final HomeConfig.Editor editor = mHomeConfig.load().edit();
+            executePendingChanges(editor);
+            editor.commit();
         }
     };
 }
--- a/mobile/android/base/home/HomeConfigLoader.java
+++ b/mobile/android/base/home/HomeConfigLoader.java
@@ -8,74 +8,74 @@ package org.mozilla.gecko.home;
 import org.mozilla.gecko.home.HomeConfig.PanelConfig;
 import org.mozilla.gecko.home.HomeConfig.OnChangeListener;
 
 import android.content.Context;
 import android.support.v4.content.AsyncTaskLoader;
 
 import java.util.List;
 
-public class HomeConfigLoader extends AsyncTaskLoader<List<PanelConfig>> {
+public class HomeConfigLoader extends AsyncTaskLoader<HomeConfig.State> {
     private final HomeConfig mConfig;
-    private List<PanelConfig> mPanelConfigs;
+    private HomeConfig.State mConfigState;
 
     public HomeConfigLoader(Context context, HomeConfig homeConfig) {
         super(context);
         mConfig = homeConfig;
     }
 
     @Override
-    public List<PanelConfig> loadInBackground() {
+    public HomeConfig.State loadInBackground() {
         return mConfig.load();
     }
 
     @Override
-    public void deliverResult(List<PanelConfig> panelConfigs) {
+    public void deliverResult(HomeConfig.State configState) {
         if (isReset()) {
-            mPanelConfigs = null;
+            mConfigState = null;
             return;
         }
 
-        mPanelConfigs = panelConfigs;
+        mConfigState = configState;
         mConfig.setOnChangeListener(new ForceLoadChangeListener());
 
         if (isStarted()) {
-            super.deliverResult(panelConfigs);
+            super.deliverResult(configState);
         }
     }
 
     @Override
     protected void onStartLoading() {
-        if (mPanelConfigs != null) {
-            deliverResult(mPanelConfigs);
+        if (mConfigState != null) {
+            deliverResult(mConfigState);
         }
 
-        if (takeContentChanged() || mPanelConfigs == null) {
+        if (takeContentChanged() || mConfigState == null) {
             forceLoad();
         }
     }
 
     @Override
     protected void onStopLoading() {
         cancelLoad();
     }
 
     @Override
-    public void onCanceled(List<PanelConfig> panelConfigs) {
-        mPanelConfigs = null;
+    public void onCanceled(HomeConfig.State configState) {
+        mConfigState = null;
     }
 
     @Override
     protected void onReset() {
         super.onReset();
 
         // Ensure the loader is stopped.
         onStopLoading();
 
-        mPanelConfigs = null;
+        mConfigState = null;
         mConfig.setOnChangeListener(null);
     }
 
     private class ForceLoadChangeListener implements OnChangeListener {
         @Override
         public void onChange() {
             onContentChanged();
         }
--- a/mobile/android/base/home/HomePager.java
+++ b/mobile/android/base/home/HomePager.java
@@ -245,17 +245,17 @@ public class HomePager extends ViewPager
     }
 
     public void onToolbarFocusChange(boolean hasFocus) {
         // We should only make the banner active if the toolbar is not focused and we are on the default page
         final boolean active = !hasFocus && getCurrentItem() == mDefaultPageIndex;
         mHomeBanner.setActive(active);
     }
 
-    private void updateUiFromPanelConfigs(List<PanelConfig> panelConfigs) {
+    private void updateUiFromConfigState(HomeConfig.State configState) {
         // We only care about the adapter if HomePager is currently
         // loaded, which means it's visible in the activity.
         if (!mLoaded) {
             return;
         }
 
         if (mDecor != null) {
             mDecor.removeAllPagerViews();
@@ -265,17 +265,17 @@ public class HomePager extends ViewPager
 
         // Destroy any existing panels currently loaded
         // in the pager.
         setAdapter(null);
 
         // Only keep enabled panels.
         final List<PanelConfig> enabledPanels = new ArrayList<PanelConfig>();
 
-        for (PanelConfig panelConfig : panelConfigs) {
+        for (PanelConfig panelConfig : configState) {
             if (!panelConfig.isDisabled()) {
                 enabledPanels.add(panelConfig);
             }
         }
 
         // Update the adapter with the new panel configs
         adapter.update(enabledPanels);
 
@@ -309,29 +309,29 @@ public class HomePager extends ViewPager
                     mDefaultPageIndex = i;
                     setCurrentItem(i, false);
                     break;
                 }
             }
         }
     }
 
-    private class ConfigLoaderCallbacks implements LoaderCallbacks<List<PanelConfig>> {
+    private class ConfigLoaderCallbacks implements LoaderCallbacks<HomeConfig.State> {
         @Override
-        public Loader<List<PanelConfig>> onCreateLoader(int id, Bundle args) {
+        public Loader<HomeConfig.State> onCreateLoader(int id, Bundle args) {
             return new HomeConfigLoader(mContext, mConfig);
         }
 
         @Override
-        public void onLoadFinished(Loader<List<PanelConfig>> loader, List<PanelConfig> panelConfigs) {
-            updateUiFromPanelConfigs(panelConfigs);
+        public void onLoadFinished(Loader<HomeConfig.State> loader, HomeConfig.State configState) {
+            updateUiFromConfigState(configState);
         }
 
         @Override
-        public void onLoaderReset(Loader<List<PanelConfig>> loader) {
+        public void onLoaderReset(Loader<HomeConfig.State> loader) {
         }
     }
 
     private class PageChangeListener implements ViewPager.OnPageChangeListener {
         @Override
         public void onPageSelected(int position) {
             if (mDecor != null) {
                 mDecor.onPageSelected(position);
--- a/mobile/android/base/home/HomePanelPicker.java
+++ b/mobile/android/base/home/HomePanelPicker.java
@@ -219,29 +219,29 @@ public class HomePanelPicker extends Fra
             mPanelInfos = panelInfos;
             notifyDataSetChanged();
         }
     }
 
     /**
      * Fetch installed Home panels and update the adapter for this activity.
      */
-    private class ConfigLoaderCallbacks implements LoaderCallbacks<List<PanelConfig>> {
+    private class ConfigLoaderCallbacks implements LoaderCallbacks<HomeConfig.State> {
         @Override
-        public Loader<List<PanelConfig>> onCreateLoader(int id, Bundle args) {
+        public Loader<HomeConfig.State> onCreateLoader(int id, Bundle args) {
             final HomeConfig homeConfig = HomeConfig.getDefault(HomePanelPicker.this);
             return new HomeConfigLoader(HomePanelPicker.this, homeConfig);
         }
 
         @Override
-        public void onLoadFinished(Loader<List<PanelConfig>> loader, List<PanelConfig> panelConfigs) {
+        public void onLoadFinished(Loader<HomeConfig.State> loader, HomeConfig.State configState) {
             mCurrentPanelsIds = new ArrayList<String>();
-            for (PanelConfig panelConfig : panelConfigs) {
+            for (PanelConfig panelConfig : configState) {
                 mCurrentPanelsIds.add(panelConfig.getId());
             }
 
             updatePanelsAdapter(mPanelInfos);
         }
 
         @Override
-        public void onLoaderReset(Loader<List<PanelConfig>> loader) {}
+        public void onLoaderReset(Loader<HomeConfig.State> loader) {}
     }
 }
--- a/mobile/android/base/preferences/PanelsPreferenceCategory.java
+++ b/mobile/android/base/preferences/PanelsPreferenceCategory.java
@@ -15,20 +15,19 @@ import org.mozilla.gecko.util.UiAsyncTas
 import android.content.Context;
 import android.text.TextUtils;
 import android.util.AttributeSet;
 
 public class PanelsPreferenceCategory extends CustomListCategory {
     public static final String LOGTAG = "PanelsPrefCategory";
 
     protected HomeConfig mHomeConfig;
-    protected List<PanelConfig> mPanelConfigs;
+    protected HomeConfig.Editor mConfigEditor;
 
-    protected UiAsyncTask<Void, Void, List<PanelConfig>> mLoadTask;
-    protected UiAsyncTask<Void, Void, Void> mSaveTask;
+    protected UiAsyncTask<Void, Void, HomeConfig.State> mLoadTask;
 
     public PanelsPreferenceCategory(Context context) {
         super(context);
         initConfig(context);
     }
 
     public PanelsPreferenceCategory(Context context, AttributeSet attrs) {
         super(context, attrs);
@@ -50,193 +49,114 @@ public class PanelsPreferenceCategory ex
 
         loadHomeConfig();
     }
 
     /**
      * Load the Home Panels config and populate the preferences screen and maintain local state.
      */
     private void loadHomeConfig() {
-        mLoadTask = new UiAsyncTask<Void, Void, List<PanelConfig>>(ThreadUtils.getBackgroundHandler()) {
+        mLoadTask = new UiAsyncTask<Void, Void, HomeConfig.State>(ThreadUtils.getBackgroundHandler()) {
             @Override
-            public List<PanelConfig> doInBackground(Void... params) {
+            public HomeConfig.State doInBackground(Void... params) {
                 return mHomeConfig.load();
             }
 
             @Override
-            public void onPostExecute(List<PanelConfig> panelConfigs) {
-                mPanelConfigs = panelConfigs;
-                displayHomeConfig();
+            public void onPostExecute(HomeConfig.State configState) {
+                mConfigEditor = configState.edit();
+                displayHomeConfig(configState);
             }
         };
         mLoadTask.execute();
     }
 
-    private void displayHomeConfig() {
-        for (PanelConfig panelConfig : mPanelConfigs) {
+    private void displayHomeConfig(HomeConfig.State configState) {
+        for (PanelConfig panelConfig : configState) {
             // Create and add the pref.
             final PanelsPreference pref = new PanelsPreference(getContext(), PanelsPreferenceCategory.this);
             pref.setTitle(panelConfig.getTitle());
             pref.setKey(panelConfig.getId());
             // XXX: Pull icon from PanelInfo.
             addPreference(pref);
 
-            if (panelConfig.isDefault()) {
-                mDefaultReference = pref;
-                pref.setIsDefault(true);
-            }
-
             if (panelConfig.isDisabled()) {
                 pref.setHidden(true);
             }
         }
+
+        setDefaultFromConfig();
     }
 
-    /**
-     * Update HomeConfig off the main thread.
-     *
-     * @param panelConfigs Configuration to be saved
-     */
-    private void saveHomeConfig() {
-        if (mPanelConfigs == null) {
+    private void setDefaultFromConfig() {
+        final String defaultPanelId = mConfigEditor.getDefaultPanelId();
+        if (defaultPanelId == null) {
+            mDefaultReference = null;
             return;
         }
 
-        final List<PanelConfig> panelConfigs = makeConfigListDeepCopy();
-        mSaveTask = new UiAsyncTask<Void, Void, Void>(ThreadUtils.getBackgroundHandler()) {
-            @Override
-            public Void doInBackground(Void... params) {
-                mHomeConfig.save(panelConfigs);
-                return null;
+        final int prefCount = getPreferenceCount();
+
+        // First preference (index 0) is Preference to add panels.
+        for (int i = 1; i < prefCount; i++) {
+            final PanelsPreference pref = (PanelsPreference) getPreference(i);
+
+            if (defaultPanelId.equals(pref.getKey())) {
+                super.setDefault(pref);
+                break;
             }
-        };
-        mSaveTask.execute();
-    }
-
-    private List<PanelConfig> makeConfigListDeepCopy() {
-        List<PanelConfig> copiedList = new ArrayList<PanelConfig>();
-        for (PanelConfig panelConfig : mPanelConfigs) {
-            copiedList.add(new PanelConfig(panelConfig));
         }
-        return copiedList;
     }
 
     @Override
     public void setDefault(CustomListPreference pref) {
         super.setDefault(pref);
-        updateConfigDefault();
-        saveHomeConfig();
+
+        final String id = pref.getKey();
+
+        final String defaultPanelId = mConfigEditor.getDefaultPanelId();
+        if (defaultPanelId != null && defaultPanelId.equals(id)) {
+            return;
+        }
+
+        mConfigEditor.setDefault(id);
+        mConfigEditor.apply();
     }
 
     @Override
     protected void onPrepareForRemoval() {
         if (mLoadTask != null) {
             mLoadTask.cancel(true);
         }
-
-        if (mSaveTask != null) {
-            mSaveTask.cancel(true);
-        }
-     }
-
-    /**
-     * Update the local HomeConfig default state from mDefaultReference.
-     */
-    private void updateConfigDefault() {
-        String id = null;
-        if (mDefaultReference != null) {
-            id = mDefaultReference.getKey();
-        }
-
-        for (PanelConfig panelConfig : mPanelConfigs) {
-            if (TextUtils.equals(panelConfig.getId(), id)) {
-                panelConfig.setIsDefault(true);
-                panelConfig.setIsDisabled(false);
-            } else {
-                panelConfig.setIsDefault(false);
-            }
-        }
     }
 
     @Override
     public void uninstall(CustomListPreference pref) {
-        super.uninstall(pref);
-        // This could change the default, so update the local version of the config.
-        updateConfigDefault();
+        mConfigEditor.uninstall(pref.getKey());
+        mConfigEditor.apply();
 
-        final String id = pref.getKey();
-        PanelConfig toRemove = null;
-        for (PanelConfig panelConfig : mPanelConfigs) {
-            if (TextUtils.equals(panelConfig.getId(), id)) {
-                toRemove = panelConfig;
-                break;
-            }
-        }
-        mPanelConfigs.remove(toRemove);
-
-        saveHomeConfig();
+        super.uninstall(pref);
     }
 
     /**
      * Update the hide/show state of the preference and save the HomeConfig
      * changes.
      *
      * @param pref Preference to update
      * @param toHide New hidden state of the preference
      */
     protected void setHidden(PanelsPreference pref, boolean toHide) {
-        pref.setHidden(toHide);
-        ensureDefaultForHide(pref, toHide);
-
-        final String id = pref.getKey();
-        for (PanelConfig panelConfig : mPanelConfigs) {
-            if (TextUtils.equals(panelConfig.getId(), id)) {
-                panelConfig.setIsDisabled(toHide);
-                break;
-            }
-        }
-
-        saveHomeConfig();
-    }
+        mConfigEditor.setDisabled(pref.getKey(), toHide);
+        mConfigEditor.apply();
 
-    /**
-     * Ensure a default is set (if possible) for hiding/showing a pref.
-     * If hiding, try to find an enabled pref to set as the default.
-     * If showing, set it as the default if there is no default currently.
-     *
-     * This updates the local HomeConfig state.
-     *
-     * @param pref Preference getting updated
-     * @param toHide Boolean of the new hidden state
-     */
-    private void ensureDefaultForHide(PanelsPreference pref, boolean toHide) {
-        if (toHide) {
-            // Set a default if there is an enabled panel left.
-            if (pref == mDefaultReference) {
-                setFallbackDefault();
-                updateConfigDefault();
-            }
-        } else {
-            if (mDefaultReference == null) {
-                super.setDefault(pref);
-                updateConfigDefault();
-            }
-        }
+        pref.setHidden(toHide);
+        setDefaultFromConfig();
     }
 
     /**
      * When the default panel is removed or disabled, find an enabled panel
      * if possible and set it as mDefaultReference.
      */
     @Override
     protected void setFallbackDefault() {
-        // First preference (index 0) is Preference to add panels.
-        final int prefsCount = getPreferenceCount();
-        for (int i = 1; i < prefsCount; i++) {
-            final PanelsPreference pref = (PanelsPreference) getPreference(i);
-            if (!pref.isHidden()) {
-                super.setDefault(pref);
-                return;
-            }
-        }
-        mDefaultReference = null;
+        setDefaultFromConfig();
     }
 }
index ae39f6583c7ebdc21b64b8e07f169e8b7f5f1e58..42566ff2e3322f62ecb36bb3b6b8a4f094abc11e
GIT binary patch
literal 451
zc$@*l0X+VRP)<h;3K|Lk000e1NJLTq001cf001Be1^@s6k8e>v0000PbVXQnQ*UN;
zcVTj606}DLVr3vnZDD6+Qe|Oed2z{QJOBUzWl2OqR9M69n9EATKoo|*X?n4^bLV^X
zA>8;9g7k&lWbd1}Ev^Mo#7#voUO>f5=6BJPc1TlgW3?&GfuEC^keqK0XU@PlNjT@k
z#2gheNjN}N1Iu&PkjsRuy48?1WDWVJkV$_vggfyp$apaF{oZ(-oc7Abae*3vve@2~
zn%B3GI^LZb>HcK6C`c6eOf8^kY8cQ5@M?MkK10wlkVPP&GJsa#Gqp_}^O0rm8AyOT
z;L%{yDM%enm#Q>kk|yJ)v(yDZ?SkN2rmiV`?&jv3mbnDD1}=di@Bq9QL24dId@%MP
zs~~v?9weKyck&wh(YJzB_79tvkmm~Nz(8*;o0WqEX%9FDZh)66LS7&p{*=kz)?_EB
z$)Z{;s?%tpnq1QE&hPA5yE|J{WbJ0Y-Mz#<*~lsDmCf8j%J)g)0ovX#1Nna^&U&5Q
t>k7F-t06ZJvcAFC9LR#JziJUm?hD*qSqwLXHrfCH002ovPDHLkV1f={&Ab2r
index 31d7f0bd004d28cd419ecc14458ab327038d4869..370898c4a4b6460acca43de7c87a5b11bf83d18c
GIT binary patch
literal 426
zc$@*M0agBqP)<h;3K|Lk000e1NJLTq000~S000&U1^@s6)0X`50000PbVXQnQ*UN;
zcVTj606}DLVr3vnZDD6+Qe|Oed2z{QJOBUzOi4sRR7l6&mO)O#Fc3xG#3`lQT#`F*
z1}=~l7vT&n*dnoDQ-nkXi&9l>=CkOyvZ6o>sIAnIzJBB=|9d=RJ0=M;6Pqv&7?LnR
zhc+rbPhxn?IFaG(>Nsvq`#(b~P!-6l=7BC)`xp+1rh+p@Kpm)}5m&$>umsjki)X_j
z(Gpo~RB*;cU>uwxt2r<O-hdBa(X^~f!z_L=Aaog+1j^@0Z@i9-X23J>1WbYVre$dW
ztDE``nW@9%REWym5*?L9k}KgPIkj9{#w|%oblR+nsN@(@Cox631s=lmwQ2dxhC+rM
z<eLh}4IHExi+6Hu88;+BC#3wHKnfjQ1NXoq@Y1x*f0JP?C&`uh?-~A3!*Nl=q1p}A
zHSMh7jxOzY=yzS3Czycst}flR533&XUaWhQ+It@!_oO-d+v3b`{N7L0VcjJA0!H`s
U{aZymmjD0&07*qoM6N<$g1vFKga7~l
index f9cfb79c91e5faf762e0d0140b39700b01da8502..93546b15fb9bc0cd40c8ccd80ddf1270f24ca226
GIT binary patch
literal 572
zc$@(|0>k}@P)<h;3K|Lk000e1NJLTq001@s001fo1^@s6_)(}m0000PbVXQnQ*UN;
zcVTj606}DLVr3vnZDD6+Qe|Oed2z{QJOBUz-bqA3RA}Dqn!Qp2F%ZYI$DI~d+VLK?
zK7fVK!4p{e7B=+0fiK{UjfJI^mD;G|2!0_tj9E8#8_31naexSiyEA__At9GvlG!W+
zfQSkJP~{C&jV)y|zV09)HUSf`B?4PT6EFc2FaZ-V0h`_0=h;&>_EHbM<6M0fOUG($
z&k2kt9vpd^$!pByCqj`CgyD?+1oXhX74Dwi*6s{$wF6*y)Xa7ILojd@vB2>*r`4n{
z$KxWv!0}Ouw`!CqVweHxgVbj~;P){IhKJ?x#eC}N0wJ)9YRu#tfl0>&^@Pi=w`B`v
zfSyT@X5C3|q)*efxzhyN2%HG4n?XhVkl1;ct2i(@e}bX1)taMquioIJ_d+tq=!SGf
zdLX@;oht&vaXXfFMU^iPJ@-R3s%xiBt5KWwJT;(p`yn4L^v1(sfAVKk*k?Gid;L`b
zGlgAxy(Ha}dIGlOU?)^KsB#pi4IXaPiwmM!GA82Lz>yRLkHiTcCL-REt}6wPiuYJ3
z+=moRa6iQ*ixLOI{X4^TSmoh<ju$li(gkfZj{)Ch)h_4-r&MLT4qi%yd?amJr7NXW
zE1LxS)@jv_>z{+w*aS?#1WdrD2a{hk1x&!^2v+b1mU>gsSNm`1d+`kRa$@2D0000<
KMNUMnLSTX$K>q0f
--- a/mobile/android/base/resources/layout-large-v11/browser_toolbar.xml
+++ b/mobile/android/base/resources/layout-large-v11/browser_toolbar.xml
@@ -102,15 +102,16 @@
                android:layout_width="fill_parent"
                android:layout_height="2dp"
                android:layout_alignParentBottom="true"
                android:background="@color/url_bar_shadow"
                android:contentDescription="@null"/>
 
     <org.mozilla.gecko.toolbar.ToolbarProgressView android:id="@+id/progress"
                                                    android:layout_width="fill_parent"
-                                                   android:layout_height="2dp"
+                                                   android:layout_height="14dp"
+                                                   android:layout_marginBottom="-7dp"
                                                    android:layout_alignBottom="@id/shadow"
                                                    android:src="@drawable/progress"
                                                    android:background="@null"
                                                    android:visibility="gone" />
 
 </merge>
--- a/mobile/android/base/resources/layout/browser_toolbar.xml
+++ b/mobile/android/base/resources/layout/browser_toolbar.xml
@@ -102,16 +102,16 @@
                android:layout_width="fill_parent"
                android:layout_height="2dp"
                android:layout_alignParentBottom="true"
                android:background="@color/url_bar_shadow"
                android:contentDescription="@null"/>
 
     <org.mozilla.gecko.toolbar.ToolbarProgressView android:id="@+id/progress"
                                                    android:layout_width="fill_parent"
-                                                   android:layout_height="16dp"
-                                                   android:layout_marginBottom="-8dp"
+                                                   android:layout_height="14dp"
+                                                   android:layout_marginBottom="-7dp"
                                                    android:layout_alignBottom="@id/shadow"
                                                    android:src="@drawable/progress"
                                                    android:background="@null"
                                                    android:visibility="gone" />
 
 </merge>
--- a/mobile/android/base/resources/layout/gecko_app.xml
+++ b/mobile/android/base/resources/layout/gecko_app.xml
@@ -89,16 +89,20 @@
 
         <org.mozilla.gecko.widget.GeckoViewFlipper android:id="@id/browser_actionbar"
                 android:layout_width="fill_parent"
                 android:layout_height="@dimen/browser_toolbar_height"
                 android:clickable="true"
                 android:clipChildren="false"
                 android:focusable="true">
 
+            <!-- clipChildren="false" allows the child ToolbarProgressView to be drawn
+                 outside of BrowserToolbar's boundaries. Likewise, we need this property
+                 on BrowserToolbar's parent ViewFlipper, then on its parent MainLayout
+                 to allow the progress to overlap the content LayerView. -->
             <org.mozilla.gecko.toolbar.BrowserToolbar
                 android:id="@+id/browser_toolbar"
                 style="@style/BrowserToolbar"
                 android:layout_width="fill_parent"
                 android:layout_height="@dimen/browser_toolbar_height"
                 android:clickable="true"
                 android:focusable="true"
                 android:clipChildren="false"
--- a/services/common/hawkclient.js
+++ b/services/common/hawkclient.js
@@ -194,18 +194,23 @@ this.HawkClient.prototype = {
       deferred.resolve(this.response.body);
     };
 
     let extra = {
       now: this.now(),
       localtimeOffsetMsec: this.localtimeOffsetMsec,
     };
 
-    let request = new HAWKAuthenticatedRESTRequest(uri, credentials, extra);
+    let request = this.newHAWKAuthenticatedRESTRequest(uri, credentials, extra);
     if (method == "post" || method == "put") {
       request[method](payloadObj, onComplete);
     } else {
       request[method](onComplete);
     }
 
     return deferred.promise;
-  }
+  },
+
+  // override points for testing.
+  newHAWKAuthenticatedRESTRequest: function(uri, credentials, extra) {
+    return new HAWKAuthenticatedRESTRequest(uri, credentials, extra);
+  },
 }
--- a/services/common/tokenserverclient.js
+++ b/services/common/tokenserverclient.js
@@ -240,17 +240,17 @@ TokenServerClient.prototype = {
     }
 
     if (!cb) {
       throw new TokenServerClientError("cb argument is not valid.");
     }
 
     this._log.debug("Beginning BID assertion exchange: " + url);
 
-    let req = new RESTRequest(url);
+    let req = this.newRESTRequest(url);
     req.setHeader("Accept", "application/json");
     req.setHeader("Authorization", "BrowserID " + assertion);
 
     for (let header in addHeaders) {
       req.setHeader(header, addHeaders[header]);
     }
 
     let client = this;
@@ -398,10 +398,15 @@ TokenServerClient.prototype = {
     this._log.debug("Successful token response: " + result.id);
     cb(null, {
       id:       result.id,
       key:      result.key,
       endpoint: result.api_endpoint,
       uid:      result.uid,
       duration: result.duration,
     });
+  },
+
+  // override points for testing.
+  newRESTRequest: function(url) {
+    return new RESTRequest(url);
   }
 };
--- a/services/sync/modules-testing/utils.js
+++ b/services/sync/modules-testing/utils.js
@@ -112,16 +112,19 @@ this.makeIdentityConfig = function(overr
 
 // Configure an instance of an FxAccount identity provider with the specified
 // config (or the default config if not specified).
 this.configureFxAccountIdentity = function(authService,
                                            config = makeIdentityConfig()) {
   let MockInternal = {};
   let fxa = new FxAccounts(MockInternal);
 
+  // until we get better test infrastructure for bid_identity, we set the
+  // signedin user's "email" to the username, simply as many tests rely on this.
+  config.fxaccount.user.email = config.username;
   fxa.internal.currentAccountState.signedInUser = {
     version: DATA_FORMAT_VERSION,
     accountData: config.fxaccount.user
   };
   fxa.internal.currentAccountState.getCertificate = function(data, keyPair, mustBeValidUntil) {
     this.cert = {
       validUntil: fxa.internal.now() + CERT_LIFETIME,
       cert: "certificate",
@@ -134,16 +137,17 @@ this.configureFxAccountIdentity = functi
       config.fxaccount.token.uid = config.username;
       cb(null, config.fxaccount.token);
     },
   };
   authService._fxaService = fxa;
   authService._tokenServerClient = mockTSC;
   // Set the "account" of the browserId manager to be the "email" of the
   // logged in user of the mockFXA service.
+  authService._signedInUser = fxa.internal.currentAccountState.signedInUser.accountData;
   authService._account = config.fxaccount.user.email;
 }
 
 this.configureIdentity = function(identityOverrides) {
   let config = makeIdentityConfig(identityOverrides);
   let ns = {};
   Cu.import("resource://services-sync/service.js", ns);
 
--- a/services/sync/modules/browserid_identity.js
+++ b/services/sync/modules/browserid_identity.js
@@ -18,35 +18,41 @@ Cu.import("resource://services-sync/util
 Cu.import("resource://services-common/tokenserverclient.js");
 Cu.import("resource://gre/modules/Services.jsm");
 Cu.import("resource://services-sync/constants.js");
 Cu.import("resource://gre/modules/Promise.jsm");
 Cu.import("resource://services-sync/stages/cluster.js");
 Cu.import("resource://gre/modules/FxAccounts.jsm");
 
 // Lazy imports to prevent unnecessary load on startup.
+XPCOMUtils.defineLazyModuleGetter(this, "Weave",
+                                  "resource://services-sync/main.js");
+
 XPCOMUtils.defineLazyModuleGetter(this, "BulkKeyBundle",
                                   "resource://services-sync/keys.js");
 
 XPCOMUtils.defineLazyModuleGetter(this, "fxAccounts",
                                   "resource://gre/modules/FxAccounts.jsm");
 
-XPCOMUtils.defineLazyGetter(this, 'fxAccountsCommon', function() {
-  let ob = {};
-  Cu.import("resource://gre/modules/FxAccountsCommon.js", ob);
-  return ob;
-});
-
 XPCOMUtils.defineLazyGetter(this, 'log', function() {
   let log = Log.repository.getLogger("Sync.BrowserIDManager");
   log.addAppender(new Log.DumpAppender());
   log.level = Log.Level[Svc.Prefs.get("log.logger.identity")] || Log.Level.Error;
   return log;
 });
 
+// FxAccountsCommon.js doesn't use a "namespace", so create one here.
+let fxAccountsCommon = {};
+Cu.import("resource://gre/modules/FxAccountsCommon.js", fxAccountsCommon);
+
+const OBSERVER_TOPICS = [
+  fxAccountsCommon.ONLOGIN_NOTIFICATION,
+  fxAccountsCommon.ONLOGOUT_NOTIFICATION,
+];
+
 const PREF_SYNC_SHOW_CUSTOMIZATION = "services.sync.ui.showCustomizationDialog";
 
 function deriveKeyBundle(kB) {
   let out = CryptoUtils.hkdf(kB, undefined,
                              "identity.mozilla.com/picl/v1/oldsync", 2*32);
   let bundle = new BulkKeyBundle();
   // [encryptionKey, hmacKey]
   bundle.keyPair = [out.slice(0, 32), out.slice(32, 64)];
@@ -82,17 +88,22 @@ this.BrowserIDManager = function Browser
 
 this.BrowserIDManager.prototype = {
   __proto__: IdentityManager.prototype,
 
   _fxaService: null,
   _tokenServerClient: null,
   // https://docs.services.mozilla.com/token/apis.html
   _token: null,
-  _account: null,
+  _signedInUser: null, // the signedinuser we got from FxAccounts.
+
+  // null if no error, otherwise a LOGIN_FAILED_* value that indicates why
+  // we failed to authenticate (but note it might not be an actual
+  // authentication problem, just a transient network error or similar)
+  _authFailureReason: null,
 
   // it takes some time to fetch a sync key bundle, so until this flag is set,
   // we don't consider the lack of a keybundle as a failure state.
   _shouldHaveSyncKeyBundle: false,
 
   get readyToAuthenticate() {
     // We are finished initializing when we *should* have a sync key bundle,
     // although we might not actually have one due to auth failures etc.
@@ -103,44 +114,89 @@ this.BrowserIDManager.prototype = {
     try {
       return Services.prefs.getBoolPref(PREF_SYNC_SHOW_CUSTOMIZATION);
     } catch (e) {
       return false;
     }
   },
 
   initialize: function() {
-    Services.obs.addObserver(this, fxAccountsCommon.ONLOGIN_NOTIFICATION, false);
-    Services.obs.addObserver(this, fxAccountsCommon.ONLOGOUT_NOTIFICATION, false);
-    Services.obs.addObserver(this, "weave:service:logout:finish", false);
+    for (let topic of OBSERVER_TOPICS) {
+      Services.obs.addObserver(this, topic, false);
+    }
     return this.initializeWithCurrentIdentity();
   },
 
+  /**
+   * Ensure the user is logged in.  Returns a promise that resolves when
+   * the user is logged in, or is rejected if the login attempt has failed.
+   */
+  ensureLoggedIn: function() {
+    if (!this._shouldHaveSyncKeyBundle) {
+      // We are already in the process of logging in.
+      return this.whenReadyToAuthenticate.promise;
+    }
+
+    // If we are already happy then there is nothing more to do.
+    if (Weave.Status.login == LOGIN_SUCCEEDED) {
+      return Promise.resolve();
+    }
+
+    // Similarly, if we have a previous failure that implies an explicit
+    // re-entering of credentials by the user is necessary we don't take any
+    // further action - an observer will fire when the user does that.
+    if (Weave.Status.login == LOGIN_FAILED_LOGIN_REJECTED) {
+      return Promise.reject();
+    }
+
+    // So - we've a previous auth problem and aren't currently attempting to
+    // log in - so fire that off.
+    this.initializeWithCurrentIdentity();
+    return this.whenReadyToAuthenticate.promise;
+  },
+
+  finalize: function() {
+    // After this is called, we can expect Service.identity != this.
+    for (let topic of OBSERVER_TOPICS) {
+      Services.obs.removeObserver(this, topic);
+    }
+    this.resetCredentials();
+    this._signedInUser = null;
+    return Promise.resolve();
+  },
+
   initializeWithCurrentIdentity: function(isInitialSync=false) {
+    // While this function returns a promise that resolves once we've started
+    // the auth process, that process is complete when
+    // this.whenReadyToAuthenticate.promise resolves.
     this._log.trace("initializeWithCurrentIdentity");
-    Components.utils.import("resource://services-sync/main.js");
 
     // Reset the world before we do anything async.
     this.whenReadyToAuthenticate = Promise.defer();
     this._shouldHaveSyncKeyBundle = false;
+    this._authFailureReason = null;
 
     return this._fxaService.getSignedInUser().then(accountData => {
       if (!accountData) {
         this._log.info("initializeWithCurrentIdentity has no user logged in");
-        this._account = null;
+        this.account = null;
+        // and we are as ready as we can ever be for auth.
+        this._shouldHaveSyncKeyBundle = true;
+        this.whenReadyToAuthenticate.reject("no user is logged in");
         return;
       }
 
-      this._account = accountData.email;
+      this.account = accountData.email;
+      this._updateSignedInUser(accountData);
       // The user must be verified before we can do anything at all; we kick
       // this and the rest of initialization off in the background (ie, we
       // don't return the promise)
       this._log.info("Waiting for user to be verified.");
       this._fxaService.whenVerified(accountData).then(accountData => {
-        // We do the background keybundle fetch...
+        this._updateSignedInUser(accountData);
         this._log.info("Starting fetch for key bundle.");
         if (this.needsCustomization) {
           // If the user chose to "Customize sync options" when signing
           // up with Firefox Accounts, ask them to choose what to sync.
           const url = "chrome://browser/content/sync/customize.xul";
           const features = "centerscreen,chrome,modal,dialog,resizable=no";
           let win = Services.wm.getMostRecentWindow("navigator:browser");
 
@@ -155,16 +211,17 @@ this.BrowserIDManager.prototype = {
           }
         }
       }).then(() => {
         return this._fetchSyncKeyBundle();
       }).then(() => {
         this._shouldHaveSyncKeyBundle = true; // and we should actually have one...
         this.whenReadyToAuthenticate.resolve();
         this._log.info("Background fetch for key bundle done");
+        Weave.Status.login = LOGIN_SUCCEEDED;
         if (isInitialSync) {
           this._log.info("Doing initial sync actions");
           Svc.Prefs.set("firstSync", "resetClient");
           Services.obs.notifyObservers(null, "weave:service:setup-complete", null);
           Weave.Utils.nextTick(Weave.Service.sync, Weave.Service);
         }
       }).then(null, err => {
         this._shouldHaveSyncKeyBundle = true; // but we probably don't have one...
@@ -173,43 +230,60 @@ this.BrowserIDManager.prototype = {
         this._log.error("Background fetch for key bundle failed: " + err);
       });
       // and we are done - the fetch continues on in the background...
     }).then(null, err => {
       this._log.error("Processing logged in account: " + err);
     });
   },
 
+  _updateSignedInUser: function(userData) {
+    // This object should only ever be used for a single user.  It is an
+    // error to update the data if the user changes (but updates are still
+    // necessary, as each call may add more attributes to the user).
+    // We start with no user, so an initial update is always ok.
+    if (this._signedInUser && this._signedInUser.email != userData.email) {
+      throw new Error("Attempting to update to a different user.")
+    }
+    this._signedInUser = userData;
+  },
+
+  logout: function() {
+    // This will be called when sync fails (or when the account is being
+    // unlinked etc).  It may have failed because we got a 401 from a sync
+    // server, so we nuke the token.  Next time sync runs and wants an
+    // authentication header, we will notice the lack of the token and fetch a
+    // new one.
+    this._token = null;
+  },
+
   observe: function (subject, topic, data) {
     this._log.debug("observed " + topic);
     switch (topic) {
     case fxAccountsCommon.ONLOGIN_NOTIFICATION:
+      // This should only happen if we've been initialized without a current
+      // user - otherwise we'd have seen the LOGOUT notification and been
+      // thrown away.
+      // The exception is when we've initialized with a user that needs to
+      // reauth with the server - in that case we will also get here, but
+      // should have the same identity.
+      // initializeWithCurrentIdentity will throw and log if these contraints
+      // aren't met, so just go ahead and do the init.
       this.initializeWithCurrentIdentity(true);
       break;
 
     case fxAccountsCommon.ONLOGOUT_NOTIFICATION:
-      Components.utils.import("resource://services-sync/main.js");
-      // Setting .username calls resetCredentials which drops the key bundle
-      // and resets _shouldHaveSyncKeyBundle.
-      this.username = "";
-      this._account = null;
-      Weave.Service.logout();
-      break;
-
-    case "weave:service:logout:finish":
-      // This signals an auth error with the storage server,
-      // or that the user unlinked her account from the browser.
-      // Either way, we clear our auth token. In the case of an
-      // auth error, this will force the fetch of a new one.
-      this._token = null;
+      Weave.Service.startOver();
+      // startOver will cause this instance to be thrown away, so there's
+      // nothing else to do.
       break;
     }
   },
 
-   /**
+  /**
    * Compute the sha256 of the message bytes.  Return bytes.
    */
   _sha256: function(message) {
     let hasher = Cc["@mozilla.org/security/hash;1"]
                     .createInstance(Ci.nsICryptoHash);
     hasher.init(hasher.SHA256);
     return CryptoUtils.digestBytes(message, hasher);
   },
@@ -229,33 +303,19 @@ this.BrowserIDManager.prototype = {
   _now: function() {
     return this._fxaService.now()
   },
 
   get _localtimeOffsetMsec() {
     return this._fxaService.localtimeOffsetMsec;
   },
 
-  get account() {
-    return this._account;
-  },
-
-  /**
-   * Sets the active account name.
-   *
-   * This should almost always be called in favor of setting username, as
-   * username is derived from account.
-   *
-   * Changing the account name has the side-effect of wiping out stored
-   * credentials.
-   *
-   * Set this value to null to clear out identity information.
-   */
-  set account(value) {
-    throw "account setter should be not used in BrowserIDManager";
+  usernameFromAccount: function(val) {
+    // we don't differentiate between "username" and "account"
+    return val;
   },
 
   /**
    * Obtains the HTTP Basic auth password.
    *
    * Returns a string if set or null if it is not set.
    */
   get basicPassword() {
@@ -303,18 +363,18 @@ this.BrowserIDManager.prototype = {
   get syncKeyBundle() {
     return this._syncKeyBundle;
   },
 
   /**
    * Resets/Drops all credentials we hold for the current user.
    */
   resetCredentials: function() {
-    // the only credentials we hold are the sync key.
     this.resetSyncKey();
+    this._token = null;
   },
 
   /**
    * Resets/Drops the sync key we hold for the current user.
    */
   resetSyncKey: function() {
     this._syncKey = null;
     this._syncKeyBundle = null;
@@ -324,16 +384,21 @@ this.BrowserIDManager.prototype = {
 
   /**
    * The current state of the auth credentials.
    *
    * This essentially validates that enough credentials are available to use
    * Sync.
    */
   get currentAuthState() {
+    if (this._authFailureReason) {
+      this._log.info("currentAuthState returning " + this._authFailureReason +
+                     " due to previous failure");
+      return this._authFailureReason;
+    }
     // TODO: need to revisit this. Currently this isn't ready to go until
     // both the username and syncKeyBundle are both configured and having no
     // username seems to make things fail fast so that's good.
     if (!this.username) {
       return LOGIN_FAILED_NO_USERNAME;
     }
 
     // No need to check this.syncKey as our getter for that attribute
@@ -342,171 +407,135 @@ this.BrowserIDManager.prototype = {
     if (this._shouldHaveSyncKeyBundle && !this.syncKeyBundle) {
       return LOGIN_FAILED_NO_PASSPHRASE;
     }
 
     return STATUS_OK;
   },
 
   /**
-   * Do we have a non-null, not yet expired token whose email field
-   * matches (when normalized) our account field?
+   * Do we have a non-null, not yet expired token for the user currently
+   * signed in?
    */
   hasValidToken: function() {
     if (!this._token) {
       return false;
     }
     if (this._token.expiration < this._now()) {
       return false;
     }
-    let signedInUser = this._getSignedInUser();
-    if (!signedInUser) {
-      return false;
-    }
-    // Does the signed in user match the user we retrieved the token for?
-    if (signedInUser.email !== this.account) {
-      return false;
-    }
     return true;
   },
 
-  /**
-   * Wrap and synchronize FxAccounts.getSignedInUser().
-   *
-   * @return credentials per wrapped.
-   */
-  _getSignedInUser: function() {
-    let userData;
-    let cb = Async.makeSpinningCallback();
-
-    this._fxaService.getSignedInUser().then(function (result) {
-        cb(null, result);
-    },
-    function (err) {
-        cb(err);
-    });
-
-    try {
-      userData = cb.wait();
-    } catch (err) {
-      this._log.error("FxAccounts.getSignedInUser() failed with: " + err);
-      return null;
-    }
-    return userData;
-  },
-
   _fetchSyncKeyBundle: function() {
     // Fetch a sync token for the logged in user from the token server.
     return this._fxaService.getKeys().then(userData => {
-      // Unlikely, but if the logged in user somehow changed between these
-      // calls we better fail. TODO: add tests for these
-      if (!userData) {
-        throw new AuthenticationError("No userData in _fetchSyncKeyBundle");
-      } else if (userData.email !== this.account) {
-        throw new AuthenticationError("Unexpected user change in _fetchSyncKeyBundle");
-      }
-      return this._fetchTokenForUser(userData).then(token => {
+      this._updateSignedInUser(userData); // throws if the user changed.
+      return this._fetchTokenForUser().then(token => {
         this._token = token;
-        // Set the username to be the uid returned by the token server.
-        this.username = this._token.uid.toString();
         // both Jelly and FxAccounts give us kA/kB as hex.
         let kB = Utils.hexToBytes(userData.kB);
         this._syncKeyBundle = deriveKeyBundle(kB);
         return;
       });
     });
   },
 
-  // Refresh the sync token for the specified Firefox Accounts user.
-  _fetchTokenForUser: function(userData) {
+  // Refresh the sync token for our user.
+  _fetchTokenForUser: function() {
     let tokenServerURI = Svc.Prefs.get("tokenServerURI");
     let log = this._log;
     let client = this._tokenServerClient;
     let fxa = this._fxaService;
+    let userData = this._signedInUser;
 
     // Both Jelly and FxAccounts give us kB as hex
     let kBbytes = CommonUtils.hexToBytes(userData.kB);
     let headers = {"X-Client-State": this._computeXClientState(kBbytes)};
     log.info("Fetching assertion and token from: " + tokenServerURI);
 
     function getToken(tokenServerURI, assertion) {
       log.debug("Getting a token");
       let deferred = Promise.defer();
       let cb = function (err, token) {
         if (err) {
-          log.info("TokenServerClient.getTokenFromBrowserIDAssertion() failed with: " + err.message);
-          return deferred.reject(new AuthenticationError(err));
+          log.info("TokenServerClient.getTokenFromBrowserIDAssertion() failed with: " + err);
+          if (err.response && err.response.status === 401) {
+            err = new AuthenticationError(err);
+          }
+          return deferred.reject(err);
         } else {
           log.debug("Successfully got a sync token");
           return deferred.resolve(token);
         }
       };
 
       client.getTokenFromBrowserIDAssertion(tokenServerURI, assertion, cb, headers);
       return deferred.promise;
     }
 
     function getAssertion() {
       log.debug("Getting an assertion");
       let audience = Services.io.newURI(tokenServerURI, null, null).prePath;
       return fxa.getAssertion(audience).then(null, err => {
+        log.error("fxa.getAssertion() failed with: " + err.code + " - " + err.message);
         if (err.code === 401) {
           throw new AuthenticationError("Unable to get assertion for user");
         } else {
           throw err;
         }
       });
     };
 
     // wait until the account email is verified and we know that
     // getAssertion() will return a real assertion (not null).
-    return fxa.whenVerified(userData)
+    return fxa.whenVerified(this._signedInUser)
       .then(() => getAssertion())
       .then(assertion => getToken(tokenServerURI, assertion))
       .then(token => {
         // TODO: Make it be only 80% of the duration, so refresh the token
         // before it actually expires. This is to avoid sync storage errors
         // otherwise, we get a nasty notification bar briefly. Bug 966568.
         token.expiration = this._now() + (token.duration * 1000) * 0.80;
         return token;
       })
       .then(null, err => {
         // TODO: write tests to make sure that different auth error cases are handled here
         // properly: auth error getting assertion, auth error getting token (invalid generation
         // and client-state error)
         if (err instanceof AuthenticationError) {
           this._log.error("Authentication error in _fetchTokenForUser: " + err);
-          // Drop the sync key bundle, but still expect to have one.
-          // This will arrange for us to be in the right 'currentAuthState'
-          // such that UI will show the right error.
-          this._shouldHaveSyncKeyBundle = true;
-          this._syncKeyBundle = null;
-          Weave.Status.login = this.currentAuthState;
-          Services.obs.notifyObservers(null, "weave:service:login:error", null);
+          // set it to the "fatal" LOGIN_FAILED_LOGIN_REJECTED reason.
+          this._authFailureReason = LOGIN_FAILED_LOGIN_REJECTED;
+        } else {
+          this._log.error("Non-authentication error in _fetchTokenForUser: " + err.message);
+          // for now assume it is just a transient network related problem.
+          this._authFailureReason = LOGIN_FAILED_NETWORK_ERROR;
         }
+        // Drop the sync key bundle, but still expect to have one.
+        // This will arrange for us to be in the right 'currentAuthState'
+        // such that UI will show the right error.
+        this._shouldHaveSyncKeyBundle = true;
+        this._syncKeyBundle = null;
+        Weave.Status.login = this._authFailureReason;
         throw err;
       });
   },
 
-  _fetchTokenForLoggedInUserSync: function() {
-    let cb = Async.makeSpinningCallback();
-
-    this._fxaService.getSignedInUser().then(userData => {
-      this._fetchTokenForUser(userData).then(token => {
-        cb(null, token);
-      }, err => {
-        cb(err);
-      });
-    });
-    try {
-      return cb.wait();
-    } catch (err) {
-      this._log.info("_fetchTokenForLoggedInUserSync: " + err.message);
-      return null;
+  // Returns a promise that is resolved when we have a valid token for the
+  // current user stored in this._token.  When resolved, this._token is valid.
+  _ensureValidToken: function() {
+    if (this.hasValidToken()) {
+      return Promise.resolve();
     }
+    return this._fetchTokenForUser().then(
+      token => {
+        this._token = token;
+      }
+    );
   },
 
   getResourceAuthenticator: function () {
     return this._getAuthenticationHeader.bind(this);
   },
 
   /**
    * Obtain a function to be used for adding auth to RESTRequest instances.
@@ -515,22 +544,26 @@ this.BrowserIDManager.prototype = {
     return this._addAuthenticationHeader.bind(this);
   },
 
   /**
    * @return a Hawk HTTP Authorization Header, lightly wrapped, for the .uri
    * of a RESTRequest or AsyncResponse object.
    */
   _getAuthenticationHeader: function(httpObject, method) {
-    if (!this.hasValidToken()) {
-      // Refresh token for the currently logged in FxA user
-      this._token = this._fetchTokenForLoggedInUserSync();
-      if (!this._token) {
-        return null;
-      }
+    let cb = Async.makeSpinningCallback();
+    this._ensureValidToken().then(cb, cb);
+    try {
+      cb.wait();
+    } catch (ex) {
+      this._log.error("Failed to fetch a token for authentication: " + ex);
+      return null;
+    }
+    if (!this._token) {
+      return null;
     }
     let credentials = {algorithm: "sha256",
                        id: this._token.id,
                        key: this._token.key,
                       };
     method = method || httpObject.method;
 
     // Get the local clock offset from the Firefox Accounts server.  This should
@@ -566,39 +599,40 @@ this.BrowserIDManager.prototype = {
 function BrowserIDClusterManager(service) {
   ClusterManager.call(this, service);
 }
 
 BrowserIDClusterManager.prototype = {
   __proto__: ClusterManager.prototype,
 
   _findCluster: function() {
-    let fxa = this.identity._fxaService; // will be mocked for tests.
+    let endPointFromIdentityToken = function() {
+      let endpoint = this.identity._token.endpoint;
+      // For Sync 1.5 storage endpoints, we use the base endpoint verbatim.
+      // However, it should end in "/" because we will extend it with
+      // well known path components. So we add a "/" if it's missing.
+      if (!endpoint.endsWith("/")) {
+        endpoint += "/";
+      }
+      return endpoint;
+    }.bind(this);
+
+    // Spinningly ensure we are ready to authenticate and have a valid token.
     let promiseClusterURL = function() {
-      return fxa.getSignedInUser().then(userData => {
-        return this.identity._fetchTokenForUser(userData).then(token => {
-          let endpoint = token.endpoint;
-          // For Sync 1.5 storage endpoints, we use the base endpoint verbatim.
-          // However, it should end in "/" because we will extend it with
-          // well known path components. So we add a "/" if it's missing.
-          if (!endpoint.endsWith("/")) {
-            endpoint += "/";
-          }
-          return endpoint;
-        });
-      });
+      return this.identity.whenReadyToAuthenticate.promise.then(
+        () => this.identity._ensureValidToken()
+      ).then(
+        () => endPointFromIdentityToken()
+      );
     }.bind(this);
 
     let cb = Async.makeSpinningCallback();
     promiseClusterURL().then(function (clusterURL) {
-        cb(null, clusterURL);
-    },
-    function (err) {
-        cb(err);
-    });
+      cb(null, clusterURL);
+    }).then(null, cb);
     return cb.wait();
   },
 
   getUserBaseURL: function() {
     // Legacy Sync and FxA Sync construct the userBaseURL differently. Legacy
     // Sync appends path components onto an empty path, and in FxA Sync the
     // token server constructs this for us in an opaque manner. Since the
     // cluster manager already sets the clusterURL on Service and also has
--- a/services/sync/modules/constants.js
+++ b/services/sync/modules/constants.js
@@ -49,21 +49,16 @@ MAXIMUM_BACKOFF_INTERVAL:              8
 // HMAC event handling timeout.
 // 10 minutes: a compromise between the multi-desktop sync interval
 // and the mobile sync interval.
 HMAC_EVENT_INTERVAL:                   600000,
 
 // How long to wait between sync attempts if the Master Password is locked.
 MASTER_PASSWORD_LOCKED_RETRY_INTERVAL: 15 * 60 * 1000,   // 15 minutes
 
-// How long to initially wait between sync attempts if the identity manager is
-// not ready.  As we expect this to become ready relatively quickly, we retry
-// in (IDENTITY_NOT_READY_RETRY_INTERVAL * num_failures) seconds.
-IDENTITY_NOT_READY_RETRY_INTERVAL: 5 * 1000,   // 5 seconds
-
 // Separate from the ID fetch batch size to allow tuning for mobile.
 MOBILE_BATCH_SIZE:                     50,
 
 // 50 is hardcoded here because of URL length restrictions.
 // (GUIDs can be up to 64 chars long.)
 // Individual engines can set different values for their limit if their
 // identifiers are shorter.
 DEFAULT_GUID_FETCH_BATCH_SIZE:         50,
--- a/services/sync/modules/identity.js
+++ b/services/sync/modules/identity.js
@@ -84,16 +84,37 @@ IdentityManager.prototype = {
   _syncKeyBundle: null,
 
   /**
    * Initialize the identity provider.  Returns a promise that is resolved
    * when initialization is complete and the provider can be queried for
    * its state
    */
   initialize: function() {
+    // Nothing to do for this identity provider.
+    return Promise.resolve();
+  },
+
+  finalize: function() {
+    // Nothing to do for this identity provider.
+    return Promise.resolve();
+  },
+
+  /**
+   * Called whenever Service.logout() is called.
+   */
+  logout: function() {
+    // nothing to do for this identity provider.
+  },
+
+  /**
+   * Ensure the user is logged in.  Returns a promise that resolves when
+   * the user is logged in, or is rejected if the login attempt has failed.
+   */
+  ensureLoggedIn: function() {
     // nothing to do for this identity provider
     return Promise.resolve();
   },
 
   /**
    * Indicates if the identity manager is still initializing
    */
   get readyToAuthenticate() {
--- a/services/sync/modules/policies.js
+++ b/services/sync/modules/policies.js
@@ -25,18 +25,16 @@ SyncScheduler.prototype = {
   _log: Log.repository.getLogger("Sync.SyncScheduler"),
 
   _fatalLoginStatus: [LOGIN_FAILED_NO_USERNAME,
                       LOGIN_FAILED_NO_PASSWORD,
                       LOGIN_FAILED_NO_PASSPHRASE,
                       LOGIN_FAILED_INVALID_PASSPHRASE,
                       LOGIN_FAILED_LOGIN_REJECTED],
 
-  _loginNotReadyCounter: 0,
-
   /**
    * The nsITimer object that schedules the next sync. See scheduleNextSync().
    */
   syncTimer: null,
 
   setDefaults: function setDefaults() {
     this._log.trace("Setting SyncScheduler policy values to defaults.");
 
@@ -110,20 +108,16 @@ SyncScheduler.prototype = {
       case "weave:service:sync:start":
         // Clear out any potentially pending syncs now that we're syncing
         this.clearSyncTriggers();
 
         // reset backoff info, if the server tells us to continue backing off,
         // we'll handle that later
         Status.resetBackoff();
 
-        // Reset the loginNotReady counter, just in-case the user signs in
-        // as another user and re-hits the not-ready state.
-        this._loginNotReadyCounter = 0;
-
         this.globalScore = 0;
         break;
       case "weave:service:sync:finish":
         this.nextSync = 0;
         this.adjustSyncInterval();
 
         if (Status.service == SYNC_FAILED_PARTIAL && this.requiresBackoff) {
           this.requiresBackoff = false;
@@ -156,23 +150,16 @@ SyncScheduler.prototype = {
         this.clearSyncTriggers();
 
         if (Status.login == MASTER_PASSWORD_LOCKED) {
           // Try again later, just as if we threw an error... only without the
           // error count.
           this._log.debug("Couldn't log in: master password is locked.");
           this._log.trace("Scheduling a sync at MASTER_PASSWORD_LOCKED_RETRY_INTERVAL");
           this.scheduleAtInterval(MASTER_PASSWORD_LOCKED_RETRY_INTERVAL);
-        } else if (Status.login == LOGIN_FAILED_NOT_READY) {
-          this._loginNotReadyCounter++;
-          this._log.debug("Couldn't log in: identity not ready.");
-          this._log.trace("Scheduling a sync at IDENTITY_NOT_READY_RETRY_INTERVAL * " +
-                          this._loginNotReadyCounter);
-          this.scheduleAtInterval(IDENTITY_NOT_READY_RETRY_INTERVAL *
-                                  this._loginNotReadyCounter);
         } else if (this._fatalLoginStatus.indexOf(Status.login) == -1) {
           // Not a fatal login error, just an intermittent network or server
           // issue. Keep on syncin'.
           this.checkSyncStatus();
         }
         break;
       case "weave:service:logout:finish":
         // Start or cancel the sync timer depending on if
--- a/services/sync/modules/service.js
+++ b/services/sync/modules/service.js
@@ -159,17 +159,17 @@ Sync11Service.prototype = {
     if (!this._clusterManager) {
       return null;
     }
     return this._clusterManager.getUserBaseURL();
   },
 
   _updateCachedURLs: function _updateCachedURLs() {
     // Nothing to cache yet if we don't have the building blocks
-    if (this.clusterURL == "" || this.identity.username == "")
+    if (!this.clusterURL || !this.identity.username)
       return;
 
     this._log.debug("Caching URLs under storage user base: " + this.userBaseURL);
 
     // Generate and cache various URLs under the storage API for this user
     this.infoURL = this.userBaseURL + "info/collections";
     this.storageURL = this.userBaseURL + "storage/";
     this.metaURL = this.storageURL + "meta/global";
@@ -847,39 +847,41 @@ Sync11Service.prototype = {
     })();
   },
 
   startOver: function startOver() {
     this._log.trace("Invoking Service.startOver.");
     Svc.Obs.notify("weave:engine:stop-tracking");
     this.status.resetSync();
 
-    // We want let UI consumers of the following notification know as soon as
-    // possible, so let's fake for the CLIENT_NOT_CONFIGURED status for now
-    // by emptying the passphrase (we still need the password).
-    this.identity.resetSyncKey();
-    this.status.login = LOGIN_FAILED_NO_PASSPHRASE;
-    this.logout();
-    Svc.Obs.notify("weave:service:start-over");
-
     // Deletion doesn't make sense if we aren't set up yet!
     if (this.clusterURL != "") {
       // Clear client-specific data from the server, including disabled engines.
       for each (let engine in [this.clientsEngine].concat(this.engineManager.getAll())) {
         try {
           engine.removeClientData();
         } catch(ex) {
           this._log.warn("Deleting client data for " + engine.name + " failed:"
                          + Utils.exceptionStr(ex));
         }
       }
+      this._log.debug("Finished deleting client data.");
     } else {
       this._log.debug("Skipping client data removal: no cluster URL.");
     }
 
+    // We want let UI consumers of the following notification know as soon as
+    // possible, so let's fake for the CLIENT_NOT_CONFIGURED status for now
+    // by emptying the passphrase (we still need the password).
+    this._log.info("Service.startOver dropping sync key and logging out.");
+    this.identity.resetSyncKey();
+    this.status.login = LOGIN_FAILED_NO_PASSPHRASE;
+    this.logout();
+    Svc.Obs.notify("weave:service:start-over");
+
     // Reset all engines and clear keys.
     this.resetClient();
     this.collectionKeys.clear();
     this.status.resetBackoff();
 
     // Reset Weave prefs.
     this._ignorePrefObserver = true;
     Svc.Prefs.resetBranch("");
@@ -895,31 +897,33 @@ Sync11Service.prototype = {
     try {
       keepIdentity = Services.prefs.getBoolPref("services.sync-testing.startOverKeepIdentity");
     } catch (_) { /* no such pref */ }
     if (keepIdentity) {
       Svc.Obs.notify("weave:service:start-over:finish");
       return;
     }
 
-    this.identity.username = "";
-    Services.prefs.clearUserPref("services.sync.fxaccounts.enabled");
-    this.status.__authManager = null;
-    this.identity = Status._authManager;
-    this._clusterManager = this.identity.createClusterManager(this);
-
-    // Tell the new identity manager to initialize itself
-    this.identity.initialize().then(() => {
-      Svc.Obs.notify("weave:service:start-over:finish");
-    }).then(null, err => {
-      this._log.error("startOver failed to re-initialize the identity manager: " + err);
-      // Still send the observer notification so the current state is
-      // reflected in the UI.
-      Svc.Obs.notify("weave:service:start-over:finish");
-    });
+    this.identity.finalize().then(
+      () => {
+        this.identity.username = "";
+        Services.prefs.clearUserPref("services.sync.fxaccounts.enabled");
+        this.status.__authManager = null;
+        this.identity = Status._authManager;
+        this._clusterManager = this.identity.createClusterManager(this);
+        Svc.Obs.notify("weave:service:start-over:finish");
+      }
+    ).then(null,
+      err => {
+        this._log.error("startOver failed to re-initialize the identity manager: " + err);
+        // Still send the observer notification so the current state is
+        // reflected in the UI.
+        Svc.Obs.notify("weave:service:start-over:finish");
+      }
+    );
   },
 
   persistLogin: function persistLogin() {
     try {
       this.identity.persistCredentials(true);
     } catch (ex) {
       this._log.info("Unable to persist credentials: " + ex);
     }
@@ -943,16 +947,23 @@ Sync11Service.prototype = {
       if (passphrase) {
         this.identity.syncKey = passphrase;
       }
 
       if (this._checkSetup() == CLIENT_NOT_CONFIGURED) {
         throw "Aborting login, client not configured.";
       }
 
+      // Ask the identity manager to explicitly login now.
+      let cb = Async.makeSpinningCallback();
+      this.identity.ensureLoggedIn().then(cb, cb);
+
+      // Just let any errors bubble up - they've more context than we do!
+      cb.wait();
+
       // Calling login() with parameters when the client was
       // previously not configured means setup was completed.
       if (initialStatus == CLIENT_NOT_CONFIGURED
           && (username || password || passphrase)) {
         Svc.Obs.notify("weave:service:setup-complete");
       }
 
       this._log.info("Logging in user " + this.identity.username);
@@ -973,16 +984,17 @@ Sync11Service.prototype = {
   },
 
   logout: function logout() {
     // No need to do anything if we're already logged out.
     if (!this._loggedIn)
       return;
 
     this._log.info("Logging out");
+    this.identity.logout();
     this._loggedIn = false;
 
     Svc.Obs.notify("weave:service:logout:finish");
   },
 
   checkAccount: function checkAccount(account) {
     let client = new UserAPI10Client(this.userAPIURI);
     let cb = Async.makeSpinningCallback();
--- a/services/sync/tests/unit/test_browserid_identity.js
+++ b/services/sync/tests/unit/test_browserid_identity.js
@@ -7,16 +7,19 @@ Cu.import("resource://services-sync/rest
 Cu.import("resource://services-sync/util.js");
 Cu.import("resource://services-common/utils.js");
 Cu.import("resource://services-crypto/utils.js");
 Cu.import("resource://testing-common/services/sync/utils.js");
 Cu.import("resource://services-common/hawkclient.js");
 Cu.import("resource://gre/modules/FxAccounts.jsm");
 Cu.import("resource://gre/modules/FxAccountsClient.jsm");
 Cu.import("resource://gre/modules/FxAccountsCommon.js");
+Cu.import("resource://services-common/tokenserverclient.js");
+Cu.import("resource://services-sync/status.js");
+Cu.import("resource://services-sync/constants.js");
 
 const SECOND_MS = 1000;
 const MINUTE_MS = SECOND_MS * 60;
 const HOUR_MS = MINUTE_MS * 60;
 
 let identityConfig = makeIdentityConfig();
 let browseridManager = new BrowserIDManager();
 configureFxAccountIdentity(browseridManager, identityConfig);
@@ -52,41 +55,52 @@ function MockFxAccounts() {
     return Promise.resolve(this.cert.cert);
   };
   return fxa;
 }
 
 function run_test() {
   initTestLogging("Trace");
   Log.repository.getLogger("Sync.Identity").level = Log.Level.Trace;
+  Log.repository.getLogger("Sync.BrowserIDManager").level = Log.Level.Trace;
   run_next_test();
 };
 
 add_test(function test_initial_state() {
     _("Verify initial state");
     do_check_false(!!browseridManager._token);
     do_check_false(browseridManager.hasValidToken());
     run_next_test();
   }
 );
 
+add_task(function test_initialializeWithCurrentIdentity() {
+    _("Verify start after initializeWithCurrentIdentity");
+    browseridManager.initializeWithCurrentIdentity();
+    yield browseridManager.whenReadyToAuthenticate.promise;
+    do_check_true(!!browseridManager._token);
+    do_check_true(browseridManager.hasValidToken());
+    do_check_eq(browseridManager.account, identityConfig.fxaccount.user.email);
+  }
+);
+
+
 add_test(function test_getResourceAuthenticator() {
     _("BrowserIDManager supplies a Resource Authenticator callback which returns a Hawk header.");
     let authenticator = browseridManager.getResourceAuthenticator();
     do_check_true(!!authenticator);
     let req = {uri: CommonUtils.makeURI(
       "https://example.net/somewhere/over/the/rainbow"),
                method: 'GET'};
     let output = authenticator(req, 'GET');
     do_check_true('headers' in output);
     do_check_true('authorization' in output.headers);
     do_check_true(output.headers.authorization.startsWith('Hawk'));
     _("Expected internal state after successful call.");
     do_check_eq(browseridManager._token.uid, identityConfig.fxaccount.token.uid);
-    do_check_eq(browseridManager.account, identityConfig.fxaccount.user.email);
     run_next_test();
   }
 );
 
 add_test(function test_getRESTRequestAuthenticator() {
     _("BrowserIDManager supplies a REST Request Authenticator callback which sets a Hawk header on a request object.");
     let request = new SyncStorageRequest(
       "https://example.net/somewhere/over/the/rainbow");
@@ -217,16 +231,54 @@ add_test(function test_RESTResourceAuthe
   // window.
   do_check_eq(getTimestamp(authHeader), now - 12 * HOUR_MS);
   do_check_true(
       (getTimestampDelta(authHeader, now) - 12 * HOUR_MS) < 2 * MINUTE_MS);
 
   run_next_test();
 });
 
+add_task(function test_ensureLoggedIn() {
+  configureFxAccountIdentity(browseridManager);
+  yield browseridManager.initializeWithCurrentIdentity();
+  Assert.equal(Status.login, LOGIN_SUCCEEDED, "original initialize worked");
+  yield browseridManager.ensureLoggedIn();
+  Assert.equal(Status.login, LOGIN_SUCCEEDED, "original ensureLoggedIn worked");
+  Assert.ok(browseridManager._shouldHaveSyncKeyBundle,
+            "_shouldHaveSyncKeyBundle should always be true after ensureLogin completes.");
+
+  // arrange for no logged in user.
+  let fxa = browseridManager._fxaService
+  let signedInUser = fxa.internal.currentAccountState.signedInUser;
+  fxa.internal.currentAccountState.signedInUser = null;
+  browseridManager.initializeWithCurrentIdentity();
+  Assert.ok(!browseridManager._shouldHaveSyncKeyBundle,
+            "_shouldHaveSyncKeyBundle should be false so we know we are testing what we think we are.");
+  Status.login = LOGIN_FAILED_NO_USERNAME;
+  try {
+    yield browseridManager.ensureLoggedIn();
+    Assert.ok(false, "promise should have been rejected.")
+  } catch(_) {
+  }
+  Assert.ok(browseridManager._shouldHaveSyncKeyBundle,
+            "_shouldHaveSyncKeyBundle should always be true after ensureLogin completes.");
+  fxa.internal.currentAccountState.signedInUser = signedInUser;
+  Status.login = LOGIN_FAILED_LOGIN_REJECTED;
+  try {
+    yield browseridManager.ensureLoggedIn();
+    Assert.ok(false, "LOGIN_FAILED_LOGIN_REJECTED should have caused immediate rejection");
+  } catch (_) {
+  }
+  Assert.equal(Status.login, LOGIN_FAILED_LOGIN_REJECTED,
+               "status should remain LOGIN_FAILED_LOGIN_REJECTED");
+  Status.login = LOGIN_FAILED_NETWORK_ERROR;
+  yield browseridManager.ensureLoggedIn();
+  Assert.equal(Status.login, LOGIN_SUCCEEDED, "final ensureLoggedIn worked");
+});
+
 add_test(function test_tokenExpiration() {
     _("BrowserIDManager notices token expiration:");
     let bimExp = new BrowserIDManager();
     configureFxAccountIdentity(bimExp, identityConfig);
 
     let authenticator = bimExp.getResourceAuthenticator();
     do_check_true(!!authenticator);
     let req = {uri: CommonUtils.makeURI(
@@ -244,34 +296,16 @@ add_test(function test_tokenExpiration()
     });
     do_check_true(bimExp._token.expiration < bimExp._now());
     _("... means BrowserIDManager knows to re-fetch it on the next call.");
     do_check_false(bimExp.hasValidToken());
     run_next_test();
   }
 );
 
-add_test(function test_userChangeAndLogOut() {
-    _("BrowserIDManager notices when the FxAccounts.getSignedInUser().email changes.");
-    let bidUser = new BrowserIDManager();
-    configureFxAccountIdentity(bidUser, identityConfig);
-    let request = new SyncStorageRequest(
-      "https://example.net/somewhere/over/the/rainbow");
-    let authenticator = bidUser.getRESTRequestAuthenticator();
-    do_check_true(!!authenticator);
-    let output = authenticator(request, 'GET');
-    do_check_true(!!output);
-    do_check_eq(bidUser.account, identityConfig.fxaccount.user.email);
-    do_check_true(bidUser.hasValidToken());
-    identityConfig.fxaccount.user.email = "something@new";
-    do_check_false(bidUser.hasValidToken());
-    run_next_test();
-  }
-);
-
 add_test(function test_sha256() {
   // Test vectors from http://www.bichlmeier.info/sha256test.html
   let vectors = [
     ["",
      "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"],
     ["abc",
      "ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad"],
     ["message digest",
@@ -300,19 +334,153 @@ add_test(function test_computeXClientSta
 
   let bidUser = new BrowserIDManager();
   let header = bidUser._computeXClientState(kB);
 
   do_check_eq(header, "6ae94683571c7a7c54dab4700aa3995f");
   run_next_test();
 });
 
+add_task(function test_getTokenErrors() {
+  _("BrowserIDManager correctly handles various failures to get a token.");
+
+  _("Arrange for a 401 - Sync should reflect an auth error.");
+  yield initializeIdentityWithTokenServerFailure({
+    status: 401,
+    headers: {"content-type": "application/json"},
+    body: JSON.stringify({}),
+  });
+  Assert.equal(Status.login, LOGIN_FAILED_LOGIN_REJECTED, "login was rejected");
+
+  // XXX - other interesting responses to return?
+
+  // And for good measure, some totally "unexpected" errors - we generally
+  // assume these problems are going to magically go away at some point.
+  _("Arrange for an empty body with a 200 response - should reflect a network error.");
+  yield initializeIdentityWithTokenServerFailure({
+    status: 200,
+    headers: [],
+    body: "",
+  });
+  Assert.equal(Status.login, LOGIN_FAILED_NETWORK_ERROR, "login state is LOGIN_FAILED_NETWORK_ERROR");
+});
+
+add_task(function test_getHAWKErrors() {
+  _("BrowserIDManager correctly handles various HAWK failures.");
+
+  _("Arrange for a 401 - Sync should reflect an auth error.");
+  yield initializeIdentityWithHAWKFailure({
+    status: 401,
+    headers: {"content-type": "application/json"},
+    body: JSON.stringify({}),
+  });
+  Assert.equal(Status.login, LOGIN_FAILED_LOGIN_REJECTED, "login was rejected");
+
+  // XXX - other interesting responses to return?
+
+  // And for good measure, some totally "unexpected" errors - we generally
+  // assume these problems are going to magically go away at some point.
+  _("Arrange for an empty body with a 200 response - should reflect a network error.");
+  yield initializeIdentityWithHAWKFailure({
+    status: 200,
+    headers: [],
+    body: "",
+  });
+  Assert.equal(Status.login, LOGIN_FAILED_NETWORK_ERROR, "login state is LOGIN_FAILED_NETWORK_ERROR");
+});
+
 // End of tests
 // Utility functions follow
 
+// Create a new browserid_identity object and initialize it with a
+// mocked TokenServerClient which always gets the specified response.
+function* initializeIdentityWithTokenServerFailure(response) {
+  // First create a mock "request" object that well' hack into the token server.
+  // A log for it
+  let requestLog = Log.repository.getLogger("testing.mock-rest");
+  if (!requestLog.appenders.length) { // might as well see what it says :)
+    requestLog.addAppender(new Log.DumpAppender());
+    requestLog.level = Log.Level.Trace;
+  }
+
+  // A mock request object.
+  function MockRESTRequest(url) {};
+  MockRESTRequest.prototype = {
+    _log: requestLog,
+    setHeader: function() {},
+    get: function(callback) {
+      this.response = response;
+      callback.call(this);
+    }
+  }
+  // The mocked TokenServer client which will get the response.
+  function MockTSC() { }
+  MockTSC.prototype = new TokenServerClient();
+  MockTSC.prototype.constructor = MockTSC;
+  MockTSC.prototype.newRESTRequest = function(url) {
+    return new MockRESTRequest(url);
+  }
+  // tie it all together.
+  let mockTSC = new MockTSC()
+  configureFxAccountIdentity(browseridManager);
+  browseridManager._tokenServerClient = mockTSC;
+
+  yield browseridManager.initializeWithCurrentIdentity();
+  try {
+    yield browseridManager.whenReadyToAuthenticate.promise;
+    Assert.ok(false, "expecting this promise to resolve with an error");
+  } catch (ex) {}
+}
+
+
+// Create a new browserid_identity object and initialize it with a
+// hawk mock that simulates a failure.
+// A token server mock will be used that doesn't hit a server, so we move
+// directly to a hawk request.
+function* initializeIdentityWithHAWKFailure(response) {
+  // A mock request object.
+  function MockRESTRequest() {};
+  MockRESTRequest.prototype = {
+    setHeader: function() {},
+    post: function(data, callback) {
+      this.response = response;
+      callback.call(this);
+    }
+  }
+
+  // The hawk client.
+  function MockedHawkClient() {}
+  MockedHawkClient.prototype = new HawkClient();
+  MockedHawkClient.prototype.constructor = MockedHawkClient;
+  MockedHawkClient.prototype.newHAWKAuthenticatedRESTRequest = function(uri, credentials, extra) {
+    return new MockRESTRequest();
+  }
+
+  // tie it all together - configureFxAccountIdentity isn't useful here :(
+  let fxaClient = new MockFxAccountsClient();
+  fxaClient.hawk = new MockedHawkClient();
+  let config = makeIdentityConfig();
+  let internal = {
+    fxAccountsClient: fxaClient,
+  }
+  let fxa = new FxAccounts(internal);
+  fxa.internal.currentAccountState.signedInUser = {
+      accountData: config.fxaccount.user,
+  };
+
+  browseridManager._fxaService = fxa;
+  browseridManager._signedInUser = null;
+  yield browseridManager.initializeWithCurrentIdentity();
+  try {
+    yield browseridManager.whenReadyToAuthenticate.promise;
+    Assert.ok(false, "expecting this promise to resolve with an error");
+  } catch (ex) {}
+}
+
+
 function getTimestamp(hawkAuthHeader) {
   return parseInt(/ts="(\d+)"/.exec(hawkAuthHeader)[1], 10) * SECOND_MS;
 }
 
 function getTimestampDelta(hawkAuthHeader, now=Date.now()) {
   return Math.abs(getTimestamp(hawkAuthHeader) - now);
 }
 
--- a/services/sync/tests/unit/test_errorhandler.js
+++ b/services/sync/tests/unit/test_errorhandler.js
@@ -121,23 +121,24 @@ function sync_httpd_setup() {
       upd("crypto", (new ServerWBO("keys")).handler()),
     "/maintenance/1.1/broken.wipe/storage": service_unavailable,
     "/maintenance/1.1/broken.wipe/storage/clients": upd("clients", clientsColl.handler()),
     "/maintenance/1.1/broken.wipe/storage/catapult": service_unavailable
   });
 }
 
 function setUp(server) {
-  let deferred = Promise.defer();
-  configureIdentity({username: "johndoe"}).then(() => {
-    deferred.resolve(generateAndUploadKeys());
-  });
-  Service.serverURL  = server.baseURI + "/";
-  Service.clusterURL = server.baseURI + "/";
-  return deferred.promise;
+  return configureIdentity({username: "johndoe"}).then(
+    () => {
+      Service.serverURL  = server.baseURI + "/";
+      Service.clusterURL = server.baseURI + "/";
+    }
+  ).then(
+    () => generateAndUploadKeys()
+  );
 }
 
 function generateAndUploadKeys() {
   generateNewKeys(Service.collectionKeys);
   let serverKeys = Service.collectionKeys.asWBO("crypto", "keys");
   serverKeys.encrypt(Service.identity.syncKeyBundle);
   return serverKeys.upload(Service.resource(Service.cryptoKeysURL)).success;
 }
--- a/services/sync/tests/unit/test_fxa_startOver.js
+++ b/services/sync/tests/unit/test_fxa_startOver.js
@@ -27,35 +27,36 @@ add_task(function* test_startover() {
   // we expect the "legacy" provider (but can't instanceof that, as BrowserIDManager
   // extends it)
   do_check_false(Service.identity instanceof BrowserIDManager);
   Service.login();
   // We should have a cluster URL
   do_check_true(Service.clusterURL.length > 0);
 
   // remember some stuff so we can reset it after.
-  let oldIdentidy = Service.identity;
+  let oldIdentity = Service.identity;
   let oldClusterManager = Service._clusterManager;
   let deferred = Promise.defer();
   Services.obs.addObserver(function observeStartOverFinished() {
     Services.obs.removeObserver(observeStartOverFinished, "weave:service:start-over:finish");
     deferred.resolve();
   }, "weave:service:start-over:finish", false);
 
   Service.startOver();
-  yield deferred; // wait for the observer to fire.
+  yield deferred.promise; // wait for the observer to fire.
 
   // should have reset the pref that indicates if FxA is enabled.
   do_check_true(Services.prefs.getBoolPref("services.sync.fxaccounts.enabled"));
   // the xpcom service should agree FxA is enabled.
   do_check_true(xps.fxAccountsEnabled);
   // should have swapped identities.
   do_check_true(Service.identity instanceof BrowserIDManager);
   // should have clobbered the cluster URL
   do_check_eq(Service.clusterURL, "");
 
+  // we should have thrown away the old identity provider and cluster manager.
+  do_check_neq(oldIdentity, Service.identity);
+  do_check_neq(oldClusterManager, Service._clusterManager);
+
   // reset the world.
-  Service.identity = oldIdentity = Service.identity;
-  Service._clusterManager = Service._clusterManager;
   Services.prefs.setBoolPref("services.sync.fxaccounts.enabled", false);
-
   Services.prefs.setBoolPref("services.sync-testing.startOverKeepIdentity", oldValue);
 });
--- a/services/sync/tests/unit/test_service_wipeClient.js
+++ b/services/sync/tests/unit/test_service_wipeClient.js
@@ -77,19 +77,19 @@ add_test(function test_startOver_clears_
 
   run_next_test();
 });
 
 add_test(function test_credentials_preserved() {
   _("Ensure that credentials are preserved if client is wiped.");
 
   // Required for wipeClient().
-  Service.clusterURL = "http://dummy:9000/";
   Service.identity.account = "testaccount";
   Service.identity.basicPassword = "testpassword";
+  Service.clusterURL = "http://dummy:9000/";
   let key = Utils.generatePassphrase();
   Service.identity.syncKey = key;
   Service.identity.persistCredentials();
 
   // Simulate passwords engine wipe without all the overhead. To do this
   // properly would require extra test infrastructure.
   Services.logins.removeAllLogins();
   Service.wipeClient();
--- a/toolkit/content/license.html
+++ b/toolkit/content/license.html
@@ -104,16 +104,17 @@
       <li><a href="about:license#libyuv">libyuv License</a></li>
       <li><a href="about:license#hunspell-lt">Lithuanian Spellchecking Dictionary License</a></li>
       <li><a href="about:license#maattachedwindow">MAAttachedWindow License</a></li>
       <li><a href="about:license#msinttypes">msinttypes License</a></li>
       <li><a href="about:license#myspell">MySpell License</a></li>
       <li><a href="about:license#nicer">nICEr License</a></li>
       <li><a href="about:license#nrappkit">nrappkit License</a></li>
       <li><a href="about:license#openvision">OpenVision License</a></li>
+      <li><a href="about:license#pbkdf2-sha256">pbkdf2_sha256 License</a></li>
       <li><a href="about:license#praton">praton License</a></li>
       <li><a href="about:license#qcms">qcms License</a></li>
       <li><a href="about:license#xdg">Red Hat xdg_user_dir_lookup License</a></li>
       <li><a href="about:license#hunspell-ru">Russian Spellchecking Dictionary License</a></li>
       <li><a href="about:license#sctp">SCTP Licenses</a></li>
       <li><a href="about:license#skia">Skia License</a></li>
       <li><a href="about:license#snappy">Snappy License</a></li>
       <li><a href="about:license#sparkle">Sparkle License</a></li>
@@ -3457,16 +3458,52 @@ ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQ
 WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
 ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT
 OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
 </pre>
 
 
     <hr>
 
+    <h1><a id="pbkdf2-sha256"></a>pbkdf2_sha256 License</h1>
+
+    <p>This license applies to the code
+    <span class="path">mozglue/android/pbkdf2_sha256.c</span> and
+    <span class="path">mozglue/android/pbkdf2_sha256.h</span>.
+    </p>
+
+<pre>
+Copyright 2005,2007,2009 Colin Percival
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions
+are met:
+1. Redistributions of source code must retain the above copyright
+   notice, this list of conditions and the following disclaimer.
+2. Redistributions in binary form must reproduce the above copyright
+   notice, this list of conditions and the following disclaimer in the
+   documentation and/or other materials provided with the distribution.
+
+THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND
+ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ARE DISCLAIMED.  IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE
+FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
+OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
+HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
+LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
+OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
+SUCH DAMAGE.
+</pre>
+
+
+    <hr>
+
     <h1><a id="qcms"></a>qcms License</h1>
 
     <p>This license applies to certain files in the directory
       <span class="path">gfx/qcms/</span>.</p>
 <pre>
 Copyright (C) 2009 Mozilla Corporation
 Copyright (C) 1998-2007 Marti Maria
 
new file mode 100644
--- /dev/null
+++ b/toolkit/devtools/webconsole/test/unit/test_js_property_provider.js
@@ -0,0 +1,71 @@
+/* -*- js2-basic-offset: 2; indent-tabs-mode: nil; -*- */
+// Any copyright is dedicated to the Public Domain.
+// http://creativecommons.org/publicdomain/zero/1.0/
+
+"use strict";
+const { devtools } = Components.utils.import("resource://gre/modules/devtools/Loader.jsm", {});
+let JSPropertyProvider = devtools.require("devtools/toolkit/webconsole/utils").JSPropertyProvider;
+
+Components.utils.import("resource://gre/modules/jsdebugger.jsm");
+addDebuggerToGlobal(this);
+
+function run_test() {
+  const testArray = 'var testArray = [\
+    {propA: "A"},\
+    {\
+      propB: "B", \
+      propC: [\
+        {propD: "D"}\
+      ]\
+    },\
+    [\
+      {propE: "E"}\
+    ]\
+  ];'
+
+  const testObject = 'var testObject = {"propA": [{"propB": "B"}]}';
+
+  let sandbox = Components.utils.Sandbox("http://example.com");
+  let dbg = new Debugger;
+  let dbgObject = dbg.addDebuggee(sandbox);
+  Components.utils.evalInSandbox(testArray, sandbox);
+  Components.utils.evalInSandbox(testObject, sandbox);
+
+  let results = JSPropertyProvider(dbgObject, null, "testArray[0].");
+  do_print("Test that suggestions are given for 'foo[n]' where n is an integer.");
+  test_has_result(results, "propA");
+
+  do_print("Test that suggestions are given for multidimensional arrays.");
+  results = JSPropertyProvider(dbgObject, null, "testArray[2][0].");
+  test_has_result(results, "propE");
+
+  do_print("Test that suggestions are not given for index that's out of bounds.");
+  results = JSPropertyProvider(dbgObject, null, "testArray[10].");
+  do_check_null(results);
+
+  do_print("Test that no suggestions are given if an index is not numerical somewhere in the chain.");
+  results = JSPropertyProvider(dbgObject, null, "testArray[0]['propC'][0].");
+  do_check_null(results);
+
+  results = JSPropertyProvider(dbgObject, null, "testObject['propA'][0].");
+  do_check_null(results);
+
+  results = JSPropertyProvider(dbgObject, null, "testArray[0]['propC'].");
+  do_check_null(results);
+
+  results = JSPropertyProvider(dbgObject, null, "testArray[][1].");
+  do_check_null(results);
+}
+
+/**
+ * A helper that ensures (required) results were found.
+ * @param Object aResults
+ *        The results returned by JSPropertyProvider.
+ * @param String aRequiredSuggestion
+ *        A suggestion that must be found from the results.
+ */
+function test_has_result(aResults, aRequiredSuggestion) {
+  do_check_neq(aResults, null);
+  do_check_true(aResults.matches.length > 0);
+  do_check_true(aResults.matches.indexOf(aRequiredSuggestion) !== -1);
+}
--- a/toolkit/devtools/webconsole/test/unit/xpcshell.ini
+++ b/toolkit/devtools/webconsole/test/unit/xpcshell.ini
@@ -1,6 +1,7 @@
 [DEFAULT]
 head =
 tail =
 support-files =
 
-[test_network_helper.js]
\ No newline at end of file
+[test_js_property_provider.js]
+[test_network_helper.js]
--- a/toolkit/devtools/webconsole/utils.js
+++ b/toolkit/devtools/webconsole/utils.js
@@ -833,32 +833,83 @@ function JSPropertyProvider(aDbgObject, 
   // We get the rest of the properties recursively starting from the Debugger.Object
   // that wraps the first property
   for (let prop of properties) {
     prop = prop.trim();
     if (!prop) {
       return null;
     }
 
-    obj = DevToolsUtils.getProperty(obj, prop);
+    if (/\[\d+\]$/.test(prop)) {
+      // The property to autocomplete is a member of array. For example
+      // list[i][j]..[n]. Traverse the array to get the actual element.
+      obj = getArrayMemberProperty(obj, prop);
+    }
+    else {
+      obj = DevToolsUtils.getProperty(obj, prop);
+    }
 
     if (!isObjectUsable(obj)) {
       return null;
     }
   }
 
   // If the final property is a primitive
   if (typeof obj != "object") {
     return getMatchedProps(obj, matchProp);
   }
 
   return getMatchedPropsInDbgObject(obj, matchProp);
 }
 
 /**
+ * Get the array member of aObj for the given aProp. For example, given
+ * aProp='list[0][1]' the element at [0][1] of aObj.list is returned.
+ *
+ * @param object aObj
+ *        The object to operate on.
+ * @param string aProp
+ *        The property to return.
+ * @return null or Object
+ *         Returns null if the property couldn't be located. Otherwise the array
+ *         member identified by aProp.
+ */
+function getArrayMemberProperty(aObj, aProp)
+{
+  // First get the array.
+  let obj = aObj;
+  let propWithoutIndices = aProp.substr(0, aProp.indexOf("["));
+  obj = DevToolsUtils.getProperty(obj, propWithoutIndices);
+  if (!isObjectUsable(obj)) {
+    return null;
+  }
+
+  // Then traverse the list of indices to get the actual element.
+  let result;
+  let arrayIndicesRegex = /\[[^\]]*\]/g;
+  while ((result = arrayIndicesRegex.exec(aProp)) !== null) {
+    let indexWithBrackets = result[0];
+    let indexAsText = indexWithBrackets.substr(1, indexWithBrackets.length - 2);
+    let index = parseInt(indexAsText);
+
+    if (isNaN(index)) {
+      return null;
+    }
+
+    obj = DevToolsUtils.getProperty(obj, index);
+
+    if (!isObjectUsable(obj)) {
+      return null;
+    }
+  }
+
+  return obj;
+}
+
+/**
  * Check if the given Debugger.Object can be used for autocomplete.
  *
  * @param Debugger.Object aObject
  *        The Debugger.Object to check.
  * @return boolean
  *         True if further inspection into the object is possible, or false
  *         otherwise.
  */
--- a/toolkit/themes/linux/global/jar.mn
+++ b/toolkit/themes/linux/global/jar.mn
@@ -47,9 +47,10 @@ toolkit.jar:
 +  skin/classic/global/icons/loading_16.png                    (icons/loading_16.png)
 +  skin/classic/global/icons/panelarrow-horizontal.svg         (icons/panelarrow-horizontal.svg)
 +  skin/classic/global/icons/panelarrow-vertical.svg           (icons/panelarrow-vertical.svg)
 +  skin/classic/global/icons/resizer.png                       (icons/resizer.png)
 +  skin/classic/global/icons/sslWarning.png                    (icons/sslWarning.png)
 +  skin/classic/global/icons/wrap.png                          (icons/wrap.png)
 +  skin/classic/global/icons/webapps-16.png                    (icons/webapps-16.png)
 +  skin/classic/global/icons/webapps-64.png                    (icons/webapps-64.png)
+   skin/classic/global/menu/menu-check.png                     (../../shared/menu-check.png)
 +  skin/classic/global/toolbar/spring.png                      (toolbar/spring.png)
--- a/toolkit/themes/osx/global/jar.mn
+++ b/toolkit/themes/osx/global/jar.mn
@@ -176,18 +176,18 @@ toolkit.jar:
   skin/classic/global/media/volume-empty.png                         (media/volume-empty.png)
   skin/classic/global/media/volume-empty@2x.png                      (media/volume-empty@2x.png)
   skin/classic/global/media/volume-full.png                          (media/volume-full.png)
   skin/classic/global/media/volume-full@2x.png                       (media/volume-full@2x.png)
   skin/classic/global/media/clicktoplay-bgtexture.png                (media/clicktoplay-bgtexture.png)
   skin/classic/global/media/videoClickToPlayButton.svg               (media/videoClickToPlayButton.svg)
   skin/classic/global/menu/menu-arrow.png                            (menu/menu-arrow.png)
   skin/classic/global/menu/menu-arrow@2x.png                         (menu/menu-arrow@2x.png)
-  skin/classic/global/menu/menu-check.png                            (menu/menu-check.png)
-  skin/classic/global/menu/menu-check@2x.png                         (menu/menu-check@2x.png)
+  skin/classic/global/menu/menu-check.png                            (../../shared/menu-check.png)
+  skin/classic/global/menu/menu-check@2x.png                         (../../shared/menu-check@2x.png)
   skin/classic/global/scale/scale-tray-horiz.gif                     (scale/scale-tray-horiz.gif)
   skin/classic/global/scale/scale-tray-vert.gif                      (scale/scale-tray-vert.gif)
   skin/classic/global/splitter/dimple.png                            (splitter/dimple.png)
   skin/classic/global/splitter/grip-bottom.gif                       (splitter/grip-bottom.gif)
   skin/classic/global/splitter/grip-top.gif                          (splitter/grip-top.gif)
   skin/classic/global/splitter/grip-left.gif                         (splitter/grip-left.gif)
   skin/classic/global/splitter/grip-right.gif                        (splitter/grip-right.gif)
   skin/classic/global/toolbar/spring.png                             (toolbar/spring.png)
new file mode 100644
index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..d82635a639a401185de2f31f3ea2ad8520fa1a99
GIT binary patch
literal 197
zc%17D@N?(olHy`uVBq!ia0vp^0wB!61|;P_|4#%`J)SO(Ar_~T6C{EZ6ikd*I-WBd
zid)V)+VI0v;6MALzfUAK@U4<;arU?>)4Qb7RL@n%VNI`V3|Gf}C)4IV3T>ww>=gd7
z9O27&G@H{(RwJ=*$qQ?N-;PIxIjm$)PKZ-Ey1#J;ONYHciPbE{SdX?p>e^QtyKY}J
uS+AykE_&gtc)_P7GHoW4<|MGOFfj1*<#K3rak2v4#o+1c=d#Wzp$Pyh#zO7@
new file mode 100644
index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..2b1f8361324008de1265f2a2ec6acf57ecfc0fc4
GIT binary patch
literal 377
zc$@)w0fzpGP)<h;3K|Lk000e1NJLTq001BW001Be1^@s6b9#F80003%Nkl<ZSi@sr
z7zM)}1cXL14y5I3Kzs&>wFbg+W*|NR#Q%Z#GZ077+X1yu%aQS9AZDkB<t9M<3yTBp
z0WlA)E$0N{YgjD*2gLUD@cjfVmV@vFdRXoZ^(ngLw}6<7)|Lwb@gppj{{dnvdiZ_`
zj-c-*J2**D8;ZknP-5gD$MRfQ!U19?0%|3J_$`j0HzUV#Kd5GyL92230#wce@pc@(
zZzacaT_FB~#jry_EQCdUCJxIl0Wljn4k*WAAS^x=kkqQ;a4e_<G^8Xox8iZcb09WC
zt7y=1JvFjaGag5L0^$`o<jw*yE4A`<9Uezu!#_c3h}wlq4G9jYqE!i3L4@TeLFs~)
z#cUZK%fAD$COyjfVjK=Are`y&0Ij+`Lerqf;fOq_BS3|a>OeKOvp^NrXs>4!Py+w}
X8K;zTOeY^*00000NkvXXu0mjf<Uy4=
--- a/toolkit/themes/windows/global/jar.mn
+++ b/toolkit/themes/windows/global/jar.mn
@@ -163,16 +163,17 @@ toolkit.jar:
         skin/classic/global/media/scrubberThumbWide.png          (media/scrubberThumbWide.png)
         skin/classic/global/media/throbber.png                   (media/throbber.png)
         skin/classic/global/media/stalled.png                    (media/stalled.png)
         skin/classic/global/media/volume-empty.png               (media/volume-empty.png)
         skin/classic/global/media/volume-full.png                (media/volume-full.png)
         skin/classic/global/media/error.png                      (media/error.png)
         skin/classic/global/media/clicktoplay-bgtexture.png      (media/clicktoplay-bgtexture.png)
         skin/classic/global/media/videoClickToPlayButton.svg     (media/videoClickToPlayButton.svg)
+        skin/classic/global/menu/menu-check.png                  (../../shared/menu-check.png)
         skin/classic/global/printpreview/arrow-left.png          (printpreview/arrow-left.png)
         skin/classic/global/printpreview/arrow-left-end.png      (printpreview/arrow-left-end.png)
         skin/classic/global/printpreview/arrow-right.png         (printpreview/arrow-right.png)
         skin/classic/global/printpreview/arrow-right-end.png     (printpreview/arrow-right-end.png)
         skin/classic/global/radio/radio-check.gif                (radio/radio-check.gif)
         skin/classic/global/radio/radio-check-dis.gif            (radio/radio-check-dis.gif)
         skin/classic/global/scrollbar/slider.gif                 (scrollbar/slider.gif)
         skin/classic/global/splitter/grip-bottom.gif             (splitter/grip-bottom.gif)
@@ -345,16 +346,17 @@ toolkit.jar:
         skin/classic/aero/global/media/scrubberThumbWide.png             (media/scrubberThumbWide.png)
         skin/classic/aero/global/media/throbber.png                      (media/throbber.png)
         skin/classic/aero/global/media/stalled.png                       (media/stalled.png)
         skin/classic/aero/global/media/volume-empty.png                  (media/volume-empty.png)
         skin/classic/aero/global/media/volume-full.png                   (media/volume-full.png)
         skin/classic/aero/global/media/error.png                         (media/error.png)
         skin/classic/aero/global/media/clicktoplay-bgtexture.png         (media/clicktoplay-bgtexture.png)
         skin/classic/aero/global/media/videoClickToPlayButton.svg        (media/videoClickToPlayButton.svg)
+        skin/classic/aero/global/menu/menu-check.png                     (../../shared/menu-check.png)
         skin/classic/aero/global/printpreview/arrow-left.png             (printpreview/arrow-left-aero.png)
         skin/classic/aero/global/printpreview/arrow-left-end.png         (printpreview/arrow-left-end-aero.png)
         skin/classic/aero/global/printpreview/arrow-right.png            (printpreview/arrow-right-aero.png)
         skin/classic/aero/global/printpreview/arrow-right-end.png        (printpreview/arrow-right-end-aero.png)
         skin/classic/aero/global/radio/radio-check.gif                   (radio/radio-check.gif)
         skin/classic/aero/global/radio/radio-check-dis.gif               (radio/radio-check-dis.gif)
         skin/classic/aero/global/scrollbar/slider.gif                    (scrollbar/slider.gif)
         skin/classic/aero/global/splitter/grip-bottom.gif                (splitter/grip-bottom.gif)