Merge mozilla-central to mozilla-inbound
authorCarsten "Tomcat" Book <cbook@mozilla.com>
Tue, 08 Sep 2015 15:55:01 +0200
changeset 293961 7fa38a96266144196e943333cacfec791deb6da8
parent 293960 e96a7cd7259f653279b3b7a6e0023ab38d0b3d4f (current diff)
parent 293899 b23b2fa33a9dcda59dbbca1d157eca3c32c5b862 (diff)
child 293962 1420013007956e6311edb75c7a2c158d685bcc27
push id5245
push userraliiev@mozilla.com
push dateThu, 29 Oct 2015 11:30:51 +0000
treeherdermozilla-beta@dac831dc1bd0 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
milestone43.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 mozilla-central to mozilla-inbound
--- a/addon-sdk/source/lib/sdk/l10n/json/core.js
+++ b/addon-sdk/source/lib/sdk/l10n/json/core.js
@@ -27,10 +27,10 @@ exports.get = function get(k) {
 // Returns the full length locale code: ja-JP-mac, en-US or fr
 exports.locale = function locale() {
   return bestMatchingLocale;
 }
 
 // Returns the short locale code: ja, en, fr
 exports.language = function language() {
   return bestMatchingLocale ? bestMatchingLocale.split("-")[0].toLowerCase()
-                            : null;
+                            : "en";
 }
--- a/b2g/config/aries/sources.xml
+++ b/b2g/config/aries/sources.xml
@@ -10,17 +10,17 @@
   <!--original fetch url was git://codeaurora.org/-->
   <remote fetch="https://git.mozilla.org/external/caf" name="caf"/>
   <!--original fetch url was https://git.mozilla.org/releases-->
   <remote fetch="https://git.mozilla.org/releases" name="mozillaorg"/>
   <!-- B2G specific things. -->
   <project name="platform_build" path="build" remote="b2g" revision="e935894ef5f27e2f04b9e929a45a958e6288a223">
     <copyfile dest="Makefile" src="core/root.mk"/>
   </project>
-  <project name="gaia" path="gaia" remote="mozillaorg" revision="891798f1e345bc2b69e71de42bd524a90b1745c4"/>
+  <project name="gaia" path="gaia" remote="mozillaorg" revision="b81185d30e548f782770b852473ffb53c641a490"/>
   <project name="fake-libdvm" path="dalvik" remote="b2g" revision="d50ae982b19f42f0b66d08b9eb306be81687869f"/>
   <project name="gonk-misc" path="gonk-misc" remote="b2g" revision="e9e2923fd6cab93cf88b4b9ada82225e44fe6635"/>
   <project name="librecovery" path="librecovery" remote="b2g" revision="1b3591a50ed352fc6ddb77462b7b35d0bfa555a3"/>
   <project name="moztt" path="external/moztt" remote="b2g" revision="f5d65f5b17d9766d7925aefd0486a1e526ae9bf0"/>
   <project name="rilproxy" path="rilproxy" remote="b2g" revision="5ef30994f4778b4052e58a4383dbe7890048c87e"/>
   <project name="valgrind" path="external/valgrind" remote="b2g" revision="daa61633c32b9606f58799a3186395fd2bbb8d8c"/>
   <project name="vex" path="external/VEX" remote="b2g" revision="47f031c320888fe9f3e656602588565b52d43010"/>
   <project name="apitrace" path="external/apitrace" remote="apitrace" revision="c6cace53426b5be7e56c0fd202118009689bc707"/>
--- a/b2g/config/dolphin/sources.xml
+++ b/b2g/config/dolphin/sources.xml
@@ -10,17 +10,17 @@
   <!--original fetch url was git://codeaurora.org/-->
   <remote fetch="https://git.mozilla.org/external/caf" name="caf"/>
   <!--original fetch url was https://git.mozilla.org/releases-->
   <remote fetch="https://git.mozilla.org/releases" name="mozillaorg"/>
   <!-- B2G specific things. -->
   <project name="platform_build" path="build" remote="b2g" revision="e935894ef5f27e2f04b9e929a45a958e6288a223">
     <copyfile dest="Makefile" src="core/root.mk"/>
   </project>
-  <project name="gaia" path="gaia" remote="mozillaorg" revision="891798f1e345bc2b69e71de42bd524a90b1745c4"/>
+  <project name="gaia" path="gaia" remote="mozillaorg" revision="b81185d30e548f782770b852473ffb53c641a490"/>
   <project name="fake-libdvm" path="dalvik" remote="b2g" revision="d50ae982b19f42f0b66d08b9eb306be81687869f"/>
   <project name="gonk-misc" path="gonk-misc" remote="b2g" revision="e9e2923fd6cab93cf88b4b9ada82225e44fe6635"/>
   <project name="librecovery" path="librecovery" remote="b2g" revision="1b3591a50ed352fc6ddb77462b7b35d0bfa555a3"/>
   <project name="moztt" path="external/moztt" remote="b2g" revision="f5d65f5b17d9766d7925aefd0486a1e526ae9bf0"/>
   <project name="rilproxy" path="rilproxy" remote="b2g" revision="5ef30994f4778b4052e58a4383dbe7890048c87e"/>
   <project name="valgrind" path="external/valgrind" remote="b2g" revision="daa61633c32b9606f58799a3186395fd2bbb8d8c"/>
   <project name="vex" path="external/VEX" remote="b2g" revision="47f031c320888fe9f3e656602588565b52d43010"/>
   <project name="apitrace" path="external/apitrace" remote="apitrace" revision="c6cace53426b5be7e56c0fd202118009689bc707"/>
--- a/b2g/config/emulator-ics/sources.xml
+++ b/b2g/config/emulator-ics/sources.xml
@@ -14,21 +14,21 @@
   <!--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="1b0db93fb6b870b03467aff50d6419771ba0d88c">
     <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="891798f1e345bc2b69e71de42bd524a90b1745c4"/>
+  <project name="gaia.git" path="gaia" remote="mozillaorg" revision="b81185d30e548f782770b852473ffb53c641a490"/>
   <project name="gonk-misc" path="gonk-misc" remote="b2g" revision="e9e2923fd6cab93cf88b4b9ada82225e44fe6635"/>
   <project name="rilproxy" path="rilproxy" remote="b2g" revision="5ef30994f4778b4052e58a4383dbe7890048c87e"/>
-  <project name="platform_hardware_ril" path="hardware/ril" remote="b2g" revision="2d58f4b9206b50b8fda0d5036da6f0c62608db7c"/>
-  <project name="platform_external_qemu" path="external/qemu" remote="b2g" revision="d70e4bfdcb65e7514de0f9315b74aea1c811678d"/>
+  <project name="platform_hardware_ril" path="hardware/ril" remote="b2g" revision="27eb2f04e149fc2c9976d881b1b5984bbe7ee089"/>
+  <project name="platform_external_qemu" path="external/qemu" remote="b2g" revision="067c08fb3e5744b42b68d1f861245f7d507109bc"/>
   <project name="moztt" path="external/moztt" remote="b2g" revision="f5d65f5b17d9766d7925aefd0486a1e526ae9bf0"/>
   <project name="apitrace" path="external/apitrace" remote="apitrace" revision="34ea6163f9f0e0122fb0bb03607eccdca31ced7a"/>
   <!-- Stock Android things -->
   <project name="platform/abi/cpp" path="abi/cpp" revision="dd924f92906085b831bf1cbbc7484d3c043d613c"/>
   <project name="platform_bionic" path="bionic" remote="b2g" revision="e2b3733ba3fa5e3f404e983d2e4142b1f6b1b846"/>
   <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"/>
@@ -93,17 +93,17 @@
   <project name="platform/system/bluetooth" path="system/bluetooth" revision="507e46e553586bec971551322f20d066c80a0788"/>
   <project name="platform/system/core" path="system/core" revision="91e5551f88aea5aa64e1b4f8b4b52d7be2b28b64"/>
   <project name="platform/system/extras" path="system/extras" revision="0205c49fedf29620165c6b4e6db3d13739c93396"/>
   <project name="platform/system/media" path="system/media" revision="7f17e3995d1588cfcc309b56525652794b6513ef"/>
   <project name="platform/system/netd" path="system/netd" revision="3d298fde142bee3fc4f07f63f16f2d8ce42339c0"/>
   <project name="platform/system/vold" path="system/vold" revision="919829940468066a32f403980b43f6ebfee5d314"/>
   <!-- Emulator specific things -->
   <project name="android-development" path="development" remote="b2g" revision="2bdf22305b523af644e1891b4ddfd9229336d0ce"/>
-  <project name="device_generic_goldfish" path="device/generic/goldfish" remote="b2g" revision="0d5c43228006bae775c4cb57a6d3908484d41718"/>
+  <project name="device_generic_goldfish" path="device/generic/goldfish" remote="b2g" revision="fc5f390fa314385e2a84629ea88284a60b40f7c4"/>
   <project name="platform/external/iproute2" path="external/iproute2" revision="c66c5716d5335e450f7a7b71ccc6a604fb2f41d2"/>
   <project 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="d2685281e2e54ca14d1df304867aa82c37b27162"/>
   <project 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="627f9b20fc518937b93747a7ff1ed4f5ed46e06f"/>
   <project name="platform/prebuilts/gcc/linux-x86/arm/arm-linux-androideabi-4.8" path="prebuilts/gcc/linux-x86/arm/arm-linux-androideabi-4.8" revision="d9735fc81434f2af2c44d86ca57740c673c8d9bc"/>
   <project name="platform/prebuilts/tools" path="prebuilts/tools" revision="acba00cdb4596c6dcb61ed06f14cf4ec89623539"/>
   <project name="platform_prebuilts_qemu-kernel" path="prebuilts/qemu-kernel" remote="b2g" revision="02c32feb2fe97037be0ac4dace3a6a5025ac895d"/>
   <project name="android-sdk" path="sdk" remote="b2g" revision="4f46930827957afbce500a4a920755a218bf3155"/>
   <project name="darwinstreamingserver" path="system/darwinstreamingserver" remote="b2g" revision="cf85968c7f85e0ec36e72c87ceb4837a943b8af6"/>
--- a/b2g/config/emulator-jb/sources.xml
+++ b/b2g/config/emulator-jb/sources.xml
@@ -12,17 +12,17 @@
   <!--original fetch url was https://git.mozilla.org/releases-->
   <remote fetch="https://git.mozilla.org/releases" name="mozillaorg"/>
   <!-- B2G specific things. -->
   <project name="platform_build" path="build" remote="b2g" revision="660169a3d7e034a892359e39135e8c2785a6ad6f">
     <copyfile dest="Makefile" src="core/root.mk"/>
   </project>
   <project name="rilproxy" path="rilproxy" remote="b2g" revision="5ef30994f4778b4052e58a4383dbe7890048c87e"/>
   <project name="fake-libdvm" path="dalvik" remote="b2g" revision="d50ae982b19f42f0b66d08b9eb306be81687869f"/>
-  <project name="gaia" path="gaia" remote="mozillaorg" revision="891798f1e345bc2b69e71de42bd524a90b1745c4"/>
+  <project name="gaia" path="gaia" remote="mozillaorg" revision="b81185d30e548f782770b852473ffb53c641a490"/>
   <project name="gonk-misc" path="gonk-misc" remote="b2g" revision="e9e2923fd6cab93cf88b4b9ada82225e44fe6635"/>
   <project name="moztt" path="external/moztt" remote="b2g" revision="f5d65f5b17d9766d7925aefd0486a1e526ae9bf0"/>
   <project name="apitrace" path="external/apitrace" remote="apitrace" revision="c6cace53426b5be7e56c0fd202118009689bc707"/>
   <project name="valgrind" path="external/valgrind" remote="b2g" revision="daa61633c32b9606f58799a3186395fd2bbb8d8c"/>
   <project name="vex" path="external/VEX" remote="b2g" revision="47f031c320888fe9f3e656602588565b52d43010"/>
   <!-- Stock Android things -->
   <project groups="linux" name="platform/prebuilts/clang/linux-x86/3.1" path="prebuilts/clang/linux-x86/3.1" revision="5c45f43419d5582949284eee9cef0c43d866e03b"/>
   <project groups="linux" name="platform/prebuilts/clang/linux-x86/3.2" path="prebuilts/clang/linux-x86/3.2" revision="3748b4168e7bd8d46457d4b6786003bc6a5223ce"/>
--- a/b2g/config/emulator-kk/sources.xml
+++ b/b2g/config/emulator-kk/sources.xml
@@ -10,17 +10,17 @@
   <!--original fetch url was git://codeaurora.org/-->
   <remote fetch="https://git.mozilla.org/external/caf" name="caf"/>
   <!--original fetch url was https://git.mozilla.org/releases-->
   <remote fetch="https://git.mozilla.org/releases" name="mozillaorg"/>
   <!-- B2G specific things. -->
   <project name="platform_build" path="build" remote="b2g" revision="e935894ef5f27e2f04b9e929a45a958e6288a223">
     <copyfile dest="Makefile" src="core/root.mk"/>
   </project>
-  <project name="gaia" path="gaia" remote="mozillaorg" revision="891798f1e345bc2b69e71de42bd524a90b1745c4"/>
+  <project name="gaia" path="gaia" remote="mozillaorg" revision="b81185d30e548f782770b852473ffb53c641a490"/>
   <project name="fake-libdvm" path="dalvik" remote="b2g" revision="d50ae982b19f42f0b66d08b9eb306be81687869f"/>
   <project name="gonk-misc" path="gonk-misc" remote="b2g" revision="e9e2923fd6cab93cf88b4b9ada82225e44fe6635"/>
   <project name="librecovery" path="librecovery" remote="b2g" revision="1b3591a50ed352fc6ddb77462b7b35d0bfa555a3"/>
   <project name="moztt" path="external/moztt" remote="b2g" revision="f5d65f5b17d9766d7925aefd0486a1e526ae9bf0"/>
   <project name="rilproxy" path="rilproxy" remote="b2g" revision="5ef30994f4778b4052e58a4383dbe7890048c87e"/>
   <project name="valgrind" path="external/valgrind" remote="b2g" revision="daa61633c32b9606f58799a3186395fd2bbb8d8c"/>
   <project name="vex" path="external/VEX" remote="b2g" revision="47f031c320888fe9f3e656602588565b52d43010"/>
   <project name="apitrace" path="external/apitrace" remote="apitrace" revision="c6cace53426b5be7e56c0fd202118009689bc707"/>
--- a/b2g/config/emulator-l/sources.xml
+++ b/b2g/config/emulator-l/sources.xml
@@ -7,20 +7,20 @@
   <remote fetch="https://git.mozilla.org/b2g" name="mozilla"/>
   <!--original fetch url was git://github.com/apitrace/-->
   <remote fetch="https://git.mozilla.org/external/apitrace" name="apitrace"/>
   <!--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="05a36844c1046a1eb07d5b1325f85ed741f961ea">
+  <project name="platform_build" path="build" remote="b2g" revision="c9d4fe680662ee44a4bdea42ae00366f5df399cf">
     <copyfile dest="Makefile" src="core/root.mk"/>
   </project>
-  <project name="gaia" path="gaia" remote="mozillaorg" revision="891798f1e345bc2b69e71de42bd524a90b1745c4"/>
+  <project name="gaia" path="gaia" remote="mozillaorg" revision="b81185d30e548f782770b852473ffb53c641a490"/>
   <project name="fake-libdvm" path="dalvik" remote="b2g" revision="d50ae982b19f42f0b66d08b9eb306be81687869f"/>
   <project name="gonk-misc" path="gonk-misc" remote="b2g" revision="e9e2923fd6cab93cf88b4b9ada82225e44fe6635"/>
   <project name="librecovery" path="librecovery" remote="b2g" revision="1b3591a50ed352fc6ddb77462b7b35d0bfa555a3"/>
   <project name="moztt" path="external/moztt" remote="b2g" revision="f5d65f5b17d9766d7925aefd0486a1e526ae9bf0"/>
   <project name="rilproxy" path="rilproxy" remote="b2g" revision="5ef30994f4778b4052e58a4383dbe7890048c87e"/>
   <project name="valgrind" path="external/valgrind" remote="b2g" revision="daa61633c32b9606f58799a3186395fd2bbb8d8c"/>
   <project name="vex" path="external/VEX" remote="b2g" revision="47f031c320888fe9f3e656602588565b52d43010"/>
   <project name="apitrace" path="external/apitrace" remote="apitrace" revision="c6cace53426b5be7e56c0fd202118009689bc707"/>
@@ -136,15 +136,15 @@
   <project name="platform_frameworks_av" path="frameworks/av" remote="b2g" revision="e372b6a77f71c8f9fbbf6f8adbfa7bf8ef45dc60"/>
   <project name="platform_frameworks_base" path="frameworks/base" remote="b2g" revision="04e26ebdc36ca83f4ee3e9e2082b3fcf04c5b971"/>
   <project name="platform_frameworks_wilhelm" path="frameworks/wilhelm" remote="b2g" revision="0dbf5baafadf6d233c0a29e392fa3293f0121673"/>
   <project name="platform_system_core" path="system/core" remote="b2g" revision="f594bc64eacac490857748b1139ffcb34c856bbd"/>
   <project name="platform_external_sepolicy" path="external/sepolicy" remote="b2g" revision="3f6be48a46c54dd8cacaf216ab5b145de5ffefd2"/>
   <default remote="caf" revision="refs/tags/android-5.1.0_r1" sync-j="4"/>
   <!-- Emulator specific things -->
   <project name="device/generic/armv7-a-neon" path="device/generic/armv7-a-neon" revision="fe7df1bc8dd0fd71571505d7be1c31a4ad1e40fb"/>
-  <project name="device_generic_goldfish" path="device/generic/goldfish" remote="b2g" revision="48132ec0b0dfe9fc29c7c3f0e799066be8999198"/>
+  <project name="device_generic_goldfish" path="device/generic/goldfish" remote="b2g" revision="dc8c7896562bf63190befb3e6b21310a4b7144fa"/>
   <project name="platform_external_qemu" path="external/qemu" remote="b2g" revision="59e434cbecc02653f44cedeb2ef5cc88dc8bb61b"/>
   <project name="platform_external_wpa_supplicant_8" path="external/wpa_supplicant_8" remote="b2g" revision="cbda29a58abc4ea1f7f4611fe354ab67b606219d"/>
   <project name="platform/development" path="development" revision="0c51f6e0aa2ee57fcb75ec3b2ff6bf754cece63e"/>
   <project name="android-sdk" path="sdk" remote="b2g" revision="ff4190dc603f62a7caa48342aa268acf99863c5c"/>
   <project name="platform_hardware_ril" path="hardware/ril" remote="b2g" revision="10d289639ea62f2f6fcfb5bbf6121c484e95f767"/>
 </manifest>
--- a/b2g/config/emulator/sources.xml
+++ b/b2g/config/emulator/sources.xml
@@ -14,21 +14,21 @@
   <!--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="1b0db93fb6b870b03467aff50d6419771ba0d88c">
     <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="891798f1e345bc2b69e71de42bd524a90b1745c4"/>
+  <project name="gaia.git" path="gaia" remote="mozillaorg" revision="b81185d30e548f782770b852473ffb53c641a490"/>
   <project name="gonk-misc" path="gonk-misc" remote="b2g" revision="e9e2923fd6cab93cf88b4b9ada82225e44fe6635"/>
   <project name="rilproxy" path="rilproxy" remote="b2g" revision="5ef30994f4778b4052e58a4383dbe7890048c87e"/>
-  <project name="platform_hardware_ril" path="hardware/ril" remote="b2g" revision="2d58f4b9206b50b8fda0d5036da6f0c62608db7c"/>
-  <project name="platform_external_qemu" path="external/qemu" remote="b2g" revision="d70e4bfdcb65e7514de0f9315b74aea1c811678d"/>
+  <project name="platform_hardware_ril" path="hardware/ril" remote="b2g" revision="27eb2f04e149fc2c9976d881b1b5984bbe7ee089"/>
+  <project name="platform_external_qemu" path="external/qemu" remote="b2g" revision="067c08fb3e5744b42b68d1f861245f7d507109bc"/>
   <project name="moztt" path="external/moztt" remote="b2g" revision="f5d65f5b17d9766d7925aefd0486a1e526ae9bf0"/>
   <project name="apitrace" path="external/apitrace" remote="apitrace" revision="34ea6163f9f0e0122fb0bb03607eccdca31ced7a"/>
   <!-- Stock Android things -->
   <project name="platform/abi/cpp" path="abi/cpp" revision="dd924f92906085b831bf1cbbc7484d3c043d613c"/>
   <project name="platform_bionic" path="bionic" remote="b2g" revision="e2b3733ba3fa5e3f404e983d2e4142b1f6b1b846"/>
   <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"/>
@@ -93,17 +93,17 @@
   <project name="platform/system/bluetooth" path="system/bluetooth" revision="507e46e553586bec971551322f20d066c80a0788"/>
   <project name="platform/system/core" path="system/core" revision="91e5551f88aea5aa64e1b4f8b4b52d7be2b28b64"/>
   <project name="platform/system/extras" path="system/extras" revision="0205c49fedf29620165c6b4e6db3d13739c93396"/>
   <project name="platform/system/media" path="system/media" revision="7f17e3995d1588cfcc309b56525652794b6513ef"/>
   <project name="platform/system/netd" path="system/netd" revision="3d298fde142bee3fc4f07f63f16f2d8ce42339c0"/>
   <project name="platform/system/vold" path="system/vold" revision="919829940468066a32f403980b43f6ebfee5d314"/>
   <!-- Emulator specific things -->
   <project name="android-development" path="development" remote="b2g" revision="2bdf22305b523af644e1891b4ddfd9229336d0ce"/>
-  <project name="device_generic_goldfish" path="device/generic/goldfish" remote="b2g" revision="0d5c43228006bae775c4cb57a6d3908484d41718"/>
+  <project name="device_generic_goldfish" path="device/generic/goldfish" remote="b2g" revision="fc5f390fa314385e2a84629ea88284a60b40f7c4"/>
   <project name="platform/external/iproute2" path="external/iproute2" revision="c66c5716d5335e450f7a7b71ccc6a604fb2f41d2"/>
   <project 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="d2685281e2e54ca14d1df304867aa82c37b27162"/>
   <project 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="627f9b20fc518937b93747a7ff1ed4f5ed46e06f"/>
   <project name="platform/prebuilts/gcc/linux-x86/arm/arm-linux-androideabi-4.8" path="prebuilts/gcc/linux-x86/arm/arm-linux-androideabi-4.8" revision="d9735fc81434f2af2c44d86ca57740c673c8d9bc"/>
   <project name="platform/prebuilts/tools" path="prebuilts/tools" revision="acba00cdb4596c6dcb61ed06f14cf4ec89623539"/>
   <project name="platform_prebuilts_qemu-kernel" path="prebuilts/qemu-kernel" remote="b2g" revision="02c32feb2fe97037be0ac4dace3a6a5025ac895d"/>
   <project name="android-sdk" path="sdk" remote="b2g" revision="4f46930827957afbce500a4a920755a218bf3155"/>
   <project name="darwinstreamingserver" path="system/darwinstreamingserver" remote="b2g" revision="cf85968c7f85e0ec36e72c87ceb4837a943b8af6"/>
--- a/b2g/config/flame-kk/sources.xml
+++ b/b2g/config/flame-kk/sources.xml
@@ -10,17 +10,17 @@
   <!--original fetch url was git://codeaurora.org/-->
   <remote fetch="https://git.mozilla.org/external/caf" name="caf"/>
   <!--original fetch url was https://git.mozilla.org/releases-->
   <remote fetch="https://git.mozilla.org/releases" name="mozillaorg"/>
   <!-- B2G specific things. -->
   <project name="platform_build" path="build" remote="b2g" revision="e935894ef5f27e2f04b9e929a45a958e6288a223">
     <copyfile dest="Makefile" src="core/root.mk"/>
   </project>
-  <project name="gaia" path="gaia" remote="mozillaorg" revision="891798f1e345bc2b69e71de42bd524a90b1745c4"/>
+  <project name="gaia" path="gaia" remote="mozillaorg" revision="b81185d30e548f782770b852473ffb53c641a490"/>
   <project name="fake-libdvm" path="dalvik" remote="b2g" revision="d50ae982b19f42f0b66d08b9eb306be81687869f"/>
   <project name="gonk-misc" path="gonk-misc" remote="b2g" revision="e9e2923fd6cab93cf88b4b9ada82225e44fe6635"/>
   <project name="librecovery" path="librecovery" remote="b2g" revision="1b3591a50ed352fc6ddb77462b7b35d0bfa555a3"/>
   <project name="moztt" path="external/moztt" remote="b2g" revision="f5d65f5b17d9766d7925aefd0486a1e526ae9bf0"/>
   <project name="rilproxy" path="rilproxy" remote="b2g" revision="5ef30994f4778b4052e58a4383dbe7890048c87e"/>
   <project name="valgrind" path="external/valgrind" remote="b2g" revision="daa61633c32b9606f58799a3186395fd2bbb8d8c"/>
   <project name="vex" path="external/VEX" remote="b2g" revision="47f031c320888fe9f3e656602588565b52d43010"/>
   <project name="apitrace" path="external/apitrace" remote="apitrace" revision="c6cace53426b5be7e56c0fd202118009689bc707"/>
--- a/b2g/config/gaia.json
+++ b/b2g/config/gaia.json
@@ -1,9 +1,9 @@
 {
     "git": {
-        "git_revision": "891798f1e345bc2b69e71de42bd524a90b1745c4", 
+        "git_revision": "b81185d30e548f782770b852473ffb53c641a490", 
         "remote": "https://git.mozilla.org/releases/gaia.git", 
         "branch": ""
     }, 
-    "revision": "8c6b450b2f9f4606f164c33513a023e59271e1e4", 
+    "revision": "d0a1f4d22e1a40cb89f9d592334e7506ee1317bd", 
     "repo_path": "integration/gaia-central"
 }
--- a/b2g/config/nexus-4/sources.xml
+++ b/b2g/config/nexus-4/sources.xml
@@ -12,17 +12,17 @@
   <!--original fetch url was https://git.mozilla.org/releases-->
   <remote fetch="https://git.mozilla.org/releases" name="mozillaorg"/>
   <!-- B2G specific things. -->
   <project name="platform_build" path="build" remote="b2g" revision="660169a3d7e034a892359e39135e8c2785a6ad6f">
     <copyfile dest="Makefile" src="core/root.mk"/>
   </project>
   <project name="rilproxy" path="rilproxy" remote="b2g" revision="5ef30994f4778b4052e58a4383dbe7890048c87e"/>
   <project name="fake-libdvm" path="dalvik" remote="b2g" revision="d50ae982b19f42f0b66d08b9eb306be81687869f"/>
-  <project name="gaia" path="gaia" remote="mozillaorg" revision="891798f1e345bc2b69e71de42bd524a90b1745c4"/>
+  <project name="gaia" path="gaia" remote="mozillaorg" revision="b81185d30e548f782770b852473ffb53c641a490"/>
   <project name="gonk-misc" path="gonk-misc" remote="b2g" revision="e9e2923fd6cab93cf88b4b9ada82225e44fe6635"/>
   <project name="moztt" path="external/moztt" remote="b2g" revision="f5d65f5b17d9766d7925aefd0486a1e526ae9bf0"/>
   <project name="apitrace" path="external/apitrace" remote="apitrace" revision="c6cace53426b5be7e56c0fd202118009689bc707"/>
   <project name="valgrind" path="external/valgrind" remote="b2g" revision="daa61633c32b9606f58799a3186395fd2bbb8d8c"/>
   <project name="vex" path="external/VEX" remote="b2g" revision="47f031c320888fe9f3e656602588565b52d43010"/>
   <!-- Stock Android things -->
   <project groups="linux" name="platform/prebuilts/clang/linux-x86/3.1" path="prebuilts/clang/linux-x86/3.1" revision="5c45f43419d5582949284eee9cef0c43d866e03b"/>
   <project groups="linux" name="platform/prebuilts/clang/linux-x86/3.2" path="prebuilts/clang/linux-x86/3.2" revision="3748b4168e7bd8d46457d4b6786003bc6a5223ce"/>
--- a/b2g/config/nexus-5-l/sources.xml
+++ b/b2g/config/nexus-5-l/sources.xml
@@ -7,20 +7,20 @@
   <remote fetch="https://git.mozilla.org/b2g" name="mozilla"/>
   <!--original fetch url was git://github.com/apitrace/-->
   <remote fetch="https://git.mozilla.org/external/apitrace" name="apitrace"/>
   <!--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="05a36844c1046a1eb07d5b1325f85ed741f961ea">
+  <project name="platform_build" path="build" remote="b2g" revision="c9d4fe680662ee44a4bdea42ae00366f5df399cf">
     <copyfile dest="Makefile" src="core/root.mk"/>
   </project>
-  <project name="gaia" path="gaia" remote="mozillaorg" revision="891798f1e345bc2b69e71de42bd524a90b1745c4"/>
+  <project name="gaia" path="gaia" remote="mozillaorg" revision="b81185d30e548f782770b852473ffb53c641a490"/>
   <project name="fake-libdvm" path="dalvik" remote="b2g" revision="d50ae982b19f42f0b66d08b9eb306be81687869f"/>
   <project name="gonk-misc" path="gonk-misc" remote="b2g" revision="e9e2923fd6cab93cf88b4b9ada82225e44fe6635"/>
   <project name="librecovery" path="librecovery" remote="b2g" revision="1b3591a50ed352fc6ddb77462b7b35d0bfa555a3"/>
   <project name="moztt" path="external/moztt" remote="b2g" revision="f5d65f5b17d9766d7925aefd0486a1e526ae9bf0"/>
   <project name="rilproxy" path="rilproxy" remote="b2g" revision="5ef30994f4778b4052e58a4383dbe7890048c87e"/>
   <project name="valgrind" path="external/valgrind" remote="b2g" revision="daa61633c32b9606f58799a3186395fd2bbb8d8c"/>
   <project name="vex" path="external/VEX" remote="b2g" revision="47f031c320888fe9f3e656602588565b52d43010"/>
   <project name="apitrace" path="external/apitrace" remote="apitrace" revision="c6cace53426b5be7e56c0fd202118009689bc707"/>
--- a/browser/base/content/browser-syncui.js
+++ b/browser/base/content/browser-syncui.js
@@ -98,19 +98,21 @@ let gSyncUI = {
       this._obs.splice(idx, 1);
     }
   },
 
   _needsSetup() {
     // We want to treat "account needs verification" as "needs setup". So
     // "reach in" to Weave.Status._authManager to check whether we the signed-in
     // user is verified.
-    // Referencing Weave.Status spins a nested event loop to initialize the
-    // authManager, so this should always return a value directly.
-    // This only applies to fxAccounts-based Sync.
+    // NOTE: We used to have this _authManager hack to avoid a nested
+    // event-loop from querying Weave.Status.checkSetup() - while that's no
+    // longer true, we do still have the FxA-specific requirement of checking
+    // the verified state - so the hack remains. We should consider refactoring
+    // Sync's "check setup" capabilities to take this into account at some point...
     if (Weave.Status._authManager._signedInUser !== undefined) {
       // If we have a signed in user already, and that user is not verified,
       // revert to the "needs setup" state.
       return !Weave.Status._authManager._signedInUser ||
              !Weave.Status._authManager._signedInUser.verified;
     }
 
     // We are using legacy sync - check that.
--- a/browser/base/content/urlbarBindings.xml
+++ b/browser/base/content/urlbarBindings.xml
@@ -335,17 +335,17 @@ file, You can obtain one at http://mozil
                 return;
               }
             } else if (action.type == "keyword") {
               url = action.params.url;
             } else if (action.type == "searchengine") {
               let engine = Services.search.getEngineByName(action.params.engineName);
               let query = action.params.searchSuggestion ||
                           action.params.searchQuery;
-              let submission = engine.getSubmission(query);
+              let submission = engine.getSubmission(query, null, "keyword");
 
               url = submission.uri.spec;
               postData = submission.postData;
             } else if (action.type == "visiturl") {
               url = action.params.url;
             }
             continueOperation.call(this);
           }
@@ -1385,17 +1385,17 @@ file, You can obtain one at http://mozil
                 case "visiturl": {
                   url = action.params.url;
                   break;
                 }
                 case "searchengine": {
                   let engine = Services.search.getEngineByName(action.params.engineName);
                   let query = action.params.searchSuggestion ||
                               action.params.searchQuery;
-                  let submission = engine.getSubmission(query);
+                  let submission = engine.getSubmission(query, null, "keyword");
                   url = submission.uri.spec;
                   options.postData = submission.postData;
                   break;
                 }
                 default: {
                   return;
                 }
               }
--- a/browser/components/loop/content/css/panel.css
+++ b/browser/components/loop/content/css/panel.css
@@ -288,16 +288,25 @@ html[dir="rtl"] .tab-view li:nth-child(2
 .panel-text-medium {
   font-size: 1.6rem;
 }
 
 .panel-text-large {
   font-size: 2.2rem;
 }
 
+.room-list-loading {
+  margin: 5rem 15px;
+  text-align: center;
+}
+
+.room-list-loading > img {
+  width: 66px;
+}
+
 
 /* Rooms */
 .rooms {
   min-height: 100px;
   /* To hold the conversation dropdown menu. */
   position: relative;
 }
 
@@ -316,20 +325,20 @@ html[dir="rtl"] .tab-view li:nth-child(2
   flex-direction: column;
 }
 
 .new-room-view > .context-checkbox-checked {
   background-color: #dbf7ff;
 }
 
 .new-room-view > .context {
+  border-top: 1px solid #ebebeb;
   flex: 1;
-  border-radius: 3px 3px 0 0;
-  margin: 1rem 0 .5rem;
-  padding: 1rem 15px;
+  margin: .5rem 0;
+  padding: .5rem 15px 1rem;
 }
 
 .new-room-view > .context > .context-enabled {
   margin-bottom: .5rem;
   display: block;
 }
 
 .new-room-view > .context > .context-enabled > input {
--- a/browser/components/loop/content/js/panel.js
+++ b/browser/components/loop/content/js/panel.js
@@ -804,16 +804,31 @@ loop.panel = (function(_, mozL10n) {
       this.setState(this.props.store.getStoreState());
     },
 
     _getUserDisplayName: function() {
       return this.props.userProfile && this.props.userProfile.email ||
         mozL10n.get("display_name_guest");
     },
 
+    /**
+     * Let the user know we're loading rooms
+     * @returns {Object} React render
+     */
+    _renderLoadingRoomsView: function() {
+      return (
+        React.createElement("div", {className: "room-list"}, 
+          React.createElement("div", {className: "room-list-loading"}, 
+            React.createElement("img", {src: "loop/shared/img/animated-spinner.svg"})
+          ), 
+          this._renderNewRoomButton()
+        )
+      );
+    },
+
     _renderNoRoomsView: function() {
       return (
         React.createElement("div", {className: "room-list"}, 
           React.createElement("div", {className: "room-list-empty"}, 
             React.createElement("p", {className: "panel-text-large"}, 
               mozL10n.get("no_conversations_message_heading")
             ), 
             React.createElement("p", {className: "panel-text-medium"}, 
@@ -836,16 +851,20 @@ loop.panel = (function(_, mozL10n) {
     },
 
     render: function() {
       if (this.state.error) {
         // XXX Better end user reporting of errors.
         console.error("RoomList error", this.state.error);
       }
 
+      if (this.state.pendingInitialRetrieval) {
+        return this._renderLoadingRoomsView();
+      }
+
       if (!this.state.rooms.length) {
         return this._renderNoRoomsView();
       }
 
       return (
         React.createElement("div", {className: "rooms"}, 
           React.createElement("h1", null, mozL10n.get("rooms_list_recent_conversations")), 
           React.createElement("div", {className: "room-list"}, 
--- a/browser/components/loop/content/js/panel.jsx
+++ b/browser/components/loop/content/js/panel.jsx
@@ -804,16 +804,31 @@ loop.panel = (function(_, mozL10n) {
       this.setState(this.props.store.getStoreState());
     },
 
     _getUserDisplayName: function() {
       return this.props.userProfile && this.props.userProfile.email ||
         mozL10n.get("display_name_guest");
     },
 
+    /**
+     * Let the user know we're loading rooms
+     * @returns {Object} React render
+     */
+    _renderLoadingRoomsView: function() {
+      return (
+        <div className="room-list">
+          <div className="room-list-loading">
+            <img src="loop/shared/img/animated-spinner.svg" />
+          </div>
+          {this._renderNewRoomButton()}
+        </div>
+      );
+    },
+
     _renderNoRoomsView: function() {
       return (
         <div className="room-list">
           <div className="room-list-empty">
             <p className="panel-text-large">
               {mozL10n.get("no_conversations_message_heading")}
             </p>
             <p className="panel-text-medium">
@@ -836,16 +851,20 @@ loop.panel = (function(_, mozL10n) {
     },
 
     render: function() {
       if (this.state.error) {
         // XXX Better end user reporting of errors.
         console.error("RoomList error", this.state.error);
       }
 
+      if (this.state.pendingInitialRetrieval) {
+        return this._renderLoadingRoomsView();
+      }
+
       if (!this.state.rooms.length) {
         return this._renderNoRoomsView();
       }
 
       return (
         <div className="rooms">
           <h1>{mozL10n.get("rooms_list_recent_conversations")}</h1>
           <div className="room-list">{
--- a/browser/components/loop/content/js/roomStore.js
+++ b/browser/components/loop/content/js/roomStore.js
@@ -109,17 +109,17 @@ loop.store = loop.store || {};
       }
     },
 
     getInitialStoreState: function() {
       return {
         activeRoom: this.activeRoomStore ? this.activeRoomStore.getStoreState() : {},
         error: null,
         pendingCreation: false,
-        pendingInitialRetrieval: false,
+        pendingInitialRetrieval: true,
         rooms: [],
         savingContext: false
       };
     },
 
     /**
      * Registers mozLoop.rooms events.
      */
--- a/browser/components/loop/content/js/roomViews.js
+++ b/browser/components/loop/content/js/roomViews.js
@@ -588,18 +588,30 @@ loop.roomViews = (function(mozL10n) {
     publishStream: function(type, enabled) {
       this.props.dispatcher.dispatch(
         new sharedActions.SetMute({
           type: type,
           enabled: enabled
         }));
     },
 
+    /**
+     * Determine if the invitation controls should be shown.
+     *
+     * @return {Boolean} True if there's no guests.
+     */
     _shouldRenderInvitationOverlay: function() {
-      return (this.state.roomState !== ROOM_STATES.HAS_PARTICIPANTS);
+      var hasGuests = typeof this.state.participants === "object" &&
+        this.state.participants.filter(function(participant) {
+          return !participant.owner;
+        }).length > 0;
+
+      // Don't show if the room has participants whether from the room state or
+      // there being non-owner guests in the participants array.
+      return this.state.roomState !== ROOM_STATES.HAS_PARTICIPANTS && !hasGuests;
     },
 
     /**
      * Works out if remote video should be rended or not, depending on the
      * room state and other flags.
      *
      * @return {Boolean} True if remote video should be rended.
      *
--- a/browser/components/loop/content/js/roomViews.jsx
+++ b/browser/components/loop/content/js/roomViews.jsx
@@ -588,18 +588,30 @@ loop.roomViews = (function(mozL10n) {
     publishStream: function(type, enabled) {
       this.props.dispatcher.dispatch(
         new sharedActions.SetMute({
           type: type,
           enabled: enabled
         }));
     },
 
+    /**
+     * Determine if the invitation controls should be shown.
+     *
+     * @return {Boolean} True if there's no guests.
+     */
     _shouldRenderInvitationOverlay: function() {
-      return (this.state.roomState !== ROOM_STATES.HAS_PARTICIPANTS);
+      var hasGuests = typeof this.state.participants === "object" &&
+        this.state.participants.filter(function(participant) {
+          return !participant.owner;
+        }).length > 0;
+
+      // Don't show if the room has participants whether from the room state or
+      // there being non-owner guests in the participants array.
+      return this.state.roomState !== ROOM_STATES.HAS_PARTICIPANTS && !hasGuests;
     },
 
     /**
      * Works out if remote video should be rended or not, depending on the
      * room state and other flags.
      *
      * @return {Boolean} True if remote video should be rended.
      *
new file mode 100644
--- /dev/null
+++ b/browser/components/loop/content/shared/img/animated-spinner.svg
@@ -0,0 +1,1 @@
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><g fill="#0095FF"><path opacity=".15" d="M4.9 11.8c.2-.4.7-.5 1.1-.3.4.2.5.7.3 1.1l-1.2 2.1c-.2.3-.7.5-1.1.2-.4-.2-.5-.7-.3-1.1l1.2-2z"><animate attributeType="CSS" attributeName="opacity" from="1" to=".15" dur="0.96s" repeatCount="indefinite"/></path><path opacity=".2" d="M3.4 9.7c.4-.2.9-.1 1.1.3.2.4.1.9-.3 1.1l-2.1 1.2c-.4.2-.9.1-1.1-.3-.2-.4-.1-.9.3-1.1l2.1-1.2z"><animate attributeType="CSS" attributeName="opacity" from="1" to=".15" dur="0.96s" repeatCount="indefinite" begin="0.08s"/></path><path opacity=".25" d="M3.2 7.2c.4 0 .8.4.8.8s-.4.8-.8.8H.8C.4 8.8 0 8.4 0 8s.4-.8.8-.8h2.4z"><animate attributeType="CSS" attributeName="opacity" from="1" to=".15" dur="0.96s" repeatCount="indefinite" begin="0.16s"/></path><path opacity=".35" d="M1.4 5.1C1 4.9.8 4.4 1.1 4c.2-.4.7-.5 1.1-.3l2.1 1.2c.3.2.5.7.2 1.1-.2.4-.7.5-1.1.3l-2-1.2z"><animate attributeType="CSS" attributeName="opacity" from="1" to=".15" dur="0.96s" repeatCount="indefinite" begin="0.24s"/></path><path opacity=".45" d="M3.7 2.2c-.2-.4-.1-.9.3-1.1.4-.2.9-.1 1.1.3l1.2 2.1c.2.3.1.8-.3 1-.4.3-.9.1-1.1-.3l-1.2-2z"><animate attributeType="CSS" attributeName="opacity" from="1" to=".15" dur="0.96s" repeatCount="indefinite" begin="0.32s"/></path><path opacity=".5" d="M8.8 3.2c0 .4-.4.8-.8.8s-.8-.4-.8-.8V.8c0-.4.4-.8.8-.8s.8.4.8.8v2.4z"><animate attributeType="CSS" attributeName="opacity" from="1" to=".15" dur="0.96s" repeatCount="indefinite" begin="0.40s"/></path><path opacity=".55" d="M10.9 1.4c.2-.4.7-.6 1.1-.3.4.2.5.7.3 1.1l-1.2 2.1c-.2.4-.7.5-1.1.3-.4-.3-.5-.8-.3-1.2l1.2-2z"><animate attributeType="CSS" attributeName="opacity" from="1" to=".15" dur="0.96s" repeatCount="indefinite" begin="0.48s"/></path><path opacity=".6" d="M13.8 3.7c.4-.2.9-.1 1.1.3.2.4.1.9-.3 1.1l-2.1 1.2c-.4.2-.9.1-1.1-.3-.2-.4-.1-.9.3-1.1l2.1-1.2z"><animate attributeType="CSS" attributeName="opacity" from="1" to=".15" dur="0.96s" repeatCount="indefinite" begin="0.56s"/></path><path opacity=".65" d="M15.2 7.2c.4 0 .8.4.8.8s-.4.8-.8.8h-2.4c-.4 0-.8-.4-.8-.8s.4-.8.8-.8h2.4z"><animate attributeType="CSS" attributeName="opacity" from="1" to=".15" dur="0.96s" repeatCount="indefinite" begin="0.64s"/></path><path opacity=".7" d="M11.8 11.1c-.4-.2-.5-.7-.3-1.1.2-.4.7-.5 1.1-.3l2.1 1.2c.4.2.5.7.3 1.1-.2.4-.7.5-1.1.3l-2.1-1.2z"><animate attributeType="CSS" attributeName="opacity" from="1" to=".15" dur="0.96s" repeatCount="indefinite" begin="0.70s"/></path><path opacity=".8" d="M9.7 12.6c-.2-.4-.1-.9.3-1.1.4-.2.9-.1 1.1.3l1.2 2.1c.2.4.1.9-.3 1.1-.4.2-.9.1-1.1-.3l-1.2-2.1z"><animate attributeType="CSS" attributeName="opacity" from="1" to=".15" dur="0.96s" repeatCount="indefinite" begin="0.78s"/></path><path d="M8.8 15.2c0 .4-.4.8-.8.8s-.8-.4-.8-.8v-2.4c0-.4.4-.8.8-.8s.8.4.8.8v2.4z"><animate attributeType="CSS" attributeName="opacity" from="1" to=".15" dur="0.96s" repeatCount="indefinite" begin="0.86s"/></path></g><path fill-rule="evenodd" clip-rule="evenodd" fill="#0095FF" d="M8 8.7c-1.5 0-2.6-.5-2.6-.5S5.9 10 8 10s2.6-1.9 2.6-1.9-1.1.6-2.6.6zM9.3 6c.4 0 .7.3.7.7s-.3.7-.7.7-.7-.3-.7-.7.3-.7.7-.7zM6.7 6c.4 0 .7.3.7.7s-.3.7-.7.7-.7-.3-.7-.7.3-.7.7-.7z"/></svg>
--- a/browser/components/loop/content/shared/js/activeRoomStore.js
+++ b/browser/components/loop/content/shared/js/activeRoomStore.js
@@ -46,16 +46,17 @@ loop.store.ActiveRoomStore = (function()
 
   var ROOM_STATES = loop.store.ROOM_STATES;
 
   var ROOM_INFO_FAILURES = loop.shared.utils.ROOM_INFO_FAILURES;
 
   var OPTIONAL_ROOMINFO_FIELDS = {
     urls: "roomContextUrls",
     description: "roomDescription",
+    participants: "participants",
     roomInfoFailure: "roomInfoFailure",
     roomName: "roomName",
     roomState: "roomState"
   };
 
   /**
    * Active room store.
    *
@@ -291,16 +292,17 @@ loop.store.ActiveRoomStore = (function()
             this.dispatchAction(new sharedActions.RoomFailure({
               error: error,
               failedJoinRequest: false
             }));
             return;
           }
 
           this.dispatchAction(new sharedActions.SetupRoomInfo({
+            participants: roomData.participants,
             roomToken: actionData.roomToken,
             roomContextUrls: roomData.decryptedContext.urls,
             roomDescription: roomData.decryptedContext.description,
             roomName: roomData.decryptedContext.roomName,
             roomUrl: roomData.roomUrl,
             socialShareProviders: this._mozLoop.getSocialShareProviders()
           }));
 
@@ -413,16 +415,17 @@ loop.store.ActiveRoomStore = (function()
      */
     setupRoomInfo: function(actionData) {
       if (this._onUpdateListener) {
         console.error("Room info already set up!");
         return;
       }
 
       this.setStoreState({
+        participants: actionData.participants,
         roomContextUrls: actionData.roomContextUrls,
         roomDescription: actionData.roomDescription,
         roomName: actionData.roomName,
         roomState: ROOM_STATES.READY,
         roomToken: actionData.roomToken,
         roomUrl: actionData.roomUrl,
         socialShareProviders: actionData.socialShareProviders
       });
@@ -444,17 +447,17 @@ loop.store.ActiveRoomStore = (function()
      */
     updateRoomInfo: function(actionData) {
       var newState = {
         roomUrl: actionData.roomUrl
       };
       // Iterate over the optional fields that _may_ be present on the actionData
       // object.
       Object.keys(OPTIONAL_ROOMINFO_FIELDS).forEach(function(field) {
-        if (actionData[field]) {
+        if (actionData[field] !== undefined) {
           newState[OPTIONAL_ROOMINFO_FIELDS[field]] = actionData[field];
         }
       });
       this.setStoreState(newState);
     },
 
     /**
      * Handles the updateSocialShareInfo action. Updates the room data with new
@@ -473,16 +476,17 @@ loop.store.ActiveRoomStore = (function()
      *
      * @param {String} eventName The name of the event
      * @param {Object} roomData  The new roomData.
      */
     _handleRoomUpdate: function(eventName, roomData) {
       this.dispatchAction(new sharedActions.UpdateRoomInfo({
         urls: roomData.decryptedContext.urls,
         description: roomData.decryptedContext.description,
+        participants: roomData.participants,
         roomName: roomData.decryptedContext.roomName,
         roomUrl: roomData.roomUrl
       }));
     },
 
     /**
      * Handles the deletion of a room, notified by the mozLoop rooms API.
      *
@@ -787,17 +791,26 @@ loop.store.ActiveRoomStore = (function()
     },
 
     /**
      * Handles a remote peer disconnecting from the session. As we currently only
      * support 2 participants, we declare the room as SESSION_CONNECTED as soon as
      * one participantleaves.
      */
     remotePeerDisconnected: function() {
+      // Update the participants to just the owner.
+      var participants = this.getStoreState("participants");
+      if (participants) {
+        participants = participants.filter(function(participant) {
+          return participant.owner;
+        });
+      }
+
       this.setStoreState({
+        participants: participants,
         roomState: ROOM_STATES.SESSION_CONNECTED,
         remoteSrcVideoObject: null
       });
     },
 
     /**
      * Handles an SDK status update, forwarding it to the server.
      *
--- a/browser/components/loop/jar.mn
+++ b/browser/components/loop/jar.mn
@@ -89,16 +89,17 @@ browser.jar:
   content/browser/loop/shared/img/02@2x.png                     (content/shared/img/02@2x.png)
   content/browser/loop/shared/img/telefonica.png                (content/shared/img/telefonica.png)
   content/browser/loop/shared/img/hello_logo.svg                (content/shared/img/hello_logo.svg)
   content/browser/loop/shared/img/telefonica@2x.png             (content/shared/img/telefonica@2x.png)
   content/browser/loop/shared/img/ellipsis-v.svg                (content/shared/img/ellipsis-v.svg)
   content/browser/loop/shared/img/empty_contacts.svg            (content/shared/img/empty_contacts.svg)
   content/browser/loop/shared/img/empty_conversations.svg       (content/shared/img/empty_conversations.svg)
   content/browser/loop/shared/img/empty_search.svg              (content/shared/img/empty_search.svg)
+  content/browser/loop/shared/img/animated-spinner.svg          (content/shared/img/animated-spinner.svg)
   content/browser/loop/shared/img/avatars.svg                   (content/shared/img/avatars.svg)
   content/browser/loop/shared/img/firefox-avatar.svg            (content/shared/img/firefox-avatar.svg)
 
   # Shared scripts
   content/browser/loop/shared/js/actions.js             (content/shared/js/actions.js)
   content/browser/loop/shared/js/conversationStore.js   (content/shared/js/conversationStore.js)
   content/browser/loop/shared/js/store.js               (content/shared/js/store.js)
   content/browser/loop/shared/js/activeRoomStore.js     (content/shared/js/activeRoomStore.js)
--- a/browser/components/loop/test/desktop-local/panel_test.js
+++ b/browser/components/loop/test/desktop-local/panel_test.js
@@ -845,16 +845,23 @@ describe("loop.panel", function() {
     });
 
     it("should call mozL10n.get for room empty strings", function() {
       var view = createTestComponent();
 
       sinon.assert.calledWithExactly(document.mozL10n.get,
                                      "no_conversations_message_heading");
     });
+
+    it("should display a loading animation when rooms are pending", function() {
+      var view = createTestComponent();
+      roomStore.setStoreState({pendingInitialRetrieval: true});
+
+      expect(view.getDOMNode().querySelectorAll(".room-list-loading").length).to.eql(1);
+    });
   });
 
   describe("loop.panel.NewRoomView", function() {
     var roomStore, dispatcher, fakeEmail, dispatch;
 
     beforeEach(function() {
       fakeEmail = "fakeEmail@example.com";
       dispatcher = new loop.Dispatcher();
--- a/browser/components/loop/test/desktop-local/roomStore_test.js
+++ b/browser/components/loop/test/desktop-local/roomStore_test.js
@@ -64,17 +64,17 @@ describe("loop.store.RoomStore", functio
   });
 
   describe("constructed", function() {
     var fakeMozLoop, fakeNotifications, store;
 
     var defaultStoreState = {
       error: undefined,
       pendingCreation: false,
-      pendingInitialRetrieval: false,
+      pendingInitialRetrieval: true,
       rooms: [],
       activeRoom: {}
     };
 
     beforeEach(function() {
       fakeMozLoop = {
         SHARING_ROOM_URL: {
           COPY_FROM_PANEL: 0,
--- a/browser/components/loop/test/desktop-local/roomViews_test.js
+++ b/browser/components/loop/test/desktop-local/roomViews_test.js
@@ -465,28 +465,73 @@ describe("loop.roomViews", function () {
         });
 
       it("should render the DesktopRoomInvitationView if roomState is `JOINED`",
         function() {
           activeRoomStore.setStoreState({roomState: ROOM_STATES.JOINED});
 
           view = mountTestComponent();
 
+          expect(TestUtils.findRenderedComponentWithType(view,
+            loop.roomViews.DesktopRoomInvitationView).getDOMNode()).to.not.eql(null);
+        });
+
+      it("should render the DesktopRoomInvitationView if roomState is `JOINED` with just owner",
+        function() {
+          activeRoomStore.setStoreState({
+            participants: [{owner: true}],
+            roomState: ROOM_STATES.JOINED
+          });
+
+          view = mountTestComponent();
+
+          expect(TestUtils.findRenderedComponentWithType(view,
+            loop.roomViews.DesktopRoomInvitationView).getDOMNode()).to.not.eql(null);
+        });
+
+      it("should render the DesktopRoomConversationView if roomState is `JOINED` with remote participant",
+        function() {
+          activeRoomStore.setStoreState({
+            participants: [{}],
+            roomState: ROOM_STATES.JOINED
+          });
+
+          view = mountTestComponent();
+
           TestUtils.findRenderedComponentWithType(view,
-            loop.roomViews.DesktopRoomInvitationView);
+            loop.roomViews.DesktopRoomConversationView);
+          expect(TestUtils.findRenderedComponentWithType(view,
+            loop.roomViews.DesktopRoomInvitationView).getDOMNode()).to.eql(null);
+        });
+
+      it("should render the DesktopRoomConversationView if roomState is `JOINED` with participants",
+        function() {
+          activeRoomStore.setStoreState({
+            participants: [{owner: true}, {}],
+            roomState: ROOM_STATES.JOINED
+          });
+
+          view = mountTestComponent();
+
+          TestUtils.findRenderedComponentWithType(view,
+            loop.roomViews.DesktopRoomConversationView);
+          expect(TestUtils.findRenderedComponentWithType(view,
+            loop.roomViews.DesktopRoomInvitationView).getDOMNode()).to.eql(null);
         });
 
       it("should render the DesktopRoomConversationView if roomState is `HAS_PARTICIPANTS`",
         function() {
           activeRoomStore.setStoreState({roomState: ROOM_STATES.HAS_PARTICIPANTS});
 
           view = mountTestComponent();
 
           TestUtils.findRenderedComponentWithType(view,
             loop.roomViews.DesktopRoomConversationView);
+          expect(TestUtils.findRenderedComponentWithType(view,
+            loop.roomViews.DesktopRoomInvitationView).getDOMNode()).to.eql(null);
         });
 
       it("should call onCallTerminated when the call ended", function() {
         activeRoomStore.setStoreState({
           roomState: ROOM_STATES.ENDED,
           used: true
         });
 
--- a/browser/components/loop/test/shared/activeRoomStore_test.js
+++ b/browser/components/loop/test/shared/activeRoomStore_test.js
@@ -305,16 +305,17 @@ describe("loop.store.ActiveRoomStore", f
     var fakeToken, fakeRoomData;
 
     beforeEach(function() {
       fakeToken = "337-ff-54";
       fakeRoomData = {
         decryptedContext: {
           roomName: "Monkeys"
         },
+        participants: [],
         roomUrl: "http://invalid"
       };
 
       store = new loop.store.ActiveRoomStore(dispatcher, {
         mozLoop: fakeMozLoop,
         sdkDriver: {}
       });
       fakeMozLoop.rooms.get.withArgs(fakeToken).callsArgOnWith(
@@ -345,16 +346,17 @@ describe("loop.store.ActiveRoomStore", f
           roomToken: fakeToken
         }));
 
         sinon.assert.calledTwice(dispatcher.dispatch);
         sinon.assert.calledWithExactly(dispatcher.dispatch,
           new sharedActions.SetupRoomInfo({
             roomContextUrls: undefined,
             roomDescription: undefined,
+            participants: [],
             roomToken: fakeToken,
             roomName: fakeRoomData.decryptedContext.roomName,
             roomUrl: fakeRoomData.roomUrl,
             socialShareProviders: []
           }));
       });
 
     it("should dispatch a JoinRoom action if the get is successful",
@@ -1272,16 +1274,40 @@ describe("loop.store.ActiveRoomStore", f
       store.setStoreState({
         remoteSrcVideoObject: { name: "fakeVideoElement" }
       });
 
       store.remotePeerDisconnected();
 
       expect(store.getStoreState().remoteSrcVideoObject).eql(null);
     });
+
+    it("should remove non-owner participants", function() {
+      store.setStoreState({
+        participants: [{owner: true}, {}]
+      });
+
+      store.remotePeerDisconnected();
+
+      var participants = store.getStoreState().participants;
+      expect(participants).to.have.length.of(1);
+      expect(participants[0].owner).eql(true);
+    });
+
+    it("should keep the owner participant", function() {
+      store.setStoreState({
+        participants: [{owner: true}]
+      });
+
+      store.remotePeerDisconnected();
+
+      var participants = store.getStoreState().participants;
+      expect(participants).to.have.length.of(1);
+      expect(participants[0].owner).eql(true);
+    });
   });
 
   describe("#connectionStatus", function() {
     it("should call rooms.sendConnectionStatus on mozLoop", function() {
       store.setStoreState({
         roomToken: "fakeToken",
         sessionToken: "9876543210"
       });
@@ -1513,16 +1539,17 @@ describe("loop.store.ActiveRoomStore", f
         };
 
         fakeMozLoop.rooms.on.callArgWith(1, "update", fakeRoomData);
 
         sinon.assert.calledOnce(dispatcher.dispatch);
         sinon.assert.calledWithExactly(dispatcher.dispatch,
           new sharedActions.UpdateRoomInfo({
             description: "fakeDescription",
+            participants: undefined,
             roomName: fakeRoomData.decryptedContext.roomName,
             roomUrl: fakeRoomData.roomUrl,
             urls: {
               fake: "url"
             }
           }));
       });
 
--- a/browser/components/preferences/in-content/sync.js
+++ b/browser/components/preferences/in-content/sync.js
@@ -181,23 +181,37 @@ let gSyncPane = {
     if (save) {
       Weave.Service.clientsEngine.localName = textbox.value;
     }
     else {
       textbox.value = Weave.Service.clientsEngine.localName;
     }
   },
 
+  _closeSyncStatusMessageBox: function() {
+    document.getElementById("syncStatusMessage").removeAttribute("message-type");
+    document.getElementById("syncStatusMessageTitle").textContent = "";
+    document.getElementById("syncStatusMessageDescription").textContent = "";
+    let learnMoreLink = document.getElementById("learnMoreLink");
+    if (learnMoreLink) {
+      learnMoreLink.parentNode.removeChild(learnMoreLink);
+    }
+    document.getElementById("sync-migration-buttons-deck").hidden = true;
+  },
+
   _setupEventListeners: function() {
     function setEventListener(aId, aEventType, aCallback)
     {
       document.getElementById(aId)
               .addEventListener(aEventType, aCallback.bind(gSyncPane));
     }
 
+    setEventListener("syncStatusMessageClose", "command", function () {
+      gSyncPane._closeSyncStatusMessageBox();
+    });
     setEventListener("noAccountSetup", "click", function (aEvent) {
       aEvent.stopPropagation();
       gSyncPane.openSetup(null);
     });
     setEventListener("noAccountPair", "click", function (aEvent) {
       aEvent.stopPropagation();
       gSyncPane.openSetup('pair');
     });
@@ -426,39 +440,41 @@ let gSyncPane = {
       this.page = PAGE_HAS_ACCOUNT;
       document.getElementById("accountName").textContent = Weave.Service.identity.account;
       document.getElementById("syncComputerName").value = Weave.Service.clientsEngine.localName;
       document.getElementById("tosPP-normal").hidden = this._usingCustomServer;
     }
   },
 
   updateMigrationState: function(subject, state) {
+    this._closeSyncStatusMessageBox();
     let selIndex;
+    let sb = this._accountsStringBundle;
     switch (state) {
       case fxaMigrator.STATE_USER_FXA: {
-        let sb = this._accountsStringBundle;
         // There are 2 cases here - no email address means it is an offer on
         // the first device (so the user is prompted to create an account).
         // If there is an email address it is the "join the party" flow, so the
         // user is prompted to sign in with the address they previously used.
         let email = subject ? subject.QueryInterface(Components.interfaces.nsISupportsString).data : null;
-        let elt = document.getElementById("sync-migrate-upgrade-description");
+        let elt = document.getElementById("syncStatusMessageDescription");
         elt.textContent = email ?
                           sb.formatStringFromName("signInAfterUpgradeOnOtherDevice.description",
                                                   [email], 1) :
                           sb.GetStringFromName("needUserLong");
 
         // The "Learn more" link.
         if (!email) {
           let learnMoreLink = document.createElement("label");
+          learnMoreLink.id = "learnMoreLink";
           learnMoreLink.className = "text-link";
           let { text, href } = fxaMigrator.learnMoreLink;
           learnMoreLink.setAttribute("value", text);
           learnMoreLink.href = href;
-          elt.appendChild(learnMoreLink);
+          elt.parentNode.insertBefore(learnMoreLink, elt.nextSibling);
         }
 
         // The "upgrade" button.
         let button = document.getElementById("sync-migrate-upgrade");
         button.setAttribute("label",
                             sb.GetStringFromName(email
                                                  ? "signInAfterUpgradeOnOtherDevice.label"
                                                  : "upgradeToFxA.label"));
@@ -476,17 +492,17 @@ let gSyncPane = {
         }
         selIndex = 0;
         break;
       }
       case fxaMigrator.STATE_USER_FXA_VERIFIED: {
         let sb = this._accountsStringBundle;
         let email = subject.QueryInterface(Components.interfaces.nsISupportsString).data;
         let label = sb.formatStringFromName("needVerifiedUserLong", [email], 1);
-        let elt = document.getElementById("sync-migrate-verify-label");
+        let elt = document.getElementById("syncStatusMessageDescription");
         elt.setAttribute("value", label);
         // The "resend" button.
         let button = document.getElementById("sync-migrate-resend");
         button.setAttribute("label", sb.GetStringFromName("resendVerificationEmail.label"));
         button.setAttribute("accesskey", sb.GetStringFromName("resendVerificationEmail.accessKey"));
         // The "forget" button.
         button = document.getElementById("sync-migrate-forget");
         button.setAttribute("label", sb.GetStringFromName("forgetMigration.label"));
@@ -496,18 +512,18 @@ let gSyncPane = {
       }
       default:
         if (state) { // |null| is expected, but everything else is not.
           Cu.reportError("updateMigrationState has unknown state: " + state);
         }
         document.getElementById("sync-migration").hidden = true;
         return;
     }
-    document.getElementById("sync-migration").hidden = false;
-    document.getElementById("sync-migration-deck").selectedIndex = selIndex;
+    document.getElementById("sync-migration-buttons-deck").selectedIndex = selIndex;
+    document.getElementById("syncStatusMessage").setAttribute("message-type", "migration");
   },
 
   // Called whenever one of the sync engine preferences is changed.
   onPreferenceChanged: function() {
     let prefElts = document.querySelectorAll("#syncEnginePrefs > preference");
     let syncEnabled = false;
     for (let elt of prefElts) {
       if (elt.name.startsWith("services.sync.") && elt.value) {
@@ -673,33 +689,49 @@ let gSyncPane = {
       .then(url => {
         this.openContentInBrowser(url, {
           replaceQueryString: true
         });
       });
   },
 
   verifyFirefoxAccount: function() {
-    fxAccounts.resendVerificationEmail().then(() => {
-      fxAccounts.getSignedInUser().then(data => {
-        let sb = this._accountsStringBundle;
-        let title = sb.GetStringFromName("verificationSentTitle");
-        let heading = sb.formatStringFromName("verificationSentHeading",
-                                              [data.email], 1);
-        let description = sb.GetStringFromName("verificationSentDescription");
+    this._closeSyncStatusMessageBox();
+    let changesyncStatusMessage = (data) => {
+      let isError = !data;
+      let syncStatusMessage = document.getElementById("syncStatusMessage");
+      let syncStatusMessageTitle = document.getElementById("syncStatusMessageTitle");
+      let syncStatusMessageDescription = document.getElementById("syncStatusMessageDescription");
+      let maybeNot = isError ? "Not" : "";
+      let sb = this._accountsStringBundle;
+      let title = sb.GetStringFromName("verification" + maybeNot + "SentTitle");
+      let email = !isError && data ? data.email : "";
+      let description = sb.formatStringFromName("verification" + maybeNot + "SentFull", [email], 1)
 
-        let factory = Cc["@mozilla.org/prompter;1"]
-                        .getService(Ci.nsIPromptFactory);
-        let prompt = factory.getPrompt(window, Ci.nsIPrompt);
-        let bag = prompt.QueryInterface(Ci.nsIWritablePropertyBag2);
-        bag.setPropertyAsBool("allowTabModal", true);
+      syncStatusMessageTitle.textContent = title;
+      syncStatusMessageDescription.textContent = description;
+      let messageType = isError ? "verify-error" : "verify-success";
+      syncStatusMessage.setAttribute("message-type", messageType);
+    }
+
+    let onError = () => {
+      changesyncStatusMessage();
+    };
 
-        prompt.alert(title, heading + "\n\n" + description);
-      });
-    });
+    let onSuccess = data => {
+      if (data) {
+        changesyncStatusMessage(data);
+      } else {
+        onError();
+      }
+    };
+
+    fxAccounts.resendVerificationEmail()
+      .then(fxAccounts.getSignedInUser, onError)
+      .then(onSuccess, onError);
   },
 
   openOldSyncSupportPage: function() {
     let url = Services.urlFormatter.formatURLPref("app.support.baseURL") + "old-sync";
     this.openContentInBrowser(url);
   },
 
   unlinkFirefoxAccount: function(confirm) {
--- a/browser/components/preferences/in-content/sync.xul
+++ b/browser/components/preferences/in-content/sync.xul
@@ -33,41 +33,39 @@
 
 <hbox id="header-sync"
       class="header"
       hidden="true"
       data-category="paneSync">
   <label class="header-name">&paneSync.title;</label>
 </hbox>
 
-<hbox id="sync-migration-container"
-       data-category="paneSync"
-       hidden="true">
-
-  <vbox id="sync-migration" flex="1" hidden="true">
-
-    <deck id="sync-migration-deck">
-      <!-- When we are in the "need FxA user" state -->
-      <hbox align="center">
-        <description id="sync-migrate-upgrade-description" flex="1"/>
-        <spacer flex="1"/>
-        <button id="sync-migrate-unlink"/>
-        <button id="sync-migrate-upgrade"/>
-      </hbox>
-
-      <!-- When we are in the "need the user to be verified" state -->
-      <hbox align="center">
-        <label id="sync-migrate-verify-label"/>
-        <spacer flex="1"/>
-        <button id="sync-migrate-forget"/>
-        <button id="sync-migrate-resend"/>
-      </hbox>
-    </deck>
-  </vbox>
-</hbox>
+<vbox id="syncStatusMessage-container" data-category="paneSync" hidden="true">
+  <hbox id="syncStatusMessage">
+    <vbox id="syncStatusMessageWrapper">
+      <label id="syncStatusMessageTitle"></label>
+      <description id="syncStatusMessageDescription"></description>
+      <deck id="sync-migration-buttons-deck">
+        <!-- When we are in the "need FxA user" state -->
+        <hbox>
+          <button id="sync-migrate-unlink"/>
+          <button id="sync-migrate-upgrade"/>
+        </hbox>
+        <!-- When we are in the "need the user to be verified" state -->
+        <hbox>
+          <button id="sync-migrate-forget"/>
+          <button id="sync-migrate-resend"/>
+        </hbox>
+      </deck>
+    </vbox>
+    <vbox>
+      <button id="syncStatusMessageClose" class="close-icon"/>
+    </vbox>
+  </hbox>
+</vbox>
 
 <deck id="weavePrefsDeck" data-category="paneSync" hidden="true">
   <!-- These panels are for the "legacy" sync provider -->
   <vbox id="noAccount" align="center">
     <spacer flex="1"/>
     <description id="syncDesc">
       &weaveDesc.label;
     </description>
--- a/browser/components/preferences/in-content/tests/browser_bug731866.js
+++ b/browser/components/preferences/in-content/tests/browser_bug731866.js
@@ -14,20 +14,21 @@ let gElements;
 function checkElements(expectedPane) {
   for (let element of gElements) {
     // preferences elements fail is_element_visible checks because they are never visible.
     // special-case the drmGroup item because its visibility depends on pref + OS version
     if (element.nodeName == "preferences" || element.id === "drmGroup") {
       continue;
     }
     let attributeValue = element.getAttribute("data-category");
+    let suffix = " (id=" + element.id + ")";
     if (attributeValue == "pane" + expectedPane) {
-      is_element_visible(element, expectedPane + " elements should be visible");
+      is_element_visible(element, expectedPane + " elements should be visible" + suffix);
     } else {
-      is_element_hidden(element, "Elements not in " + expectedPane + " should be hidden");
+      is_element_hidden(element, "Elements not in " + expectedPane + " should be hidden" + suffix);
     }
   }
 }
 
 function runTest(win) {
   is(gBrowser.currentURI.spec, "about:preferences", "about:preferences loaded");
 
   let tab = win.document;
--- a/browser/devtools/animationinspector/animation-controller.js
+++ b/browser/devtools/animationinspector/animation-controller.js
@@ -18,16 +18,17 @@ Cu.import("resource:///modules/devtools/
 loader.lazyRequireGetter(this, "promise");
 loader.lazyRequireGetter(this, "EventEmitter",
                                "devtools/toolkit/event-emitter");
 loader.lazyRequireGetter(this, "AnimationsFront",
                                "devtools/server/actors/animation", true);
 
 const STRINGS_URI = "chrome://browser/locale/devtools/animationinspector.properties";
 const L10N = new ViewHelpers.L10N(STRINGS_URI);
+const V3_UI_PREF = "devtools.inspector.animationInspectorV3";
 
 // Global toolbox/inspector, set when startup is called.
 let gToolbox, gInspector;
 
 /**
  * Startup the animationinspector controller and view, called by the sidebar
  * widget when loading/unloading the iframe into the tab.
  */
@@ -71,37 +72,38 @@ function destroy() {
 
 /**
  * Get all the server-side capabilities (traits) so the UI knows whether or not
  * features should be enabled/disabled.
  * @param {Target} target The current toolbox target.
  * @return {Object} An object with boolean properties.
  */
 let getServerTraits = Task.async(function*(target) {
-  let config = [{
-    name: "hasToggleAll", actor: "animations", method: "toggleAll"
-  }, {
-    name: "hasSetCurrentTime", actor: "animationplayer", method: "setCurrentTime"
-  }, {
-    name: "hasMutationEvents", actor: "animations", method: "stopAnimationPlayerUpdates"
-  }, {
-    name: "hasSetPlaybackRate", actor: "animationplayer", method: "setPlaybackRate"
-  }, {
-    name: "hasTargetNode", actor: "domwalker", method: "getNodeFromActor"
-  }, {
-    name: "hasSetCurrentTimes", actor: "animations", method: "setCurrentTimes"
-  }];
+  let config = [
+    { name: "hasToggleAll", actor: "animations",
+      method: "toggleAll" },
+    { name: "hasSetCurrentTime", actor: "animationplayer",
+      method: "setCurrentTime" },
+    { name: "hasMutationEvents", actor: "animations",
+     method: "stopAnimationPlayerUpdates" },
+    { name: "hasSetPlaybackRate", actor: "animationplayer",
+      method: "setPlaybackRate" },
+    { name: "hasTargetNode", actor: "domwalker",
+      method: "getNodeFromActor" },
+    { name: "hasSetCurrentTimes", actor: "animations",
+      method: "setCurrentTimes" }
+  ];
 
   let traits = {};
   for (let {name, actor, method} of config) {
     traits[name] = yield target.actorHasMethod(actor, method);
   }
 
   // Special pref-based UI trait.
-  traits.isNewUI = Services.prefs.getBoolPref("devtools.inspector.animationInspectorV3");
+  traits.isNewUI = Services.prefs.getBoolPref(V3_UI_PREF);
 
   return traits;
 });
 
 /**
  * The animationinspector controller's job is to retrieve AnimationPlayerFronts
  * from the server. It is also responsible for keeping the list of players up to
  * date when the node selection changes in the inspector, as well as making sure
@@ -109,29 +111,31 @@ let getServerTraits = Task.async(functio
  *
  * AnimationPlayerFronts are available in AnimationsController.animationPlayers.
  *
  * Note also that all AnimationPlayerFronts handled by the controller are set to
  * auto-refresh (except when the sidebar panel is not visible).
  *
  * Usage example:
  *
- * AnimationsController.on(AnimationsController.PLAYERS_UPDATED_EVENT, onPlayers);
+ * AnimationsController.on(AnimationsController.PLAYERS_UPDATED_EVENT,
+ *                         onPlayers);
  * function onPlayers() {
  *   for (let player of AnimationsController.animationPlayers) {
  *     // do something with player
  *   }
  * }
  */
 let AnimationsController = {
   PLAYERS_UPDATED_EVENT: "players-updated",
 
   initialize: Task.async(function*() {
     if (this.initialized) {
-      return this.initialized.promise;
+      yield this.initialized.promise;
+      return;
     }
     this.initialized = promise.defer();
 
     this.onPanelVisibilityChange = this.onPanelVisibilityChange.bind(this);
     this.onNewNodeFront = this.onNewNodeFront.bind(this);
     this.onAnimationMutations = this.onAnimationMutations.bind(this);
 
     let target = gToolbox.target;
@@ -152,17 +156,18 @@ let AnimationsController = {
   }),
 
   destroy: Task.async(function*() {
     if (!this.initialized) {
       return;
     }
 
     if (this.destroyed) {
-      return this.destroyed.promise;
+      yield this.destroyed.promise;
+      return;
     }
     this.destroyed = promise.defer();
 
     this.stopListeners();
     yield this.destroyAnimationPlayers();
     this.nodeFront = null;
 
     if (this.animationsFront) {
@@ -267,17 +272,18 @@ let AnimationsController = {
   // retrieved when refreshAnimationPlayers is called, stored in the
   // animationPlayers array, and destroyed when refreshAnimationPlayers is
   // called again.
   animationPlayers: [],
 
   refreshAnimationPlayers: Task.async(function*(nodeFront) {
     yield this.destroyAnimationPlayers();
 
-    this.animationPlayers = yield this.animationsFront.getAnimationPlayersForNode(nodeFront);
+    this.animationPlayers = yield this.animationsFront
+                                      .getAnimationPlayersForNode(nodeFront);
     this.startAllAutoRefresh();
 
     // Start listening for animation mutations only after the first method call
     // otherwise events won't be sent.
     if (!this.isListeningToMutations && this.traits.hasMutationEvents) {
       this.animationsFront.on("mutations", this.onAnimationMutations);
       this.isListeningToMutations = true;
     }
@@ -303,16 +309,35 @@ let AnimationsController = {
         this.animationPlayers.splice(index, 1);
       }
     }
 
     // Let the UI know the list has been updated.
     this.emit(this.PLAYERS_UPDATED_EVENT, this.animationPlayers);
   }),
 
+  /**
+   * Get the latest known current time of document.timeline.
+   * This value is sent along with all AnimationPlayerActors' states, but it
+   * isn't updated after that, so this function loops over all know animations
+   * to find the highest value.
+   * @return {Number|Boolean} False is returned if this server version doesn't
+   * provide document's current time.
+   */
+  get documentCurrentTime() {
+    let time = 0;
+    for (let {state} of this.animationPlayers) {
+      if (!state.documentCurrentTime) {
+        return false;
+      }
+      time = Math.max(time, state.documentCurrentTime);
+    }
+    return time;
+  },
+
   startAllAutoRefresh: function() {
     if (this.traits.isNewUI) {
       return;
     }
 
     for (let front of this.animationPlayers) {
       front.startAutoRefresh();
     }
--- a/browser/devtools/animationinspector/animation-panel.js
+++ b/browser/devtools/animationinspector/animation-panel.js
@@ -25,17 +25,18 @@ let AnimationsPanel = {
 
   initialize: Task.async(function*() {
     if (AnimationsController.destroyed) {
       console.warn("Could not initialize the animation-panel, controller " +
                    "was destroyed");
       return;
     }
     if (this.initialized) {
-      return this.initialized.promise;
+      yield this.initialized.promise;
+      return;
     }
     this.initialized = promise.defer();
 
     this.playersEl = document.querySelector("#players");
     this.errorMessageEl = document.querySelector("#error-message");
     this.pickerButtonEl = document.querySelector("#element-picker");
     this.toggleAllButtonEl = document.querySelector("#toggle-all");
 
@@ -69,17 +70,18 @@ let AnimationsPanel = {
   }),
 
   destroy: Task.async(function*() {
     if (!this.initialized) {
       return;
     }
 
     if (this.destroyed) {
-      return this.destroyed.promise;
+      yield this.destroyed.promise;
+      return;
     }
     this.destroyed = promise.defer();
 
     this.stopListeners();
 
     if (this.animationsTimelineComponent) {
       this.animationsTimelineComponent.destroy();
       this.animationsTimelineComponent = null;
@@ -152,43 +154,46 @@ let AnimationsPanel = {
       // the current players get their states updated, so toggle locally too, to
       // avoid the timelines from jumping back and forth.
       if (this.playerWidgets) {
         let currentWidgetStateChange = [];
         for (let widget of this.playerWidgets) {
           currentWidgetStateChange.push(btnClass.contains("paused")
             ? widget.play() : widget.pause());
         }
-        yield promise.all(currentWidgetStateChange).catch(e => console.error(e));
+        yield promise.all(currentWidgetStateChange)
+                     .catch(error => console.error(error));
       }
     }
 
     btnClass.toggle("paused");
     yield AnimationsController.toggleAll();
   }),
 
   onTabNavigated: function() {
     this.toggleAllButtonEl.classList.remove("paused");
   },
 
   onTimelineTimeChanged: function(e, time) {
-    AnimationsController.setCurrentTimeAll(time, true).catch(e => console.error(e));
+    AnimationsController.setCurrentTimeAll(time, true)
+                        .catch(error => console.error(error));
   },
 
   refreshAnimations: Task.async(function*() {
     let done = gInspector.updating("animationspanel");
 
     // Empty the whole panel first.
     this.hideErrorMessage();
     yield this.destroyPlayerWidgets();
 
     // Re-render the timeline component.
     if (this.animationsTimelineComponent) {
       this.animationsTimelineComponent.render(
-        AnimationsController.animationPlayers);
+        AnimationsController.animationPlayers,
+        AnimationsController.documentCurrentTime);
     }
 
     // If there are no players to show, show the error message instead and
     // return.
     if (!AnimationsController.animationPlayers.length) {
       this.displayErrorMessage();
       this.emit(this.UI_UPDATED_EVENT);
       done();
--- a/browser/devtools/animationinspector/components.js
+++ b/browser/devtools/animationinspector/components.js
@@ -562,17 +562,22 @@ let TimeScale = {
 
   /**
    * Add a new animation to time scale.
    * @param {Object} state A PlayerFront.state object.
    */
   addAnimation: function(state) {
     let {startTime, delay, duration, iterationCount, playbackRate} = state;
 
-    this.minStartTime = Math.min(this.minStartTime, startTime);
+    // Negative-delayed animations have their startTimes set such that we would
+    // be displaying the delay outside the time window if we didn't take it into
+    // account here.
+    let relevantDelay = delay < 0 ? delay / playbackRate : 0;
+
+    this.minStartTime = Math.min(this.minStartTime, startTime + relevantDelay);
     let length = (delay / playbackRate) +
                  ((duration / playbackRate) *
                   (!iterationCount ? 1 : iterationCount));
     this.maxEndTime = Math.max(this.maxEndTime, startTime + length);
   },
 
   /**
    * Reset the current time scale.
@@ -786,17 +791,17 @@ AnimationsTimeline.prototype = {
 
     this.scrubberEl.style.left = offset + "px";
 
     let time = TimeScale.distanceToRelativeTime(offset,
       this.timeHeaderEl.offsetWidth);
     this.emit("current-time-changed", time);
   },
 
-  render: function(animations) {
+  render: function(animations, documentCurrentTime) {
     this.unrender();
 
     this.animations = animations;
     if (!this.animations.length) {
       return;
     }
 
     // Loop first to set the time scale for all current animations.
@@ -844,22 +849,21 @@ AnimationsTimeline.prototype = {
       // Save the targetNode so it can be destroyed later.
       this.targetNodes.push(targetNode);
     }
 
     // Use the document's current time to position the scrubber (if the server
     // doesn't provide it, hide the scrubber entirely).
     // Note that because the currentTime was sent via the protocol, some time
     // may have gone by since then, and so the scrubber might be a bit late.
-    let time = this.animations[0].state.documentCurrentTime;
-    if (!time) {
+    if (!documentCurrentTime) {
       this.scrubberEl.style.display = "none";
     } else {
       this.scrubberEl.style.display = "block";
-      this.startAnimatingScrubber(time);
+      this.startAnimatingScrubber(documentCurrentTime);
     }
   },
 
   startAnimatingScrubber: function(time) {
     let x = TimeScale.startTimeToDistance(time, this.timeHeaderEl.offsetWidth);
     this.scrubberEl.style.left = x + "px";
 
     if (time < TimeScale.minStartTime ||
@@ -948,31 +952,39 @@ AnimationsTimeline.prototype = {
         // repeating linear-gradient.
         "style": `left:${x}px;
                   width:${w * (count || 1)}px;
                   background-size:${Math.max(w, 2)}px 100%;`
       }
     });
 
     // The animation name is displayed over the iterations.
+    // Note that in case of negative delay, we push the name towards the right
+    // so the delay can be shown.
     createNode({
       parent: iterations,
       attributes: {
         "class": "name",
-        "title": this.getAnimationTooltipText(state)
+        "title": this.getAnimationTooltipText(state),
+        "style": delay < 0
+                 ? "margin-left:" +
+                   TimeScale.durationToDistance(Math.abs(delay), width) + "px"
+                 : ""
       },
       textContent: state.name
     });
 
     // Delay.
     if (delay) {
-      let w = TimeScale.durationToDistance(delay / rate, width);
+      // Negative delays need to start at 0.
+      let x = TimeScale.durationToDistance((delay < 0 ? 0 : delay) / rate, width);
+      let w = TimeScale.durationToDistance(Math.abs(delay) / rate, width);
       createNode({
         parent: iterations,
         attributes: {
           "class": "delay",
-          "style": `left:-${w}px;
+          "style": `left:-${x}px;
                     width:${w}px;`
         }
       });
     }
   }
 };
--- a/browser/devtools/animationinspector/test/browser_animation_timeline_takes_rate_into_account.js
+++ b/browser/devtools/animationinspector/test/browser_animation_timeline_takes_rate_into_account.js
@@ -17,23 +17,28 @@ add_task(function*() {
   let {panel} = yield openAnimationInspectorNewUI();
   yield waitForAllAnimationTargets(panel);
 
   let timelineEl = panel.animationsTimelineComponent.rootWrapperEl;
 
   let timeBlocks = timelineEl.querySelectorAll(".time-block");
   is(timeBlocks.length, 2, "2 animations are displayed");
 
-  info("The first animation has its rate to 1, let's measure it");
+  info("The first animation has its rate set to 1, let's measure it");
 
   let el = timeBlocks[0];
-  let duration = el.querySelector(".iterations").getBoundingClientRect().width;
-  let delay = el.querySelector(".delay").getBoundingClientRect().width;
+  let duration = parseInt(el.querySelector(".iterations").style.width, 10);
+  let delay = parseInt(el.querySelector(".delay").style.width, 10);
 
   info("The second animation has its rate set to 2, so should be shorter");
 
   let el2 = timeBlocks[1];
-  let duration2 = el2.querySelector(".iterations").getBoundingClientRect().width;
-  let delay2 = el2.querySelector(".delay").getBoundingClientRect().width;
+  let duration2 = parseInt(el2.querySelector(".iterations").style.width, 10);
+  let delay2 = parseInt(el2.querySelector(".delay").style.width, 10);
 
-  is(duration, 2 * duration2, "The duration width is correct");
-  is(delay, 2 * delay2, "The delay width is correct");
+  // The width are calculated by the animation-inspector dynamically depending
+  // on the size of the panel, and therefore depends on the test machine/OS.
+  // Let's not try to be too precise here and compare numbers.
+  let durationDelta = (2 * duration2) - duration;
+  ok(durationDelta <= 1, "The duration width is correct");
+  let delayDelta = (2 * delay2) - delay;
+  ok(delayDelta <= 1, "The delay width is correct");
 });
--- a/browser/locales/en-US/chrome/browser/accounts.properties
+++ b/browser/locales/en-US/chrome/browser/accounts.properties
@@ -41,12 +41,15 @@ unlinkVerificationDescription = If you n
 unlinkVerificationConfirm = Unlink
 
 # These strings are used in a dialog we display after the user requests we resend
 # a verification email.
 verificationSentTitle = Verification Sent
 # LOCALIZATION NOTE (verificationSentHeading) - %S = Email address of user's Firefox Account
 verificationSentHeading = A verification link has been sent to %S
 verificationSentDescription = Please check your email and click the link to begin syncing.
+# LOCALIZATION NOTE (verificationSentFull) - %S = Email address of user's Firefox Account
+verificationSentFull = A verification link has been sent to %S. Please check your email and click the link to begin syncing.
 
 verificationNotSentTitle = Unable to Send Verification
 verificationNotSentHeading = We are unable to send a verification mail at this time
 verificationNotSentDescription = Please try again later.
+verificationNotSentFull = We are unable to send a verification mail at this time, please try again later.
--- a/browser/themes/osx/browser.css
+++ b/browser/themes/osx/browser.css
@@ -1609,17 +1609,16 @@ toolbarbutton[constrain-size="true"][cui
   box-shadow: @focusRingShadow@;
 }
 
 #urlbar-container {
   -moz-box-align: center;
 }
 
 #urlbar {
-  -moz-padding-end: 4px;
   border-radius: @toolbarbuttonCornerRadius@;
 }
 
 @conditionalForwardWithUrlbar@ > #urlbar {
   -moz-border-start: none;
   margin-left: 0;
 }
 
@@ -1901,16 +1900,17 @@ richlistitem[type~="action"][actiontype=
   list-style-image: url("chrome://browser/skin/reload-stop-go.png");
   padding: 0 9px;
   margin-inline-start: 2px;
   border-inline-start: 1px solid var(--urlbar-separator-color);
   border-image: linear-gradient(transparent 15%,
                                 var(--urlbar-separator-color) 15%,
                                 var(--urlbar-separator-color) 85%,
                                 transparent 85%);
+  border-image-slice: 1;
 }
 
 #urlbar-go-button {
   -moz-image-region: rect(0, 42px, 14px, 28px);
 }
 
 #urlbar-go-button:hover:active {
   -moz-image-region: rect(14px, 42px, 28px, 28px);
--- a/browser/themes/shared/devtools/animationinspector.css
+++ b/browser/themes/shared/devtools/animationinspector.css
@@ -233,17 +233,17 @@ body {
                     var(--timelime-border-color) 1px,
                     transparent 1px,
                     transparent 2px);
   background-repeat: repeat-x;
   background-position: -1px 0;
   border: 1px solid var(--timelime-border-color);
 
   /* The background color is set independently */
-  background: var(--timeline-background-color);
+  background-color: var(--timeline-background-color);
 }
 
 .animation-timeline .animation .cssanimation {
   --timelime-border-color: var(--theme-highlight-lightorange);
   --timeline-background-color: var(--theme-contrast-background);
 }
 
 .animation-timeline .animation .csstransition {
@@ -282,35 +282,26 @@ body {
   white-space: nowrap;
   line-height: 150%;
   padding: 0 2px;
 }
 
 .animation-timeline .animation .delay {
   position: absolute;
   top: 0;
+  /* Make sure the delay covers up the animation border */
+  transform: translate(-1px, -1px);
   height: 100%;
-  background-image: linear-gradient(to bottom,
-                                    transparent,
-                                    transparent 9px,
-                                    var(--timelime-border-color) 9px,
-                                    var(--timelime-border-color) 11px,
-                                    transparent 11px,
-                                    transparent);
-}
-
-.animation-timeline .animation .delay::before {
-  position: absolute;
-  content: "";
-  left: 0;
-  width: 2px;
-  height: 8px;
-  top: 50%;
-  margin-top: -4px;
-  background: var(--timelime-border-color);
+  background-image: repeating-linear-gradient(45deg,
+                                              transparent,
+                                              transparent 1px,
+                                              var(--theme-selection-color) 1px,
+                                              var(--theme-selection-color) 4px);
+  background-color: var(--timelime-border-color);
+  border: 1px solid var(--timelime-border-color);
 }
 
 /* Animation target node gutter, contains a preview of the dom node */
 
 .animation-target {
   background-color: var(--theme-toolbar-background);
   padding: 1px 4px;
   box-sizing: border-box;
--- a/browser/themes/shared/incontentprefs/preferences.inc.css
+++ b/browser/themes/shared/incontentprefs/preferences.inc.css
@@ -410,16 +410,111 @@ description > html|a {
   padding-right: 15px;
 }
 
 #noFxaGroup > vbox,
 #fxaGroup {
   -moz-box-align: start;
 }
 
+#syncStatusMessage {
+  visibility: collapse;
+  opacity: 0;
+  transition: opacity 1s linear;
+  padding: 14px 8px 14px 14px;
+  border-radius: 2px;
+}
+
+#syncStatusMessage[message-type] {
+  visibility: visible;
+  opacity: 1;
+}
+
+#syncStatusMessage[message-type="verify-success"] {
+  background-color: #74BF43;
+}
+
+#syncStatusMessage[message-type="verify-error"] {
+  background-color: #D74345;
+}
+
+#syncStatusMessage[message-type="migration"] {
+  background-color: #FF9500;
+}
+
+#sync-migration-buttons-deck {
+  visibility: collapse;
+}
+
+#learnMoreLink {
+  margin: 0;
+  color: #FBFBFB;
+  text-decoration: underline;
+}
+
+#syncStatusMessage[message-type="migration"] #sync-migration-buttons-deck {
+  visibility: visible;
+}
+
+#sync-migration-buttons-deck {
+  margin-top: 20px;
+}
+
+#sync-migration-buttons-deck button {
+  margin: 0 10px 0 0;
+  border: 0;
+  border-radius: 2px;
+}
+
+#sync-migrate-upgrade,
+#sync-migrate-resend {
+  background-color: #0095DD;
+  color: #FBFBFB;
+}
+
+#sync-migrate-upgrade:hover,
+#sync-migrate-resend:hover {
+  background-color: #008ACB;
+}
+
+#sync-migrate-upgrade:hover:active,
+#sync-migrate-resend:hover:active {
+  background-color: #006B9D;
+}
+
+#syncStatusMessageWrapper {
+  -moz-box-flex: 1;
+  padding-right: 5px;
+}
+
+#syncStatusMessageTitle, #syncStatusMessageDescription {
+  color: #FBFBFB;
+}
+
+#syncStatusMessage[message-type="migration"] #syncStatusMessageTitle {
+  display: none;
+}
+
+#syncStatusMessageTitle {
+  font-weight: bold !important;
+  font-size: 16px;
+  line-height: 157%;
+  margin: 0 0 20px;
+}
+
+#syncStatusMessageDescription {
+  font-size: 14px;
+  line-height: 158%;
+  margin: 0 !important;
+}
+
+#syncStatusMessageClose {
+  margin: 0px;
+}
+
 #fxaSyncEngines > vbox:first-child {
   margin-right: 80px;
 }
 
 #fxaSyncComputerName {
   margin-inline-start: 0px;
   -moz-box-flex: 1;
 }
--- a/browser/themes/windows/browser.css
+++ b/browser/themes/windows/browser.css
@@ -1225,20 +1225,16 @@ toolbarbutton[constrain-size="true"][cui
 .searchbar-textbox {
   -moz-appearance: none;
   margin: 0 3px;
   padding: 0;
   background-clip: padding-box;
   border: 1px solid ThreeDShadow;
 }
 
-#urlbar {
-  -moz-padding-end: 2px;
-}
-
 /* overlap the urlbar's border */
 #PopupAutoCompleteRichResult {
   margin-top: -1px;
 }
 
 @media (-moz-windows-default-theme) {
   #urlbar,
   .searchbar-textbox {
@@ -1262,17 +1258,16 @@ toolbarbutton[constrain-size="true"][cui
     }
   }
 
   @media (-moz-os-version: windows-win10) {
     #urlbar:not(:-moz-lwtheme),
     .searchbar-textbox:not(:-moz-lwtheme) {
       border-color: hsl(0,0%,90%);
       padding: 1px;
-      -moz-padding-end: 3px;
       transition-property: border-color, box-shadow;
       transition-duration: .1s;
     }
 
     #urlbar:not(:-moz-lwtheme):hover,
     .searchbar-textbox:not(:-moz-lwtheme):hover {
       border-color: hsl(0,0%,80%);
     }
new file mode 100644
--- /dev/null
+++ b/dom/bluetooth/bluedroid/BluetoothMapFolder.cpp
@@ -0,0 +1,94 @@
+/* -*- Mode: c++; c-basic-offset: 2; indent-tabs-mode: nil; tab-width: 40 -*- */
+/* vim: set ts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+#include "base/basictypes.h"
+#include "BluetoothMapFolder.h"
+
+BEGIN_BLUETOOTH_NAMESPACE
+
+BluetoothMapFolder::~BluetoothMapFolder()
+{ }
+
+BluetoothMapFolder::BluetoothMapFolder(const nsAString& aFolderName,
+                                       BluetoothMapFolder* aParent)
+  : mName(aFolderName)
+  , mParent(aParent)
+{
+}
+
+BluetoothMapFolder*
+BluetoothMapFolder::AddSubFolder(const nsAString& aFolderName)
+{
+  nsRefPtr<BluetoothMapFolder> folder = new BluetoothMapFolder(aFolderName,
+                                                               this);
+  mSubFolders.Put(nsString(aFolderName), folder);
+
+  return folder;
+}
+
+BluetoothMapFolder*
+BluetoothMapFolder::GetSubFolder(const nsAString& aFolderName)
+{
+  BluetoothMapFolder* subfolder;
+  mSubFolders.Get(aFolderName, &subfolder);
+
+  return subfolder;
+}
+
+BluetoothMapFolder*
+BluetoothMapFolder::GetParentFolder()
+{
+  return mParent;
+}
+
+int
+BluetoothMapFolder::GetSubFolderCount()
+{
+  return mSubFolders.Count();
+}
+
+void
+BluetoothMapFolder::GetFolderListingObjectString(nsAString& aString,
+                                                 uint16_t aMaxListCount,
+                                                 uint16_t aStartOffset)
+{
+  const char* folderListingPrefix =
+    "<?xml version=\"1.0\"?>\n"
+    "<!DOCTYPE folder-listing SYSTEM \" obex-folder-listing.dtd\">\n"
+    "<folder-listing version=\"1.0\">\n";
+  const char* folderListingSuffix = "</folder-listing>";
+
+  // Based on Element Specification, 9.1.1, IrObex 1.2
+  nsAutoCString folderListingObejct(folderListingPrefix);
+
+  int count = 0;
+  for (auto iter = mSubFolders.Iter(); !iter.Done(); iter.Next()) {
+    if (count < aStartOffset) {
+      continue;
+    }
+
+    if (count > aMaxListCount) {
+      break;
+    }
+
+    const nsAString& key = iter.Key();
+    folderListingObejct.Append("<folder name=\"");
+    folderListingObejct.Append(NS_ConvertUTF16toUTF8(key).get());
+    folderListingObejct.Append("\">");
+    count++;
+  }
+
+  folderListingObejct.Append(folderListingSuffix);
+  aString = NS_ConvertUTF8toUTF16(folderListingObejct);
+}
+
+void
+BluetoothMapFolder::DumpFolderInfo()
+{
+  BT_LOGR("Folder name: %s, subfolder counts: %d",
+          NS_ConvertUTF16toUTF8(mName).get(), mSubFolders.Count());
+}
+
+END_BLUETOOTH_NAMESPACE
new file mode 100644
--- /dev/null
+++ b/dom/bluetooth/bluedroid/BluetoothMapFolder.h
@@ -0,0 +1,41 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef mozilla_dom_bluetooth_bluedroid_BluetoothMapFolder_h
+#define mozilla_dom_bluetooth_bluedroid_BluetoothMapFolder_h
+
+#include "BluetoothCommon.h"
+#include "nsAutoPtr.h"
+#include "nsRefPtrHashtable.h"
+#include "nsTArray.h"
+
+BEGIN_BLUETOOTH_NAMESPACE
+
+/* This class maps MAP virtual folder structures */
+class BluetoothMapFolder
+{
+public:
+  NS_INLINE_DECL_THREADSAFE_REFCOUNTING(BluetoothMapFolder)
+
+  BluetoothMapFolder(const nsAString& aFolderName, BluetoothMapFolder* aParent);
+  // Add virtual folder to subfolders
+  BluetoothMapFolder* AddSubFolder(const nsAString& aFolderName);
+  BluetoothMapFolder* GetSubFolder(const nsAString& aFolderName);
+  BluetoothMapFolder* GetParentFolder();
+  int GetSubFolderCount();
+  // Format folder listing object string
+  void GetFolderListingObjectString(nsAString& aString, uint16_t aMaxListCount,
+                                    uint16_t aStartOffset);
+  void DumpFolderInfo();
+private:
+  ~BluetoothMapFolder();
+  nsString mName;
+  nsRefPtr<BluetoothMapFolder> mParent;
+  nsRefPtrHashtable<nsStringHashKey, BluetoothMapFolder> mSubFolders;
+};
+
+END_BLUETOOTH_NAMESPACE
+#endif //mozilla_dom_bluetooth_bluedroid_BluetoothMapFolder_h
new file mode 100644
--- /dev/null
+++ b/dom/bluetooth/bluedroid/BluetoothMapSmsManager.cpp
@@ -0,0 +1,915 @@
+/* -*- Mode: c++; c-basic-offset: 2; indent-tabs-mode: nil; tab-width: 40 -*- */
+/* vim: set ts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "base/basictypes.h"
+#include "BluetoothMapSmsManager.h"
+
+#include "BluetoothService.h"
+#include "BluetoothSocket.h"
+#include "BluetoothUuid.h"
+#include "ObexBase.h"
+
+#include "mozilla/RefPtr.h"
+#include "mozilla/Services.h"
+#include "mozilla/StaticPtr.h"
+#include "nsAutoPtr.h"
+#include "nsIObserver.h"
+#include "nsIObserverService.h"
+
+USING_BLUETOOTH_NAMESPACE
+using namespace mozilla;
+using namespace mozilla::dom;
+using namespace mozilla::ipc;
+
+namespace {
+  // UUID of Map Mas
+  static const BluetoothUuid kMapMas = {
+    {
+      0x00, 0x00, 0x11, 0x32, 0x00, 0x00, 0x10, 0x00,
+      0x80, 0x00, 0x00, 0x80, 0x5F, 0x9B, 0x34, 0xFB
+    }
+  };
+  // UUID of Map Mns
+  static const BluetoothUuid kMapMns = {
+    {
+      0x00, 0x00, 0x11, 0x33, 0x00, 0x00, 0x10, 0x00,
+      0x80, 0x00, 0x00, 0x80, 0x5F, 0x9B, 0x34, 0xFB
+    }
+  };
+  // UUID used in Map OBEX MAS target header
+  static const BluetoothUuid kMapMasObexTarget = {
+    {
+      0xBB, 0x58, 0x2B, 0x40, 0x42, 0x0C, 0x11, 0xDB,
+      0xB0, 0xDE, 0x08, 0x00, 0x20, 0x0C, 0x9A, 0x66
+    }
+  };
+
+  // UUID used in Map OBEX MNS target header
+  static const BluetoothUuid kMapMnsObexTarget = {
+    {
+      0xBB, 0x58, 0x2B, 0x41, 0x42, 0x0C, 0x11, 0xDB,
+      0xB0, 0xDE, 0x08, 0x00, 0x20, 0x0C, 0x9A, 0x66
+    }
+  };
+
+  StaticRefPtr<BluetoothMapSmsManager> sMapSmsManager;
+  static bool sInShutdown = false;
+}
+
+BEGIN_BLUETOOTH_NAMESPACE
+
+NS_IMETHODIMP
+BluetoothMapSmsManager::Observe(nsISupports* aSubject,
+                                const char* aTopic,
+                                const char16_t* aData)
+{
+  MOZ_ASSERT(sMapSmsManager);
+
+  if (!strcmp(aTopic, NS_XPCOM_SHUTDOWN_OBSERVER_ID)) {
+    HandleShutdown();
+    return NS_OK;
+  }
+
+  MOZ_ASSERT(false, "MapSmsManager got unexpected topic!");
+  return NS_ERROR_UNEXPECTED;
+}
+
+void
+BluetoothMapSmsManager::HandleShutdown()
+{
+  MOZ_ASSERT(NS_IsMainThread());
+
+  sInShutdown = true;
+  Disconnect(nullptr);
+  sMapSmsManager = nullptr;
+}
+
+BluetoothMapSmsManager::BluetoothMapSmsManager() : mMasConnected(false),
+                                                   mMnsConnected(false),
+                                                   mNtfRequired(false)
+{
+  mDeviceAddress.AssignLiteral(BLUETOOTH_ADDRESS_NONE);
+  BuildDefaultFolderStructure();
+}
+
+BluetoothMapSmsManager::~BluetoothMapSmsManager()
+{
+  nsCOMPtr<nsIObserverService> obs = services::GetObserverService();
+  if (NS_WARN_IF(!obs)) {
+    return;
+  }
+
+  NS_WARN_IF(NS_FAILED(
+    obs->RemoveObserver(this, NS_XPCOM_SHUTDOWN_OBSERVER_ID)));
+}
+
+bool
+BluetoothMapSmsManager::Init()
+{
+  nsCOMPtr<nsIObserverService> obs = services::GetObserverService();
+  if (NS_WARN_IF(!obs)) {
+    return false;
+  }
+
+  if (NS_WARN_IF(NS_FAILED(
+        obs->AddObserver(this, NS_XPCOM_SHUTDOWN_OBSERVER_ID, false)))) {
+    return false;
+  }
+
+  /**
+   * We don't start listening here as BluetoothServiceBluedroid calls Listen()
+   * immediately when BT stops.
+   *
+   * If we start listening here, the listening fails when device boots up since
+   * Listen() is called again and restarts server socket. The restart causes
+   * absence of read events when device boots up.
+   */
+
+  return true;
+}
+
+//static
+BluetoothMapSmsManager*
+BluetoothMapSmsManager::Get()
+{
+  MOZ_ASSERT(NS_IsMainThread());
+
+  // Exit early if sMapSmsManager already exists
+  if (sMapSmsManager) {
+    return sMapSmsManager;
+  }
+
+  // Do not create a new instance if we're in shutdown
+  if (NS_WARN_IF(sInShutdown)) {
+    return nullptr;
+  }
+
+  // Create a new instance, register, and return
+  BluetoothMapSmsManager *manager = new BluetoothMapSmsManager();
+  if (NS_WARN_IF(!manager->Init())) {
+    return nullptr;
+  }
+
+  sMapSmsManager = manager;
+
+  return sMapSmsManager;
+}
+
+bool
+BluetoothMapSmsManager::Listen()
+{
+  MOZ_ASSERT(NS_IsMainThread());
+
+  // Fail to listen if |mMasSocket| already exists
+  if (NS_WARN_IF(mMasSocket)) {
+    return false;
+  }
+
+  /**
+   * Restart server socket since its underlying fd becomes invalid when
+   * BT stops; otherwise no more read events would be received even if
+   * BT restarts.
+   */
+  if (mMasServerSocket) {
+    mMasServerSocket->Close();
+    mMasServerSocket = nullptr;
+  }
+
+  mMasServerSocket = new BluetoothSocket(this);
+
+  nsString sdpString;
+#if ANDROID_VERSION >= 21
+  /**
+   * The way bluedroid handles MAP SDP record is very hacky.
+   * In Lollipop version, SDP string format would be instanceId + msg type
+   * + msg name. See add_maps_sdp in btif/src/btif_sock_sdp.c
+   */
+  // MAS instance id
+  sdpString.AppendPrintf("%02x", SDP_SMS_MMS_INSTANCE_ID);
+  // Supported message type
+  sdpString.AppendPrintf("%02x", SDP_MESSAGE_TYPE_SMS_GSM |
+                                 SDP_MESSAGE_TYPE_SMS_CDMA |
+                                 SDP_MESSAGE_TYPE_MMS);
+#endif
+  /**
+   * SDP service name, we don't assign RFCOMM channel directly
+   * bluedroid automatically assign channel number randomly.
+   */
+  sdpString.AppendLiteral("SMS/MMS Message Access");
+  nsresult rv = mMasServerSocket->Listen(sdpString, kMapMas,
+                                         BluetoothSocketType::RFCOMM, -1, false,
+                                         true);
+  if (NS_FAILED(rv)) {
+    mMasServerSocket = nullptr;
+    return false;
+  }
+
+  return true;
+}
+
+void
+BluetoothMapSmsManager::MnsDataHandler(UnixSocketBuffer* aMessage)
+{
+  // Ensure valid access to data[0], i.e., opCode
+  int receivedLength = aMessage->GetSize();
+  if (receivedLength < 1) {
+    BT_LOGR("Receive empty response packet");
+    return;
+  }
+
+  const uint8_t* data = aMessage->GetData();
+  uint8_t opCode = data[0];
+  if (opCode != ObexResponseCode::Success) {
+    BT_LOGR("Unexpected OpCode: %x", opCode);
+    if (mLastCommand == ObexRequestCode::Put ||
+        mLastCommand == ObexRequestCode::Abort ||
+        mLastCommand == ObexRequestCode::PutFinal) {
+      SendMnsDisconnectRequest();
+    }
+  }
+}
+
+void
+BluetoothMapSmsManager::MasDataHandler(UnixSocketBuffer* aMessage)
+{
+  /**
+   * Ensure
+   * - valid access to data[0], i.e., opCode
+   * - received packet length smaller than max packet length
+   */
+  int receivedLength = aMessage->GetSize();
+  if (receivedLength < 1 || receivedLength > MAX_PACKET_LENGTH) {
+    ReplyError(ObexResponseCode::BadRequest);
+    return;
+  }
+
+  const uint8_t* data = aMessage->GetData();
+  uint8_t opCode = data[0];
+  ObexHeaderSet pktHeaders;
+  nsString type;
+  switch (opCode) {
+    case ObexRequestCode::Connect:
+      // Section 3.3.1 "Connect", IrOBEX 1.2
+      // [opcode:1][length:2][version:1][flags:1][MaxPktSizeWeCanReceive:2]
+      // [Headers:var]
+      if (receivedLength < 7 ||
+          !ParseHeaders(&data[7], receivedLength - 7, &pktHeaders)) {
+        ReplyError(ObexResponseCode::BadRequest);
+        return;
+      }
+
+      // "Establishing an OBEX Session"
+      // The OBEX header target shall equal to MAS obex target UUID.
+      if (!CompareHeaderTarget(pktHeaders)) {
+        ReplyError(ObexResponseCode::BadRequest);
+        return;
+      }
+
+      mRemoteMaxPacketLength = ((static_cast<int>(data[5]) << 8) | data[6]);
+
+      if (mRemoteMaxPacketLength < 255) {
+        BT_LOGR("Remote maximum packet length %d", mRemoteMaxPacketLength);
+        mRemoteMaxPacketLength = 0;
+        ReplyError(ObexResponseCode::BadRequest);
+        return;
+      }
+
+      ReplyToConnect();
+      AfterMapSmsConnected();
+      break;
+    case ObexRequestCode::Disconnect:
+    case ObexRequestCode::Abort:
+      // Section 3.3.2 "Disconnect" and Section 3.3.5 "Abort", IrOBEX 1.2
+      // The format of request packet of "Disconnect" and "Abort" are the same
+      // [opcode:1][length:2][Headers:var]
+      if (receivedLength < 3 ||
+          !ParseHeaders(&data[3], receivedLength - 3, &pktHeaders)) {
+        ReplyError(ObexResponseCode::BadRequest);
+        return;
+      }
+
+      ReplyToDisconnectOrAbort();
+      AfterMapSmsDisconnected();
+      break;
+    case ObexRequestCode::SetPath: {
+        // Section 3.3.6 "SetPath", IrOBEX 1.2
+        // [opcode:1][length:2][flags:1][contants:1][Headers:var]
+        if (receivedLength < 5 ||
+            !ParseHeaders(&data[5], receivedLength - 5, &pktHeaders)) {
+          ReplyError(ObexResponseCode::BadRequest);
+          return;
+        }
+
+        uint8_t response = SetPath(data[3], pktHeaders);
+        if (response != ObexResponseCode::Success) {
+          ReplyError(response);
+          return;
+        }
+
+        ReplyToSetPath();
+      }
+      break;
+    case ObexRequestCode::Put:
+    case ObexRequestCode::PutFinal:
+      // Section 3.3.3 "Put", IrOBEX 1.2
+      // [opcode:1][length:2][Headers:var]
+      if (receivedLength < 3 ||
+          !ParseHeaders(&data[3], receivedLength - 3, &pktHeaders)) {
+        ReplyError(ObexResponseCode::BadRequest);
+        return;
+      }
+
+      if (pktHeaders.Has(ObexHeaderId::Type)) {
+        pktHeaders.GetContentType(type);
+        BT_LOGR("Type: %s", NS_ConvertUTF16toUTF8(type).get());
+        ReplyToPut();
+
+        if (type.EqualsLiteral("x-bt/MAP-NotificationRegistration")) {
+          HandleNotificationRegistration(pktHeaders);
+        } else if (type.EqualsLiteral("x-bt/MAP-event-report")) {
+          HandleEventReport(pktHeaders);
+        } else if (type.EqualsLiteral("x-bt/messageStatus")) {
+          HandleMessageStatus(pktHeaders);
+        }
+      }
+      break;
+    case ObexRequestCode::Get:
+    case ObexRequestCode::GetFinal: {
+        // [opcode:1][length:2][Headers:var]
+        if (receivedLength < 3 ||
+          !ParseHeaders(&data[3], receivedLength - 3, &pktHeaders)) {
+          ReplyError(ObexResponseCode::BadRequest);
+          return;
+        }
+        pktHeaders.GetContentType(type);
+        if (type.EqualsLiteral("x-obex/folder-listing")) {
+          HandleSmsMmsFolderListing(pktHeaders);
+        } else if (type.EqualsLiteral("x-bt/MAP-msg-listing")) {
+          // TODO: Implement this feature in Bug 1166675
+        } else if (type.EqualsLiteral("x-bt/message"))  {
+          // TODO: Implement this feature in Bug 1166679
+        } else {
+          BT_LOGR("Unknown MAP request type: %s",
+            NS_ConvertUTF16toUTF8(type).get());
+        }
+      }
+      break;
+    default:
+      ReplyError(ObexResponseCode::NotImplemented);
+      BT_LOGR("Unrecognized ObexRequestCode %x", opCode);
+      break;
+  }
+}
+
+// Virtual function of class SocketConsumer
+void
+BluetoothMapSmsManager::ReceiveSocketData(BluetoothSocket* aSocket,
+                                          nsAutoPtr<UnixSocketBuffer>& aMessage)
+{
+  MOZ_ASSERT(NS_IsMainThread());
+
+  if (aSocket == mMnsSocket) {
+    MnsDataHandler(aMessage);
+  } else {
+    MasDataHandler(aMessage);
+  }
+}
+
+bool
+BluetoothMapSmsManager::CompareHeaderTarget(const ObexHeaderSet& aHeader)
+{
+  if (!aHeader.Has(ObexHeaderId::Target)) {
+    BT_LOGR("No ObexHeaderId::Target in header");
+    return false;
+  }
+
+  uint8_t* targetPtr;
+  int targetLength;
+  aHeader.GetTarget(&targetPtr, &targetLength);
+
+  if (targetLength != sizeof(BluetoothUuid)) {
+    BT_LOGR("Length mismatch: %d != 16", targetLength);
+    return false;
+  }
+
+  for (uint8_t i = 0; i < sizeof(BluetoothUuid); i++) {
+    if (targetPtr[i] != kMapMasObexTarget.mUuid[i]) {
+      BT_LOGR("UUID mismatch: received target[%d]=0x%x != 0x%x",
+              i, targetPtr[i], kMapMasObexTarget.mUuid[i]);
+      return false;
+    }
+  }
+
+  return true;
+}
+
+uint8_t
+BluetoothMapSmsManager::SetPath(uint8_t flags,
+                                const ObexHeaderSet& aHeader)
+{
+  // Section 5.2 "SetPath Function", MapSms 1.2
+  // flags bit 1 must be 1 and bit 2~7 be 0
+  if ((flags >> 1) != 1) {
+    BT_LOGR("Illegal flags [0x%x]: bits 1~7 must be 0x01", flags);
+    return ObexResponseCode::BadRequest;
+  }
+
+  /**
+   * Three cases:
+   * 1) Go up 1 level   - flags bit 0 is 1
+   * 2) Go back to root - flags bit 0 is 0 AND name header is empty
+   * 3) Go down 1 level - flags bit 0 is 0 AND name header is not empty,
+   *                      where name header is the name of child folder
+   */
+  if (flags & 1) {
+    // Go up 1 level
+    BluetoothMapFolder* parent = mCurrentFolder->GetParentFolder();
+    if (!parent) {
+      mCurrentFolder = parent;
+      BT_LOGR("MAS SetPath Go up 1 level");
+    }
+  } else {
+    MOZ_ASSERT(aHeader.Has(ObexHeaderId::Name));
+
+    nsString childFolderName;
+    aHeader.GetName(childFolderName);
+
+    if (childFolderName.IsEmpty()) {
+      // Go back to root
+      mCurrentFolder = mRootFolder;
+      BT_LOGR("MAS SetPath Go back to root");
+    } else {
+      // Go down 1 level
+      BluetoothMapFolder* child = mCurrentFolder->GetSubFolder(childFolderName);
+      if (!child) {
+        BT_LOGR("Illegal sub-folder name [%s]",
+                NS_ConvertUTF16toUTF8(childFolderName).get());
+        return ObexResponseCode::NotFound;
+      }
+
+      mCurrentFolder = child;
+      BT_LOGR("MAS SetPath Go down to 1 level");
+    }
+  }
+
+  mCurrentFolder->DumpFolderInfo();
+
+  return ObexResponseCode::Success;
+}
+
+void
+BluetoothMapSmsManager::AfterMapSmsConnected()
+{
+  mMasConnected = true;
+}
+
+void
+BluetoothMapSmsManager::AfterMapSmsDisconnected()
+{
+  mMasConnected = false;
+  // To ensure we close MNS connection
+  DestroyMnsObexConnection();
+}
+
+bool
+BluetoothMapSmsManager::IsConnected()
+{
+  return mMasConnected;
+}
+
+void
+BluetoothMapSmsManager::GetAddress(nsAString& aDeviceAddress)
+{
+  return mMasSocket->GetAddress(aDeviceAddress);
+}
+
+void
+BluetoothMapSmsManager::ReplyToConnect()
+{
+  if (mMasConnected) {
+    return;
+  }
+
+  // Section 3.3.1 "Connect", IrOBEX 1.2
+  // [opcode:1][length:2][version:1][flags:1][MaxPktSizeWeCanReceive:2]
+  // [Headers:var]
+  uint8_t req[255];
+  int index = 7;
+
+  req[3] = 0x10; // version=1.0
+  req[4] = 0x00; // flag=0x00
+  req[5] = BluetoothMapSmsManager::MAX_PACKET_LENGTH >> 8;
+  req[6] = (uint8_t)BluetoothMapSmsManager::MAX_PACKET_LENGTH;
+
+  // Section 6.4 "Establishing an OBEX Session", MapSms 1.2
+  // Headers: [Who:16][Connection ID]
+  index += AppendHeaderWho(&req[index], 255, kMapMasObexTarget.mUuid,
+                           sizeof(BluetoothUuid));
+  index += AppendHeaderConnectionId(&req[index], 0x01);
+  SendMasObexData(req, ObexResponseCode::Success, index);
+}
+
+void
+BluetoothMapSmsManager::ReplyToDisconnectOrAbort()
+{
+  if (!mMasConnected) {
+    return;
+  }
+
+  // Section 3.3.2 "Disconnect" and Section 3.3.5 "Abort", IrOBEX 1.2
+  // The format of response packet of "Disconnect" and "Abort" are the same
+  // [opcode:1][length:2][Headers:var]
+  uint8_t req[255];
+  int index = 3;
+
+  SendMasObexData(req, ObexResponseCode::Success, index);
+}
+
+void
+BluetoothMapSmsManager::ReplyToSetPath()
+{
+  if (!mMasConnected) {
+    return;
+  }
+
+  // Section 3.3.6 "SetPath", IrOBEX 1.2
+  // [opcode:1][length:2][Headers:var]
+  uint8_t req[255];
+  int index = 3;
+
+  SendMasObexData(req, ObexResponseCode::Success, index);
+}
+
+void
+BluetoothMapSmsManager::ReplyToPut()
+{
+  if (!mMasConnected) {
+    return;
+  }
+
+  // Section 3.3.3.2 "PutResponse", IrOBEX 1.2
+  // [opcode:1][length:2][Headers:var]
+  uint8_t req[255];
+  int index = 3;
+
+  SendMasObexData(req, ObexResponseCode::Success, index);
+}
+
+void
+BluetoothMapSmsManager::CreateMnsObexConnection()
+{
+  if (mMnsSocket) {
+    return;
+  }
+
+  mMnsSocket = new BluetoothSocket(this);
+  // Already encrypted in previous session
+  mMnsSocket->Connect(mDeviceAddress, kMapMns,
+                      BluetoothSocketType::RFCOMM, -1, false, false);
+}
+
+void
+BluetoothMapSmsManager::DestroyMnsObexConnection()
+{
+  if (!mMnsSocket) {
+    return;
+  }
+
+  mMnsSocket->Close();
+  mMnsSocket = nullptr;
+  mNtfRequired = false;
+}
+
+void
+BluetoothMapSmsManager::SendMnsConnectRequest()
+{
+  MOZ_ASSERT(mMnsSocket);
+
+  // Section 3.3.1 "Connect", IrOBEX 1.2
+  // [opcode:1][length:2][version:1][flags:1][MaxPktSizeWeCanReceive:2]
+  // [Headers:var]
+  uint8_t req[255];
+  int index = 7;
+
+  req[3] = 0x10; // version=1.0
+  req[4] = 0x00; // flag=0x00
+  req[5] = BluetoothMapSmsManager::MAX_PACKET_LENGTH >> 8;
+  req[6] = (uint8_t)BluetoothMapSmsManager::MAX_PACKET_LENGTH;
+
+  index += AppendHeaderTarget(&req[index], 255, kMapMnsObexTarget.mUuid,
+                              sizeof(BluetoothUuid));
+  SendMnsObexData(req, ObexRequestCode::Connect, index);
+}
+
+void
+BluetoothMapSmsManager::SendMnsDisconnectRequest()
+{
+  MOZ_ASSERT(mMnsSocket);
+
+  if (!mMasConnected) {
+    return;
+  }
+
+  // Section 3.3.2 "Disconnect", IrOBEX 1.2
+  // [opcode:1][length:2][Headers:var]
+  uint8_t req[255];
+  int index = 3;
+
+  SendMnsObexData(req, ObexRequestCode::Disconnect, index);
+}
+
+void
+BluetoothMapSmsManager::HandleSmsMmsFolderListing(const ObexHeaderSet& aHeader)
+{
+  MOZ_ASSERT(NS_IsMainThread());
+
+  uint8_t buf[64];
+  uint16_t maxListCount = 0;
+
+  if (aHeader.GetAppParameter(Map::AppParametersTagId::MaxListCount,
+                              buf, 64)) {
+    maxListCount = *((uint16_t *)buf);
+    // convert big endian to little endian
+    maxListCount = (maxListCount >> 8) | (maxListCount << 8);
+  }
+
+  uint16_t startOffset = 0;
+  if (aHeader.GetAppParameter(Map::AppParametersTagId::StartOffset,
+                              buf, 64)) {
+    startOffset = *((uint16_t *)buf);
+    // convert big endian to little endian
+    startOffset = (startOffset >> 8) | (startOffset << 8);
+  }
+
+  // Folder listing size
+  int foldersize = mCurrentFolder->GetSubFolderCount();
+
+  // Convert little endian to big endian
+  uint8_t folderListingSizeValue[2];
+  folderListingSizeValue[0] = (foldersize & 0xFF00) >> 8;
+  folderListingSizeValue[1] = (foldersize & 0x00FF);
+
+  // Section 3.3.4 "GetResponse", IrOBEX 1.2
+  // [opcode:1][length:2][FolderListingSize:4][Headers:var] where
+  // Application Parameter [FolderListingSize:4] = [tagId:1][length:1][value: 2]
+  uint8_t appParameter[4];
+  AppendAppParameter(appParameter, sizeof(appParameter),
+                     (uint8_t)Map::AppParametersTagId::FolderListingSize,
+                     folderListingSizeValue, sizeof(folderListingSizeValue));
+
+  uint8_t resp[255];
+  int index = 3;
+  index += AppendHeaderAppParameters(&resp[index], 255, appParameter,
+                                     sizeof(appParameter));
+
+  /*
+   * MCE wants to query sub-folder size FolderListingSize AppParameter shall
+   * be used in the response if the value of MaxListCount in the request is 0.
+   * If MaxListCount = 0, the MSE shall ignore all other applications
+   * parameters that may be presented in the request. The response shall
+   * contain any Body header.
+   */
+  if (maxListCount) {
+    nsString output;
+    mCurrentFolder->GetFolderListingObjectString(output, maxListCount,
+                                                 startOffset);
+    index += AppendHeaderBody(&resp[index],
+                              mRemoteMaxPacketLength - index,
+                              reinterpret_cast<const uint8_t*>(
+                                NS_ConvertUTF16toUTF8(output).get()),
+                              NS_ConvertUTF16toUTF8(output).Length());
+
+    index += AppendHeaderEndOfBody(&resp[index]);
+  }
+
+  SendMasObexData(resp, ObexResponseCode::Success, index);
+}
+
+void
+BluetoothMapSmsManager::BuildDefaultFolderStructure()
+{
+  /* MAP specification defines virtual folders structure
+   * /
+   * /telecom
+   * /telecom/msg
+   * /telecom/msg/inbox
+   * /telecom/msg/draft
+   * /telecom/msg/outbox
+   * /telecom/msg/sent
+   * /telecom/msg/deleted
+   */
+  mRootFolder = new BluetoothMapFolder(NS_LITERAL_STRING("root"), nullptr);
+  BluetoothMapFolder* folder =
+    mRootFolder->AddSubFolder(NS_LITERAL_STRING("telecom"));
+  folder = folder->AddSubFolder(NS_LITERAL_STRING("msg"));
+
+  // Add mandatory folders
+  folder->AddSubFolder(NS_LITERAL_STRING("inbox"));
+  folder->AddSubFolder(NS_LITERAL_STRING("sent"));
+  folder->AddSubFolder(NS_LITERAL_STRING("deleted"));
+  folder->AddSubFolder(NS_LITERAL_STRING("outbox"));
+  folder->AddSubFolder(NS_LITERAL_STRING("draft"));
+  mCurrentFolder = mRootFolder;
+}
+
+void
+BluetoothMapSmsManager::HandleNotificationRegistration(
+  const ObexHeaderSet& aHeader)
+{
+  MOZ_ASSERT(NS_IsMainThread());
+
+  uint8_t buf[64];
+  if (!aHeader.GetAppParameter(Map::AppParametersTagId::NotificationStatus,
+                               buf, 64)) {
+    return;
+  }
+
+  bool ntfRequired = static_cast<bool>(buf[0]);
+  if (mNtfRequired == ntfRequired) {
+    // Ignore request
+    return;
+  }
+
+  mNtfRequired = ntfRequired;
+  /*
+   * Initialization sequence for a MAP session that uses both the Messsage
+   * Access service and the Message Notification service. The MNS connection
+   * shall be established by the first SetNotificationRegistration set to ON
+   * during MAP session. Only one MNS connection per device pair.
+   * Section 6.4.2, MAP
+   * If the Message Access connection is disconnected after Message Notification
+   * connection establishment, this will automatically indicate a MAS
+   * Notification-Deregistration for this MAS instance.
+   */
+  if (mNtfRequired) {
+    CreateMnsObexConnection();
+  } else {
+    /*
+     * TODO: we shall check multiple MAS instances unregister notification to
+     * drop MNS connection, but now we only support SMS/MMS, so drop connection
+     * directly.
+     */
+    DestroyMnsObexConnection();
+  }
+}
+
+void
+BluetoothMapSmsManager::HandleEventReport(const ObexHeaderSet& aHeader)
+{
+  // TODO: Handle event report in Bug 1166666
+}
+
+void
+BluetoothMapSmsManager::HandleMessageStatus(const ObexHeaderSet& aHeader)
+{
+  // TODO: Handle MessageStatus update in Bug 1186836
+}
+
+void
+BluetoothMapSmsManager::ReplyError(uint8_t aError)
+{
+  BT_LOGR("[0x%x]", aError);
+
+  // Section 3.2 "Response Format", IrOBEX 1.2
+  // [opcode:1][length:2][Headers:var]
+  uint8_t req[255];
+  int index = 3;
+
+  SendMasObexData(req, aError, index);
+}
+
+void
+BluetoothMapSmsManager::SendMasObexData(uint8_t* aData, uint8_t aOpcode,
+                                        int aSize)
+{
+  SetObexPacketInfo(aData, aOpcode, aSize);
+  mMasSocket->SendSocketData(new UnixSocketRawData(aData, aSize));
+}
+
+void
+BluetoothMapSmsManager::SendMnsObexData(uint8_t* aData, uint8_t aOpcode,
+                                        int aSize)
+{
+  mLastCommand = aOpcode;
+  SetObexPacketInfo(aData, aOpcode, aSize);
+  mMnsSocket->SendSocketData(new UnixSocketRawData(aData, aSize));
+}
+
+void
+BluetoothMapSmsManager::OnSocketConnectSuccess(BluetoothSocket* aSocket)
+{
+  MOZ_ASSERT(aSocket);
+
+  // MNS socket is connected
+  if (aSocket == mMnsSocket) {
+    mMnsConnected = true;
+    SendMnsConnectRequest();
+    return;
+  }
+  // MAS socket is connected
+  // Close server socket as only one session is allowed at a time
+  mMasServerSocket.swap(mMasSocket);
+
+  // Cache device address since we can't get socket address when a remote
+  // device disconnect with us.
+  mMasSocket->GetAddress(mDeviceAddress);
+}
+
+void
+BluetoothMapSmsManager::OnSocketConnectError(BluetoothSocket* aSocket)
+{
+  // MNS socket connection error
+  if (aSocket == mMnsSocket) {
+    mMnsConnected = false;
+    mMnsSocket = nullptr;
+    return;
+  }
+
+  // MAS socket connection error
+  mMasServerSocket = nullptr;
+  mMasSocket = nullptr;
+}
+
+void
+BluetoothMapSmsManager::OnSocketDisconnect(BluetoothSocket* aSocket)
+{
+  MOZ_ASSERT(aSocket);
+
+  // MNS socket is disconnected
+  if (aSocket == mMnsSocket) {
+    mMnsConnected = false;
+    mMnsSocket = nullptr;
+    BT_LOGR("MNS socket disconnected");
+    return;
+  }
+
+  // MAS server socket is closed
+  if (aSocket != mMasSocket) {
+    // Do nothing when a listening server socket is closed.
+    return;
+  }
+
+  // MAS socket is disconnected
+  AfterMapSmsDisconnected();
+  mDeviceAddress.AssignLiteral(BLUETOOTH_ADDRESS_NONE);
+  mMasSocket = nullptr;
+
+  Listen();
+}
+
+void
+BluetoothMapSmsManager::Disconnect(BluetoothProfileController* aController)
+{
+  if (!mMasSocket) {
+    BT_WARNING("%s: No ongoing connection to disconnect", __FUNCTION__);
+    return;
+  }
+
+  mMasSocket->Close();
+}
+
+NS_IMPL_ISUPPORTS(BluetoothMapSmsManager, nsIObserver)
+
+void
+BluetoothMapSmsManager::Connect(const nsAString& aDeviceAddress,
+                                BluetoothProfileController* aController)
+{
+  MOZ_ASSERT(false);
+}
+
+void
+BluetoothMapSmsManager::OnGetServiceChannel(const nsAString& aDeviceAddress,
+                                            const nsAString& aServiceUuid,
+                                            int aChannel)
+{
+  MOZ_ASSERT(false);
+}
+
+void
+BluetoothMapSmsManager::OnUpdateSdpRecords(const nsAString& aDeviceAddress)
+{
+  MOZ_ASSERT(false);
+}
+
+void
+BluetoothMapSmsManager::OnConnect(const nsAString& aErrorStr)
+{
+  MOZ_ASSERT(false);
+}
+
+void
+BluetoothMapSmsManager::OnDisconnect(const nsAString& aErrorStr)
+{
+  MOZ_ASSERT(false);
+}
+
+void
+BluetoothMapSmsManager::Reset()
+{
+  MOZ_ASSERT(false);
+}
+
+END_BLUETOOTH_NAMESPACE
new file mode 100644
--- /dev/null
+++ b/dom/bluetooth/bluedroid/BluetoothMapSmsManager.h
@@ -0,0 +1,150 @@
+/* -*- Mode: c++; c-basic-offset: 2; indent-tabs-mode: nil; tab-width: 40 -*- */
+/* vim: set ts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef mozilla_dom_bluetooth_bluedroid_BluetoothMapSmsManager_h
+#define mozilla_dom_bluetooth_bluedroid_BluetoothMapSmsManager_h
+
+#include "BluetoothCommon.h"
+#include "BluetoothMapFolder.h"
+#include "BluetoothProfileManagerBase.h"
+#include "BluetoothSocketObserver.h"
+#include "mozilla/ipc/SocketBase.h"
+
+BEGIN_BLUETOOTH_NAMESPACE
+
+struct Map {
+  enum AppParametersTagId {
+    MaxListCount                  = 0x1,
+    StartOffset                   = 0x2,
+    FilterMessageType             = 0x3,
+    FilterPeriodBegin             = 0x4,
+    FilterPeriodEnd               = 0x5,
+    FilterReadStatus              = 0x6,
+    FilterRecipient               = 0x7,
+    FilterOriginator              = 0x8,
+    FilterPriority                = 0x9,
+    Attachment                    = 0x0A,
+    Transparent                   = 0x0B,
+    Retry                         = 0x0C,
+    NewMessage                    = 0x0D,
+    NotificationStatus            = 0x0E,
+    MASInstanceId                 = 0x0F,
+    ParameterMask                 = 0x10,
+    FolderListingSize             = 0x11,
+    MessagesListingSize           = 0x12,
+    SubjectLength                 = 0x13,
+    Charset                       = 0x14,
+    FractionRequest               = 0x15,
+    FractionDeliver               = 0x16,
+    StatusIndicator               = 0x17,
+    StatusValue                   = 0x18,
+    MSETime                       = 0x19
+  };
+};
+
+class BluetoothNamedValue;
+class BluetoothSocket;
+class ObexHeaderSet;
+
+/*
+ * BluetoothMapSmsManager acts as Message Server Equipment (MSE) and runs both
+ * MAS server and MNS client to exchange SMS/MMS message.
+ */
+
+class BluetoothMapSmsManager : public BluetoothSocketObserver
+                             , public BluetoothProfileManagerBase
+{
+public:
+  BT_DECL_PROFILE_MGR_BASE
+  BT_DECL_SOCKET_OBSERVER
+  virtual void GetName(nsACString& aName)
+  {
+    aName.AssignLiteral("MapSms");
+  }
+
+  static const int MAX_PACKET_LENGTH = 0xFFFE;
+  static const int MAX_INSTANCE_ID = 255;
+  // SDP record for SupportedMessageTypes
+  static const int SDP_MESSAGE_TYPE_EMAIL = 0x01;
+  static const int SDP_MESSAGE_TYPE_SMS_GSM = 0x02;
+  static const int SDP_MESSAGE_TYPE_SMS_CDMA = 0x04;
+  static const int SDP_MESSAGE_TYPE_MMS = 0x08;
+  // By defualt SMS/MMS is default supported
+  static const int SDP_SMS_MMS_INSTANCE_ID = 0;
+
+  static BluetoothMapSmsManager* Get();
+  bool Listen();
+
+protected:
+  virtual ~BluetoothMapSmsManager();
+
+private:
+  BluetoothMapSmsManager();
+  bool Init();
+  void HandleShutdown();
+
+  void ReplyToConnect();
+  void ReplyToDisconnectOrAbort();
+  void ReplyToSetPath();
+  void ReplyToPut();
+  void ReplyError(uint8_t aError);
+
+  void HandleNotificationRegistration(const ObexHeaderSet& aHeader);
+  void HandleEventReport(const ObexHeaderSet& aHeader);
+  void HandleMessageStatus(const ObexHeaderSet& aHeader);
+  void HandleSmsMmsFolderListing(const ObexHeaderSet& aHeader);
+  void SendMasObexData(uint8_t* aData, uint8_t aOpcode, int aSize);
+  void SendMnsObexData(uint8_t* aData, uint8_t aOpcode, int aSize);
+
+  uint8_t SetPath(uint8_t flags, const ObexHeaderSet& aHeader);
+  bool CompareHeaderTarget(const ObexHeaderSet& aHeader);
+  void AfterMapSmsConnected();
+  void AfterMapSmsDisconnected();
+  void CreateMnsObexConnection();
+  void DestroyMnsObexConnection();
+  void SendMnsConnectRequest();
+  void SendMnsDisconnectRequest();
+  void MnsDataHandler(mozilla::ipc::UnixSocketBuffer* aMessage);
+  void MasDataHandler(mozilla::ipc::UnixSocketBuffer* aMessage);
+  /*
+   * Build mandatory folders
+   */
+  void BuildDefaultFolderStructure();
+  /**
+   * Current virtual folder path
+   */
+  BluetoothMapFolder* mCurrentFolder;
+  nsRefPtr<BluetoothMapFolder> mRootFolder;
+
+  /*
+   * Record the last command
+   */
+  int mLastCommand;
+  // MAS OBEX session status. Set when MAS OBEX session is established.
+  bool mMasConnected;
+  // MNS OBEX session status. Set when MNS OBEX session is established.
+  bool mMnsConnected;
+  bool mNtfRequired;
+  nsString mDeviceAddress;
+  unsigned int mRemoteMaxPacketLength;
+
+  // If a connection has been established, mMasSocket will be the socket
+  // communicating with the remote socket. We maintain the invariant that if
+  // mMasSocket is non-null, mServerSocket must be null (and vice versa).
+  nsRefPtr<BluetoothSocket> mMasSocket;
+
+  // Server socket. Once an inbound connection is established, it will hand
+  // over the ownership to mMasSocket, and get a new server socket while Listen()
+  // is called.
+  nsRefPtr<BluetoothSocket> mMasServerSocket;
+
+  // Message notification service client socket
+  nsRefPtr<BluetoothSocket> mMnsSocket;
+};
+
+END_BLUETOOTH_NAMESPACE
+
+#endif //mozilla_dom_bluetooth_bluedroid_BluetoothMapSmsManager_h
--- a/dom/bluetooth/bluedroid/BluetoothOppManager.cpp
+++ b/dom/bluetooth/bluedroid/BluetoothOppManager.cpp
@@ -375,17 +375,16 @@ BluetoothOppManager::Listen()
   return true;
 }
 
 void
 BluetoothOppManager::StartSendingNextFile()
 {
   MOZ_ASSERT(NS_IsMainThread());
 
-  MOZ_ASSERT(!IsConnected());
   MOZ_ASSERT(!mBatches.IsEmpty());
   MOZ_ASSERT((int)mBatches[0].mBlobs.Length() > mCurrentBlobIndex + 1);
 
   mBlob = mBatches[0].mBlobs[++mCurrentBlobIndex];
 
   // Before sending content, we have to send a header including
   // information such as file name, file length and content type.
   ExtractBlobHeaders();
@@ -468,16 +467,17 @@ BluetoothOppManager::DiscardBlobsToSend(
     FileTransferComplete();
   }
 }
 
 bool
 BluetoothOppManager::ProcessNextBatch()
 {
   MOZ_ASSERT(NS_IsMainThread());
+  MOZ_ASSERT(!IsConnected());
 
   // Remove the processed batch.
   // A batch is processed if we've incremented mCurrentBlobIndex for it.
   if (mCurrentBlobIndex >= 0) {
     ClearQueue();
     mBatches.RemoveElementAt(0);
     BT_LOGR("REMOVE. %d remaining", mBatches.Length());
   }
@@ -600,19 +600,21 @@ BluetoothOppManager::AfterOppDisconnecte
     mInputStream = nullptr;
   }
 
   if (mOutputStream) {
     mOutputStream->Close();
     mOutputStream = nullptr;
   }
 
-  if (mReadFileThread) {
-    mReadFileThread->Shutdown();
-    mReadFileThread = nullptr;
+  // Store local pointer of |mReadFileThread| to avoid shutdown reentry crash
+  // See bug 1191715 comment 19 for more details.
+  nsCOMPtr<nsIThread> thread = mReadFileThread.forget();
+  if (thread) {
+    thread->Shutdown();
   }
 
   // Release the mount lock if file transfer completed
   if (mMountLock) {
     // The mount lock will be implicitly unlocked
     mMountLock = nullptr;
   }
 }
@@ -785,17 +787,17 @@ BluetoothOppManager::RetrieveSentFileNam
   mFileName.Truncate();
 
   nsRefPtr<File> file = static_cast<Blob*>(mBlob.get())->ToFile();
   if (file) {
     file->GetName(mFileName);
   }
 
   /**
-   * We try our best to get the file extention to avoid interoperability issues.
+   * We try our best to get the file extension to avoid interoperability issues.
    * However, once we found that we are unable to get suitable extension or
    * information about the content type, sending a pre-defined file name without
    * extension would be fine.
    */
   if (mFileName.IsEmpty()) {
     mFileName.AssignLiteral("Unknown");
   }
 
--- a/dom/bluetooth/bluedroid/BluetoothServiceBluedroid.cpp
+++ b/dom/bluetooth/bluedroid/BluetoothServiceBluedroid.cpp
@@ -18,16 +18,17 @@
 
 #include "BluetoothServiceBluedroid.h"
 
 #include "BluetoothA2dpManager.h"
 #include "BluetoothAvrcpManager.h"
 #include "BluetoothGattManager.h"
 #include "BluetoothHfpManager.h"
 #include "BluetoothHidManager.h"
+#include "BluetoothMapSmsManager.h"
 #include "BluetoothOppManager.h"
 #include "BluetoothPbapManager.h"
 #include "BluetoothProfileController.h"
 #include "BluetoothReplyRunnable.h"
 #include "BluetoothUtils.h"
 #include "BluetoothUuid.h"
 #include "mozilla/dom/bluetooth/BluetoothTypes.h"
 #include "mozilla/ipc/SocketBase.h"
@@ -296,16 +297,17 @@ BluetoothServiceBluedroid::StopInternal(
   MOZ_ASSERT(NS_IsMainThread());
 
   static BluetoothProfileManagerBase* sProfiles[] = {
     BluetoothHfpManager::Get(),
     BluetoothAvrcpManager::Get(),
     BluetoothA2dpManager::Get(),
     BluetoothOppManager::Get(),
     BluetoothPbapManager::Get(),
+    BluetoothMapSmsManager::Get(),
     BluetoothHidManager::Get()
   };
 
   // Disconnect all connected profiles
   for (uint8_t i = 0; i < MOZ_ARRAY_LENGTH(sProfiles); i++) {
     nsCString profileName;
     sProfiles[i]->GetName(profileName);
 
@@ -1515,16 +1517,21 @@ BluetoothServiceBluedroid::AdapterStateC
     if (!opp || !opp->Listen()) {
       BT_LOGR("Fail to start BluetoothOppManager listening");
     }
 
     BluetoothPbapManager* pbap = BluetoothPbapManager::Get();
     if (!pbap || !pbap->Listen()) {
       BT_LOGR("Fail to start BluetoothPbapManager listening");
     }
+
+    BluetoothMapSmsManager* map = BluetoothMapSmsManager::Get();
+    if (!map || !map->Listen()) {
+      BT_LOGR("Fail to start BluetoothMapSmsManager listening");
+    }
   }
 
   // Resolve promise if existed
   if (!mChangeAdapterStateRunnables.IsEmpty()) {
     DispatchReplySuccess(mChangeAdapterStateRunnables[0]);
     mChangeAdapterStateRunnables.RemoveElementAt(0);
   }
 
--- a/dom/bluetooth/bluez/BluetoothOppManager.cpp
+++ b/dom/bluetooth/bluez/BluetoothOppManager.cpp
@@ -362,17 +362,16 @@ BluetoothOppManager::Listen()
   return true;
 }
 
 void
 BluetoothOppManager::StartSendingNextFile()
 {
   MOZ_ASSERT(NS_IsMainThread());
 
-  MOZ_ASSERT(!IsConnected());
   MOZ_ASSERT(!mBatches.IsEmpty());
   MOZ_ASSERT((int)mBatches[0].mBlobs.Length() > mCurrentBlobIndex + 1);
 
   mBlob = mBatches[0].mBlobs[++mCurrentBlobIndex];
 
   // Before sending content, we have to send a header including
   // information such as file name, file length and content type.
   ExtractBlobHeaders();
@@ -455,16 +454,17 @@ BluetoothOppManager::DiscardBlobsToSend(
     FileTransferComplete();
   }
 }
 
 bool
 BluetoothOppManager::ProcessNextBatch()
 {
   MOZ_ASSERT(NS_IsMainThread());
+  MOZ_ASSERT(!IsConnected());
 
   // Remove the processed batch.
   // A batch is processed if we've incremented mCurrentBlobIndex for it.
   if (mCurrentBlobIndex >= 0) {
     ClearQueue();
     mBatches.RemoveElementAt(0);
     BT_LOGR("REMOVE. %d remaining", mBatches.Length());
   }
@@ -587,19 +587,21 @@ BluetoothOppManager::AfterOppDisconnecte
     mInputStream = nullptr;
   }
 
   if (mOutputStream) {
     mOutputStream->Close();
     mOutputStream = nullptr;
   }
 
-  if (mReadFileThread) {
-    mReadFileThread->Shutdown();
-    mReadFileThread = nullptr;
+  // Store local pointer of |mReadFileThread| to avoid shutdown reentry crash
+  // See bug 1191715 comment 19 for more details.
+  nsCOMPtr<nsIThread> thread = mReadFileThread.forget();
+  if (thread) {
+    thread->Shutdown();
   }
 
   // Release the mount lock if file transfer completed
   if (mMountLock) {
     // The mount lock will be implicitly unlocked
     mMountLock = nullptr;
   }
 }
@@ -772,17 +774,17 @@ BluetoothOppManager::RetrieveSentFileNam
   mFileName.Truncate();
 
   nsRefPtr<File> file = mBlob->ToFile();
   if (file) {
     file->GetName(mFileName);
   }
 
   /**
-   * We try our best to get the file extention to avoid interoperability issues.
+   * We try our best to get the file extension to avoid interoperability issues.
    * However, once we found that we are unable to get suitable extension or
    * information about the content type, sending a pre-defined file name without
    * extension would be fine.
    */
   if (mFileName.IsEmpty()) {
     mFileName.AssignLiteral("Unknown");
   }
 
--- a/dom/bluetooth/common/ObexBase.cpp
+++ b/dom/bluetooth/common/ObexBase.cpp
@@ -61,16 +61,24 @@ int
 AppendHeaderBody(uint8_t* aRetBuf, int aBufferSize, const uint8_t* aBody,
                  int aLength)
 {
   return AppendHeader(ObexHeaderId::Body, aRetBuf, aBufferSize,
                       aBody, aLength);
 }
 
 int
+AppendHeaderTarget(uint8_t* aRetBuf, int aBufferSize, const uint8_t* aTarget,
+                   int aLength)
+{
+  return AppendHeader(ObexHeaderId::Target, aRetBuf, aBufferSize,
+                      aTarget, aLength);
+}
+
+int
 AppendHeaderWho(uint8_t* aRetBuf, int aBufferSize, const uint8_t* aWho,
                 int aLength)
 {
   return AppendHeader(ObexHeaderId::Who, aRetBuf, aBufferSize,
                       aWho, aLength);
 }
 
 int
--- a/dom/bluetooth/common/ObexBase.h
+++ b/dom/bluetooth/common/ObexBase.h
@@ -338,16 +338,18 @@ public:
 private:
   nsTArray<nsAutoPtr<ObexHeader> > mHeaders;
 };
 
 int AppendHeaderName(uint8_t* aRetBuf, int aBufferSize, const uint8_t* aName,
                      int aLength);
 int AppendHeaderBody(uint8_t* aRetBuf, int aBufferSize, const uint8_t* aBody,
                      int aLength);
+int AppendHeaderTarget(uint8_t* aRetBuf, int aBufferSize, const uint8_t* aTarget,
+                       int aLength);
 int AppendHeaderWho(uint8_t* aRetBuf, int aBufferSize, const uint8_t* aWho,
                     int aLength);
 int AppendHeaderAppParameters(uint8_t* aRetBuf, int aBufferSize,
                               const uint8_t* aAppParameters, int aLength);
 int AppendAppParameter(uint8_t* aRetBuf, int aBufferSize, const uint8_t aTagId,
                        const uint8_t* aValue, int aLength);
 int AppendHeaderLength(uint8_t* aRetBuf, int aObjectLength);
 int AppendHeaderConnectionId(uint8_t* aRetBuf, int aConnectionId);
--- a/dom/bluetooth/moz.build
+++ b/dom/bluetooth/moz.build
@@ -76,16 +76,18 @@ if CONFIG['MOZ_B2G_BT']:
                 'bluedroid/BluetoothDaemonAvrcpInterface.cpp',
                 'bluedroid/BluetoothDaemonGattInterface.cpp',
                 'bluedroid/BluetoothDaemonHandsfreeInterface.cpp',
                 'bluedroid/BluetoothDaemonHelpers.cpp',
                 'bluedroid/BluetoothDaemonInterface.cpp',
                 'bluedroid/BluetoothDaemonSetupInterface.cpp',
                 'bluedroid/BluetoothDaemonSocketInterface.cpp',
                 'bluedroid/BluetoothGattManager.cpp',
+                'bluedroid/BluetoothMapFolder.cpp',
+                'bluedroid/BluetoothMapSmsManager.cpp',
                 'bluedroid/BluetoothOppManager.cpp',
                 'bluedroid/BluetoothPbapManager.cpp',
                 'bluedroid/BluetoothServiceBluedroid.cpp',
                 'bluedroid/BluetoothSocket.cpp',
                 'bluedroid/BluetoothSocketMessageWatcher.cpp'
             ]
             LOCAL_INCLUDES += [
                 'bluedroid',
--- a/dom/settings/SettingsManager.js
+++ b/dom/settings/SettingsManager.js
@@ -376,16 +376,17 @@ SettingsManager.prototype = {
       this._callbacks[aName].push(aCallback);
     }
 
     let length = this._callbacks[aName].length;
     if (length >= kObserverSoftLimit) {
       debug("WARNING: MORE THAN " + kObserverSoftLimit + " OBSERVERS FOR " +
             aName + ": " + length + " FROM" + (new Error).stack);
 #ifdef DEBUG
+      debug("JS STOPS EXECUTING AT THIS POINT IN DEBUG BUILDS!");
       throw Components.results.NS_ERROR_ABORT;
 #endif
     }
 
     this.checkMessageRegistration();
   },
 
   removeObserver: function removeObserver(aName, aCallback) {
--- a/dom/wifi/test/marionette/head.js
+++ b/dom/wifi/test/marionette/head.js
@@ -771,17 +771,22 @@ let gTestSuite = (function() {
     }
 
     function startOneHostapd(aIndex) {
       let configFileName = HOSTAPD_CONFIG_PATH + 'ap' + aIndex + '.conf';
       return writeHostapdConfFile(configFileName, createConfigFromCommon(aIndex))
         .then(() => runEmulatorShellSafe(['hostapd', '-B', configFileName]))
         .then(function (reply) {
           // It may fail at the first time due to the previous ungracefully terminated one.
-          if (reply[0] === 'bind(PF_UNIX): Address already in use') {
+          if (reply.length === 0) {
+            // The hostapd starts successfully
+            return;
+          }
+
+          if (reply[0].indexOf('bind(PF_UNIX): Address already in use') !== -1) {
             return startOneHostapd(aIndex);
           }
         });
     }
 
     return Promise.all(aConfigList.map(function(aConfig, aIndex) {
       return startOneHostapd(aIndex);
     }));
--- a/dom/wifi/test/marionette/test_wifi_static_ip.js
+++ b/dom/wifi/test/marionette/test_wifi_static_ip.js
@@ -9,16 +9,18 @@ const STATIC_IP_CONFIG = {
   ipaddr: "192.168.111.222",
   proxy: "",
   maskLength: 24,
   gateway: "192.168.111.1",
   dns1: "8.8.8.8",
   dns2: "8.8.4.4",
 };
 
+const TESTING_HOSTAPD = [{ ssid: 'ap0' }];
+
 function testAssociateWithStaticIp(aNetwork, aStaticIpConfig) {
   return gTestSuite.setStaticIpMode(aNetwork, aStaticIpConfig)
     .then(() => gTestSuite.testAssociate(aNetwork))
     // Check ip address and prefix.
     .then(() => gTestSuite.exeAndParseNetcfg())
     .then((aResult) => {
       is(aResult["wlan0"].ip, aStaticIpConfig.ipaddr, "Check ip address");
       is(aResult["wlan0"].prefix, aStaticIpConfig.maskLength, "Check prefix");
@@ -27,15 +29,37 @@ function testAssociateWithStaticIp(aNetw
     .then(() => gTestSuite.exeAndParseIpRoute())
     .then((aResult) => {
       is(aResult["wlan0"].src, aStaticIpConfig.ipaddr, "Check ip address");
       is(aResult["wlan0"].default, true, "Check default route");
       is(aResult["wlan0"].gateway, aStaticIpConfig.gateway, "Check gateway");
     });
 }
 
+function findDesireNetwork(aNetworks) {
+  let i = gTestSuite.getFirstIndexBySsid(TESTING_HOSTAPD[0].ssid, aNetworks);
+
+  if (-1 !== i) {
+    return aNetworks[i];
+  }
+
+  return aNetworks[0];
+}
+
 // Start test.
-gTestSuite.doTest(function() {
+gTestSuite.doTestWithoutStockAp(function() {
   return gTestSuite.ensureWifiEnabled(true)
+
+    // Start custom hostapd for testing.
+    .then(() => gTestSuite.startHostapds(TESTING_HOSTAPD))
+    .then(() => gTestSuite.verifyNumOfProcesses('hostapd',
+                                                TESTING_HOSTAPD.length))
+
+    // Perform a wifi scan, and then run the static ip test
     .then(() => gTestSuite.requestWifiScan())
-    .then((aNetworks) => testAssociateWithStaticIp(aNetworks[0],
-                                                   STATIC_IP_CONFIG));
+    .then((aNetworks) => findDesireNetwork(aNetworks))
+    .then((aNetwork) => testAssociateWithStaticIp(aNetwork,
+                                                   STATIC_IP_CONFIG))
+
+    // Kill running hostapd.
+    .then(gTestSuite.killAllHostapd)
+    .then(() => gTestSuite.verifyNumOfProcesses('hostapd', 0));
 });
new file mode 100644
--- /dev/null
+++ b/mobile/android/base/resources/values-sw360dp/dimens.xml
@@ -0,0 +1,10 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+   - License, v. 2.0. If a copy of the MPL was not distributed with this
+   - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<resources>
+    <dimen name="tab_panel_column_width">156dip</dimen>
+    <dimen name="tab_thumbnail_height">110dip</dimen>
+    <dimen name="tab_thumbnail_width">148dip</dimen>
+</resources>
--- a/mobile/android/base/resources/values-sw400dp/dimens.xml
+++ b/mobile/android/base/resources/values-sw400dp/dimens.xml
@@ -1,10 +1,10 @@
 <?xml version="1.0" encoding="utf-8"?>
 <!-- This Source Code Form is subject to the terms of the Mozilla Public
    - License, v. 2.0. If a copy of the MPL was not distributed with this
    - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
 
 <resources>
-    <dimen name="tab_panel_column_width">174dip</dimen>
+    <dimen name="tab_panel_column_width">176dip</dimen>
     <dimen name="tab_thumbnail_height">120dip</dimen>
     <dimen name="tab_thumbnail_width">168dip</dimen>
 </resources>
--- a/services/sync/modules/browserid_identity.js
+++ b/services/sync/modules/browserid_identity.js
@@ -99,43 +99,49 @@ this.BrowserIDManager.prototype = {
   // 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.
-    return this._shouldHaveSyncKeyBundle;
-  },
-
   get needsCustomization() {
     try {
       return Services.prefs.getBoolPref(PREF_SYNC_SHOW_CUSTOMIZATION);
     } catch (e) {
       return false;
     }
   },
 
   initialize: function() {
     for (let topic of OBSERVER_TOPICS) {
       Services.obs.addObserver(this, topic, false);
     }
-    return this.initializeWithCurrentIdentity();
+    // and a background fetch of account data just so we can set this.account,
+    // so we have a username available before we've actually done a login.
+    // XXX - this is actually a hack just for tests and really shouldn't be
+    // necessary. Also, you'd think it would be safe to allow this.account to
+    // be set to null when there's no user logged in, but argue with the test
+    // suite, not with me :)
+    this._fxaService.getSignedInUser().then(accountData => {
+      if (accountData) {
+        this.account = accountData.email;
+      }
+    }).catch(err => {
+      // As above, this is only for tests so it is safe to ignore.
+    });
   },
 
   /**
    * 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) {
+    if (!this._shouldHaveSyncKeyBundle && this.whenReadyToAuthenticate) {
       // 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 (this._syncKeyBundle) {
       return Promise.resolve();
     }
@@ -155,17 +161,16 @@ this.BrowserIDManager.prototype = {
 
   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();
   },
 
   offerSyncOptions: function () {
     // 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");
@@ -289,17 +294,18 @@ this.BrowserIDManager.prototype = {
     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 constraints
-      // aren't met, so just go ahead and do the init.
+      // aren't met (indirectly, via _updateSignedInUser()), so just go ahead
+      // and do the init.
       this.initializeWithCurrentIdentity(true);
       break;
 
     case fxAccountsCommon.ONLOGOUT_NOTIFICATION:
       Weave.Service.startOver();
       // startOver will cause this instance to be thrown away, so there's
       // nothing else to do.
       break;
@@ -631,17 +637,16 @@ this.BrowserIDManager.prototype = {
           this._authFailureReason = LOGIN_FAILED_NETWORK_ERROR;
         }
         // this._authFailureReason being set to be non-null in the above if clause
         // ensures we are in the correct currentAuthState, and
         // this._shouldHaveSyncKeyBundle being true ensures everything that cares knows
         // that there is no authentication dance still under way.
         this._shouldHaveSyncKeyBundle = true;
         Weave.Status.login = this._authFailureReason;
-        Services.obs.notifyObservers(null, "weave:ui:login:error", null);
         throw err;
       });
   },
 
   // 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()) {
--- a/services/sync/modules/constants.js
+++ b/services/sync/modules/constants.js
@@ -117,17 +117,16 @@ ENGINE_SUCCEEDED:                      "
 // login failure status codes:
 LOGIN_FAILED_NO_USERNAME:              "error.login.reason.no_username",
 LOGIN_FAILED_NO_PASSWORD:              "error.login.reason.no_password2",
 LOGIN_FAILED_NO_PASSPHRASE:            "error.login.reason.no_recoverykey",
 LOGIN_FAILED_NETWORK_ERROR:            "error.login.reason.network",
 LOGIN_FAILED_SERVER_ERROR:             "error.login.reason.server",
 LOGIN_FAILED_INVALID_PASSPHRASE:       "error.login.reason.recoverykey",
 LOGIN_FAILED_LOGIN_REJECTED:           "error.login.reason.account",
-LOGIN_FAILED_NOT_READY:                "error.login.reason.initializing",
 
 // sync failure status codes
 METARECORD_DOWNLOAD_FAIL:              "error.sync.reason.metarecord_download_fail",
 VERSION_OUT_OF_DATE:                   "error.sync.reason.version_out_of_date",
 DESKTOP_VERSION_OUT_OF_DATE:           "error.sync.reason.desktop_version_out_of_date",
 SETUP_FAILED_NO_PASSPHRASE:            "error.sync.reason.setup_failed_no_passphrase",
 CREDENTIALS_CHANGED:                   "error.sync.reason.credentials_changed",
 ABORT_SYNC_COMMAND:                    "aborting sync, process commands said so",
--- a/services/sync/modules/identity.js
+++ b/services/sync/modules/identity.js
@@ -80,28 +80,24 @@ IdentityManager.prototype = {
 
   _syncKey: null,
   _syncKeyAllowLookup: true,
   _syncKeySet: false,
 
   _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 the identity provider.
    */
   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.
   },
@@ -110,24 +106,16 @@ IdentityManager.prototype = {
    * 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() {
-    // We initialize in a fully sync manner, so we are always finished.
-    return true;
-  },
-
   get account() {
     return Svc.Prefs.get("account", this.username);
   },
 
   /**
    * Sets the active account name.
    *
    * This should almost always be called in favor of setting username, as
--- a/services/sync/modules/service.js
+++ b/services/sync/modules/service.js
@@ -685,23 +685,16 @@ Sync11Service.prototype = {
       this._log.debug("Failed to fetch and verify keys: "
                       + Utils.exceptionStr(ex));
       this.errorHandler.checkServerError(ex);
       return false;
     }
   },
 
   verifyLogin: function verifyLogin(allow40XRecovery = true) {
-    // If the identity isn't ready it  might not know the username...
-    if (!this.identity.readyToAuthenticate) {
-      this._log.info("Not ready to authenticate in verifyLogin.");
-      this.status.login = LOGIN_FAILED_NOT_READY;
-      return false;
-    }
-
     if (!this.identity.username) {
       this._log.warn("No username in verifyLogin.");
       this.status.login = LOGIN_FAILED_NO_USERNAME;
       return false;
     }
 
     // Attaching auth credentials to a request requires access to
     // passwords, which means that Resource.get can throw MP-related
@@ -938,35 +931,32 @@ 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.finalize().then(
-      () => {
-        // an observer so the FxA migration code can take some action before
-        // the new identity is created.
-        Svc.Obs.notify("weave:service:start-over:init-identity");
-        this.identity.username = "";
-        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");
-      }
-    );
+    try {
+      this.identity.finalize();
+      // an observer so the FxA migration code can take some action before
+      // the new identity is created.
+      Svc.Obs.notify("weave:service:start-over:init-identity");
+      this.identity.username = "";
+      this.status.__authManager = null;
+      this.identity = Status._authManager;
+      this._clusterManager = this.identity.createClusterManager(this);
+      Svc.Obs.notify("weave:service:start-over:finish");
+    } catch (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);
     }
--- a/services/sync/modules/status.js
+++ b/services/sync/modules/status.js
@@ -25,20 +25,17 @@ this.Status = {
     if (this.__authManager) {
       return this.__authManager;
     }
     let service = Components.classes["@mozilla.org/weave/service;1"]
                     .getService(Components.interfaces.nsISupports)
                     .wrappedJSObject;
     let idClass = service.fxAccountsEnabled ? BrowserIDManager : IdentityManager;
     this.__authManager = new idClass();
-    // .initialize returns a promise, so we need to spin until it resolves.
-    let cb = Async.makeSpinningCallback();
-    this.__authManager.initialize().then(cb, cb);
-    cb.wait();
+    this.__authManager.initialize();
     return this.__authManager;
   },
 
   get service() {
     return this._service;
   },
 
   set service(code) {
--- a/toolkit/components/places/nsLivemarkService.js
+++ b/toolkit/components/places/nsLivemarkService.js
@@ -647,18 +647,18 @@ Livemark.prototype = {
       }
     }
 
     for (let [ container, observer ] of this._resultObservers) {
       if (this._nodes.has(container)) {
         let nodes = this._nodes.get(container);
         for (let node of nodes) {
           // Workaround for bug 449811.
-          localObserver = observer;
-          localNode = node;
+          let localObserver = observer;
+          let localNode = node;
           if (!aURI || node.uri == aURI.spec) {
             Services.tm.mainThread.dispatch(() => {
               localObserver.nodeHistoryDetailsChanged(localNode, 0, aVisitedStatus);
             }, Ci.nsIThread.DISPATCH_NORMAL);
           }
         }
       }
     }
--- a/toolkit/components/telemetry/TelemetryStorage.jsm
+++ b/toolkit/components/telemetry/TelemetryStorage.jsm
@@ -70,22 +70,23 @@ const ARCHIVE_SIZE_PROBE_SPECIAL_VALUE =
 const PENDING_PINGS_SIZE_PROBE_SPECIAL_VALUE = 17;
 
 const UUID_REGEX = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
 
 /**
  * This is thrown by |TelemetryStorage.loadPingFile| when reading the ping
  * from the disk fails.
  */
-function PingReadError(message="Error reading the ping file") {
+function PingReadError(message="Error reading the ping file", becauseNoSuchFile = false) {
   Error.call(this, message);
   let error = new Error();
   this.name = "PingReadError";
   this.message = message;
   this.stack = error.stack;
+  this.becauseNoSuchFile = becauseNoSuchFile;
 }
 PingReadError.prototype = Object.create(Error.prototype);
 PingReadError.prototype.constructor = PingReadError;
 
 /**
  * This is thrown by |TelemetryStorage.loadPingFile| when parsing the ping JSON
  * content fails.
  */
@@ -1440,17 +1441,17 @@ let TelemetryStorageImpl = {
       options.compression = "lz4";
     }
 
     let array;
     try {
       array = yield OS.File.read(aFilePath, options);
     } catch(e) {
       this._log.trace("loadPingfile - unreadable ping " + aFilePath, e);
-      throw new PingReadError(e.message);
+      throw new PingReadError(e.message, e.becauseNoSuchFile);
     }
 
     let decoder = new TextDecoder();
     let string = decoder.decode(array);
     let ping;
     try {
       ping = JSON.parse(string);
       // The ping's payload used to be stringified JSON.  Deal with that.
--- a/toolkit/devtools/server/actors/animation.js
+++ b/toolkit/devtools/server/actors/animation.js
@@ -319,17 +319,24 @@ let AnimationPlayerActor = ActorClass({
   }),
 
   /**
    * Executed when the current animation changes, used to emit the new state
    * the the front.
    */
   onAnimationMutation: function(mutations) {
     let hasChanged = false;
-    for (let {changedAnimations} of mutations) {
+    for (let {removedAnimations, changedAnimations} of mutations) {
+      if (removedAnimations.length) {
+        // Reset the local copy of the state on removal, since the animation can
+        // be kept on the client and re-added, its state needs to be sent in
+        // full.
+        this.currentState = null;
+      }
+
       if (!changedAnimations.length) {
         return;
       }
       if (changedAnimations.some(animation => animation === this.player)) {
         hasChanged = true;
         break;
       }
     }
@@ -684,21 +691,24 @@ let AnimationsActor = exports.Animations
         // it out when it was reported as removed. So filter it out here too.
         if (this.actors.find(a => a.player === player)) {
           continue;
         }
         // If the added player has the same name and target node as a player we
         // already have, it means it's a transition that's re-starting. So send
         // a "removed" event for the one we already have.
         let index = this.actors.findIndex(a => {
-          return a.player.constructor === player.constructor &&
-                 ((a.isAnimation() &&
-                   a.player.animationName === player.animationName) ||
-                  (a.isTransition() &&
-                   a.player.transitionProperty === player.transitionProperty));
+          let isSameType = a.player.constructor === player.constructor;
+          let isSameName = (a.isAnimation() &&
+                            a.player.animationName === player.animationName) ||
+                           (a.isTransition() &&
+                            a.player.transitionProperty === player.transitionProperty);
+          let isSameNode = a.player.effect.target === player.effect.target;
+
+          return isSameType && isSameNode && isSameName;
         });
         if (index !== -1) {
           eventData.push({
             type: "removed",
             player: this.actors[index]
           });
           this.actors.splice(index, 1);
         }