Merge m-c to inbound, a=merge
authorWes Kocher <wkocher@mozilla.com>
Tue, 15 Sep 2015 17:20:16 -0700
changeset 295312 ab6060fa1b89045e4ae3f2a9bcbde37d30ad8cda
parent 295311 36f11da8d2d9a649a0fafb4ce85ee41b552239ff (current diff)
parent 295237 df10a3f6060f208bc3f5f0e1d9965e083076a952 (diff)
child 295313 91103a26f5e462959d5135022676c2dba976b7ba
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)
reviewersmerge
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 m-c to inbound, a=merge
browser/base/content/test/general/browser_devices_get_user_media_in_frame.js
browser/components/search/test/browser_oneOffHeader.js
browser/devtools/netmonitor/netmonitor-controller.js
mobile/android/chrome/content/browser.js
mobile/android/modules/Accounts.jsm
toolkit/components/search/nsSearchService.js
--- 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="8d83715f08b7849f16a0dfc88f78d5c3a89c0a54">
     <copyfile dest="Makefile" src="core/root.mk"/>
   </project>
-  <project name="gaia" path="gaia" remote="mozillaorg" revision="d2e5c49440bf8410ae747b15c0dd11c54053ef3e"/>
+  <project name="gaia" path="gaia" remote="mozillaorg" revision="994ff1537c2d7ca4d1658806c50f3ceba1053f9b"/>
   <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="fb28def18ceb2516c460c4bd5825d2dc656c7818"/>
--- 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="8d83715f08b7849f16a0dfc88f78d5c3a89c0a54">
     <copyfile dest="Makefile" src="core/root.mk"/>
   </project>
-  <project name="gaia" path="gaia" remote="mozillaorg" revision="d2e5c49440bf8410ae747b15c0dd11c54053ef3e"/>
+  <project name="gaia" path="gaia" remote="mozillaorg" revision="994ff1537c2d7ca4d1658806c50f3ceba1053f9b"/>
   <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="fb28def18ceb2516c460c4bd5825d2dc656c7818"/>
--- a/b2g/config/emulator-ics/sources.xml
+++ b/b2g/config/emulator-ics/sources.xml
@@ -14,17 +14,17 @@
   <!--original fetch url was git://github.com/apitrace/-->
   <remote fetch="https://git.mozilla.org/external/apitrace" name="apitrace"/>
   <default remote="caf" revision="refs/tags/android-4.0.4_r2.1" sync-j="4"/>
   <!-- Gonk specific things and forks -->
   <project name="platform_build" path="build" remote="b2g" revision="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="d2e5c49440bf8410ae747b15c0dd11c54053ef3e"/>
+  <project name="gaia.git" path="gaia" remote="mozillaorg" revision="994ff1537c2d7ca4d1658806c50f3ceba1053f9b"/>
   <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="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"/>
--- 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="d2e5c49440bf8410ae747b15c0dd11c54053ef3e"/>
+  <project name="gaia" path="gaia" remote="mozillaorg" revision="994ff1537c2d7ca4d1658806c50f3ceba1053f9b"/>
   <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="fb28def18ceb2516c460c4bd5825d2dc656c7818"/>
   <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="8d83715f08b7849f16a0dfc88f78d5c3a89c0a54">
     <copyfile dest="Makefile" src="core/root.mk"/>
   </project>
-  <project name="gaia" path="gaia" remote="mozillaorg" revision="d2e5c49440bf8410ae747b15c0dd11c54053ef3e"/>
+  <project name="gaia" path="gaia" remote="mozillaorg" revision="994ff1537c2d7ca4d1658806c50f3ceba1053f9b"/>
   <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="fb28def18ceb2516c460c4bd5825d2dc656c7818"/>
--- a/b2g/config/emulator-l/sources.xml
+++ b/b2g/config/emulator-l/sources.xml
@@ -10,17 +10,17 @@
   <!--original fetch url was git://codeaurora.org/-->
   <remote fetch="https://git.mozilla.org/external/caf" name="caf"/>
   <!--original fetch url was https://git.mozilla.org/releases-->
   <remote fetch="https://git.mozilla.org/releases" name="mozillaorg"/>
   <!-- B2G specific things. -->
   <project name="platform_build" path="build" remote="b2g" revision="c9d4fe680662ee44a4bdea42ae00366f5df399cf">
     <copyfile dest="Makefile" src="core/root.mk"/>
   </project>
-  <project name="gaia" path="gaia" remote="mozillaorg" revision="d2e5c49440bf8410ae747b15c0dd11c54053ef3e"/>
+  <project name="gaia" path="gaia" remote="mozillaorg" revision="994ff1537c2d7ca4d1658806c50f3ceba1053f9b"/>
   <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="fb28def18ceb2516c460c4bd5825d2dc656c7818"/>
--- a/b2g/config/emulator/sources.xml
+++ b/b2g/config/emulator/sources.xml
@@ -14,17 +14,17 @@
   <!--original fetch url was git://github.com/apitrace/-->
   <remote fetch="https://git.mozilla.org/external/apitrace" name="apitrace"/>
   <default remote="caf" revision="refs/tags/android-4.0.4_r2.1" sync-j="4"/>
   <!-- Gonk specific things and forks -->
   <project name="platform_build" path="build" remote="b2g" revision="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="d2e5c49440bf8410ae747b15c0dd11c54053ef3e"/>
+  <project name="gaia.git" path="gaia" remote="mozillaorg" revision="994ff1537c2d7ca4d1658806c50f3ceba1053f9b"/>
   <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="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"/>
--- 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="8d83715f08b7849f16a0dfc88f78d5c3a89c0a54">
     <copyfile dest="Makefile" src="core/root.mk"/>
   </project>
-  <project name="gaia" path="gaia" remote="mozillaorg" revision="d2e5c49440bf8410ae747b15c0dd11c54053ef3e"/>
+  <project name="gaia" path="gaia" remote="mozillaorg" revision="994ff1537c2d7ca4d1658806c50f3ceba1053f9b"/>
   <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="fb28def18ceb2516c460c4bd5825d2dc656c7818"/>
--- a/b2g/config/gaia.json
+++ b/b2g/config/gaia.json
@@ -1,9 +1,9 @@
 {
     "git": {
-        "git_revision": "d2e5c49440bf8410ae747b15c0dd11c54053ef3e", 
+        "git_revision": "994ff1537c2d7ca4d1658806c50f3ceba1053f9b", 
         "remote": "https://git.mozilla.org/releases/gaia.git", 
         "branch": ""
     }, 
-    "revision": "4b08f8a2624bc83d1f839f79b4969671417c42d6", 
+    "revision": "9090c80639ae3689dddbefb8a76ba82c1268b63a", 
     "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="d2e5c49440bf8410ae747b15c0dd11c54053ef3e"/>
+  <project name="gaia" path="gaia" remote="mozillaorg" revision="994ff1537c2d7ca4d1658806c50f3ceba1053f9b"/>
   <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="fb28def18ceb2516c460c4bd5825d2dc656c7818"/>
   <project name="valgrind" path="external/valgrind" remote="b2g" revision="daa61633c32b9606f58799a3186395fd2bbb8d8c"/>
   <project name="vex" path="external/VEX" remote="b2g" revision="47f031c320888fe9f3e656602588565b52d43010"/>
   <!-- Stock Android things -->
   <project groups="linux" name="platform/prebuilts/clang/linux-x86/3.1" path="prebuilts/clang/linux-x86/3.1" revision="5c45f43419d5582949284eee9cef0c43d866e03b"/>
   <project groups="linux" name="platform/prebuilts/clang/linux-x86/3.2" path="prebuilts/clang/linux-x86/3.2" revision="3748b4168e7bd8d46457d4b6786003bc6a5223ce"/>
--- a/b2g/config/nexus-5-l/sources.xml
+++ b/b2g/config/nexus-5-l/sources.xml
@@ -10,17 +10,17 @@
   <!--original fetch url was git://codeaurora.org/-->
   <remote fetch="https://git.mozilla.org/external/caf" name="caf"/>
   <!--original fetch url was https://git.mozilla.org/releases-->
   <remote fetch="https://git.mozilla.org/releases" name="mozillaorg"/>
   <!-- B2G specific things. -->
   <project name="platform_build" path="build" remote="b2g" revision="c9d4fe680662ee44a4bdea42ae00366f5df399cf">
     <copyfile dest="Makefile" src="core/root.mk"/>
   </project>
-  <project name="gaia" path="gaia" remote="mozillaorg" revision="d2e5c49440bf8410ae747b15c0dd11c54053ef3e"/>
+  <project name="gaia" path="gaia" remote="mozillaorg" revision="994ff1537c2d7ca4d1658806c50f3ceba1053f9b"/>
   <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="fb28def18ceb2516c460c4bd5825d2dc656c7818"/>
--- a/browser/base/content/test/general/browser_devices_get_user_media_in_frame.js
+++ b/browser/base/content/test/general/browser_devices_get_user_media_in_frame.js
@@ -264,26 +264,33 @@ var gTests = [
       info("requesting devices");
       global.requestDevice(true, true);
     });
     expectObserverCalled("getUserMedia:request");
     checkDeviceSelectors(true, true);
 
     let indicator = promiseIndicatorWindow();
     yield promiseMessage("ok", () => {
-      PopupNotifications.panel.firstChild.button.click();
+      activateSecondaryAction(kActionAlways);
     });
     expectObserverCalled("getUserMedia:response:allow");
     expectObserverCalled("recording-device-events");
     is(getMediaCaptureState(), "CameraAndMicrophone",
        "expected camera and microphone to be shared");
 
     yield indicator;
     yield checkSharingUI({video: true, audio: true});
 
+    let Perms = Services.perms;
+    let uri = Services.io.newURI("https://example.com/", null, null);
+    is(Perms.testExactPermission(uri, "microphone"), Perms.ALLOW_ACTION,
+                                 "microphone persistently allowed");
+    is(Perms.testExactPermission(uri, "camera"), Perms.ALLOW_ACTION,
+                                 "camera persistently allowed");
+
     yield promiseNotificationShown(PopupNotifications.getNotification("webRTC-sharingDevices"));
     activateSecondaryAction(kActionDeny);
 
     yield promiseObserverCalled("recording-device-events");
     expectObserverCalled("getUserMedia:revoke");
 
     yield promiseNoPopupNotification("webRTC-sharingDevices");
     expectObserverCalled("recording-window-ended");
@@ -291,16 +298,22 @@ var gTests = [
     if (gObservedTopics["recording-device-events"] == 1) {
       todo(false, "Got the 'recording-device-events' notification twice, likely because of bug 962719");
       gObservedTopics["recording-device-events"] = 0;
     }
 
     expectNoObserverCalled();
     yield checkNotSharing();
 
+    // The persistent permissions for the frame should have been removed.
+    is(Perms.testExactPermission(uri, "microphone"), Perms.UNKNOWN_ACTION,
+                                 "microphone not persistently allowed");
+    is(Perms.testExactPermission(uri, "camera"), Perms.UNKNOWN_ACTION,
+                                 "camera not persistently allowed");
+
     // the stream is already closed, but this will do some cleanup anyway
     yield closeStream(global, true);
   }
 },
 
 {
   desc: "getUserMedia audio+video: reloading the frame removes all sharing UI",
   run: function checkReloading() {
--- a/browser/base/content/urlbarBindings.xml
+++ b/browser/base/content/urlbarBindings.xml
@@ -34,22 +34,22 @@ file, You can obtain one at http://mozil
                   class="textbox-input-box urlbar-input-box"
                   flex="1" xbl:inherits="tooltiptext=inputtooltiptext">
           <children/>
           <html:input anonid="input"
                       class="autocomplete-textbox urlbar-input textbox-input uri-element-right-align"
                       allowevents="true"
                       xbl:inherits="tooltiptext=inputtooltiptext,value,type,maxlength,disabled,size,readonly,placeholder,tabindex,accesskey"/>
         </xul:hbox>
+        <xul:dropmarker anonid="historydropmarker"
+                        class="autocomplete-history-dropmarker urlbar-history-dropmarker"
+                        allowevents="true"
+                        xbl:inherits="open,enablehistory,parentfocused=focused"/>
         <children includes="hbox"/>
       </xul:hbox>
-      <xul:dropmarker anonid="historydropmarker"
-                      class="autocomplete-history-dropmarker urlbar-history-dropmarker"
-                      allowevents="true"
-                      xbl:inherits="open,enablehistory,parentfocused=focused"/>
       <xul:popupset anonid="popupset"
                     class="autocomplete-result-popupset"/>
       <children includes="toolbarbutton"/>
     </content>
 
     <implementation implements="nsIObserver, nsIDOMEventListener">
       <field name="AppConstants" readonly="true">
         (Components.utils.import("resource://gre/modules/AppConstants.jsm", {})).AppConstants;
--- a/browser/components/search/content/search.xml
+++ b/browser/components/search/content/search.xml
@@ -63,24 +63,21 @@
 
     <implementation implements="nsIObserver">
       <constructor><![CDATA[
         if (this.parentNode.parentNode.localName == "toolbarpaletteitem")
           return;
         // Make sure we rebuild the popup in onpopupshowing
         this._needToBuildPopup = true;
 
-        var os =
-               Components.classes["@mozilla.org/observer-service;1"]
-                         .getService(Components.interfaces.nsIObserverService);
-        os.addObserver(this, "browser-search-engine-modified", false);
+        Services.obs.addObserver(this, "browser-search-engine-modified", false);
 
         this._initialized = true;
 
-        this.searchService.init((function search_init_cb(aStatus) {
+        Services.search.init((function search_init_cb(aStatus) {
           // Bail out if the binding's been destroyed
           if (!this._initialized)
             return;
 
           if (Components.isSuccessCode(aStatus)) {
             // Refresh the display (updating icon, etc)
             this.updateDisplay();
           } else {
@@ -93,19 +90,17 @@
         this.destroy();
       ]]></destructor>
 
       <method name="destroy">
         <body><![CDATA[
         if (this._initialized) {
           this._initialized = false;
 
-          var os = Components.classes["@mozilla.org/observer-service;1"]
-                             .getService(Components.interfaces.nsIObserverService);
-          os.removeObserver(this, "browser-search-engine-modified");
+          Services.obs.removeObserver(this, "browser-search-engine-modified");
         }
 
         // Make sure to break the cycle from _textbox to us. Otherwise we leak
         // the world. But make sure it's actually pointing to us.
         // Also make sure the textbox has ever been constructed, otherwise the
         // _textbox getter will cause the textbox constructor to run, add an
         // observer, and leak the world too.
         if (this._textboxInitialized && this._textbox.mController.input == this)
@@ -115,63 +110,50 @@
 
       <field name="_ignoreFocus">false</field>
       <field name="_clickClosedPopup">false</field>
       <field name="_stringBundle">document.getAnonymousElementByAttribute(this,
           "anonid", "searchbar-stringbundle");</field>
       <field name="_textboxInitialized">false</field>
       <field name="_textbox">document.getAnonymousElementByAttribute(this,
           "anonid", "searchbar-textbox");</field>
-      <field name="_ss">null</field>
       <field name="_engines">null</field>
       <field name="FormHistory" readonly="true">
         (Components.utils.import("resource://gre/modules/FormHistory.jsm", {})).FormHistory;
       </field>
       <field name="PlacesUtils" readonly="true">
         (Components.utils.import("resource://gre/modules/PlacesUtils.jsm", {})).PlacesUtils;
       </field>
 
       <property name="engines" readonly="true">
         <getter><![CDATA[
           if (!this._engines)
-            this._engines = this.searchService.getVisibleEngines();
+            this._engines = Services.search.getVisibleEngines();
           return this._engines;
         ]]></getter>
       </property>
 
       <property name="currentEngine">
         <setter><![CDATA[
-          let ss = this.searchService;
+          let ss = Services.search;
           ss.defaultEngine = ss.currentEngine = val;
           return val;
         ]]></setter>
         <getter><![CDATA[
-          var currentEngine = this.searchService.currentEngine;
+          var currentEngine = Services.search.currentEngine;
           // Return a dummy engine if there is no currentEngine
           return currentEngine || {name: "", uri: null};
         ]]></getter>
       </property>
 
       <!-- textbox is used by sanitize.js to clear the undo history when
            clearing form information. -->
       <property name="textbox" readonly="true"
                 onget="return this._textbox;"/>
 
-      <property name="searchService" readonly="true">
-        <getter><![CDATA[
-          if (!this._ss) {
-            const nsIBSS = Components.interfaces.nsIBrowserSearchService;
-            this._ss =
-                 Components.classes["@mozilla.org/browser/search-service;1"]
-                           .getService(nsIBSS);
-          }
-          return this._ss;
-        ]]></getter>
-      </property>
-
       <property name="value" onget="return this._textbox.value;"
                              onset="return this._textbox.value = val;"/>
 
       <method name="focus">
         <body><![CDATA[
           this._textbox.focus();
         ]]></body>
       </method>
@@ -361,17 +343,17 @@
 
           // Open ctrl/cmd clicks on one-off buttons in a new background tab.
           if (aEvent && aEvent.originalTarget.getAttribute("anonid") == "search-go-button") {
             if (aEvent.button == 2)
               return;
             where = whereToOpenLink(aEvent, false, true);
           }
           else {
-            var newTabPref = textBox._prefBranch.getBoolPref("browser.search.openintab");
+            var newTabPref = Services.prefs.getBoolPref("browser.search.openintab");
             if (((aEvent instanceof KeyboardEvent) && aEvent.altKey) ^ newTabPref)
               where = "tab";
             if ((aEvent instanceof MouseEvent) &&
                 (aEvent.button == 1 || aEvent.getModifierState("Accel"))) {
               where = "tab-background";
             }
           }
 
@@ -451,28 +433,25 @@
     </implementation>
 
     <handlers>
       <handler event="command"><![CDATA[
         const target = event.originalTarget;
         if (target.engine) {
           this.currentEngine = target.engine;
         } else if (target.classList.contains("addengine-item")) {
-          var searchService =
-            Components.classes["@mozilla.org/browser/search-service;1"]
-                      .getService(Components.interfaces.nsIBrowserSearchService);
           // We only detect OpenSearch files
-          var type = Components.interfaces.nsISearchEngine.DATA_XML;
+          var type = Ci.nsISearchEngine.DATA_XML;
           // Select the installed engine if the installation succeeds
           var installCallback = {
             onSuccess: engine => this.currentEngine = engine
           }
-          searchService.addEngine(target.getAttribute("uri"), type,
-                                  target.getAttribute("src"), false,
-                                  installCallback);
+          Services.search.addEngine(target.getAttribute("uri"), type,
+                                    target.getAttribute("src"), false,
+                                    installCallback);
         }
         else
           return;
 
         this.focus();
         this.select();
       ]]></handler>
 
@@ -554,23 +533,20 @@
           "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul";
 
         if (document.getBindingParent(this).parentNode.parentNode.localName ==
             "toolbarpaletteitem")
           return;
 
         // Initialize fields
         this._stringBundle = document.getBindingParent(this)._stringBundle;
-        this._prefBranch =
-                  Components.classes["@mozilla.org/preferences-service;1"]
-                            .getService(Components.interfaces.nsIPrefBranch);
         this._suggestEnabled =
-            this._prefBranch.getBoolPref("browser.search.suggest.enabled");
+          Services.prefs.getBoolPref("browser.search.suggest.enabled");
 
-        if (this._prefBranch.getBoolPref("browser.urlbar.clickSelectsAll"))
+        if (Services.prefs.getBoolPref("browser.urlbar.clickSelectsAll"))
           this.setAttribute("clickSelectsAll", true);
 
         // Add items to context menu and attach controller to handle them
         var textBox = document.getAnonymousElementByAttribute(this,
                                               "anonid", "textbox-input-box");
         var cxmenu = document.getAnonymousElementByAttribute(textBox,
                                           "anonid", "input-box-contextmenu");
         var pasteAndSearch;
@@ -632,35 +608,30 @@
           if (navigator.platform.startsWith("Mac") && aEvent.keyCode == KeyEvent.VK_F4)
             this.openSearch()
         }, true);
 
         this.controllers.appendController(this.searchbarController);
         document.getBindingParent(this)._textboxInitialized = true;
 
         // Add observer for suggest preference
-        var prefs = Components.classes["@mozilla.org/preferences-service;1"]
-                            .getService(Components.interfaces.nsIPrefBranch);
-        prefs.addObserver("browser.search.suggest.enabled", this, false);
+        Services.prefs.addObserver("browser.search.suggest.enabled", this, false);
       ]]></constructor>
 
       <destructor><![CDATA[
-          var prefs = Components.classes["@mozilla.org/preferences-service;1"]
-                              .getService(Components.interfaces.nsIPrefBranch);
-          prefs.removeObserver("browser.search.suggest.enabled", this);
+        Services.prefs.removeObserver("browser.search.suggest.enabled", this);
 
         // Because XBL and the customize toolbar code interacts poorly,
         // there may not be anything to remove here
         try {
           this.controllers.removeController(this.searchbarController);
         } catch (ex) { }
       ]]></destructor>
 
       <field name="_stringBundle"/>
-      <field name="_prefBranch"/>
       <field name="_suggestMenuItem"/>
       <field name="_suggestEnabled"/>
 
       <!--
         This overrides the searchParam property in autocomplete.xml.  We're
         hijacking this property as a vehicle for delivering the privacy
         information about the window into the guts of nsSearchSuggestions.
 
@@ -693,17 +664,17 @@
             popup.hidden = false;
 
             // Don't roll up on mouse click in the anchor for the search UI.
             if (popup.id == "PopupSearchAutoComplete") {
               popup.setAttribute("norolluponanchor", "true");
             }
 
             popup.mInput = this;
-            popup.view = this.controller.QueryInterface(Components.interfaces.nsITreeView);
+            popup.view = this.controller.QueryInterface(Ci.nsITreeView);
             popup.invalidate();
 
             popup.showCommentColumn = this.showCommentColumn;
             popup.showImageColumn = this.showImageColumn;
 
             document.popupNode = null;
 
             const isRTL = getComputedStyle(this, "").direction == "rtl";
@@ -725,17 +696,17 @@
 
       <method name="observe">
         <parameter name="aSubject"/>
         <parameter name="aTopic"/>
         <parameter name="aData"/>
         <body><![CDATA[
           if (aTopic == "nsPref:changed") {
             this._suggestEnabled =
-              this._prefBranch.getBoolPref("browser.search.suggest.enabled");
+              Services.prefs.getBoolPref("browser.search.suggest.enabled");
             this._suggestMenuItem.setAttribute("checked", this._suggestEnabled);
           }
         ]]></body>
       </method>
 
       <method name="openSearch">
         <body>
           <![CDATA[
@@ -751,17 +722,17 @@
       <!-- override |onTextEntered| in autocomplete.xml -->
       <method name="onTextEntered">
         <parameter name="aEvent"/>
         <body><![CDATA[
           var evt = aEvent || this.mEnterEvent;
 
           let engine;
           let oneOff = this.selectedButton;
-          if (oneOff) {
+          if (oneOff && !this.selectionFromMouseOver) {
             if (!oneOff.engine) {
               oneOff.doCommand();
               this.mEnterEvent = null;
               return;
             }
             engine = oneOff.engine;
           }
           if (this.mEnterEvent && this._selectionDetails &&
@@ -773,16 +744,19 @@
 
           this.mEnterEvent = null;
         ]]></body>
       </method>
 
       <field name="_selectedButton"/>
       <property name="selectedButton" onget="return this._selectedButton;">
         <setter><![CDATA[
+          // Set to true from the mouseover handler right after this setter call.
+          this.selectionFromMouseOver = false;
+
           if (this._selectedButton)
             this._selectedButton.removeAttribute("selected");
 
           let textbox = document.getBindingParent(this).textbox;
           let header =
             document.getAnonymousElementByAttribute(this.popup, "anonid",
                                                     "search-panel-one-offs-header");
           // Avoid selecting dummy buttons.
@@ -969,18 +943,18 @@
               let searchBar = this._self.parentNode;
 
               BrowserSearch.searchBar.FormHistory.update({ op : "remove", fieldname : param }, null);
               this._self.value = "";
               break;
             case "cmd_togglesuggest":
               // The pref observer will update _suggestEnabled and the menu
               // checkmark.
-              this._self._prefBranch.setBoolPref("browser.search.suggest.enabled",
-                                                 !this._self._suggestEnabled);
+              Services.prefs.setBoolPref("browser.search.suggest.enabled",
+                                         !this._self._suggestEnabled);
               break;
             default:
               // do nothing with unrecognized command
           }
         }
       })]]></field>
     </implementation>
 
@@ -1164,16 +1138,20 @@
         let textbox = searchbar.textbox;
         let self = this;
         let inputHandler = function() {
           headerSearchText.setAttribute("value", textbox.value);
           let groupText;
           let isOneOffSelected =
             this.selectedButton &&
             this.selectedButton.classList.contains("searchbar-engine-one-off-item");
+          // Typing de-selects the settings or opensearch buttons at the bottom
+          // of the search panel, as typing shows the user intends to search.
+          if (this.selectedButton && !isOneOffSelected)
+            this.selectedButton = null;
           if (textbox.value) {
             self.removeAttribute("showonlysettings");
             groupText = headerSearchText.previousSibling.value +
                         '"' + headerSearchText.value + '"' +
                         headerSearchText.nextSibling.value;
             if (!isOneOffSelected)
               headerPanel.selectedIndex = 1;
           }
@@ -1227,20 +1205,16 @@
             addEngineList.appendChild(button);
           }
         }
 
         // Finally, build the list of one-off buttons.
         while (list.firstChild)
           list.firstChild.remove();
 
-        // Avoid setting the selection based on mouse events before
-        // the 'popupshown' event has fired.
-        this._ignoreMouseEvents = true;
-
         let Preferences =
           Cu.import("resource://gre/modules/Preferences.jsm", {}).Preferences;
         let pref = Preferences.get("browser.search.hiddenOneOffs");
         let hiddenList = pref ? pref.split(",") : [];
 
         let currentEngineName = Services.search.currentEngine.name;
         let engines = Services.search.getVisibleEngines()
                               .filter(e => e.name != currentEngineName &&
@@ -1322,41 +1296,34 @@
 
           if (!--dummyItems)
             button.classList.add("last-of-row");
 
           list.appendChild(button);
         }
       ]]></handler>
 
-      <handler event="popupshown"><![CDATA[
-        this._ignoreMouseEvents = false;
-      ]]></handler>
-
       <handler event="mousedown"><![CDATA[
         // Required to receive click events from the buttons on Linux.
         event.preventDefault();
       ]]></handler>
 
       <handler event="mouseover"><![CDATA[
         let target = event.originalTarget;
         if (target.localName != "button")
           return;
 
-        // We ignore mouse events between the popupshowing and popupshown
-        // events to avoid selecting the button that happens to be under the
-        // mouse when the panel opens.
-        if (this._ignoreMouseEvents)
-          return;
-
         if ((target.classList.contains("searchbar-engine-one-off-item") &&
              !target.classList.contains("dummy")) ||
             target.classList.contains("addengine-item") ||
-            target.classList.contains("search-setting-button"))
-          document.getElementById("searchbar").textbox.selectedButton = target;
+            target.classList.contains("search-setting-button")) {
+          let textbox = document.getElementById("searchbar").textbox;
+          textbox.selectedButton = target;
+          textbox.selectionFromMouseOver = true;
+        }
       ]]></handler>
 
       <handler event="mouseout"><![CDATA[
         let target = event.originalTarget;
         if (target.localName != "button")
           return;
 
         let textbox = document.getElementById("searchbar").textbox;
--- a/browser/components/search/test/browser_oneOffHeader.js
+++ b/browser/components/search/test/browser_oneOffHeader.js
@@ -133,9 +133,12 @@ add_task(function* test_text() {
      "Header has the correct index selected when search terms have been entered and the Change Search Settings button is selected.");
   is(getHeaderText(), "Search for foo with:",
      "Header has the correct text when search terms have been entered and the Change Search Settings button is selected.");
 
   promise = promiseEvent(searchPopup, "popuphidden");
   info("Closing search panel");
   EventUtils.synthesizeKey("VK_ESCAPE", {});
   yield promise;
+
+  // Move the cursor out of the panel area to avoid messing with other tests.
+  yield synthesizeNativeMouseMove(searchbar);
 });
--- a/browser/components/search/test/browser_searchbar_keyboard_navigation.js
+++ b/browser/components/search/test/browser_searchbar_keyboard_navigation.js
@@ -147,16 +147,33 @@ add_task(function* test_arrows() {
     EventUtils.synthesizeKey("VK_UP", {});
   }
 
   is(searchPopup.selectedIndex, -1, "no suggestion should be selected");
   is(textbox.value, kUserValue,
      "the textfield value should be back to initial value");
 });
 
+add_task(function* test_typing_clears_button_selection() {
+  is(Services.focus.focusedElement, textbox.inputField,
+     "the search bar should be focused"); // from the previous test.
+  ok(!textbox.selectedButton, "no button should be selected");
+
+  EventUtils.synthesizeKey("VK_UP", {});
+  is(textbox.selectedButton.getAttribute("anonid"), "search-settings",
+     "the settings item should be selected");
+
+  // Type a character.
+  EventUtils.synthesizeKey("a", {});
+  ok(!textbox.selectedButton, "the settings item should be de-selected");
+
+  // Remove the character.
+  EventUtils.synthesizeKey("VK_BACK_SPACE", {});
+});
+
 add_task(function* test_tab() {
   is(Services.focus.focusedElement, textbox.inputField,
      "the search bar should be focused"); // from the previous test.
 
   let oneOffs = getOneOffs();
   ok(!textbox.selectedButton, "no one-off button should be selected");
 
   // Pressing tab should select the first one-off without selecting suggestions.
--- a/browser/devtools/netmonitor/netmonitor-controller.js
+++ b/browser/devtools/netmonitor/netmonitor-controller.js
@@ -333,19 +333,26 @@ var NetMonitorController = {
   inspectRequest: function(requestId) {
     // Look for the request in the existing ones or wait for it to appear, if
     // the network monitor is still loading.
     let deferred = promise.defer();
     let request = null;
     let inspector = function() {
       let predicate = i => i.value === requestId;
       request = NetMonitorView.RequestsMenu.getItemForPredicate(predicate);
+      if (!request) {
+        // Reset filters so that the request is visible.
+        NetMonitorView.RequestsMenu.filterOn("all");
+        request = NetMonitorView.RequestsMenu.getItemForPredicate(predicate);
+      }
+
+      // If the request was found, select it. Otherwise this function will be
+      // called again once new requests arrive.
       if (request) {
         window.off(EVENTS.REQUEST_ADDED, inspector);
-        NetMonitorView.RequestsMenu.filterOn("all");
         NetMonitorView.RequestsMenu.selectedItem = request;
         deferred.resolve();
       }
     }
 
     inspector();
     if (!request) {
       window.on(EVENTS.REQUEST_ADDED, inspector);
--- a/browser/devtools/webconsole/test/browser.ini
+++ b/browser/devtools/webconsole/test/browser.ini
@@ -323,16 +323,17 @@ skip-if = buildapp == 'mulet' || e10s # 
 [browser_webconsole_inspect-parsed-documents.js]
 [browser_webconsole_js_input_expansion.js]
 [browser_webconsole_jsterm.js]
 skip-if = e10s # Bug 1042253 - webconsole e10s tests (Linux debug timeout)
 [browser_webconsole_live_filtering_of_message_types.js]
 [browser_webconsole_live_filtering_on_search_strings.js]
 [browser_webconsole_message_node_id.js]
 [browser_webconsole_netlogging.js]
+[browser_webconsole_netlogging_reset_filter.js]
 [browser_webconsole_notifications.js]
 [browser_webconsole_open-links-without-callback.js]
 [browser_webconsole_promise.js]
 [browser_webconsole_output_copy_newlines.js]
 [browser_webconsole_output_order.js]
 [browser_webconsole_property_provider.js]
 skip-if = e10s # Bug 1042253 - webconsole tests disabled with e10s
 [browser_webconsole_scratchpad_panel_link.js]
new file mode 100644
--- /dev/null
+++ b/browser/devtools/webconsole/test/browser_webconsole_netlogging_reset_filter.js
@@ -0,0 +1,91 @@
+/* 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/. */
+
+// Tests that network log messages bring up the network panel and select the
+// right request even if it was previously filtered off.
+
+"use strict";
+
+const TEST_FILE_URI = "http://example.com/browser/browser/" +
+                      "devtools/webconsole/test/test-network.html";
+const TEST_URI = "data:text/html;charset=utf8,<p>test file URI";
+
+let hud;
+
+let test = asyncTest(function* () {
+  let requests = [];
+  let { browser } = yield loadTab(TEST_URI);
+
+  yield pushPrefEnv();
+  hud = yield openConsole();
+  hud.jsterm.clearOutput();
+
+  HUDService.lastFinishedRequest.callback = request => requests.push(request);
+
+  let loaded = loadBrowser(browser);
+  content.location = TEST_FILE_URI;
+  yield loaded;
+
+  yield testMessages();
+  let htmlRequest = requests.find(e => e.request.url.endsWith("html"));
+  ok(htmlRequest, "htmlRequest was a html");
+
+  yield hud.ui.openNetworkPanel(htmlRequest.actor);
+  let toolbox = gDevTools.getToolbox(hud.target);
+  is(toolbox.currentToolId, "netmonitor", "Network panel was opened");
+
+  let panel = toolbox.getCurrentPanel();
+  let selected = panel.panelWin.NetMonitorView.RequestsMenu.selectedItem;
+  is(selected.attachment.method, htmlRequest.request.method,
+     "The correct request is selected");
+  is(selected.attachment.url, htmlRequest.request.url,
+     "The correct request is definitely selected");
+
+  // Filter out the HTML request.
+  panel.panelWin.NetMonitorView.RequestsMenu.filterOn("js");
+
+  yield toolbox.selectTool("webconsole");
+  is(toolbox.currentToolId, "webconsole", "Web console was selected");
+  yield hud.ui.openNetworkPanel(htmlRequest.actor);
+
+  panel.panelWin.NetMonitorView.RequestsMenu.selectedItem;
+  is(selected.attachment.method, htmlRequest.request.method,
+     "The correct request is selected");
+  is(selected.attachment.url, htmlRequest.request.url,
+     "The correct request is definitely selected");
+
+  // All tests are done. Shutdown.
+  HUDService.lastFinishedRequest.callback = null;
+  htmlRequest = browser = requests = hud = null;
+});
+
+function testMessages() {
+  return waitForMessages({
+    webconsole: hud,
+    messages: [{
+      text: "running network console logging tests",
+      category: CATEGORY_WEBDEV,
+      severity: SEVERITY_LOG,
+    },
+    {
+      text: "test-network.html",
+      category: CATEGORY_NETWORK,
+      severity: SEVERITY_LOG,
+    },
+    {
+      text: "testscript.js",
+      category: CATEGORY_NETWORK,
+      severity: SEVERITY_LOG,
+    }],
+  });
+}
+
+function pushPrefEnv() {
+  let deferred = promise.defer();
+  let options = {
+    set: [["devtools.webconsole.filter.networkinfo", true]]
+  };
+  SpecialPowers.pushPrefEnv(options, deferred.resolve);
+  return deferred.promise;
+}
--- a/browser/modules/webrtcUI.jsm
+++ b/browser/modules/webrtcUI.jsm
@@ -388,17 +388,26 @@ function prompt(aBrowser, aRequest) {
         // and will grant audio access immediately.
         if ((!audioDevices.length || micPerm) && (!videoDevices.length || camPerm)) {
           // All permissions we were about to request are already persistently set.
           let allowedDevices = [];
           if (videoDevices.length && camPerm == perms.ALLOW_ACTION)
             allowedDevices.push(videoDevices[0].deviceIndex);
           if (audioDevices.length && micPerm == perms.ALLOW_ACTION)
             allowedDevices.push(audioDevices[0].deviceIndex);
-          let mm = this.browser.messageManager;
+
+          // Remember on which URIs we found persistent permissions so that we
+          // can remove them if the user clicks 'Stop Sharing'. There's no
+          // other way for the stop sharing code to know the hostnames of frames
+          // using devices until bug 1066082 is fixed.
+          let browser = this.browser;
+          browser._devicePermissionURIs = browser._devicePermissionURIs || [];
+          browser._devicePermissionURIs.push(uri);
+
+          let mm = browser.messageManager;
           mm.sendAsyncMessage("webrtc:Allow", {callID: aRequest.callID,
                                                windowID: aRequest.windowID,
                                                devices: allowedDevices});
           this.remove();
           return true;
         }
       }
 
@@ -529,16 +538,23 @@ function prompt(aBrowser, aRequest) {
           }
         }
 
         if (!allowedDevices.length) {
           denyRequest(notification.browser, aRequest);
           return;
         }
 
+        if (aRemember) {
+          // Remember on which URIs we set persistent permissions so that we
+          // can remove them if the user clicks 'Stop Sharing'.
+          aBrowser._devicePermissionURIs = aBrowser._devicePermissionURIs || [];
+          aBrowser._devicePermissionURIs.push(uri);
+        }
+
         let mm = notification.browser.messageManager;
         mm.sendAsyncMessage("webrtc:Allow", {callID: aRequest.callID,
                                              windowID: aRequest.windowID,
                                              devices: allowedDevices});
       };
       return false;
     }
   };
@@ -857,25 +873,27 @@ function updateBrowserSpecificIndicator(
     accessKey: stringBundle.getString("getUserMedia.continueSharing.accesskey"),
     callback: function () {},
     dismiss: true
   };
   let secondaryActions = [{
     label: stringBundle.getString("getUserMedia.stopSharing.label"),
     accessKey: stringBundle.getString("getUserMedia.stopSharing.accesskey"),
     callback: function () {
-      let uri = Services.io.newURI(aState.documentURI, null, null);
+      let uris = aBrowser._devicePermissionURIs || [];
+      uris = uris.concat(Services.io.newURI(aState.documentURI, null, null));
       let perms = Services.perms;
-      if (aState.camera &&
-          perms.testExactPermission(uri, "camera") == perms.ALLOW_ACTION)
-        perms.remove(uri, "camera");
-      if (aState.microphone &&
-          perms.testExactPermission(uri, "microphone") == perms.ALLOW_ACTION)
-        perms.remove(uri, "microphone");
-
+      for (let uri of uris) {
+        if (aState.camera &&
+            perms.testExactPermission(uri, "camera") == perms.ALLOW_ACTION)
+          perms.remove(uri, "camera");
+        if (aState.microphone &&
+            perms.testExactPermission(uri, "microphone") == perms.ALLOW_ACTION)
+          perms.remove(uri, "microphone");
+      }
       let mm = notification.browser.messageManager;
       mm.sendAsyncMessage("webrtc:StopSharing", windowId);
     }
   }];
   let options = {
     hideNotNow: true,
     dismissed: true,
     eventCallback: function(aTopic, aNewBrowser) {
@@ -897,22 +915,23 @@ function updateBrowserSpecificIndicator(
     let anchorId = captureState == "Microphone" ? "webRTC-sharingMicrophone-notification-icon"
                                                 : "webRTC-sharingDevices-notification-icon";
     let message = stringBundle.getString("getUserMedia.sharing" + captureState + ".message2");
     notification =
       chromeWin.PopupNotifications.show(aBrowser, "webRTC-sharingDevices", message,
                                         anchorId, mainAction, secondaryActions, options);
   }
   else {
-    removeBrowserNotification(aBrowser,"webRTC-sharingDevices");
+    removeBrowserNotification(aBrowser, "webRTC-sharingDevices");
+    aBrowser._devicePermissionURIs = null;
   }
 
   // Now handle the screen sharing indicator.
   if (!aState.screen) {
-    removeBrowserNotification(aBrowser,"webRTC-sharingScreen");
+    removeBrowserNotification(aBrowser, "webRTC-sharingScreen");
     return;
   }
 
   let screenSharingNotif; // Used by action callbacks.
   let isBrowserSharing = aState.screen == "Browser";
   options = {
     hideNotNow: !isBrowserSharing,
     dismissed: true,
--- a/browser/themes/linux/browser.css
+++ b/browser/themes/linux/browser.css
@@ -885,16 +885,21 @@ toolbarbutton[constrain-size="true"][cui
 }
 
 .urlbar-input-box {
   -moz-margin-start: 0;
 }
 
 .urlbar-history-dropmarker {
   -moz-appearance: toolbarbutton-dropdown;
+  transition: opacity 0.15s ease;
+}
+
+#urlbar:not(:hover) > .urlbar-textbox-container > .urlbar-history-dropmarker {
+  opacity: 0;
 }
 
 #urlbar-container {
   -moz-box-align: center;
 }
 
 @conditionalForwardWithUrlbar@ > #urlbar {
   -moz-border-start: none;
--- a/browser/themes/osx/browser.css
+++ b/browser/themes/osx/browser.css
@@ -1694,16 +1694,21 @@ toolbarbutton[constrain-size="true"][cui
   -moz-margin-start: 0;
   padding: 3px 0 2px;
 }
 
 .urlbar-history-dropmarker {
   padding: 0 3px;
   list-style-image: var(--urlbar-dropmarker-url);
   -moz-image-region: var(--urlbar-dropmarker-region);
+  transition: opacity 0.15s ease;
+}
+
+#urlbar:not(:hover) > .urlbar-textbox-container > .urlbar-history-dropmarker {
+  opacity: 0;
 }
 
 .urlbar-history-dropmarker[open="true"],
 .urlbar-history-dropmarker:hover:active {
   -moz-image-region: var(--urlbar-dropmarker-active-region);
 }
 
 @media (min-resolution: 2dppx) {
--- a/browser/themes/windows/browser.css
+++ b/browser/themes/windows/browser.css
@@ -1471,16 +1471,21 @@ html|*.urlbar-input:-moz-lwtheme::-moz-p
 .urlbar-history-dropmarker {
   -moz-appearance: none;
   padding: 0 3px;
   background-color: transparent;
   border: none;
   width: auto;
   list-style-image: var(--urlbar-dropmarker-url);
   -moz-image-region: var(--urlbar-dropmarker-region);
+  transition: opacity 0.15s ease;
+}
+
+#urlbar:not(:hover) > .urlbar-textbox-container > .urlbar-history-dropmarker {
+  opacity: 0;
 }
 
 .urlbar-history-dropmarker:hover {
   -moz-image-region: var(--urlbar-dropmarker-hover-region);
 }
 
 .urlbar-history-dropmarker:hover:active,
 .urlbar-history-dropmarker[open="true"] {
--- a/dom/bluetooth/bluedroid/BluetoothDaemonA2dpInterface.cpp
+++ b/dom/bluetooth/bluedroid/BluetoothDaemonA2dpInterface.cpp
@@ -23,45 +23,34 @@ BluetoothA2dpNotificationHandler*
 
 void
 BluetoothDaemonA2dpModule::SetNotificationHandler(
   BluetoothA2dpNotificationHandler* aNotificationHandler)
 {
   sNotificationHandler = aNotificationHandler;
 }
 
-nsresult
-BluetoothDaemonA2dpModule::Send(DaemonSocketPDU* aPDU,
-                                BluetoothA2dpResultHandler* aRes)
-{
-  nsRefPtr<BluetoothA2dpResultHandler> res(aRes);
-  nsresult rv = Send(aPDU, static_cast<void*>(res.get()));
-  if (NS_FAILED(rv)) {
-    return rv;
-  }
-  unused << res.forget(); // Keep reference for response
-  return NS_OK;
-}
-
 void
 BluetoothDaemonA2dpModule::HandleSvc(const DaemonSocketPDUHeader& aHeader,
-                                     DaemonSocketPDU& aPDU, void* aUserData)
+                                     DaemonSocketPDU& aPDU,
+                                     DaemonSocketResultHandler* aRes)
 {
   static void (BluetoothDaemonA2dpModule::* const HandleOp[])(
-    const DaemonSocketPDUHeader&, DaemonSocketPDU&, void*) = {
+    const DaemonSocketPDUHeader&, DaemonSocketPDU&,
+    DaemonSocketResultHandler*) = {
     [0] = &BluetoothDaemonA2dpModule::HandleRsp,
     [1] = &BluetoothDaemonA2dpModule::HandleNtf
   };
 
   MOZ_ASSERT(!NS_IsMainThread());
 
   // negate twice to map bit to 0/1
   unsigned int isNtf = !!(aHeader.mOpcode & 0x80);
 
-  (this->*(HandleOp[isNtf]))(aHeader, aPDU, aUserData);
+  (this->*(HandleOp[isNtf]))(aHeader, aPDU, aRes);
 }
 
 // Commands
 //
 
 nsresult
 BluetoothDaemonA2dpModule::ConnectCmd(
   const nsAString& aRemoteAddr, BluetoothA2dpResultHandler* aRes)
@@ -134,17 +123,17 @@ BluetoothDaemonA2dpModule::DisconnectRsp
 {
   ResultRunnable::Dispatch(
     aRes, &BluetoothA2dpResultHandler::Disconnect, UnpackPDUInitOp(aPDU));
 }
 
 void
 BluetoothDaemonA2dpModule::HandleRsp(
   const DaemonSocketPDUHeader& aHeader, DaemonSocketPDU& aPDU,
-  void* aUserData)
+  DaemonSocketResultHandler* aRes)
 {
   static void (BluetoothDaemonA2dpModule::* const HandleRsp[])(
     const DaemonSocketPDUHeader&,
     DaemonSocketPDU&,
     BluetoothA2dpResultHandler*) = {
     [OPCODE_ERROR] = &BluetoothDaemonA2dpModule::ErrorRsp,
     [OPCODE_CONNECT] = &BluetoothDaemonA2dpModule::ConnectRsp,
     [OPCODE_DISCONNECT] = &BluetoothDaemonA2dpModule::DisconnectRsp
@@ -153,18 +142,17 @@ BluetoothDaemonA2dpModule::HandleRsp(
   MOZ_ASSERT(!NS_IsMainThread()); // I/O thread
 
   if (NS_WARN_IF(!(aHeader.mOpcode < MOZ_ARRAY_LENGTH(HandleRsp))) ||
       NS_WARN_IF(!HandleRsp[aHeader.mOpcode])) {
     return;
   }
 
   nsRefPtr<BluetoothA2dpResultHandler> res =
-    already_AddRefed<BluetoothA2dpResultHandler>(
-      static_cast<BluetoothA2dpResultHandler*>(aUserData));
+    static_cast<BluetoothA2dpResultHandler*>(aRes);
 
   if (!res) {
     return; // Return early if no result handler has been set for response
   }
 
   (this->*(HandleRsp[aHeader.mOpcode]))(aHeader, aPDU, res);
 }
 
@@ -310,17 +298,17 @@ BluetoothDaemonA2dpModule::AudioConfigNt
   AudioConfigNotification::Dispatch(
     &BluetoothA2dpNotificationHandler::AudioConfigNotification,
     AudioConfigInitOp(aPDU));
 }
 
 void
 BluetoothDaemonA2dpModule::HandleNtf(
   const DaemonSocketPDUHeader& aHeader, DaemonSocketPDU& aPDU,
-  void* aUserData)
+  DaemonSocketResultHandler* aRes)
 {
   static void (BluetoothDaemonA2dpModule::* const HandleNtf[])(
     const DaemonSocketPDUHeader&, DaemonSocketPDU&) = {
     [0] = &BluetoothDaemonA2dpModule::ConnectionStateNtf,
     [1] = &BluetoothDaemonA2dpModule::AudioStateNtf,
 #if ANDROID_VERSION >= 21
     [2] = &BluetoothDaemonA2dpModule::AudioConfigNtf
 #endif
--- a/dom/bluetooth/bluedroid/BluetoothDaemonA2dpInterface.h
+++ b/dom/bluetooth/bluedroid/BluetoothDaemonA2dpInterface.h
@@ -10,16 +10,17 @@
 #include "BluetoothDaemonHelpers.h"
 #include "BluetoothInterface.h"
 #include "mozilla/ipc/DaemonRunnables.h"
 
 BEGIN_BLUETOOTH_NAMESPACE
 
 using mozilla::ipc::DaemonSocketPDU;
 using mozilla::ipc::DaemonSocketPDUHeader;
+using mozilla::ipc::DaemonSocketResultHandler;
 
 class BluetoothSetupResultHandler;
 
 class BluetoothDaemonA2dpModule
 {
 public:
   enum {
     SERVICE_ID = 0x06
@@ -28,17 +29,18 @@ public:
   enum {
     OPCODE_ERROR = 0x00,
     OPCODE_CONNECT = 0x01,
     OPCODE_DISCONNECT = 0x02
   };
 
   static const int MAX_NUM_CLIENTS;
 
-  virtual nsresult Send(DaemonSocketPDU* aPDU, void* aUserData) = 0;
+  virtual nsresult Send(DaemonSocketPDU* aPDU,
+                        DaemonSocketResultHandler* aRes) = 0;
 
   virtual nsresult RegisterModule(uint8_t aId, uint8_t aMode,
                                   uint32_t aMaxNumClients,
                                   BluetoothSetupResultHandler* aRes) = 0;
 
   virtual nsresult UnregisterModule(uint8_t aId,
                                     BluetoothSetupResultHandler* aRes) = 0;
 
@@ -50,21 +52,18 @@ public:
   //
 
   nsresult ConnectCmd(const nsAString& aBdAddr,
                       BluetoothA2dpResultHandler* aRes);
   nsresult DisconnectCmd(const nsAString& aBdAddr,
                          BluetoothA2dpResultHandler* aRes);
 
 protected:
-  nsresult Send(DaemonSocketPDU* aPDU,
-                BluetoothA2dpResultHandler* aRes);
-
   void HandleSvc(const DaemonSocketPDUHeader& aHeader,
-                 DaemonSocketPDU& aPDU, void* aUserData);
+                 DaemonSocketPDU& aPDU, DaemonSocketResultHandler* aRes);
 
   //
   // Responses
   //
 
   typedef mozilla::ipc::DaemonResultRunnable0<
     BluetoothA2dpResultHandler, void>
     ResultRunnable;
@@ -82,17 +81,17 @@ protected:
                   BluetoothA2dpResultHandler* aRes);
 
   void DisconnectRsp(const DaemonSocketPDUHeader& aHeader,
                      DaemonSocketPDU& aPDU,
                      BluetoothA2dpResultHandler* aRes);
 
   void HandleRsp(const DaemonSocketPDUHeader& aHeader,
                  DaemonSocketPDU& aPDU,
-                 void* aUserData);
+                 DaemonSocketResultHandler* aRes);
 
   //
   // Notifications
   //
 
   class NotificationHandlerWrapper;
 
   typedef mozilla::ipc::DaemonNotificationRunnable2<
@@ -120,42 +119,42 @@ protected:
   void AudioStateNtf(const DaemonSocketPDUHeader& aHeader,
                      DaemonSocketPDU& aPDU);
 
   void AudioConfigNtf(const DaemonSocketPDUHeader& aHeader,
                       DaemonSocketPDU& aPDU);
 
   void HandleNtf(const DaemonSocketPDUHeader& aHeader,
                  DaemonSocketPDU& aPDU,
-                 void* aUserData);
+                 DaemonSocketResultHandler* aRes);
 
   static BluetoothA2dpNotificationHandler* sNotificationHandler;
 };
 
 class BluetoothDaemonA2dpInterface final
   : public BluetoothA2dpInterface
 {
   class CleanupResultHandler;
   class InitResultHandler;
 
 public:
   BluetoothDaemonA2dpInterface(BluetoothDaemonA2dpModule* aModule);
   ~BluetoothDaemonA2dpInterface();
 
   void Init(
     BluetoothA2dpNotificationHandler* aNotificationHandler,
-    BluetoothA2dpResultHandler* aRes);
-  void Cleanup(BluetoothA2dpResultHandler* aRes);
+    BluetoothA2dpResultHandler* aRes) override;
+  void Cleanup(BluetoothA2dpResultHandler* aRes) override;
 
   /* Connect / Disconnect */
 
   void Connect(const nsAString& aBdAddr,
-               BluetoothA2dpResultHandler* aRes);
+               BluetoothA2dpResultHandler* aRes) override;
   void Disconnect(const nsAString& aBdAddr,
-                  BluetoothA2dpResultHandler* aRes);
+                  BluetoothA2dpResultHandler* aRes) override;
 
 private:
   void DispatchError(BluetoothA2dpResultHandler* aRes,
                      BluetoothStatus aStatus);
   void DispatchError(BluetoothA2dpResultHandler* aRes, nsresult aRv);
 
   BluetoothDaemonA2dpModule* mModule;
 };
--- a/dom/bluetooth/bluedroid/BluetoothDaemonAvrcpInterface.cpp
+++ b/dom/bluetooth/bluedroid/BluetoothDaemonAvrcpInterface.cpp
@@ -23,44 +23,33 @@ BluetoothAvrcpNotificationHandler*
 
 void
 BluetoothDaemonAvrcpModule::SetNotificationHandler(
   BluetoothAvrcpNotificationHandler* aNotificationHandler)
 {
   sNotificationHandler = aNotificationHandler;
 }
 
-nsresult
-BluetoothDaemonAvrcpModule::Send(DaemonSocketPDU* aPDU,
-                                 BluetoothAvrcpResultHandler* aRes)
-{
-  nsRefPtr<BluetoothAvrcpResultHandler> res(aRes);
-  nsresult rv = Send(aPDU, static_cast<void*>(res.get()));
-  if (NS_FAILED(rv)) {
-    return rv;
-  }
-  unused << res.forget(); // Keep reference for response
-  return NS_OK;
-}
-
 void
 BluetoothDaemonAvrcpModule::HandleSvc(const DaemonSocketPDUHeader& aHeader,
-                                      DaemonSocketPDU& aPDU, void* aUserData)
+                                      DaemonSocketPDU& aPDU,
+                                      DaemonSocketResultHandler* aRes)
 {
   static void (BluetoothDaemonAvrcpModule::* const HandleOp[])(
-    const DaemonSocketPDUHeader&, DaemonSocketPDU&, void*) = {
+    const DaemonSocketPDUHeader&, DaemonSocketPDU&,
+    DaemonSocketResultHandler*) = {
     [0] = &BluetoothDaemonAvrcpModule::HandleRsp,
     [1] = &BluetoothDaemonAvrcpModule::HandleNtf
   };
 
   MOZ_ASSERT(!NS_IsMainThread());
 
   unsigned int isNtf = !!(aHeader.mOpcode & 0x80);
 
-  (this->*(HandleOp[isNtf]))(aHeader, aPDU, aUserData);
+  (this->*(HandleOp[isNtf]))(aHeader, aPDU, aRes);
 }
 
 // Commands
 //
 
 nsresult
 BluetoothDaemonAvrcpModule::GetPlayStatusRspCmd(
   ControlPlayStatus aPlayStatus, uint32_t aSongLen, uint32_t aSongPos,
@@ -416,17 +405,17 @@ BluetoothDaemonAvrcpModule::SetVolumeRsp
   ResultRunnable::Dispatch(
     aRes, &BluetoothAvrcpResultHandler::SetVolume,
     UnpackPDUInitOp(aPDU));
 }
 
 void
 BluetoothDaemonAvrcpModule::HandleRsp(
   const DaemonSocketPDUHeader& aHeader, DaemonSocketPDU& aPDU,
-  void* aUserData)
+  DaemonSocketResultHandler* aRes)
 {
   static void (BluetoothDaemonAvrcpModule::* const HandleRsp[])(
     const DaemonSocketPDUHeader&,
     DaemonSocketPDU&,
     BluetoothAvrcpResultHandler*) = {
     [OPCODE_ERROR] =
       &BluetoothDaemonAvrcpModule::ErrorRsp,
     [OPCODE_GET_PLAY_STATUS_RSP] =
@@ -454,18 +443,17 @@ BluetoothDaemonAvrcpModule::HandleRsp(
   MOZ_ASSERT(!NS_IsMainThread()); // I/O thread
 
   if (NS_WARN_IF(!(aHeader.mOpcode < MOZ_ARRAY_LENGTH(HandleRsp))) ||
       NS_WARN_IF(!HandleRsp[aHeader.mOpcode])) {
     return;
   }
 
   nsRefPtr<BluetoothAvrcpResultHandler> res =
-    already_AddRefed<BluetoothAvrcpResultHandler>(
-      static_cast<BluetoothAvrcpResultHandler*>(aUserData));
+    static_cast<BluetoothAvrcpResultHandler*>(aRes);
 
   if (!res) {
     return; // Return early if no result handler has been set for response
   }
 
   (this->*(HandleRsp[aHeader.mOpcode]))(aHeader, aPDU, res);
 }
 
@@ -787,17 +775,17 @@ BluetoothDaemonAvrcpModule::PassthroughC
     &BluetoothAvrcpNotificationHandler::PassthroughCmdNotification,
     PassthroughCmdInitOp(aPDU));
 }
 #endif
 
 void
 BluetoothDaemonAvrcpModule::HandleNtf(
   const DaemonSocketPDUHeader& aHeader, DaemonSocketPDU& aPDU,
-  void* aUserData)
+  DaemonSocketResultHandler* aRes)
 {
   static void (BluetoothDaemonAvrcpModule::* const HandleNtf[])(
     const DaemonSocketPDUHeader&, DaemonSocketPDU&) = {
 #if ANDROID_VERSION >= 19
     [0] = &BluetoothDaemonAvrcpModule::RemoteFeatureNtf,
     [1] = &BluetoothDaemonAvrcpModule::GetPlayStatusNtf,
     [2] = &BluetoothDaemonAvrcpModule::ListPlayerAppAttrNtf,
     [3] = &BluetoothDaemonAvrcpModule::ListPlayerAppValuesNtf,
--- a/dom/bluetooth/bluedroid/BluetoothDaemonAvrcpInterface.h
+++ b/dom/bluetooth/bluedroid/BluetoothDaemonAvrcpInterface.h
@@ -10,16 +10,17 @@
 #include "BluetoothDaemonHelpers.h"
 #include "BluetoothInterface.h"
 #include "mozilla/ipc/DaemonRunnables.h"
 
 BEGIN_BLUETOOTH_NAMESPACE
 
 using mozilla::ipc::DaemonSocketPDU;
 using mozilla::ipc::DaemonSocketPDUHeader;
+using mozilla::ipc::DaemonSocketResultHandler;
 
 class BluetoothSetupResultHandler;
 
 class BluetoothDaemonAvrcpModule
 {
 public:
   enum {
     SERVICE_ID = 0x08
@@ -60,17 +61,18 @@ public:
     OPCODE_SET_PLAYER_APP_VALUE_NTF = 0x87,
     OPCODE_GET_ELEMENT_ATTR_NTF = 0x88,
     OPCODE_REGISTER_NOTIFICATION_NTF = 0x89
 #endif
   };
 
   static const int MAX_NUM_CLIENTS;
 
-  virtual nsresult Send(DaemonSocketPDU* aPDU, void* aUserData) = 0;
+  virtual nsresult Send(DaemonSocketPDU* aPDU,
+                        DaemonSocketResultHandler* aRes) = 0;
 
   virtual nsresult RegisterModule(uint8_t aId, uint8_t aMode,
                                   uint32_t aMaxNumClients,
                                   BluetoothSetupResultHandler* aRes) = 0;
 
   virtual nsresult UnregisterModule(uint8_t aId,
                                     BluetoothSetupResultHandler* aRes) = 0;
 
@@ -114,21 +116,18 @@ public:
   nsresult RegisterNotificationRspCmd(
     BluetoothAvrcpEvent aEvent, BluetoothAvrcpNotification aType,
     const BluetoothAvrcpNotificationParam& aParam,
     BluetoothAvrcpResultHandler* aRes);
 
   nsresult SetVolumeCmd(uint8_t aVolume, BluetoothAvrcpResultHandler* aRes);
 
 protected:
-  nsresult Send(DaemonSocketPDU* aPDU,
-                BluetoothAvrcpResultHandler* aRes);
-
   void HandleSvc(const DaemonSocketPDUHeader& aHeader,
-                 DaemonSocketPDU& aPDU, void* aUserData);
+                 DaemonSocketPDU& aPDU, DaemonSocketResultHandler* aRes);
 
   //
   // Responses
   //
 
   typedef mozilla::ipc::DaemonResultRunnable0<
     BluetoothAvrcpResultHandler, void>
     ResultRunnable;
@@ -178,17 +177,17 @@ protected:
                                   BluetoothAvrcpResultHandler* aRes);
 
   void SetVolumeRsp(const DaemonSocketPDUHeader& aHeader,
                     DaemonSocketPDU& aPDU,
                     BluetoothAvrcpResultHandler* aRes);
 
   void HandleRsp(const DaemonSocketPDUHeader& aHeader,
                  DaemonSocketPDU& aPDU,
-                 void* aUserData);
+                 DaemonSocketResultHandler* aRes);
 
   //
   // Notifications
   //
 
   class NotificationHandlerWrapper;
 
   typedef mozilla::ipc::DaemonNotificationRunnable2<
@@ -288,17 +287,17 @@ protected:
   void VolumeChangeNtf(const DaemonSocketPDUHeader& aHeader,
                        DaemonSocketPDU& aPDU);
 
   void PassthroughCmdNtf(const DaemonSocketPDUHeader& aHeader,
                          DaemonSocketPDU& aPDU);
 
   void HandleNtf(const DaemonSocketPDUHeader& aHeader,
                  DaemonSocketPDU& aPDU,
-                 void* aUserData);
+                 DaemonSocketResultHandler* aRes);
 
   static BluetoothAvrcpNotificationHandler* sNotificationHandler;
 };
 
 class BluetoothDaemonAvrcpInterface final
   : public BluetoothAvrcpInterface
 {
   class CleanupResultHandler;
--- a/dom/bluetooth/bluedroid/BluetoothDaemonGattInterface.cpp
+++ b/dom/bluetooth/bluedroid/BluetoothDaemonGattInterface.cpp
@@ -23,45 +23,34 @@ BluetoothGattNotificationHandler*
 
 void
 BluetoothDaemonGattModule::SetNotificationHandler(
   BluetoothGattNotificationHandler* aNotificationHandler)
 {
   sNotificationHandler = aNotificationHandler;
 }
 
-nsresult
-BluetoothDaemonGattModule::Send(DaemonSocketPDU* aPDU,
-                                BluetoothGattResultHandler* aRes)
-{
-  nsRefPtr<BluetoothGattResultHandler> res(aRes);
-  nsresult rv = Send(aPDU, static_cast<void*>(res.get()));
-  if (NS_FAILED(rv)) {
-    return rv;
-  }
-  unused << res.forget(); // Keep reference for response
-  return NS_OK;
-}
-
 void
 BluetoothDaemonGattModule::HandleSvc(const DaemonSocketPDUHeader& aHeader,
-                                     DaemonSocketPDU& aPDU, void* aUserData)
+                                     DaemonSocketPDU& aPDU,
+                                     DaemonSocketResultHandler* aRes)
 {
   static void (BluetoothDaemonGattModule::* const HandleOp[])(
-    const DaemonSocketPDUHeader&, DaemonSocketPDU&, void*) = {
+    const DaemonSocketPDUHeader&, DaemonSocketPDU&,
+    DaemonSocketResultHandler*) = {
     [0] = &BluetoothDaemonGattModule::HandleRsp,
     [1] = &BluetoothDaemonGattModule::HandleNtf
   };
 
   MOZ_ASSERT(!NS_IsMainThread());
 
   // Negate twice to map bit to 0/1
   unsigned long isNtf = !!(aHeader.mOpcode & 0x80);
 
-  (this->*(HandleOp[isNtf]))(aHeader, aPDU, aUserData);
+  (this->*(HandleOp[isNtf]))(aHeader, aPDU, aRes);
 }
 
 // Commands
 //
 
 nsresult
 BluetoothDaemonGattModule::ClientRegisterCmd(
   const BluetoothUuid& aUuid, BluetoothGattResultHandler* aRes)
@@ -1400,17 +1389,17 @@ BluetoothDaemonGattModule::ServerSendRes
   ResultRunnable::Dispatch(
     aRes, &BluetoothGattResultHandler::SendResponse,
     UnpackPDUInitOp(aPDU));
 }
 
 void
 BluetoothDaemonGattModule::HandleRsp(
   const DaemonSocketPDUHeader& aHeader, DaemonSocketPDU& aPDU,
-  void* aUserData)
+  DaemonSocketResultHandler* aRes)
 {
   static void (BluetoothDaemonGattModule::* const HandleRsp[])(
     const DaemonSocketPDUHeader&,
     DaemonSocketPDU&,
     BluetoothGattResultHandler*) = {
     [OPCODE_ERROR] =
       &BluetoothDaemonGattModule::ErrorRsp,
     [OPCODE_CLIENT_REGISTER] =
@@ -1488,18 +1477,17 @@ BluetoothDaemonGattModule::HandleRsp(
   MOZ_ASSERT(!NS_IsMainThread()); // I/O thread
 
   if (NS_WARN_IF(!(aHeader.mOpcode < MOZ_ARRAY_LENGTH(HandleRsp))) ||
       NS_WARN_IF(!HandleRsp[aHeader.mOpcode])) {
     return;
   }
 
   nsRefPtr<BluetoothGattResultHandler> res =
-    already_AddRefed<BluetoothGattResultHandler>(
-      static_cast<BluetoothGattResultHandler*>(aUserData));
+    static_cast<BluetoothGattResultHandler*>(aRes);
 
   if (!res) {
     return;
   } // Return early if no result handler has been set for response
 
   (this->*(HandleRsp[aHeader.mOpcode]))(aHeader, aPDU, res);
 }
 
@@ -2147,17 +2135,17 @@ BluetoothDaemonGattModule::ServerRespons
   ServerResponseConfirmationNotification::Dispatch(
     &BluetoothGattNotificationHandler::ResponseConfirmationNotification,
     UnpackPDUInitOp(aPDU));
 }
 
 void
 BluetoothDaemonGattModule::HandleNtf(
   const DaemonSocketPDUHeader& aHeader, DaemonSocketPDU& aPDU,
-  void* aUserData)
+  DaemonSocketResultHandler* aRes)
 {
   static void (BluetoothDaemonGattModule::* const HandleNtf[])(
     const DaemonSocketPDUHeader&, DaemonSocketPDU&) = {
     [0] = &BluetoothDaemonGattModule::ClientRegisterNtf,
     [1] = &BluetoothDaemonGattModule::ClientScanResultNtf,
     [2] = &BluetoothDaemonGattModule::ClientConnectNtf,
     [3] = &BluetoothDaemonGattModule::ClientDisconnectNtf,
     [4] = &BluetoothDaemonGattModule::ClientSearchCompleteNtf,
--- a/dom/bluetooth/bluedroid/BluetoothDaemonGattInterface.h
+++ b/dom/bluetooth/bluedroid/BluetoothDaemonGattInterface.h
@@ -10,16 +10,17 @@
 #include "BluetoothDaemonHelpers.h"
 #include "BluetoothInterface.h"
 #include "mozilla/ipc/DaemonRunnables.h"
 
 BEGIN_BLUETOOTH_NAMESPACE
 
 using mozilla::ipc::DaemonSocketPDU;
 using mozilla::ipc::DaemonSocketPDUHeader;
+using mozilla::ipc::DaemonSocketResultHandler;
 
 class BluetoothSetupResultHandler;
 
 class BluetoothDaemonGattModule
 {
 public:
   enum {
     SERVICE_ID = 0x09
@@ -62,17 +63,18 @@ public:
     OPCODE_SERVER_DELETE_SERVICE = 0x21,
     OPCODE_SERVER_SEND_INDICATION = 0x22,
     OPCODE_SERVER_SEND_RESPONSE = 0x23
     // TODO: Add L support
   };
 
   static const int MAX_NUM_CLIENTS;
 
-  virtual nsresult Send(DaemonSocketPDU* aPDU, void* aUserData) = 0;
+  virtual nsresult Send(DaemonSocketPDU* aPDU,
+                        DaemonSocketResultHandler* aRes) = 0;
 
   virtual nsresult RegisterModule(uint8_t aId, uint8_t aMode,
                                   uint32_t aMaxNumClients,
                                   BluetoothSetupResultHandler* aRes) = 0;
 
   virtual nsresult UnregisterModule(uint8_t aId,
                                     BluetoothSetupResultHandler* aRes) = 0;
 
@@ -295,21 +297,18 @@ public:
   nsresult ServerSendResponseCmd(int aConnId,
                                  int aTransId,
                                  BluetoothGattStatus aStatus,
                                  const BluetoothGattResponse& aResponse,
                                  BluetoothGattResultHandler* aRes);
   // TODO: Add L support
 
 protected:
-  nsresult Send(DaemonSocketPDU* aPDU,
-                BluetoothGattResultHandler* aRes);
-
   void HandleSvc(const DaemonSocketPDUHeader& aHeader,
-                 DaemonSocketPDU& aPDU, void* aUserData);
+                 DaemonSocketPDU& aPDU, DaemonSocketResultHandler* aRes);
 
   //
   // Responses
   //
 
   typedef mozilla::ipc::DaemonResultRunnable0<
     BluetoothGattResultHandler, void>
     ResultRunnable;
@@ -466,17 +465,17 @@ protected:
   void ServerSendResponseRsp(const DaemonSocketPDUHeader& aHeader,
                              DaemonSocketPDU& aPDU,
                              BluetoothGattResultHandler* aRes);
 
   // TODO: Add L support
 
   void HandleRsp(const DaemonSocketPDUHeader& aHeader,
                  DaemonSocketPDU& aPDU,
-                 void* aUserData);
+                 DaemonSocketResultHandler* aRes);
 
   //
   // Notifications
   //
 
   class NotificationHandlerWrapper;
 
   typedef mozilla::ipc::DaemonNotificationRunnable3<
@@ -765,223 +764,223 @@ protected:
 
   void ServerResponseConfirmationNtf(const DaemonSocketPDUHeader& aHeader,
                                      DaemonSocketPDU& aPDU);
 
   // TODO: Add L support
 
   void HandleNtf(const DaemonSocketPDUHeader& aHeader,
                  DaemonSocketPDU& aPDU,
-                 void* aUserData);
+                 DaemonSocketResultHandler* aRes);
 
   static BluetoothGattNotificationHandler* sNotificationHandler;
 };
 
 class BluetoothDaemonGattInterface final
   : public BluetoothGattInterface
 {
   class CleanupResultHandler;
   class InitResultHandler;
 
 public:
   BluetoothDaemonGattInterface(BluetoothDaemonGattModule* aModule);
   ~BluetoothDaemonGattInterface();
 
   void Init(BluetoothGattNotificationHandler* aNotificationHandler,
-            BluetoothGattResultHandler* aRes);
-  void Cleanup(BluetoothGattResultHandler* aRes);
+            BluetoothGattResultHandler* aRes) override;
+  void Cleanup(BluetoothGattResultHandler* aRes) override;
 
   /* Register / Unregister */
   void RegisterClient(const BluetoothUuid& aUuid,
-                      BluetoothGattResultHandler* aRes);
+                      BluetoothGattResultHandler* aRes) override;
   void UnregisterClient(int aClientIf,
-                        BluetoothGattResultHandler* aRes);
+                        BluetoothGattResultHandler* aRes) override;
 
   /* Start / Stop LE Scan */
   void Scan(int aClientIf, bool aStart,
-            BluetoothGattResultHandler* aRes);
+            BluetoothGattResultHandler* aRes) override;
 
   /* Connect / Disconnect */
   void Connect(int aClientIf,
                const nsAString& aBdAddr,
                bool aIsDirect, /* auto connect */
                BluetoothTransport aTransport,
-               BluetoothGattResultHandler* aRes);
+               BluetoothGattResultHandler* aRes) override;
   void Disconnect(int aClientIf,
                   const nsAString& aBdAddr,
                   int aConnId,
-                  BluetoothGattResultHandler* aRes);
+                  BluetoothGattResultHandler* aRes) override;
 
   /* Start / Stop advertisements to listen for incoming connections */
   void Listen(int aClientIf,
               bool aIsStart,
-              BluetoothGattResultHandler* aRes);
+              BluetoothGattResultHandler* aRes) override;
 
   /* Clear the attribute cache for a given device*/
   void Refresh(int aClientIf,
                const nsAString& aBdAddr,
-               BluetoothGattResultHandler* aRes);
+               BluetoothGattResultHandler* aRes) override;
 
   /* Enumerate Attributes */
   void SearchService(int aConnId,
                      bool aSearchAll,
                      const BluetoothUuid& aUuid,
-                     BluetoothGattResultHandler* aRes);
+                     BluetoothGattResultHandler* aRes) override;
   void GetIncludedService(int aConnId,
                           const BluetoothGattServiceId& aServiceId,
                           bool aFirst,
                           const BluetoothGattServiceId& aStartServiceId,
-                          BluetoothGattResultHandler* aRes);
+                          BluetoothGattResultHandler* aRes) override;
   void GetCharacteristic(int aConnId,
                          const BluetoothGattServiceId& aServiceId,
                          bool aFirst,
                          const BluetoothGattId& aStartCharId,
-                         BluetoothGattResultHandler* aRes);
+                         BluetoothGattResultHandler* aRes) override;
   void GetDescriptor(int aConnId,
                      const BluetoothGattServiceId& aServiceId,
                      const BluetoothGattId& aCharId,
                      bool aFirst,
                      const BluetoothGattId& aDescriptorId,
-                     BluetoothGattResultHandler* aRes);
+                     BluetoothGattResultHandler* aRes) override;
 
   /* Read / Write An Attribute */
   void ReadCharacteristic(int aConnId,
                           const BluetoothGattServiceId& aServiceId,
                           const BluetoothGattId& aCharId,
                           BluetoothGattAuthReq aAuthReq,
-                          BluetoothGattResultHandler* aRes);
+                          BluetoothGattResultHandler* aRes) override;
   void WriteCharacteristic(int aConnId,
                            const BluetoothGattServiceId& aServiceId,
                            const BluetoothGattId& aCharId,
                            BluetoothGattWriteType aWriteType,
                            BluetoothGattAuthReq aAuthReq,
                            const nsTArray<uint8_t>& aValue,
-                           BluetoothGattResultHandler* aRes);
+                           BluetoothGattResultHandler* aRes) override;
   void ReadDescriptor(int aConnId,
                       const BluetoothGattServiceId& aServiceId,
                       const BluetoothGattId& aCharId,
                       const BluetoothGattId& aDescriptorId,
                       BluetoothGattAuthReq aAuthReq,
-                      BluetoothGattResultHandler* aRes);
+                      BluetoothGattResultHandler* aRes) override;
   void WriteDescriptor(int aConnId,
                        const BluetoothGattServiceId& aServiceId,
                        const BluetoothGattId& aCharId,
                        const BluetoothGattId& aDescriptorId,
                        BluetoothGattWriteType aWriteType,
                        BluetoothGattAuthReq aAuthReq,
                        const nsTArray<uint8_t>& aValue,
-                       BluetoothGattResultHandler* aRes);
+                       BluetoothGattResultHandler* aRes) override;
 
   /* Execute / Abort Prepared Write*/
   void ExecuteWrite(int aConnId,
                     int aIsExecute,
-                    BluetoothGattResultHandler* aRes);
+                    BluetoothGattResultHandler* aRes) override;
 
 
   /* Register / Deregister Characteristic Notifications or Indications */
   void RegisterNotification(int aClientIf,
                             const nsAString& aBdAddr,
                             const BluetoothGattServiceId& aServiceId,
                             const BluetoothGattId& aCharId,
-                            BluetoothGattResultHandler* aRes);
+                            BluetoothGattResultHandler* aRes) override;
   void DeregisterNotification(int aClientIf,
                               const nsAString& aBdAddr,
                               const BluetoothGattServiceId& aServiceId,
                               const BluetoothGattId& aCharId,
-                              BluetoothGattResultHandler* aRes);
+                              BluetoothGattResultHandler* aRes) override;
 
   void ReadRemoteRssi(int aClientIf,
                       const nsAString& aBdAddr,
-                      BluetoothGattResultHandler* aRes);
+                      BluetoothGattResultHandler* aRes) override;
 
   void GetDeviceType(const nsAString& aBdAddr,
-                     BluetoothGattResultHandler* aRes);
+                     BluetoothGattResultHandler* aRes) override;
 
   /* Set advertising data or scan response data */
   void SetAdvData(int aServerIf,
                   bool aIsScanRsp,
                   bool aIsNameIncluded,
                   bool aIsTxPowerIncluded,
                   int aMinInterval,
                   int aMaxInterval,
                   int aApperance,
                   uint16_t aManufacturerLen, char* aManufacturerData,
                   uint16_t aServiceDataLen, char* aServiceData,
                   uint16_t aServiceUuidLen, char* aServiceUuid,
-                  BluetoothGattResultHandler* aRes);
+                  BluetoothGattResultHandler* aRes) override;
 
   void TestCommand(int aCommand,
                    const BluetoothGattTestParam& aTestParam,
-                   BluetoothGattResultHandler* aRes);
+                   BluetoothGattResultHandler* aRes) override;
 
   /* Register / Unregister */
   void RegisterServer(const BluetoothUuid& aUuid,
-                      BluetoothGattResultHandler* aRes);
+                      BluetoothGattResultHandler* aRes) override;
   void UnregisterServer(int aServerIf,
-                        BluetoothGattResultHandler* aRes);
+                        BluetoothGattResultHandler* aRes) override;
 
   /* Connect / Disconnect */
   void ConnectPeripheral(int aServerIf,
                          const nsAString& aBdAddr,
                          bool aIsDirect, /* auto connect */
                          BluetoothTransport aTransport,
-                         BluetoothGattResultHandler* aRes);
+                         BluetoothGattResultHandler* aRes) override;
   void DisconnectPeripheral(int aServerIf,
                             const nsAString& aBdAddr,
                             int aConnId,
-                            BluetoothGattResultHandler* aRes);
+                            BluetoothGattResultHandler* aRes) override;
 
   /* Add a services / a characteristic / a descriptor */
   void AddService(int aServerIf,
                   const BluetoothGattServiceId& aServiceId,
                   int aNumHandles,
-                  BluetoothGattResultHandler* aRes);
+                  BluetoothGattResultHandler* aRes) override;
   void AddIncludedService(int aServerIf,
                           int aServiceHandle,
                           int aIncludedServiceHandle,
-                          BluetoothGattResultHandler* aRes);
+                          BluetoothGattResultHandler* aRes) override;
   void AddCharacteristic(int aServerIf,
                          int aServiceHandle,
                          const BluetoothUuid& aUuid,
                          BluetoothGattCharProp aProperties,
                          BluetoothGattAttrPerm aPermissions,
-                         BluetoothGattResultHandler* aRes);
+                         BluetoothGattResultHandler* aRes) override;
   void AddDescriptor(int aServerIf,
                      int aServiceHandle,
                      const BluetoothUuid& aUuid,
                      BluetoothGattAttrPerm aPermissions,
-                     BluetoothGattResultHandler* aRes);
+                     BluetoothGattResultHandler* aRes) override;
 
   /* Start / Stop / Delete a service */
   void StartService(int aServerIf,
                     int aServiceHandle,
                     BluetoothTransport aTransport,
-                    BluetoothGattResultHandler* aRes);
+                    BluetoothGattResultHandler* aRes) override;
   void StopService(int aServerIf,
                    int aServiceHandle,
-                   BluetoothGattResultHandler* aRes);
+                   BluetoothGattResultHandler* aRes) override;
   void DeleteService(int aServerIf,
                      int aServiceHandle,
-                     BluetoothGattResultHandler* aRes);
+                     BluetoothGattResultHandler* aRes) override;
 
   /* Send an indication or a notification */
   void SendIndication(
     int aServerIf,
     int aAttributeHandle,
     int aConnId,
     const nsTArray<uint8_t>& aValue,
     bool aConfirm, /* true: indication, false: notification */
-    BluetoothGattResultHandler* aRes);
+    BluetoothGattResultHandler* aRes) override;
 
   /* Send a response for an incoming indication */
   void SendResponse(int aConnId,
                     int aTransId,
                     BluetoothGattStatus aStatus,
                     const BluetoothGattResponse& aResponse,
-                    BluetoothGattResultHandler* aRes);
+                    BluetoothGattResultHandler* aRes) override;
 
 private:
   void DispatchError(BluetoothGattResultHandler* aRes,
                      BluetoothStatus aStatus);
   void DispatchError(BluetoothGattResultHandler* aRes, nsresult aRv);
 
   BluetoothDaemonGattModule* mModule;
 };
--- a/dom/bluetooth/bluedroid/BluetoothDaemonHandsfreeInterface.cpp
+++ b/dom/bluetooth/bluedroid/BluetoothDaemonHandsfreeInterface.cpp
@@ -26,45 +26,34 @@ nsString BluetoothDaemonHandsfreeModule:
 
 void
 BluetoothDaemonHandsfreeModule::SetNotificationHandler(
   BluetoothHandsfreeNotificationHandler* aNotificationHandler)
 {
   sNotificationHandler = aNotificationHandler;
 }
 
-nsresult
-BluetoothDaemonHandsfreeModule::Send(DaemonSocketPDU* aPDU,
-                                     BluetoothHandsfreeResultHandler* aRes)
-{
-  nsRefPtr<BluetoothHandsfreeResultHandler> res(aRes);
-  nsresult rv = Send(aPDU, static_cast<void*>(res.get()));
-  if (NS_FAILED(rv)) {
-    return rv;
-  }
-  unused << res.forget(); // Keep reference for response
-  return NS_OK;
-}
-
 void
 BluetoothDaemonHandsfreeModule::HandleSvc(
-  const DaemonSocketPDUHeader& aHeader, DaemonSocketPDU& aPDU, void* aUserData)
+  const DaemonSocketPDUHeader& aHeader, DaemonSocketPDU& aPDU,
+  DaemonSocketResultHandler* aRes)
 {
   static void (BluetoothDaemonHandsfreeModule::* const HandleOp[])(
-    const DaemonSocketPDUHeader&, DaemonSocketPDU&, void*) = {
+    const DaemonSocketPDUHeader&, DaemonSocketPDU&,
+    DaemonSocketResultHandler*) = {
     [0] = &BluetoothDaemonHandsfreeModule::HandleRsp,
     [1] = &BluetoothDaemonHandsfreeModule::HandleNtf
   };
 
   MOZ_ASSERT(!NS_IsMainThread());
 
   // Negate twice to map bit to 0/1
   unsigned long isNtf = !!(aHeader.mOpcode & 0x80);
 
-  (this->*(HandleOp[isNtf]))(aHeader, aPDU, aUserData);
+  (this->*(HandleOp[isNtf]))(aHeader, aPDU, aRes);
 }
 
 // Commands
 //
 
 nsresult
 BluetoothDaemonHandsfreeModule::ConnectCmd(
   const nsAString& aRemoteAddr, BluetoothHandsfreeResultHandler* aRes)
@@ -674,17 +663,17 @@ BluetoothDaemonHandsfreeModule::Configur
   ResultRunnable::Dispatch(
     aRes, &BluetoothHandsfreeResultHandler::ConfigureWbs,
     UnpackPDUInitOp(aPDU));
 }
 
 void
 BluetoothDaemonHandsfreeModule::HandleRsp(
   const DaemonSocketPDUHeader& aHeader, DaemonSocketPDU& aPDU,
-  void* aUserData)
+  DaemonSocketResultHandler* aRes)
 {
   static void (BluetoothDaemonHandsfreeModule::* const HandleRsp[])(
     const DaemonSocketPDUHeader&,
     DaemonSocketPDU&,
     BluetoothHandsfreeResultHandler*) = {
     [OPCODE_ERROR] =
       &BluetoothDaemonHandsfreeModule::ErrorRsp,
     [OPCODE_CONNECT] =
@@ -722,18 +711,17 @@ BluetoothDaemonHandsfreeModule::HandleRs
   MOZ_ASSERT(!NS_IsMainThread()); // I/O thread
 
   if (NS_WARN_IF(!(aHeader.mOpcode < MOZ_ARRAY_LENGTH(HandleRsp))) ||
       NS_WARN_IF(!HandleRsp[aHeader.mOpcode])) {
     return;
   }
 
   nsRefPtr<BluetoothHandsfreeResultHandler> res =
-    already_AddRefed<BluetoothHandsfreeResultHandler>(
-      static_cast<BluetoothHandsfreeResultHandler*>(aUserData));
+    static_cast<BluetoothHandsfreeResultHandler*>(aRes);
 
   if (!res) {
     return; // Return early if no result handler has been set for response
   }
 
   (this->*(HandleRsp[aHeader.mOpcode]))(aHeader, aPDU, res);
 }
 
@@ -1458,17 +1446,17 @@ BluetoothDaemonHandsfreeModule::WbsNtf(
   WbsNotification::Dispatch(
     &BluetoothHandsfreeNotificationHandler::WbsNotification,
     WbsInitOp(aPDU));
 }
 
 void
 BluetoothDaemonHandsfreeModule::HandleNtf(
   const DaemonSocketPDUHeader& aHeader, DaemonSocketPDU& aPDU,
-  void* aUserData)
+  DaemonSocketResultHandler* aRes)
 {
   static void (BluetoothDaemonHandsfreeModule::* const HandleNtf[])(
     const DaemonSocketPDUHeader&, DaemonSocketPDU&) = {
     [0] = &BluetoothDaemonHandsfreeModule::ConnectionStateNtf,
     [1] = &BluetoothDaemonHandsfreeModule::AudioStateNtf,
     [2] = &BluetoothDaemonHandsfreeModule::VoiceRecognitionNtf,
     [3] = &BluetoothDaemonHandsfreeModule::AnswerCallNtf,
     [4] = &BluetoothDaemonHandsfreeModule::HangupCallNtf,
--- a/dom/bluetooth/bluedroid/BluetoothDaemonHandsfreeInterface.h
+++ b/dom/bluetooth/bluedroid/BluetoothDaemonHandsfreeInterface.h
@@ -10,16 +10,17 @@
 #include "BluetoothDaemonHelpers.h"
 #include "BluetoothInterface.h"
 #include "mozilla/ipc/DaemonRunnables.h"
 
 BEGIN_BLUETOOTH_NAMESPACE
 
 using mozilla::ipc::DaemonSocketPDU;
 using mozilla::ipc::DaemonSocketPDUHeader;
+using mozilla::ipc::DaemonSocketResultHandler;
 
 class BluetoothSetupResultHandler;
 
 class BluetoothDaemonHandsfreeModule
 {
 public:
   enum {
     SERVICE_ID = 0x05
@@ -39,17 +40,18 @@ public:
     OPCODE_CIND_RESPONSE = 0x0a,
     OPCODE_FORMATTED_AT_RESPONSE = 0x0b,
     OPCODE_AT_RESPONSE = 0x0c,
     OPCODE_CLCC_RESPONSE = 0x0d,
     OPCODE_PHONE_STATE_CHANGE = 0x0e,
     OPCODE_CONFIGURE_WBS = 0x0f
   };
 
-  virtual nsresult Send(DaemonSocketPDU* aPDU, void* aUserData) = 0;
+  virtual nsresult Send(DaemonSocketPDU* aPDU,
+                        DaemonSocketResultHandler* aRes) = 0;
 
   virtual nsresult RegisterModule(uint8_t aId, uint8_t aMode,
                                   uint32_t aMaxNumClients,
                                   BluetoothSetupResultHandler* aRes) = 0;
 
   virtual nsresult UnregisterModule(uint8_t aId,
                                     BluetoothSetupResultHandler* aRes) = 0;
 
@@ -123,21 +125,19 @@ public:
 
   /* Wide Band Speech */
 
   nsresult ConfigureWbsCmd(const nsAString& aBdAddr,
                            BluetoothHandsfreeWbsConfig aConfig,
                            BluetoothHandsfreeResultHandler* aRes);
 
 protected:
-  nsresult Send(DaemonSocketPDU* aPDU,
-                BluetoothHandsfreeResultHandler* aRes);
-
   void HandleSvc(const DaemonSocketPDUHeader& aHeader,
-                 DaemonSocketPDU& aPDU, void* aUserData);
+                 DaemonSocketPDU& aPDU,
+                 DaemonSocketResultHandler* aRes);
 
   //
   // Responses
   //
 
   typedef mozilla::ipc::DaemonResultRunnable0<
     BluetoothHandsfreeResultHandler, void>
     ResultRunnable;
@@ -207,17 +207,17 @@ protected:
                            BluetoothHandsfreeResultHandler* aRes);
 
   void ConfigureWbsRsp(const DaemonSocketPDUHeader& aHeader,
                        DaemonSocketPDU& aPDU,
                        BluetoothHandsfreeResultHandler* aRes);
 
   void HandleRsp(const DaemonSocketPDUHeader& aHeader,
                  DaemonSocketPDU& aPDU,
-                 void* aUserData);
+                 DaemonSocketResultHandler* aRes);
 
   //
   // Notifications
   //
 
   class NotificationHandlerWrapper;
 
   typedef mozilla::ipc::DaemonNotificationRunnable2<
@@ -364,17 +364,17 @@ protected:
   void KeyPressedNtf(const DaemonSocketPDUHeader& aHeader,
                      DaemonSocketPDU& aPDU);
 
   void WbsNtf(const DaemonSocketPDUHeader& aHeader,
               DaemonSocketPDU& aPDU);
 
   void HandleNtf(const DaemonSocketPDUHeader& aHeader,
                  DaemonSocketPDU& aPDU,
-                 void* aUserData);
+                 DaemonSocketResultHandler* aRes);
 
   static BluetoothHandsfreeNotificationHandler* sNotificationHandler;
 #if ANDROID_VERSION < 21
   /* |sConnectedDeviceAddress| stores Bluetooth device address of the
    * connected device. Before BlueZ 5.25, we maintain this address by ourselves
    * through ConnectionStateNtf(); after BlueZ 5.25, every callback carries
    * this address directly so we don't have to keep it.
    */
@@ -395,85 +395,85 @@ class BluetoothDaemonHandsfreeInterface 
   };
 
 public:
   BluetoothDaemonHandsfreeInterface(BluetoothDaemonHandsfreeModule* aModule);
   ~BluetoothDaemonHandsfreeInterface();
 
   void Init(
     BluetoothHandsfreeNotificationHandler* aNotificationHandler,
-    int aMaxNumClients, BluetoothHandsfreeResultHandler* aRes);
-  void Cleanup(BluetoothHandsfreeResultHandler* aRes);
+    int aMaxNumClients, BluetoothHandsfreeResultHandler* aRes) override;
+  void Cleanup(BluetoothHandsfreeResultHandler* aRes) override;
 
   /* Connect / Disconnect */
 
   void Connect(const nsAString& aBdAddr,
-               BluetoothHandsfreeResultHandler* aRes);
+               BluetoothHandsfreeResultHandler* aRes) override;
   void Disconnect(const nsAString& aBdAddr,
-                  BluetoothHandsfreeResultHandler* aRes);
+                  BluetoothHandsfreeResultHandler* aRes) override;
   void ConnectAudio(const nsAString& aBdAddr,
-                    BluetoothHandsfreeResultHandler* aRes);
+                    BluetoothHandsfreeResultHandler* aRes) override;
   void DisconnectAudio(const nsAString& aBdAddr,
-                       BluetoothHandsfreeResultHandler* aRes);
+                       BluetoothHandsfreeResultHandler* aRes) override;
 
   /* Voice Recognition */
 
   void StartVoiceRecognition(const nsAString& aBdAddr,
-                             BluetoothHandsfreeResultHandler* aRes);
+                             BluetoothHandsfreeResultHandler* aRes) override;
   void StopVoiceRecognition(const nsAString& aBdAddr,
-                            BluetoothHandsfreeResultHandler* aRes);
+                            BluetoothHandsfreeResultHandler* aRes) override;
 
   /* Volume */
 
   void VolumeControl(BluetoothHandsfreeVolumeType aType, int aVolume,
                      const nsAString& aBdAddr,
-                     BluetoothHandsfreeResultHandler* aRes);
+                     BluetoothHandsfreeResultHandler* aRes) override;
 
   /* Device status */
 
   void DeviceStatusNotification(BluetoothHandsfreeNetworkState aNtkState,
                                 BluetoothHandsfreeServiceType aSvcType,
                                 int aSignal, int aBattChg,
-                                BluetoothHandsfreeResultHandler* aRes);
+                                BluetoothHandsfreeResultHandler* aRes) override;
 
   /* Responses */
 
   void CopsResponse(const char* aCops, const nsAString& aBdAddr,
                     BluetoothHandsfreeResultHandler* aRes);
   void CindResponse(int aSvc, int aNumActive, int aNumHeld,
                     BluetoothHandsfreeCallState aCallSetupState,
                     int aSignal, int aRoam, int aBattChg,
                     const nsAString& aBdAddr,
-                    BluetoothHandsfreeResultHandler* aRes);
+                    BluetoothHandsfreeResultHandler* aRes) override;
   void FormattedAtResponse(const char* aRsp, const nsAString& aBdAddr,
-                           BluetoothHandsfreeResultHandler* aRes);
+                           BluetoothHandsfreeResultHandler* aRes) override;
   void AtResponse(BluetoothHandsfreeAtResponse aResponseCode, int aErrorCode,
                   const nsAString& aBdAddr,
-                  BluetoothHandsfreeResultHandler* aRes);
+                  BluetoothHandsfreeResultHandler* aRes) override;
   void ClccResponse(int aIndex, BluetoothHandsfreeCallDirection aDir,
                     BluetoothHandsfreeCallState aState,
                     BluetoothHandsfreeCallMode aMode,
                     BluetoothHandsfreeCallMptyType aMpty,
                     const nsAString& aNumber,
                     BluetoothHandsfreeCallAddressType aType,
                     const nsAString& aBdAddr,
-                    BluetoothHandsfreeResultHandler* aRes);
+                    BluetoothHandsfreeResultHandler* aRes) override;
 
   /* Phone State */
 
   void PhoneStateChange(int aNumActive, int aNumHeld,
                         BluetoothHandsfreeCallState aCallSetupState,
                         const nsAString& aNumber,
                         BluetoothHandsfreeCallAddressType aType,
-                        BluetoothHandsfreeResultHandler* aRes);
+                        BluetoothHandsfreeResultHandler* aRes) override;
 
   /* Wide Band Speech */
   void ConfigureWbs(const nsAString& aBdAddr,
                     BluetoothHandsfreeWbsConfig aConfig,
-                    BluetoothHandsfreeResultHandler* aRes);
+                    BluetoothHandsfreeResultHandler* aRes) override;
 
 private:
   void DispatchError(BluetoothHandsfreeResultHandler* aRes,
                      BluetoothStatus aStatus);
   void DispatchError(BluetoothHandsfreeResultHandler* aRes, nsresult aRv);
 
   BluetoothDaemonHandsfreeModule* mModule;
 };
--- a/dom/bluetooth/bluedroid/BluetoothDaemonInterface.cpp
+++ b/dom/bluetooth/bluedroid/BluetoothDaemonInterface.cpp
@@ -29,17 +29,18 @@ static const int sRetryInterval = 100; /
 
 //
 // Protocol initialization and setup
 //
 
 class BluetoothDaemonSetupModule
 {
 public:
-  virtual nsresult Send(DaemonSocketPDU* aPDU, void* aUserData) = 0;
+  virtual nsresult Send(DaemonSocketPDU* aPDU,
+                        DaemonSocketResultHandler* aRes) = 0;
 
   // Commands
   //
 
   nsresult RegisterModuleCmd(uint8_t aId, uint8_t aMode,
                              uint32_t aMaxNumClients,
                              BluetoothSetupResultHandler* aRes)
   {
@@ -102,17 +103,17 @@ public:
     return rv;
   }
 
 protected:
 
   // Called to handle PDUs with Service field equal to 0x00, which
   // contains internal operations for setup and configuration.
   void HandleSvc(const DaemonSocketPDUHeader& aHeader,
-                 DaemonSocketPDU& aPDU, void* aUserData)
+                 DaemonSocketPDU& aPDU, DaemonSocketResultHandler* aRes)
   {
     static void (BluetoothDaemonSetupModule::* const HandleRsp[])(
       const DaemonSocketPDUHeader&,
       DaemonSocketPDU&,
       BluetoothSetupResultHandler*) = {
       [0x00] = &BluetoothDaemonSetupModule::ErrorRsp,
       [0x01] = &BluetoothDaemonSetupModule::RegisterModuleRsp,
       [0x02] = &BluetoothDaemonSetupModule::UnregisterModuleRsp,
@@ -120,37 +121,25 @@ protected:
     };
 
     if (NS_WARN_IF(aHeader.mOpcode >= MOZ_ARRAY_LENGTH(HandleRsp)) ||
         NS_WARN_IF(!HandleRsp[aHeader.mOpcode])) {
       return;
     }
 
     nsRefPtr<BluetoothSetupResultHandler> res =
-      already_AddRefed<BluetoothSetupResultHandler>(
-        static_cast<BluetoothSetupResultHandler*>(aUserData));
+      static_cast<BluetoothSetupResultHandler*>(aRes);
 
-    if (!res) {
+    if (!aRes) {
       return; // Return early if no result handler has been set
     }
 
     (this->*(HandleRsp[aHeader.mOpcode]))(aHeader, aPDU, res);
   }
 
-  nsresult Send(DaemonSocketPDU* aPDU, BluetoothSetupResultHandler* aRes)
-  {
-    nsRefPtr<BluetoothSetupResultHandler> res(aRes);
-    nsresult rv = Send(aPDU, static_cast<void*>(res.get()));
-    if (NS_FAILED(rv)) {
-      return rv;
-    }
-    unused << res.forget(); // Keep reference for response
-    return NS_OK;
-  }
-
 private:
 
   // Responses
   //
 
   typedef mozilla::ipc::DaemonResultRunnable0<
     BluetoothSetupResultHandler, void>
     ResultRunnable;
@@ -206,17 +195,18 @@ private:
 static BluetoothNotificationHandler* sNotificationHandler;
 
 class BluetoothDaemonCoreModule
 {
 public:
 
   static const int MAX_NUM_CLIENTS;
 
-  virtual nsresult Send(DaemonSocketPDU* aPDU, void* aUserData) = 0;
+  virtual nsresult Send(DaemonSocketPDU* aPDU,
+                        DaemonSocketResultHandler* aRes) = 0;
 
   nsresult EnableCmd(BluetoothResultHandler* aRes)
   {
     MOZ_ASSERT(NS_IsMainThread());
 
     nsAutoPtr<DaemonSocketPDU> pdu(new DaemonSocketPDU(0x01, 0x01, 0));
 
     nsresult rv = Send(pdu, aRes);
@@ -595,38 +585,28 @@ public:
     }
     unused << pdu.forget();
     return rv;
   }
 
 protected:
 
   void HandleSvc(const DaemonSocketPDUHeader& aHeader,
-                 DaemonSocketPDU& aPDU, void* aUserData)
+                 DaemonSocketPDU& aPDU, DaemonSocketResultHandler* aRes)
   {
     static void (BluetoothDaemonCoreModule::* const HandleOp[])(
-      const DaemonSocketPDUHeader&, DaemonSocketPDU&, void*) = {
+      const DaemonSocketPDUHeader&, DaemonSocketPDU&,
+      DaemonSocketResultHandler*) = {
       [0] = &BluetoothDaemonCoreModule::HandleRsp,
       [1] = &BluetoothDaemonCoreModule::HandleNtf
     };
 
     MOZ_ASSERT(!NS_IsMainThread());
 
-    (this->*(HandleOp[!!(aHeader.mOpcode & 0x80)]))(aHeader, aPDU, aUserData);
-  }
-
-  nsresult Send(DaemonSocketPDU* aPDU, BluetoothResultHandler* aRes)
-  {
-    nsRefPtr<BluetoothResultHandler> res(aRes);
-    nsresult rv = Send(aPDU, static_cast<void*>(res.get()));
-    if (NS_FAILED(rv)) {
-      return rv;
-    }
-    unused << res.forget(); // Keep reference for response
-    return NS_OK;
+    (this->*(HandleOp[!!(aHeader.mOpcode & 0x80)]))(aHeader, aPDU, aRes);
   }
 
 private:
 
   // Responses
   //
 
   typedef mozilla::ipc::DaemonResultRunnable0<
@@ -820,17 +800,17 @@ private:
                      BluetoothResultHandler* aRes)
   {
     ResultRunnable::Dispatch(
       aRes, &BluetoothResultHandler::LeTestMode,
       UnpackPDUInitOp(aPDU));
   }
 
   void HandleRsp(const DaemonSocketPDUHeader& aHeader,
-                 DaemonSocketPDU& aPDU, void* aUserData)
+                 DaemonSocketPDU& aPDU, DaemonSocketResultHandler* aRes)
   {
     static void (BluetoothDaemonCoreModule::* const HandleRsp[])(
       const DaemonSocketPDUHeader&,
       DaemonSocketPDU&,
       BluetoothResultHandler*) = {
       [0x00] = &BluetoothDaemonCoreModule::ErrorRsp,
       [0x01] = &BluetoothDaemonCoreModule::EnableRsp,
       [0x02] = &BluetoothDaemonCoreModule::DisableRsp,
@@ -857,18 +837,17 @@ private:
     MOZ_ASSERT(!NS_IsMainThread());
 
     if (NS_WARN_IF(!(aHeader.mOpcode < MOZ_ARRAY_LENGTH(HandleRsp))) ||
         NS_WARN_IF(!HandleRsp[aHeader.mOpcode])) {
       return;
     }
 
     nsRefPtr<BluetoothResultHandler> res =
-      already_AddRefed<BluetoothResultHandler>(
-        static_cast<BluetoothResultHandler*>(aUserData));
+      static_cast<BluetoothResultHandler*>(aRes);
 
     if (!res) {
       return; // Return early if no result handler has been set for response
     }
 
     (this->*(HandleRsp[aHeader.mOpcode]))(aHeader, aPDU, res);
   }
 
@@ -1340,17 +1319,17 @@ private:
                      DaemonSocketPDU& aPDU)
   {
     LeTestModeNotification::Dispatch(
       &BluetoothNotificationHandler::LeTestModeNotification,
       UnpackPDUInitOp(aPDU));
   }
 
   void HandleNtf(const DaemonSocketPDUHeader& aHeader,
-                 DaemonSocketPDU& aPDU, void* aUserData)
+                 DaemonSocketPDU& aPDU, DaemonSocketResultHandler* aRes)
   {
     static void (BluetoothDaemonCoreModule::* const HandleNtf[])(
       const DaemonSocketPDUHeader&, DaemonSocketPDU&) = {
       [0] = &BluetoothDaemonCoreModule::AdapterStateChangedNtf,
       [1] = &BluetoothDaemonCoreModule::AdapterPropertiesNtf,
       [2] = &BluetoothDaemonCoreModule::RemoteDevicePropertiesNtf,
       [3] = &BluetoothDaemonCoreModule::DeviceFoundNtf,
       [4] = &BluetoothDaemonCoreModule::DiscoveryStateChangedNtf,
@@ -1448,45 +1427,54 @@ public:
                           BluetoothSetupResultHandler* aRes) override;
 
   nsresult UnregisterModule(uint8_t aId,
                             BluetoothSetupResultHandler* aRes) override;
 
   // Outgoing PDUs
   //
 
-  nsresult Send(DaemonSocketPDU* aPDU, void* aUserData) override;
+  nsresult Send(DaemonSocketPDU* aPDU,
+                DaemonSocketResultHandler* aRes) override;
 
-  void StoreUserData(const DaemonSocketPDU& aPDU) override;
+  void StoreResultHandler(const DaemonSocketPDU& aPDU) override;
 
   // Incoming PUDs
   //
 
   void Handle(DaemonSocketPDU& aPDU) override;
 
-  void* FetchUserData(const DaemonSocketPDUHeader& aHeader);
+  already_AddRefed<DaemonSocketResultHandler> FetchResultHandler(
+    const DaemonSocketPDUHeader& aHeader);
 
 private:
   void HandleSetupSvc(const DaemonSocketPDUHeader& aHeader,
-                      DaemonSocketPDU& aPDU, void* aUserData);
+                      DaemonSocketPDU& aPDU,
+                      DaemonSocketResultHandler* aRes);
   void HandleCoreSvc(const DaemonSocketPDUHeader& aHeader,
-                     DaemonSocketPDU& aPDU, void* aUserData);
+                     DaemonSocketPDU& aPDU,
+                     DaemonSocketResultHandler* aRes);
   void HandleSocketSvc(const DaemonSocketPDUHeader& aHeader,
-                       DaemonSocketPDU& aPDU, void* aUserData);
+                       DaemonSocketPDU& aPDU,
+                       DaemonSocketResultHandler* aRes);
   void HandleHandsfreeSvc(const DaemonSocketPDUHeader& aHeader,
-                          DaemonSocketPDU& aPDU, void* aUserData);
+                          DaemonSocketPDU& aPDU,
+                          DaemonSocketResultHandler* aRes);
   void HandleA2dpSvc(const DaemonSocketPDUHeader& aHeader,
-                     DaemonSocketPDU& aPDU, void* aUserData);
+                     DaemonSocketPDU& aPDU,
+                     DaemonSocketResultHandler* aUserData);
   void HandleAvrcpSvc(const DaemonSocketPDUHeader& aHeader,
-                      DaemonSocketPDU& aPDU, void* aUserData);
+                      DaemonSocketPDU& aPDU,
+                      DaemonSocketResultHandler* aRes);
   void HandleGattSvc(const DaemonSocketPDUHeader& aHeader,
-                     DaemonSocketPDU& aPDU, void* aUserData);
+                     DaemonSocketPDU& aPDU,
+                     DaemonSocketResultHandler* aRes);
 
   DaemonSocket* mConnection;
-  nsTArray<void*> mUserDataQ;
+  nsTArray<nsRefPtr<DaemonSocketResultHandler>> mResQ;
 };
 
 BluetoothDaemonProtocol::BluetoothDaemonProtocol()
 { }
 
 void
 BluetoothDaemonProtocol::SetConnection(DaemonSocket* aConnection)
 {
@@ -1505,96 +1493,98 @@ BluetoothDaemonProtocol::RegisterModule(
 nsresult
 BluetoothDaemonProtocol::UnregisterModule(uint8_t aId,
                                           BluetoothSetupResultHandler* aRes)
 {
   return BluetoothDaemonSetupModule::UnregisterModuleCmd(aId, aRes);
 }
 
 nsresult
-BluetoothDaemonProtocol::Send(DaemonSocketPDU* aPDU, void* aUserData)
+BluetoothDaemonProtocol::Send(DaemonSocketPDU* aPDU,
+                              DaemonSocketResultHandler* aRes)
 {
   MOZ_ASSERT(mConnection);
   MOZ_ASSERT(aPDU);
 
   aPDU->SetConsumer(this);
-  aPDU->SetUserData(aUserData);
+  aPDU->SetResultHandler(aRes);
   aPDU->UpdateHeader();
 
   if (mConnection->GetConnectionStatus() == SOCKET_DISCONNECTED) {
     BT_LOGR("Connection to Bluetooth daemon is closed.");
     return NS_ERROR_FAILURE;
   }
 
   mConnection->SendSocketData(aPDU); // Forward PDU to command channel
 
   return NS_OK;
 }
 
 void
 BluetoothDaemonProtocol::HandleSetupSvc(
   const DaemonSocketPDUHeader& aHeader, DaemonSocketPDU& aPDU,
-  void* aUserData)
+  DaemonSocketResultHandler* aRes)
 {
-  BluetoothDaemonSetupModule::HandleSvc(aHeader, aPDU, aUserData);
+  BluetoothDaemonSetupModule::HandleSvc(aHeader, aPDU, aRes);
 }
 
 void
 BluetoothDaemonProtocol::HandleCoreSvc(
   const DaemonSocketPDUHeader& aHeader, DaemonSocketPDU& aPDU,
-  void* aUserData)
+  DaemonSocketResultHandler* aRes)
 {
-  BluetoothDaemonCoreModule::HandleSvc(aHeader, aPDU, aUserData);
+  BluetoothDaemonCoreModule::HandleSvc(aHeader, aPDU, aRes);
 }
 
 void
 BluetoothDaemonProtocol::HandleSocketSvc(
   const DaemonSocketPDUHeader& aHeader, DaemonSocketPDU& aPDU,
-  void* aUserData)
+  DaemonSocketResultHandler* aRes)
 {
-  BluetoothDaemonSocketModule::HandleSvc(aHeader, aPDU, aUserData);
+  BluetoothDaemonSocketModule::HandleSvc(aHeader, aPDU, aRes);
 }
 
 void
 BluetoothDaemonProtocol::HandleHandsfreeSvc(
   const DaemonSocketPDUHeader& aHeader, DaemonSocketPDU& aPDU,
-  void* aUserData)
+  DaemonSocketResultHandler* aRes)
 {
-  BluetoothDaemonHandsfreeModule::HandleSvc(aHeader, aPDU, aUserData);
+  BluetoothDaemonHandsfreeModule::HandleSvc(aHeader, aPDU, aRes);
 }
 
 void
 BluetoothDaemonProtocol::HandleA2dpSvc(
   const DaemonSocketPDUHeader& aHeader, DaemonSocketPDU& aPDU,
-  void* aUserData)
+  DaemonSocketResultHandler* aRes)
 {
-  BluetoothDaemonA2dpModule::HandleSvc(aHeader, aPDU, aUserData);
+  BluetoothDaemonA2dpModule::HandleSvc(aHeader, aPDU, aRes);
 }
 
 void
 BluetoothDaemonProtocol::HandleAvrcpSvc(
   const DaemonSocketPDUHeader& aHeader, DaemonSocketPDU& aPDU,
-  void* aUserData)
+  DaemonSocketResultHandler* aRes)
 {
-  BluetoothDaemonAvrcpModule::HandleSvc(aHeader, aPDU, aUserData);
+  BluetoothDaemonAvrcpModule::HandleSvc(aHeader, aPDU, aRes);
 }
 
 void
 BluetoothDaemonProtocol::HandleGattSvc(
   const DaemonSocketPDUHeader& aHeader, DaemonSocketPDU& aPDU,
-  void* aUserData)
+  DaemonSocketResultHandler* aRes)
 {
-  BluetoothDaemonGattModule::HandleSvc(aHeader, aPDU, aUserData);
+  BluetoothDaemonGattModule::HandleSvc(aHeader, aPDU, aRes);
 }
 
 void
 BluetoothDaemonProtocol::Handle(DaemonSocketPDU& aPDU)
 {
   static void (BluetoothDaemonProtocol::* const HandleSvc[])(
-    const DaemonSocketPDUHeader&, DaemonSocketPDU&, void*) = {
+    const DaemonSocketPDUHeader&, DaemonSocketPDU&,
+    DaemonSocketResultHandler*) = {
     [0x00] = &BluetoothDaemonProtocol::HandleSetupSvc,
     [0x01] = &BluetoothDaemonProtocol::HandleCoreSvc,
     [0x02] = &BluetoothDaemonProtocol::HandleSocketSvc,
     [0x03] = nullptr, // HID host
     [0x04] = nullptr, // PAN
     [BluetoothDaemonHandsfreeModule::SERVICE_ID] =
       &BluetoothDaemonProtocol::HandleHandsfreeSvc,
     [BluetoothDaemonA2dpModule::SERVICE_ID] =
@@ -1609,40 +1599,43 @@ BluetoothDaemonProtocol::Handle(DaemonSo
   DaemonSocketPDUHeader header;
 
   if (NS_FAILED(UnpackPDU(aPDU, header)) ||
       NS_WARN_IF(!(header.mService < MOZ_ARRAY_LENGTH(HandleSvc))) ||
       NS_WARN_IF(!(HandleSvc[header.mService]))) {
     return;
   }
 
-  (this->*(HandleSvc[header.mService]))(header, aPDU, FetchUserData(header));
+  nsRefPtr<DaemonSocketResultHandler> res = FetchResultHandler(header);
+
+  (this->*(HandleSvc[header.mService]))(header, aPDU, res);
 }
 
 void
-BluetoothDaemonProtocol::StoreUserData(const DaemonSocketPDU& aPDU)
+BluetoothDaemonProtocol::StoreResultHandler(const DaemonSocketPDU& aPDU)
 {
   MOZ_ASSERT(!NS_IsMainThread());
 
-  mUserDataQ.AppendElement(aPDU.GetUserData());
+  mResQ.AppendElement(aPDU.GetResultHandler());
 }
 
-void*
-BluetoothDaemonProtocol::FetchUserData(const DaemonSocketPDUHeader& aHeader)
+already_AddRefed<DaemonSocketResultHandler>
+BluetoothDaemonProtocol::FetchResultHandler(
+  const DaemonSocketPDUHeader& aHeader)
 {
   MOZ_ASSERT(!NS_IsMainThread());
 
   if (aHeader.mOpcode & 0x80) {
     return nullptr; // Ignore notifications
   }
 
-  void* userData = mUserDataQ.ElementAt(0);
-  mUserDataQ.RemoveElementAt(0);
+  nsRefPtr<DaemonSocketResultHandler> userData = mResQ.ElementAt(0);
+  mResQ.RemoveElementAt(0);
 
-  return userData;
+  return userData.forget();
 }
 
 //
 // Interface
 //
 
 /* returns the container structure of a variable; _t is the container's
  * type, _v the name of the variable, and _m is _v's field within _t
--- a/dom/bluetooth/bluedroid/BluetoothDaemonInterface.h
+++ b/dom/bluetooth/bluedroid/BluetoothDaemonInterface.h
@@ -41,90 +41,92 @@ public:
 
   friend class CleanupResultHandler;
   friend class InitResultHandler;
   friend class StartDaemonTask;
 
   static BluetoothDaemonInterface* GetInstance();
 
   void Init(BluetoothNotificationHandler* aNotificationHandler,
-            BluetoothResultHandler* aRes);
-  void Cleanup(BluetoothResultHandler* aRes);
+            BluetoothResultHandler* aRes) override;
+  void Cleanup(BluetoothResultHandler* aRes) override;
 
-  void Enable(BluetoothResultHandler* aRes);
-  void Disable(BluetoothResultHandler* aRes);
+  void Enable(BluetoothResultHandler* aRes) override;
+  void Disable(BluetoothResultHandler* aRes) override;
 
   /* Adapter Properties */
 
-  void GetAdapterProperties(BluetoothResultHandler* aRes);
+  void GetAdapterProperties(BluetoothResultHandler* aRes) override;
   void GetAdapterProperty(const nsAString& aName,
-                          BluetoothResultHandler* aRes);
+                          BluetoothResultHandler* aRes) override;
   void SetAdapterProperty(const BluetoothNamedValue& aProperty,
-                          BluetoothResultHandler* aRes);
+                          BluetoothResultHandler* aRes) override;
 
   /* Remote Device Properties */
 
   void GetRemoteDeviceProperties(const nsAString& aRemoteAddr,
-                                 BluetoothResultHandler* aRes);
+                                 BluetoothResultHandler* aRes) override;
   void GetRemoteDeviceProperty(const nsAString& aRemoteAddr,
                                const nsAString& aName,
-                               BluetoothResultHandler* aRes);
+                               BluetoothResultHandler* aRes) override;
   void SetRemoteDeviceProperty(const nsAString& aRemoteAddr,
                                const BluetoothNamedValue& aProperty,
-                               BluetoothResultHandler* aRes);
+                               BluetoothResultHandler* aRes) override;
 
   /* Remote Services */
 
   void GetRemoteServiceRecord(const nsAString& aRemoteAddr,
                               const uint8_t aUuid[16],
-                              BluetoothResultHandler* aRes);
+                              BluetoothResultHandler* aRes) override;
   void GetRemoteServices(const nsAString& aRemoteAddr,
-                         BluetoothResultHandler* aRes);
+                         BluetoothResultHandler* aRes) override;
 
   /* Discovery */
 
-  void StartDiscovery(BluetoothResultHandler* aRes);
-  void CancelDiscovery(BluetoothResultHandler* aRes);
+  void StartDiscovery(BluetoothResultHandler* aRes) override;
+  void CancelDiscovery(BluetoothResultHandler* aRes) override;
 
   /* Bonds */
 
   void CreateBond(const nsAString& aBdAddr, BluetoothTransport aTransport,
-                  BluetoothResultHandler* aRes);
-  void RemoveBond(const nsAString& aBdAddr, BluetoothResultHandler* aRes);
-  void CancelBond(const nsAString& aBdAddr, BluetoothResultHandler* aRes);
+                  BluetoothResultHandler* aRes) override;
+  void RemoveBond(const nsAString& aBdAddr,
+                  BluetoothResultHandler* aRes) override;
+  void CancelBond(const nsAString& aBdAddr,
+                  BluetoothResultHandler* aRes) override;
 
   /* Connection */
 
   void GetConnectionState(const nsAString& aBdAddr,
-                          BluetoothResultHandler* aRes);
+                          BluetoothResultHandler* aRes) override;
 
   /* Authentication */
 
   void PinReply(const nsAString& aBdAddr, bool aAccept,
                 const nsAString& aPinCode,
-                BluetoothResultHandler* aRes);
+                BluetoothResultHandler* aRes) override;
 
   void SspReply(const nsAString& aBdAddr, BluetoothSspVariant aVariant,
                 bool aAccept, uint32_t aPasskey,
-                BluetoothResultHandler* aRes);
+                BluetoothResultHandler* aRes) override;
 
   /* DUT Mode */
 
   void DutModeConfigure(bool aEnable, BluetoothResultHandler* aRes);
   void DutModeSend(uint16_t aOpcode, uint8_t* aBuf, uint8_t aLen,
-                   BluetoothResultHandler* aRes);
+                   BluetoothResultHandler* aRes) override;
 
   /* LE Mode */
 
   void LeTestMode(uint16_t aOpcode, uint8_t* aBuf, uint8_t aLen,
-                  BluetoothResultHandler* aRes);
+                  BluetoothResultHandler* aRes) override;
 
   /* Energy Information */
 
-  void ReadEnergyInfo(BluetoothResultHandler* aRes);
+  void ReadEnergyInfo(BluetoothResultHandler* aRes) override;
 
   /* Profile Interfaces */
 
   BluetoothSocketInterface* GetBluetoothSocketInterface() override;
   BluetoothHandsfreeInterface* GetBluetoothHandsfreeInterface() override;
   BluetoothA2dpInterface* GetBluetoothA2dpInterface() override;
   BluetoothAvrcpInterface* GetBluetoothAvrcpInterface() override;
   BluetoothGattInterface* GetBluetoothGattInterface() override;
--- a/dom/bluetooth/bluedroid/BluetoothDaemonSetupInterface.h
+++ b/dom/bluetooth/bluedroid/BluetoothDaemonSetupInterface.h
@@ -3,24 +3,24 @@
 /* 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_BluetoothDaemonSetupInterface_h
 #define mozilla_dom_bluetooth_bluedroid_BluetoothDaemonSetupInterface_h
 
 #include "BluetoothCommon.h"
+#include "mozilla/ipc/DaemonSocketMessageHandlers.h"
 
 BEGIN_BLUETOOTH_NAMESPACE
 
 class BluetoothSetupResultHandler
+  : public mozilla::ipc::DaemonSocketResultHandler
 {
 public:
-  NS_INLINE_DECL_THREADSAFE_REFCOUNTING(BluetoothSetupResultHandler)
-
   virtual void OnError(BluetoothStatus aStatus);
   virtual void RegisterModule();
   virtual void UnregisterModule();
   virtual void Configuration();
 
 protected:
   virtual ~BluetoothSetupResultHandler();
 };
--- a/dom/bluetooth/bluedroid/BluetoothDaemonSocketInterface.cpp
+++ b/dom/bluetooth/bluedroid/BluetoothDaemonSocketInterface.cpp
@@ -155,56 +155,42 @@ BluetoothDaemonSocketModule::CloseCmd(Bl
   XRE_GetIOMessageLoop()->PostTask(FROM_HERE, t);
 
   return NS_OK;
 }
 
 void
 BluetoothDaemonSocketModule::HandleSvc(const DaemonSocketPDUHeader& aHeader,
                                        DaemonSocketPDU& aPDU,
-                                       void* aUserData)
+                                       DaemonSocketResultHandler* aRes)
 {
   static void (BluetoothDaemonSocketModule::* const HandleRsp[])(
     const DaemonSocketPDUHeader&,
     DaemonSocketPDU&,
     BluetoothSocketResultHandler*) = {
     [0x00] = &BluetoothDaemonSocketModule::ErrorRsp,
     [0x01] = &BluetoothDaemonSocketModule::ListenRsp,
     [0x02] = &BluetoothDaemonSocketModule::ConnectRsp
   };
 
   if (NS_WARN_IF(MOZ_ARRAY_LENGTH(HandleRsp) <= aHeader.mOpcode) ||
       NS_WARN_IF(!HandleRsp[aHeader.mOpcode])) {
     return;
   }
 
   nsRefPtr<BluetoothSocketResultHandler> res =
-    already_AddRefed<BluetoothSocketResultHandler>(
-      static_cast<BluetoothSocketResultHandler*>(aUserData));
+    static_cast<BluetoothSocketResultHandler*>(aRes);
 
   if (!res) {
     return; // Return early if no result handler has been set
   }
 
   (this->*(HandleRsp[aHeader.mOpcode]))(aHeader, aPDU, res);
 }
 
-nsresult
-BluetoothDaemonSocketModule::Send(DaemonSocketPDU* aPDU,
-                                  BluetoothSocketResultHandler* aRes)
-{
-  nsRefPtr<BluetoothSocketResultHandler> res(aRes);
-  nsresult rv = Send(aPDU, static_cast<void*>(res.get()));
-  if (NS_FAILED(rv)) {
-    return rv;
-  }
-  unused << res.forget(); // Keep reference for response
-  return NS_OK;
-}
-
 uint8_t
 BluetoothDaemonSocketModule::SocketFlags(bool aEncrypt, bool aAuth)
 {
   return (0x01 * aEncrypt) | (0x02 * aAuth);
 }
 
 // Responses
 //
--- a/dom/bluetooth/bluedroid/BluetoothDaemonSocketInterface.h
+++ b/dom/bluetooth/bluedroid/BluetoothDaemonSocketInterface.h
@@ -10,23 +10,25 @@
 #include "BluetoothDaemonHelpers.h"
 #include "BluetoothInterface.h"
 #include "mozilla/ipc/DaemonRunnables.h"
 
 BEGIN_BLUETOOTH_NAMESPACE
 
 using mozilla::ipc::DaemonSocketPDU;
 using mozilla::ipc::DaemonSocketPDUHeader;
+using mozilla::ipc::DaemonSocketResultHandler;
 
 class BluetoothDaemonSocketModule
 {
 public:
   static const int MAX_NUM_CLIENTS;
 
-  virtual nsresult Send(DaemonSocketPDU* aPDU, void* aUserData) = 0;
+  virtual nsresult Send(DaemonSocketPDU* aPDU,
+                        DaemonSocketResultHandler* aRes) = 0;
 
   // Commands
   //
 
   nsresult ListenCmd(BluetoothSocketType aType,
                      const nsAString& aServiceName,
                      const uint8_t aServiceUuid[16],
                      int aChannel, bool aEncrypt, bool aAuth,
@@ -40,19 +42,17 @@ public:
 
   nsresult AcceptCmd(int aFd, BluetoothSocketResultHandler* aRes);
 
   nsresult CloseCmd(BluetoothSocketResultHandler* aRes);
 
 protected:
 
   void HandleSvc(const DaemonSocketPDUHeader& aHeader,
-                 DaemonSocketPDU& aPDU, void* aUserData);
-
-  nsresult Send(DaemonSocketPDU* aPDU, BluetoothSocketResultHandler* aRes);
+                 DaemonSocketPDU& aPDU, DaemonSocketResultHandler* aRes);
 
 private:
   class AcceptWatcher;
   class ConnectWatcher;
   class ListenInitOp;
 
   uint8_t SocketFlags(bool aEncrypt, bool aAuth);
 
@@ -95,27 +95,27 @@ class BluetoothDaemonSocketInterface fin
 public:
   BluetoothDaemonSocketInterface(BluetoothDaemonSocketModule* aModule);
   ~BluetoothDaemonSocketInterface();
 
   void Listen(BluetoothSocketType aType,
               const nsAString& aServiceName,
               const uint8_t aServiceUuid[16],
               int aChannel, bool aEncrypt, bool aAuth,
-              BluetoothSocketResultHandler* aRes);
+              BluetoothSocketResultHandler* aRes) override;
 
   void Connect(const nsAString& aBdAddr,
                BluetoothSocketType aType,
                const uint8_t aUuid[16],
                int aChannel, bool aEncrypt, bool aAuth,
-               BluetoothSocketResultHandler* aRes);
+               BluetoothSocketResultHandler* aRes) override;
 
-  void Accept(int aFd, BluetoothSocketResultHandler* aRes);
+  void Accept(int aFd, BluetoothSocketResultHandler* aRes) override;
 
-  void Close(BluetoothSocketResultHandler* aRes);
+  void Close(BluetoothSocketResultHandler* aRes) override;
 
 private:
   void DispatchError(BluetoothSocketResultHandler* aRes,
                      BluetoothStatus aStatus);
   void DispatchError(BluetoothSocketResultHandler* aRes, nsresult aRv);
 
   BluetoothDaemonSocketModule* mModule;
 };
--- a/dom/bluetooth/bluedroid/hfp/BluetoothHfpManager.cpp
+++ b/dom/bluetooth/bluedroid/hfp/BluetoothHfpManager.cpp
@@ -1394,19 +1394,17 @@ BluetoothHfpManager::ConnectionStateNoti
 
   } else if (aState == HFP_CONNECTION_STATE_DISCONNECTED) {
     DisconnectSco();
     NotifyConnectionStateChanged(
       NS_LITERAL_STRING(BLUETOOTH_HFP_STATUS_CHANGED_ID));
 
   } else if (aState == HFP_CONNECTION_STATE_CONNECTED) {
     // Once RFCOMM is connected, enable NREC before each new SLC connection
-    mNrecEnabled = HFP_NREC_STARTED;
-    NotifyConnectionStateChanged(
-      NS_LITERAL_STRING(BLUETOOTH_HFP_NREC_STATUS_CHANGED_ID));
+    NRECNotification(HFP_NREC_STARTED, mDeviceAddress);
   }
 }
 
 void
 BluetoothHfpManager::AudioStateNotification(
   BluetoothHandsfreeAudioState aState, const nsAString& aBdAddress)
 {
   MOZ_ASSERT(NS_IsMainThread());
@@ -1473,27 +1471,32 @@ BluetoothHfpManager::DtmfNotification(ch
 
   NS_ENSURE_TRUE_VOID(IsValidDtmf(aDtmf));
 
   nsAutoCString message("VTS=");
   message += aDtmf;
   NotifyDialer(NS_ConvertUTF8toUTF16(message));
 }
 
+/**
+ * NREC status will be set when:
+ * 1. Get an AT command from HF device.
+ *    (Bluetooth HFP spec v1.6 merely defines for the "Disable" part.)
+ * 2. Once RFCOMM is connected, enable NREC before each new SLC connection.
+ */
 void
 BluetoothHfpManager::NRECNotification(BluetoothHandsfreeNRECState aNrec,
                                       const nsAString& aBdAddr)
 {
   MOZ_ASSERT(NS_IsMainThread());
 
   // Notify Gecko observers
   nsCOMPtr<nsIObserverService> obs = services::GetObserverService();
   NS_ENSURE_TRUE_VOID(obs);
 
-  // Set NREC status once getting AT command
   mNrecEnabled = static_cast<bool>(aNrec);
 
   // Notify audio manager
   if (NS_FAILED(obs->NotifyObservers(this,
                                      BLUETOOTH_HFP_NREC_STATUS_CHANGED_ID,
                                      mDeviceAddress.get()))) {
     BT_WARNING("Failed to notify bluetooth-hfp-nrec-status-changed observsers!");
   }
--- a/dom/bluetooth/common/BluetoothInterface.h
+++ b/dom/bluetooth/common/BluetoothInterface.h
@@ -4,28 +4,28 @@
  * License, v. 2.0. If a copy of the MPL was not distributed with this file,
  * You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 #ifndef mozilla_dom_bluetooth_BluetoothInterface_h
 #define mozilla_dom_bluetooth_BluetoothInterface_h
 
 #include "BluetoothCommon.h"
 #include "mozilla/dom/bluetooth/BluetoothTypes.h"
+#include "mozilla/ipc/DaemonSocketMessageHandlers.h"
 
 BEGIN_BLUETOOTH_NAMESPACE
 
 //
 // Socket Interface
 //
 
 class BluetoothSocketResultHandler
+  : public mozilla::ipc::DaemonSocketResultHandler
 {
 public:
-  NS_INLINE_DECL_THREADSAFE_REFCOUNTING(BluetoothSocketResultHandler)
-
   virtual void OnError(BluetoothStatus aStatus)
   {
     BT_WARNING("Received error code %d", (int)aStatus);
   }
 
   virtual void Listen(int aSockFd) { }
   virtual void Connect(int aSockFd, const nsAString& aBdAddress,
                        int aConnectionState) { }
@@ -150,20 +150,19 @@ public:
 protected:
   BluetoothHandsfreeNotificationHandler()
   { }
 
   virtual ~BluetoothHandsfreeNotificationHandler();
 };
 
 class BluetoothHandsfreeResultHandler
+  : public mozilla::ipc::DaemonSocketResultHandler
 {
 public:
-  NS_INLINE_DECL_THREADSAFE_REFCOUNTING(BluetoothHandsfreeResultHandler)
-
   virtual void OnError(BluetoothStatus aStatus)
   {
     BT_WARNING("Received error code %d", (int)aStatus);
   }
 
   virtual void Init() { }
   virtual void Cleanup() { }
 
@@ -298,20 +297,19 @@ public:
 protected:
   BluetoothA2dpNotificationHandler()
   { }
 
   virtual ~BluetoothA2dpNotificationHandler();
 };
 
 class BluetoothA2dpResultHandler
+  : public mozilla::ipc::DaemonSocketResultHandler
 {
 public:
-  NS_INLINE_DECL_THREADSAFE_REFCOUNTING(BluetoothA2dpResultHandler)
-
   virtual void OnError(BluetoothStatus aStatus)
   {
     BT_WARNING("Received error code %d", (int)aStatus);
   }
 
   virtual void Init() { }
   virtual void Cleanup() { }
   virtual void Connect() { }
@@ -401,20 +399,19 @@ public:
 protected:
   BluetoothAvrcpNotificationHandler()
   { }
 
   virtual ~BluetoothAvrcpNotificationHandler();
 };
 
 class BluetoothAvrcpResultHandler
+  : public mozilla::ipc::DaemonSocketResultHandler
 {
 public:
-  NS_INLINE_DECL_THREADSAFE_REFCOUNTING(BluetoothAvrcpResultHandler)
-
   virtual void OnError(BluetoothStatus aStatus)
   {
     BT_WARNING("Received error code %d", (int)aStatus);
   }
 
   virtual void Init() { }
   virtual void Cleanup() { }
 
@@ -715,20 +712,19 @@ public:
 protected:
   BluetoothGattNotificationHandler()
   { }
 
   virtual ~BluetoothGattNotificationHandler();
 };
 
 class BluetoothGattResultHandler
+  : public mozilla::ipc::DaemonSocketResultHandler
 {
 public:
-  NS_INLINE_DECL_THREADSAFE_REFCOUNTING(BluetoothGattResultHandler)
-
   virtual void OnError(BluetoothStatus aStatus)
   {
     BT_WARNING("Received error code %d", (int)aStatus);
   }
 
   virtual void Init() { }
   virtual void Cleanup() { }
 
@@ -1043,20 +1039,19 @@ public:
 protected:
   BluetoothNotificationHandler()
   { }
 
   virtual ~BluetoothNotificationHandler();
 };
 
 class BluetoothResultHandler
+  : public mozilla::ipc::DaemonSocketResultHandler
 {
 public:
-  NS_INLINE_DECL_THREADSAFE_REFCOUNTING(BluetoothResultHandler)
-
   virtual void OnError(BluetoothStatus aStatus)
   {
     BT_LOGR("Received error code %d", aStatus);
   }
 
   virtual void Init() { }
   virtual void Cleanup() { }
   virtual void Enable() { }
--- a/ipc/hal/DaemonSocketConsumer.h
+++ b/ipc/hal/DaemonSocketConsumer.h
@@ -18,17 +18,17 @@ class DaemonSocketPDU;
  * different than the  consumer thread.
  */
 class DaemonSocketIOConsumer
 {
 public:
   virtual ~DaemonSocketIOConsumer();
 
   virtual void Handle(DaemonSocketPDU& aPDU) = 0;
-  virtual void StoreUserData(const DaemonSocketPDU& aPDU) = 0;
+  virtual void StoreResultHandler(const DaemonSocketPDU& aPDU) = 0;
 
 protected:
   DaemonSocketIOConsumer();
 };
 
 /**
  * |DaemonSocketConsumer| handles socket events.
  */
new file mode 100644
--- /dev/null
+++ b/ipc/hal/DaemonSocketMessageHandlers.h
@@ -0,0 +1,40 @@
+/* -*- 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/. */
+
+/*
+ * Message handlers
+ *
+ * This file contains base classes for message handling.
+ */
+
+#ifndef mozilla_ipc_DaemonSocketMessageHandlers_h
+#define mozilla_ipc_DaemonSocketMessageHandlers_h
+
+#include "nsISupportsImpl.h" // for ref-counting
+
+namespace mozilla {
+namespace ipc {
+
+/**
+ * |DaemonSocketResultHandler| is the base class for all protocol-specific
+ * result handlers. It currently only manages the reference counting.
+ */
+class DaemonSocketResultHandler
+{
+public:
+  NS_INLINE_DECL_THREADSAFE_REFCOUNTING(DaemonSocketResultHandler);
+
+protected:
+  DaemonSocketResultHandler()
+  { }
+  virtual ~DaemonSocketResultHandler()
+  { }
+};
+
+} // namespace ipc
+} // namespace mozilla
+
+#endif // mozilla_ipc_DaemonSocketMessageHandlers_h
--- a/ipc/hal/DaemonSocketPDU.cpp
+++ b/ipc/hal/DaemonSocketPDU.cpp
@@ -24,19 +24,18 @@
 namespace mozilla {
 namespace ipc {
 
 //
 // DaemonSocketPDU
 //
 
 DaemonSocketPDU::DaemonSocketPDU(uint8_t aService, uint8_t aOpcode,
-                                       uint16_t aPayloadSize)
+                                 uint16_t aPayloadSize)
   : mConsumer(nullptr)
-  , mUserData(nullptr)
 {
   MOZ_COUNT_CTOR_INHERITED(DaemonSocketPDU, UnixSocketIOBuffer);
 
   // Allocate memory
   size_t availableSpace = HEADER_SIZE + aPayloadSize;
   ResetBuffer(new uint8_t[availableSpace], 0, 0, availableSpace);
 
   // Reserve PDU header
@@ -46,17 +45,16 @@ DaemonSocketPDU::DaemonSocketPDU(uint8_t
   // Setup PDU header
   data[OFF_SERVICE] = aService;
   data[OFF_OPCODE] = aOpcode;
   memcpy(data + OFF_LENGTH, &aPayloadSize, sizeof(aPayloadSize));
 }
 
 DaemonSocketPDU::DaemonSocketPDU(size_t aPayloadSize)
   : mConsumer(nullptr)
-  , mUserData(nullptr)
 {
   MOZ_COUNT_CTOR_INHERITED(DaemonSocketPDU, UnixSocketIOBuffer);
 
   size_t availableSpace = HEADER_SIZE + aPayloadSize;
   ResetBuffer(new uint8_t[availableSpace], 0, 0, availableSpace);
 }
 
 DaemonSocketPDU::~DaemonSocketPDU()
@@ -97,18 +95,18 @@ DaemonSocketPDU::Send(int aFd)
     OnError("sendmsg", errno);
     return -1;
   }
 
   Consume(res);
 
   if (mConsumer) {
     // We successfully sent a PDU, now store the
-    // result runnable in the consumer.
-    mConsumer->StoreUserData(*this);
+    // result handler in the consumer.
+    mConsumer->StoreResultHandler(*this);
   }
 
   return res;
 }
 
 #define CMSGHDR_CONTAINS_FD(_cmsghdr) \
     ( ((_cmsghdr)->cmsg_level == SOL_SOCKET) && \
       ((_cmsghdr)->cmsg_type == SCM_RIGHTS) )
--- a/ipc/hal/DaemonSocketPDU.h
+++ b/ipc/hal/DaemonSocketPDU.h
@@ -4,16 +4,17 @@
  * 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_ipc_DaemonSocketPDU_h
 #define mozilla_ipc_DaemonSocketPDU_h
 
 #include "mozilla/FileUtils.h"
 #include "mozilla/ipc/SocketBase.h"
+#include "mozilla/ipc/DaemonSocketMessageHandlers.h"
 
 namespace mozilla {
 namespace ipc {
 
 class DaemonSocketIOConsumer;
 
 /**
  * |DaemonSocketPDU| represents a single PDU that is transfered from or to
@@ -50,24 +51,24 @@ public:
   DaemonSocketPDU(size_t aPayloadSize);
   ~DaemonSocketPDU();
 
   void SetConsumer(DaemonSocketIOConsumer* aConsumer)
   {
     mConsumer = aConsumer;
   }
 
-  void SetUserData(void* aUserData)
+  void SetResultHandler(DaemonSocketResultHandler* aRes)
   {
-    mUserData = aUserData;
+    mRes = aRes;
   }
 
-  void* GetUserData() const
+  DaemonSocketResultHandler* GetResultHandler() const
   {
-    return mUserData;
+    return mRes;
   }
 
   void GetHeader(uint8_t& aService, uint8_t& aOpcode,
                  uint16_t& aPayloadSize);
 
   ssize_t Send(int aFd) override;
   ssize_t Receive(int aFd) override;
 
@@ -75,17 +76,17 @@ public:
 
   nsresult UpdateHeader();
 
 private:
   size_t GetPayloadSize() const;
   void OnError(const char* aFunction, int aErrno);
 
   DaemonSocketIOConsumer* mConsumer;
-  void* mUserData;
+  nsRefPtr<DaemonSocketResultHandler> mRes;
   ScopedClose mReceivedFd;
 };
 
 }
 }
 
 #endif
 
--- a/ipc/hal/moz.build
+++ b/ipc/hal/moz.build
@@ -4,16 +4,17 @@
 # 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/.
 
 EXPORTS.mozilla.ipc += [
     'DaemonRunnables.h',
     'DaemonSocket.h',
     'DaemonSocketConnector.h',
     'DaemonSocketConsumer.h',
+    'DaemonSocketMessageHandlers.h',
     'DaemonSocketPDU.h',
     'DaemonSocketPDUHelpers.h'
 ]
 
 UNIFIED_SOURCES += [
     'DaemonSocket.cpp',
     'DaemonSocketConnector.cpp',
     'DaemonSocketConsumer.cpp',
new file mode 100644
--- /dev/null
+++ b/mobile/android/base/AccountsHelper.java
@@ -0,0 +1,254 @@
+/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.gecko;
+
+import android.accounts.Account;
+import android.accounts.AccountManager;
+import android.accounts.AccountManagerCallback;
+import android.accounts.AccountManagerFuture;
+import android.accounts.AuthenticatorException;
+import android.accounts.OperationCanceledException;
+import android.content.Context;
+import android.content.Intent;
+import android.util.Log;
+import org.json.JSONException;
+import org.json.JSONObject;
+import org.mozilla.gecko.fxa.FirefoxAccounts;
+import org.mozilla.gecko.fxa.FxAccountConstants;
+import org.mozilla.gecko.fxa.authenticator.AndroidFxAccount;
+import org.mozilla.gecko.fxa.login.Engaged;
+import org.mozilla.gecko.fxa.login.State;
+import org.mozilla.gecko.sync.Utils;
+import org.mozilla.gecko.sync.setup.SyncAccounts;
+import org.mozilla.gecko.util.EventCallback;
+import org.mozilla.gecko.util.NativeEventListener;
+import org.mozilla.gecko.util.NativeJSObject;
+
+import java.io.IOException;
+import java.io.UnsupportedEncodingException;
+import java.net.URISyntaxException;
+import java.security.GeneralSecurityException;
+
+/**
+ * Helper class to manage Android Accounts corresponding to Firefox Accounts.
+ */
+public class AccountsHelper implements NativeEventListener {
+    public static final String LOGTAG = "GeckoAccounts";
+
+    protected final Context mContext;
+    protected final GeckoProfile mProfile;
+
+    public AccountsHelper(Context context, GeckoProfile profile) {
+        mContext = context;
+        mProfile = profile;
+
+        EventDispatcher dispatcher = EventDispatcher.getInstance();
+        if (dispatcher == null) {
+            Log.e(LOGTAG, "Gecko event dispatcher must not be null", new RuntimeException());
+            return;
+        }
+        dispatcher.registerGeckoThreadListener(this,
+                "Accounts:CreateFirefoxAccountFromJSON",
+                "Accounts:UpdateFirefoxAccountFromJSON",
+                "Accounts:Create",
+                "Accounts:DeleteFirefoxAccount",
+                "Accounts:Exist");
+    }
+
+    public synchronized void uninit() {
+        EventDispatcher dispatcher = EventDispatcher.getInstance();
+        if (dispatcher == null) {
+            Log.e(LOGTAG, "Gecko event dispatcher must not be null", new RuntimeException());
+            return;
+        }
+        dispatcher.unregisterGeckoThreadListener(this,
+                "Accounts:CreateFirefoxAccountFromJSON",
+                "Accounts:UpdateFirefoxAccountFromJSON",
+                "Accounts:Create",
+                "Accounts:DeleteFirefoxAccount",
+                "Accounts:Exist");
+    }
+
+    @Override
+    public void handleMessage(String event, NativeJSObject message, final EventCallback callback) {
+        if ("Accounts:CreateFirefoxAccountFromJSON".equals(event)) {
+            AndroidFxAccount fxAccount = null;
+            try {
+                final NativeJSObject json = message.getObject("json");
+                final String email = json.getString("email");
+                final String uid = json.getString("uid");
+                final boolean verified = json.optBoolean("verified", false);
+                final byte[] unwrapkB = Utils.hex2Byte(json.getString("unwrapBKey"));
+                final byte[] sessionToken = Utils.hex2Byte(json.getString("sessionToken"));
+                final byte[] keyFetchToken = Utils.hex2Byte(json.getString("keyFetchToken"));
+                final String authServerEndpoint =
+                        json.optString("authServerEndpoint", FxAccountConstants.DEFAULT_AUTH_SERVER_ENDPOINT);
+                final String tokenServerEndpoint =
+                        json.optString("tokenServerEndpoint", FxAccountConstants.DEFAULT_TOKEN_SERVER_ENDPOINT);
+                final String profileServerEndpoint =
+                        json.optString("profileServerEndpoint", FxAccountConstants.DEFAULT_PROFILE_SERVER_ENDPOINT);
+                // TODO: handle choose what to Sync.
+                State state = new Engaged(email, uid, verified, unwrapkB, sessionToken, keyFetchToken);
+                fxAccount = AndroidFxAccount.addAndroidAccount(mContext,
+                        email,
+                        mProfile.getName(),
+                        authServerEndpoint,
+                        tokenServerEndpoint,
+                        profileServerEndpoint,
+                        state,
+                        AndroidFxAccount.DEFAULT_AUTHORITIES_TO_SYNC_AUTOMATICALLY_MAP);
+            } catch (URISyntaxException | GeneralSecurityException | UnsupportedEncodingException e) {
+                Log.w(LOGTAG, "Got exception creating Firefox Account from JSON; ignoring.", e);
+                if (callback != null) {
+                    callback.sendError("Could not create Firefox Account from JSON: " + e.toString());
+                    return;
+                }
+            }
+            if (callback != null) {
+                callback.sendSuccess(fxAccount != null);
+            }
+
+        } else if ("Accounts:UpdateFirefoxAccountFromJSON".equals(event)) {
+            try {
+                final Account account = FirefoxAccounts.getFirefoxAccount(mContext);
+                if (account == null) {
+                    if (callback != null) {
+                        callback.sendError("Could not update Firefox Account since none exists");
+                    }
+                    return;
+                }
+
+                final NativeJSObject json = message.getObject("json");
+                final String email = json.getString("email");
+                final String uid = json.getString("uid");
+
+                // Protect against cross-connecting accounts.
+                if (account.name == null || !account.name.equals(email)) {
+                    final String errorMessage = "Cannot update Firefox Account from JSON: datum has different email address!";
+                    Log.e(LOGTAG, errorMessage);
+                    if (callback != null) {
+                        callback.sendError(errorMessage);
+                    }
+                    return;
+                }
+
+                final boolean verified = json.optBoolean("verified", false);
+                final byte[] unwrapkB = Utils.hex2Byte(json.getString("unwrapBKey"));
+                final byte[] sessionToken = Utils.hex2Byte(json.getString("sessionToken"));
+                final byte[] keyFetchToken = Utils.hex2Byte(json.getString("keyFetchToken"));
+                final State state = new Engaged(email, uid, verified, unwrapkB, sessionToken, keyFetchToken);
+
+                final AndroidFxAccount fxAccount = new AndroidFxAccount(mContext, account);
+                fxAccount.setState(state);
+
+                if (callback != null) {
+                    callback.sendSuccess(true);
+                }
+            } catch (NativeJSObject.InvalidPropertyException e) {
+                Log.w(LOGTAG, "Got exception updating Firefox Account from JSON; ignoring.", e);
+                if (callback != null) {
+                    callback.sendError("Could not update Firefox Account from JSON: " + e.toString());
+                    return;
+                }
+            }
+
+        } else if ("Accounts:Create".equals(event)) {
+            // Do exactly the same thing as if you tapped 'Sync' in Settings.
+            final Intent intent = new Intent(FxAccountConstants.ACTION_FXA_GET_STARTED);
+            intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
+            final NativeJSObject extras = message.optObject("extras", null);
+            if (extras != null) {
+                intent.putExtra("extras", extras.toString());
+            }
+            mContext.startActivity(intent);
+
+        } else if ("Accounts:DeleteFirefoxAccount".equals(event)) {
+            try {
+                final Account account = FirefoxAccounts.getFirefoxAccount(mContext);
+                if (account == null) {
+                    Log.w(LOGTAG, "Could not delete Firefox Account since none exists!");
+                    if (callback != null) {
+                        callback.sendError("Could not delete Firefox Account since none exists");
+                    }
+                    return;
+                }
+
+                final AccountManagerCallback<Boolean> accountManagerCallback = new AccountManagerCallback<Boolean>() {
+                    @Override
+                    public void run(AccountManagerFuture<Boolean> future) {
+                        try {
+                            final boolean result = future.getResult();
+                            Log.i(LOGTAG, "Account named like " + Utils.obfuscateEmail(account.name) + " removed: " + result);
+                            if (callback != null) {
+                                callback.sendSuccess(result);
+                            }
+                        } catch (OperationCanceledException | IOException | AuthenticatorException e) {
+                            if (callback != null) {
+                                callback.sendError("Could not delete Firefox Account: " + e.toString());
+                            }
+                        }
+                    }
+                };
+
+                AccountManager.get(mContext).removeAccount(account, accountManagerCallback, null);
+            } catch (Exception e) {
+                Log.w(LOGTAG, "Got exception updating Firefox Account from JSON; ignoring.", e);
+                if (callback != null) {
+                    callback.sendError("Could not update Firefox Account from JSON: " + e.toString());
+                    return;
+                }
+            }
+
+        } else if ("Accounts:Exist".equals(event)) {
+            if (callback == null) {
+                Log.w(LOGTAG, "Accounts:Exist requires a callback");
+                return;
+            }
+
+            final String kind = message.optString("kind", null);
+            final JSONObject response = new JSONObject();
+
+            try {
+                if ("any".equals(kind)) {
+                    response.put("exists", SyncAccounts.syncAccountsExist(mContext) ||
+                            FirefoxAccounts.firefoxAccountsExist(mContext));
+                    callback.sendSuccess(response);
+                } else if ("fxa".equals(kind)) {
+                    final Account account = FirefoxAccounts.getFirefoxAccount(mContext);
+                    response.put("exists", account != null);
+                    if (account != null) {
+                        response.put("email", account.name);
+                        // We should always be able to extract the server endpoints.
+                        final AndroidFxAccount fxAccount = new AndroidFxAccount(mContext, account);
+                        response.put("authServerEndpoint", fxAccount.getAccountServerURI());
+                        response.put("profileServerEndpoint", fxAccount.getProfileServerURI());
+                        response.put("tokenServerEndpoint", fxAccount.getTokenServerURI());
+                        try {
+                            // It is possible for the state fetch to fail and us to not be able to provide a UID.
+                            // Long term, the UID (and verification flag) will be attached to the Android account
+                            // user data and not the internal state representation.
+                            final State state = fxAccount.getState();
+                            response.put("uid", state.uid);
+                        } catch (Exception e) {
+                            Log.w(LOGTAG, "Got exception extracting account UID; ignoring.", e);
+                        }
+                    }
+
+                    callback.sendSuccess(response);
+                } else if ("sync11".equals(kind)) {
+                    response.put("exists", SyncAccounts.syncAccountsExist(mContext));
+                    callback.sendSuccess(response);
+                } else {
+                    callback.sendError("Could not query account existence: unknown kind.");
+                }
+            } catch (JSONException e) {
+                Log.w(LOGTAG, "Got exception querying account existence; ignoring.", e);
+                callback.sendError("Could not query account existence: " + e.toString());
+                return;
+            }
+        }
+    }
+}
--- a/mobile/android/base/BrowserApp.java
+++ b/mobile/android/base/BrowserApp.java
@@ -19,22 +19,16 @@ import org.mozilla.gecko.db.BrowserContr
 import org.mozilla.gecko.db.BrowserDB;
 import org.mozilla.gecko.db.SuggestedSites;
 import org.mozilla.gecko.distribution.Distribution;
 import org.mozilla.gecko.favicons.Favicons;
 import org.mozilla.gecko.favicons.LoadFaviconTask;
 import org.mozilla.gecko.favicons.OnFaviconLoadedListener;
 import org.mozilla.gecko.favicons.decoders.IconDirectoryEntry;
 import org.mozilla.gecko.firstrun.FirstrunPane;
-import org.mozilla.gecko.fxa.FirefoxAccounts;
-import org.mozilla.gecko.fxa.FxAccountConstants;
-import org.mozilla.gecko.fxa.activities.FxAccountGetStartedActivity;
-import org.mozilla.gecko.fxa.authenticator.AndroidFxAccount;
-import org.mozilla.gecko.fxa.login.Engaged;
-import org.mozilla.gecko.fxa.login.State;
 import org.mozilla.gecko.gfx.BitmapUtils;
 import org.mozilla.gecko.gfx.DynamicToolbarAnimator;
 import org.mozilla.gecko.gfx.ImmutableViewportMetrics;
 import org.mozilla.gecko.gfx.LayerView;
 import org.mozilla.gecko.health.BrowserHealthRecorder;
 import org.mozilla.gecko.health.BrowserHealthReporter;
 import org.mozilla.gecko.health.HealthRecorder;
 import org.mozilla.gecko.health.SessionInformation;
@@ -52,19 +46,17 @@ import org.mozilla.gecko.menu.GeckoMenuI
 import org.mozilla.gecko.mozglue.ContextUtils;
 import org.mozilla.gecko.mozglue.ContextUtils.SafeIntent;
 import org.mozilla.gecko.overlays.ui.ShareDialog;
 import org.mozilla.gecko.preferences.ClearOnShutdownPref;
 import org.mozilla.gecko.preferences.GeckoPreferences;
 import org.mozilla.gecko.prompts.Prompt;
 import org.mozilla.gecko.prompts.PromptListItem;
 import org.mozilla.gecko.restrictions.Restriction;
-import org.mozilla.gecko.sync.Utils;
 import org.mozilla.gecko.sync.repositories.android.FennecTabsRepository;
-import org.mozilla.gecko.sync.setup.SyncAccounts;
 import org.mozilla.gecko.tabqueue.TabQueueHelper;
 import org.mozilla.gecko.tabqueue.TabQueuePrompt;
 import org.mozilla.gecko.tabs.TabHistoryController;
 import org.mozilla.gecko.tabs.TabHistoryController.OnShowTabHistory;
 import org.mozilla.gecko.tabs.TabHistoryFragment;
 import org.mozilla.gecko.tabs.TabHistoryPage;
 import org.mozilla.gecko.tabs.TabsPanel;
 import org.mozilla.gecko.toolbar.AutocompleteHandler;
@@ -257,16 +249,18 @@ public class BrowserApp extends GeckoApp
     private SharedPreferencesHelper mSharedPreferencesHelper;
 
     private OrderedBroadcastHelper mOrderedBroadcastHelper;
 
     private BrowserHealthReporter mBrowserHealthReporter;
 
     private ReadingListHelper mReadingListHelper;
 
+    private AccountsHelper mAccountsHelper;
+
     // The tab to be selected on editing mode exit.
     private Integer mTargetTabForEditingMode;
 
     private final TabEditingState mLastTabEditingState = new TabEditingState();
 
     // The animator used to toggle HomePager visibility has a race where if the HomePager is shown
     // (starting the animation), the HomePager is hidden, and the HomePager animation completes,
     // both the web content and the HomePager will be hidden. This flag is used to prevent the
@@ -834,22 +828,19 @@ public class BrowserApp extends GeckoApp
         mFindInPageBar = (FindInPageBar) findViewById(R.id.find_in_page);
         mMediaCastingBar = (MediaCastingBar) findViewById(R.id.media_casting);
 
         EventDispatcher.getInstance().registerGeckoThreadListener((GeckoEventListener)this,
             "Menu:Open",
             "Menu:Update",
             "LightweightTheme:Update",
             "Search:Keyword",
-            "Prompt:ShowTop",
-            "Accounts:Exist");
+            "Prompt:ShowTop");
 
         EventDispatcher.getInstance().registerGeckoThreadListener((NativeEventListener)this,
-            "Accounts:Create",
-            "Accounts:CreateFirefoxAccountFromJSON",
             "CharEncoding:Data",
             "CharEncoding:State",
             "Favicon:CacheLoad",
             "Feedback:LastUrl",
             "Feedback:MaybeLater",
             "Feedback:OpenPlayStore",
             "Menu:Add",
             "Menu:Remove",
@@ -867,16 +858,17 @@ public class BrowserApp extends GeckoApp
         final BrowserDB db = getProfile().getDB();
         db.setSuggestedSites(suggestedSites);
 
         JavaAddonManager.getInstance().init(appContext);
         mSharedPreferencesHelper = new SharedPreferencesHelper(appContext);
         mOrderedBroadcastHelper = new OrderedBroadcastHelper(appContext);
         mBrowserHealthReporter = new BrowserHealthReporter();
         mReadingListHelper = new ReadingListHelper(appContext, getProfile(), this);
+        mAccountsHelper = new AccountsHelper(appContext, getProfile());
 
         if (AppConstants.MOZ_ANDROID_BEAM) {
             NfcAdapter nfc = NfcAdapter.getDefaultAdapter(this);
             if (nfc != null) {
                 nfc.setNdefPushMessageCallback(new NfcAdapter.CreateNdefMessageCallback() {
                     @Override
                     public NdefMessage createNdefMessage(NfcEvent event) {
                         Tab tab = Tabs.getInstance().getSelectedTab();
@@ -1410,31 +1402,34 @@ public class BrowserApp extends GeckoApp
             mBrowserHealthReporter.uninit();
             mBrowserHealthReporter = null;
         }
 
         if (mReadingListHelper != null) {
             mReadingListHelper.uninit();
             mReadingListHelper = null;
         }
+
+        if (mAccountsHelper != null) {
+            mAccountsHelper.uninit();
+            mAccountsHelper = null;
+        }
+
         if (mZoomedView != null) {
             mZoomedView.destroy();
         }
 
         EventDispatcher.getInstance().unregisterGeckoThreadListener((GeckoEventListener) this,
             "Menu:Open",
             "Menu:Update",
             "LightweightTheme:Update",
             "Search:Keyword",
-            "Prompt:ShowTop",
-            "Accounts:Exist");
+            "Prompt:ShowTop");
 
         EventDispatcher.getInstance().unregisterGeckoThreadListener((NativeEventListener) this,
-            "Accounts:Create",
-            "Accounts:CreateFirefoxAccountFromJSON",
             "CharEncoding:Data",
             "CharEncoding:State",
             "Favicon:CacheLoad",
             "Feedback:LastUrl",
             "Feedback:MaybeLater",
             "Feedback:OpenPlayStore",
             "Menu:Add",
             "Menu:Remove",
@@ -1683,63 +1678,17 @@ public class BrowserApp extends GeckoApp
         }
 
         mBrowserToolbar.refresh();
     }
 
     @Override
     public void handleMessage(final String event, final NativeJSObject message,
                               final EventCallback callback) {
-        if ("Accounts:CreateFirefoxAccountFromJSON".equals(event)) {
-            AndroidFxAccount fxAccount = null;
-            try {
-                final NativeJSObject json = message.getObject("json");
-                final String email = json.getString("email");
-                final String uid = json.getString("uid");
-                final boolean verified = json.optBoolean("verified", false);
-                final byte[] unwrapkB = Utils.hex2Byte(json.getString("unwrapBKey"));
-                final byte[] sessionToken = Utils.hex2Byte(json.getString("sessionToken"));
-                final byte[] keyFetchToken = Utils.hex2Byte(json.getString("keyFetchToken"));
-                final String authServerEndpoint =
-                    json.optString("authServerEndpoint", FxAccountConstants.DEFAULT_AUTH_SERVER_ENDPOINT);
-                final String tokenServerEndpoint =
-                    json.optString("tokenServerEndpoint", FxAccountConstants.DEFAULT_TOKEN_SERVER_ENDPOINT);
-                final String profileServerEndpoint =
-                    json.optString("profileServerEndpoint", FxAccountConstants.DEFAULT_PROFILE_SERVER_ENDPOINT);
-                // TODO: handle choose what to Sync.
-                State state = new Engaged(email, uid, verified, unwrapkB, sessionToken, keyFetchToken);
-                fxAccount = AndroidFxAccount.addAndroidAccount(this,
-                        email,
-                        getProfile().getName(),
-                        authServerEndpoint,
-                        tokenServerEndpoint,
-                        profileServerEndpoint,
-                        state,
-                        AndroidFxAccount.DEFAULT_AUTHORITIES_TO_SYNC_AUTOMATICALLY_MAP);
-            } catch (Exception e) {
-                Log.w(LOGTAG, "Got exception creating Firefox Account from JSON; ignoring.", e);
-                if (callback == null) {
-                    callback.sendError("Could not create Firefox Account from JSON: " + e.toString());
-                }
-            }
-            if (callback != null) {
-                callback.sendSuccess(fxAccount != null);
-            }
-
-        } else if ("Accounts:Create".equals(event)) {
-            // Do exactly the same thing as if you tapped 'Sync' in Settings.
-            final Intent intent = new Intent(getContext(), FxAccountGetStartedActivity.class);
-            intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
-            final NativeJSObject extras = message.optObject("extras", null);
-            if (extras != null) {
-                intent.putExtra("extras", extras.toString());
-            }
-            getContext().startActivity(intent);
-
-        } else if ("CharEncoding:Data".equals(event)) {
+        if ("CharEncoding:Data".equals(event)) {
             final NativeJSObject[] charsets = message.getObjectArray("charsets");
             final int selected = message.getInt("selected");
 
             final String[] titleArray = new String[charsets.length];
             final String[] codeArray = new String[charsets.length];
             for (int i = 0; i < charsets.length; i++) {
                 final NativeJSObject charset = charsets[i];
                 titleArray[i] = charset.getString("title");
@@ -2028,34 +1977,16 @@ public class BrowserApp extends GeckoApp
                     }
                 });
             } else if (event.equals("Prompt:ShowTop")) {
                 // Bring this activity to front so the prompt is visible..
                 Intent bringToFrontIntent = new Intent();
                 bringToFrontIntent.setClassName(AppConstants.ANDROID_PACKAGE_NAME, AppConstants.MOZ_ANDROID_BROWSER_INTENT_CLASS);
                 bringToFrontIntent.setFlags(Intent.FLAG_ACTIVITY_REORDER_TO_FRONT);
                 startActivity(bringToFrontIntent);
-            } else if (event.equals("Accounts:Exist")) {
-                final String kind = message.getString("kind");
-                final JSONObject response = new JSONObject();
-
-                if ("any".equals(kind)) {
-                    response.put("exists", SyncAccounts.syncAccountsExist(getContext()) ||
-                                           FirefoxAccounts.firefoxAccountsExist(getContext()));
-                    EventDispatcher.sendResponse(message, response);
-                } else if ("fxa".equals(kind)) {
-                    response.put("exists", FirefoxAccounts.firefoxAccountsExist(getContext()));
-                    EventDispatcher.sendResponse(message, response);
-                } else if ("sync11".equals(kind)) {
-                    response.put("exists", SyncAccounts.syncAccountsExist(getContext()));
-                    EventDispatcher.sendResponse(message, response);
-                } else {
-                    response.put("error", "Unknown kind");
-                    EventDispatcher.sendError(message, response);
-                }
             } else {
                 super.handleMessage(event, message);
             }
         } catch (Exception e) {
             Log.e(LOGTAG, "Exception handling message \"" + event + "\":", e);
         }
     }
 
--- a/mobile/android/base/ThumbnailHelper.java
+++ b/mobile/android/base/ThumbnailHelper.java
@@ -27,17 +27,17 @@ import java.util.concurrent.atomic.Atomi
  * applied between thumbnail processing. This allows a single thumbnail buffer to
  * be used for all thumbnails.
  */
 public final class ThumbnailHelper {
     private static final String LOGTAG = "GeckoThumbnailHelper";
 
     public static final float TABS_PANEL_THUMBNAIL_ASPECT_RATIO = 0.8333333f;
     public static final float TOP_SITES_THUMBNAIL_ASPECT_RATIO = 0.571428571f;  // this is a 4:7 ratio (as per UX decision)
-    private static final float THUMBNAIL_ASPECT_RATIO;
+    public static final float THUMBNAIL_ASPECT_RATIO;
 
     static {
       // As we only want to generate one thumbnail for each tab, we calculate the
       // largest aspect ratio required and create the thumbnail based off that.
       // Any views with a smaller aspect ratio will use a cropped version of the
       // same image.
       THUMBNAIL_ASPECT_RATIO = Math.max(TABS_PANEL_THUMBNAIL_ASPECT_RATIO, TOP_SITES_THUMBNAIL_ASPECT_RATIO);
     }
--- a/mobile/android/base/android-services.mozbuild
+++ b/mobile/android/base/android-services.mozbuild
@@ -848,26 +848,31 @@ sync_java_files = [
     'browserid/verifier/BrowserIDVerifierDelegate.java',
     'browserid/verifier/BrowserIDVerifierException.java',
     'browserid/VerifyingPublicKey.java',
     'fxa/AccountLoader.java',
     'fxa/activities/FxAccountAbstractActivity.java',
     'fxa/activities/FxAccountAbstractSetupActivity.java',
     'fxa/activities/FxAccountAbstractUpdateCredentialsActivity.java',
     'fxa/activities/FxAccountConfirmAccountActivity.java',
+    'fxa/activities/FxAccountConfirmAccountActivityWeb.java',
     'fxa/activities/FxAccountCreateAccountActivity.java',
     'fxa/activities/FxAccountCreateAccountNotAllowedActivity.java',
     'fxa/activities/FxAccountFinishMigratingActivity.java',
+    'fxa/activities/FxAccountFinishMigratingActivityWeb.java',
     'fxa/activities/FxAccountGetStartedActivity.java',
+    'fxa/activities/FxAccountGetStartedActivityWeb.java',
     'fxa/activities/FxAccountMigrationFinishedActivity.java',
     'fxa/activities/FxAccountSignInActivity.java',
     'fxa/activities/FxAccountStatusActivity.java',
     'fxa/activities/FxAccountStatusFragment.java',
     'fxa/activities/FxAccountUpdateCredentialsActivity.java',
+    'fxa/activities/FxAccountUpdateCredentialsActivityWeb.java',
     'fxa/activities/FxAccountVerifiedAccountActivity.java',
+    'fxa/activities/FxAccountWebFlowActivity.java',
     'fxa/activities/PicassoPreferenceIconTarget.java',
     'fxa/authenticator/AccountPickler.java',
     'fxa/authenticator/AndroidFxAccount.java',
     'fxa/authenticator/FxAccountAuthenticator.java',
     'fxa/authenticator/FxAccountAuthenticatorService.java',
     'fxa/authenticator/FxAccountLoginDelegate.java',
     'fxa/authenticator/FxAccountLoginException.java',
     'fxa/authenticator/FxADefaultLoginStateMachineDelegate.java',
--- a/mobile/android/base/firstrun/WelcomePanel.java
+++ b/mobile/android/base/firstrun/WelcomePanel.java
@@ -8,31 +8,32 @@ package org.mozilla.gecko.firstrun;
 import android.content.Intent;
 import android.os.Bundle;
 import android.view.LayoutInflater;
 import android.view.View;
 import android.view.ViewGroup;
 import org.mozilla.gecko.R;
 import org.mozilla.gecko.Telemetry;
 import org.mozilla.gecko.TelemetryContract;
-import org.mozilla.gecko.fxa.activities.FxAccountGetStartedActivity;
+import org.mozilla.gecko.fxa.FxAccountConstants;
 
 public class WelcomePanel extends FirstrunPanel {
     public static final int TITLE_RES = R.string.firstrun_panel_title_welcome;
 
     @Override
     public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstance) {
         final ViewGroup root = (ViewGroup) inflater.inflate(R.layout.firstrun_welcome_fragment, container, false);
         root.findViewById(R.id.welcome_account).setOnClickListener(new View.OnClickListener() {
             @Override
             public void onClick(View v) {
                 Telemetry.sendUIEvent(TelemetryContract.Event.ACTION, TelemetryContract.Method.BUTTON, "firstrun-sync");
 
-                final Intent accountIntent = new Intent(getActivity(), FxAccountGetStartedActivity.class);
-                startActivity(accountIntent);
+                final Intent intent = new Intent(FxAccountConstants.ACTION_FXA_GET_STARTED);
+                intent.setFlags(Intent.FLAG_ACTIVITY_NO_ANIMATION);
+                startActivity(intent);
 
                 close();
             }
         });
 
         root.findViewById(R.id.welcome_browse).setOnClickListener(new View.OnClickListener() {
             @Override
             public void onClick(View v) {
--- a/mobile/android/base/fxa/FxAccountConstants.java
+++ b/mobile/android/base/fxa/FxAccountConstants.java
@@ -89,9 +89,15 @@ public class FxAccountConstants {
    * This action is broadcast when an Android Firefox Account's internal state
    * is changed.
    * <p>
    * It is protected by signing-level permission PER_ACCOUNT_TYPE_PERMISSION and
    * can be received only by Firefox versions sharing the same Android Firefox
    * Account type.
    */
   public static final String ACCOUNT_STATE_CHANGED_ACTION = AppConstants.MOZ_ANDROID_SHARED_FXACCOUNT_TYPE + ".accounts.ACCOUNT_STATE_CHANGED_ACTION";
+
+  public static final String ACTION_FXA_CONFIRM_ACCOUNT            = AppConstants.ANDROID_PACKAGE_NAME + ".ACTION_FXA_CONFIRM_ACCOUNT";
+  public static final String ACTION_FXA_FINISH_MIGRATING           = AppConstants.ANDROID_PACKAGE_NAME + ".ACTION_FXA_FINISH_MIGRATING";
+  public static final String ACTION_FXA_GET_STARTED                = AppConstants.ANDROID_PACKAGE_NAME + ".ACTION_FXA_GET_STARTED";
+  public static final String ACTION_FXA_STATUS                     = AppConstants.ANDROID_PACKAGE_NAME + ".ACTION_FXA_STATUS";
+  public static final String ACTION_FXA_UPDATE_CREDENTIALS         = AppConstants.ANDROID_PACKAGE_NAME + ".ACTION_FXA_UPDATE_CREDENTIALS";
 }
--- a/mobile/android/base/fxa/activities/FxAccountAbstractActivity.java
+++ b/mobile/android/base/fxa/activities/FxAccountAbstractActivity.java
@@ -1,28 +1,28 @@
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 package org.mozilla.gecko.fxa.activities;
 
-import org.mozilla.gecko.Locales.LocaleAwareActivity;
-import org.mozilla.gecko.background.common.log.Logger;
-import org.mozilla.gecko.background.fxa.FxAccountAgeLockoutHelper;
-import org.mozilla.gecko.fxa.FirefoxAccounts;
-import org.mozilla.gecko.fxa.authenticator.AndroidFxAccount;
-import org.mozilla.gecko.sync.setup.activities.ActivityUtils;
-
 import android.accounts.Account;
 import android.app.Activity;
 import android.content.Intent;
 import android.os.SystemClock;
 import android.view.View;
 import android.view.View.OnClickListener;
 import android.widget.TextView;
+import org.mozilla.gecko.Locales.LocaleAwareActivity;
+import org.mozilla.gecko.background.common.log.Logger;
+import org.mozilla.gecko.background.fxa.FxAccountAgeLockoutHelper;
+import org.mozilla.gecko.fxa.FirefoxAccounts;
+import org.mozilla.gecko.fxa.FxAccountConstants;
+import org.mozilla.gecko.fxa.authenticator.AndroidFxAccount;
+import org.mozilla.gecko.sync.setup.activities.ActivityUtils;
 
 public abstract class FxAccountAbstractActivity extends LocaleAwareActivity {
   private static final String LOG_TAG = FxAccountAbstractActivity.class.getSimpleName();
 
   protected final boolean cannotResumeWhenAccountsExist;
   protected final boolean cannotResumeWhenNoAccountsExist;
   protected final boolean cannotResumeWhenLockedOut;
 
@@ -38,36 +38,40 @@ public abstract class FxAccountAbstractA
     this.cannotResumeWhenLockedOut = 0 != (resume & CANNOT_RESUME_WHEN_LOCKED_OUT);
   }
 
   /**
    * Many Firefox Accounts activities shouldn't display if an account already
    * exists or if account creation is locked out due to an age verification
    * check failing (getting started, create account, sign in). This function
    * redirects as appropriate.
+   *
+   * @return true if redirected.
    */
-  protected void redirectIfAppropriate() {
+  protected boolean redirectIfAppropriate() {
     if (cannotResumeWhenAccountsExist || cannotResumeWhenNoAccountsExist) {
       final Account account = FirefoxAccounts.getFirefoxAccount(this);
       if (cannotResumeWhenAccountsExist && account != null) {
-        redirectToActivity(FxAccountStatusActivity.class);
-        return;
+        redirectToAction(FxAccountConstants.ACTION_FXA_STATUS);
+        return true;
       }
       if (cannotResumeWhenNoAccountsExist && account == null) {
-        redirectToActivity(FxAccountGetStartedActivity.class);
-        return;
+        redirectToAction(FxAccountConstants.ACTION_FXA_GET_STARTED);
+        return true;
       }
     }
     if (cannotResumeWhenLockedOut) {
       if (FxAccountAgeLockoutHelper.isLockedOut(SystemClock.elapsedRealtime())) {
         this.setResult(RESULT_CANCELED);
-        redirectToActivity(FxAccountCreateAccountNotAllowedActivity.class);
-        return;
+        launchActivity(FxAccountCreateAccountNotAllowedActivity.class);
+        finish();
+        return true;
       }
     }
+    return false;
   }
 
   @Override
   public void onResume() {
     super.onResume();
     redirectIfAppropriate();
   }
 
@@ -80,18 +84,22 @@ public abstract class FxAccountAbstractA
   protected void launchActivity(Class<? extends Activity> activityClass) {
     Intent intent = new Intent(this, activityClass);
     // Per http://stackoverflow.com/a/8992365, this triggers a known bug with
     // the soft keyboard not being shown for the started activity. Why, Android, why?
     intent.setFlags(Intent.FLAG_ACTIVITY_NO_ANIMATION);
     startActivity(intent);
   }
 
-  protected void redirectToActivity(Class<? extends Activity> activityClass) {
-    launchActivity(activityClass);
+  protected void redirectToAction(final String action) {
+    final Intent intent = new Intent(action);
+    // Per http://stackoverflow.com/a/8992365, this triggers a known bug with
+    // the soft keyboard not being shown for the started activity. Why, Android, why?
+    intent.setFlags(Intent.FLAG_ACTIVITY_NO_ANIMATION);
+    startActivity(intent);
     finish();
   }
 
   /**
    * Helper to find view or error if it is missing.
    *
    * @param id of view to find.
    * @param description to print in error.
--- a/mobile/android/base/fxa/activities/FxAccountAbstractUpdateCredentialsActivity.java
+++ b/mobile/android/base/fxa/activities/FxAccountAbstractUpdateCredentialsActivity.java
@@ -1,44 +1,44 @@
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 package org.mozilla.gecko.fxa.activities;
 
-import java.util.concurrent.Executor;
-import java.util.concurrent.Executors;
-
+import android.content.Intent;
+import android.os.Bundle;
+import android.view.View;
+import android.view.View.OnClickListener;
+import android.widget.AutoCompleteTextView;
+import android.widget.Button;
+import android.widget.EditText;
+import android.widget.ProgressBar;
+import android.widget.TextView;
 import org.mozilla.gecko.R;
 import org.mozilla.gecko.background.common.log.Logger;
 import org.mozilla.gecko.background.common.telemetry.TelemetryWrapper;
 import org.mozilla.gecko.background.fxa.FxAccountClient;
 import org.mozilla.gecko.background.fxa.FxAccountClient10.RequestDelegate;
 import org.mozilla.gecko.background.fxa.FxAccountClient20;
 import org.mozilla.gecko.background.fxa.FxAccountClient20.LoginResponse;
 import org.mozilla.gecko.background.fxa.FxAccountClientException.FxAccountClientRemoteException;
 import org.mozilla.gecko.background.fxa.FxAccountUtils;
 import org.mozilla.gecko.background.fxa.PasswordStretcher;
 import org.mozilla.gecko.fxa.FirefoxAccounts;
+import org.mozilla.gecko.fxa.FxAccountConstants;
 import org.mozilla.gecko.fxa.authenticator.AndroidFxAccount;
 import org.mozilla.gecko.fxa.login.Engaged;
 import org.mozilla.gecko.fxa.login.State;
 import org.mozilla.gecko.fxa.tasks.FxAccountSignInTask;
 import org.mozilla.gecko.sync.setup.activities.ActivityUtils;
 import org.mozilla.gecko.sync.telemetry.TelemetryContract;
 
-import android.content.Intent;
-import android.os.Bundle;
-import android.view.View;
-import android.view.View.OnClickListener;
-import android.widget.AutoCompleteTextView;
-import android.widget.Button;
-import android.widget.EditText;
-import android.widget.ProgressBar;
-import android.widget.TextView;
+import java.util.concurrent.Executor;
+import java.util.concurrent.Executors;
 
 /**
  * Abstract activity which displays a screen for updating the local password.
  */
 public abstract class FxAccountAbstractUpdateCredentialsActivity extends FxAccountAbstractSetupActivity {
   protected static final String LOG_TAG = FxAccountAbstractUpdateCredentialsActivity.class.getSimpleName();
 
   protected AndroidFxAccount fxAccount;
@@ -104,17 +104,17 @@ public abstract class FxAccountAbstractU
 
     @Override
     public void handleFailure(FxAccountClientRemoteException e) {
       if (e.isUpgradeRequired()) {
         Logger.error(LOG_TAG, "Got upgrade required from remote server; transitioning Firefox Account to Doghouse state.");
         final State state = fxAccount.getState();
         fxAccount.setState(state.makeDoghouseState());
         // The status activity will say that the user needs to upgrade.
-        redirectToActivity(FxAccountStatusActivity.class);
+        redirectToAction(FxAccountConstants.ACTION_FXA_STATUS);
         return;
       }
       showRemoteError(e, R.string.fxaccount_update_credentials_unknown_error);
     }
 
     @Override
     public void handleSuccess(LoginResponse result) {
       Logger.info(LOG_TAG, "Got success signing in.");
--- a/mobile/android/base/fxa/activities/FxAccountConfirmAccountActivity.java
+++ b/mobile/android/base/fxa/activities/FxAccountConfirmAccountActivity.java
@@ -1,34 +1,34 @@
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 package org.mozilla.gecko.fxa.activities;
 
-import org.mozilla.gecko.R;
-import org.mozilla.gecko.background.common.log.Logger;
-import org.mozilla.gecko.fxa.FirefoxAccounts;
-import org.mozilla.gecko.fxa.SyncStatusListener;
-import org.mozilla.gecko.fxa.authenticator.AndroidFxAccount;
-import org.mozilla.gecko.fxa.login.Engaged;
-import org.mozilla.gecko.fxa.login.State;
-import org.mozilla.gecko.fxa.login.State.Action;
-import org.mozilla.gecko.fxa.sync.FxAccountSyncStatusHelper;
-import org.mozilla.gecko.fxa.tasks.FxAccountCodeResender;
-import org.mozilla.gecko.sync.setup.activities.ActivityUtils;
-
 import android.accounts.Account;
 import android.app.Activity;
 import android.content.Context;
 import android.content.Intent;
 import android.os.Bundle;
 import android.view.View;
 import android.view.View.OnClickListener;
 import android.widget.TextView;
+import org.mozilla.gecko.R;
+import org.mozilla.gecko.background.common.log.Logger;
+import org.mozilla.gecko.fxa.FirefoxAccounts;
+import org.mozilla.gecko.fxa.FxAccountConstants;
+import org.mozilla.gecko.fxa.SyncStatusListener;
+import org.mozilla.gecko.fxa.authenticator.AndroidFxAccount;
+import org.mozilla.gecko.fxa.login.Engaged;
+import org.mozilla.gecko.fxa.login.State;
+import org.mozilla.gecko.fxa.login.State.Action;
+import org.mozilla.gecko.fxa.sync.FxAccountSyncStatusHelper;
+import org.mozilla.gecko.fxa.tasks.FxAccountCodeResender;
+import org.mozilla.gecko.sync.setup.activities.ActivityUtils;
 
 /**
  * Activity which displays account created successfully screen to the user, and
  * starts them on the email verification path.
  */
 public class FxAccountConfirmAccountActivity extends FxAccountAbstractActivity implements OnClickListener {
   private static final String LOG_TAG = FxAccountConfirmAccountActivity.class.getSimpleName();
 
@@ -141,17 +141,17 @@ public class FxAccountConfirmAccountActi
     case NeedsVerification:
       // This is what we're here to handle.
       break;
     default:
       // We're not in the right place!  Redirect to status.
       Logger.warn(LOG_TAG, "No need to verify Firefox Account that needs action " + neededAction.toString() +
           " (in state " + state.getStateLabel() + ").");
       setResult(RESULT_CANCELED);
-      this.redirectToActivity(FxAccountStatusActivity.class);
+      redirectToAction(FxAccountConstants.ACTION_FXA_STATUS);
       return;
     }
 
     final String email = fxAccount.getEmail();
     final String text = getResources().getString(R.string.fxaccount_confirm_account_verification_link, email);
     verificationLinkTextView.setText(text);
 
     boolean resendLinkShouldBeEnabled = ((Engaged) state).getSessionToken() != null;
copy from mobile/android/base/fxa/activities/FxAccountConfirmAccountActivity.java
copy to mobile/android/base/fxa/activities/FxAccountConfirmAccountActivityWeb.java
--- a/mobile/android/base/fxa/activities/FxAccountConfirmAccountActivity.java
+++ b/mobile/android/base/fxa/activities/FxAccountConfirmAccountActivityWeb.java
@@ -1,172 +1,11 @@
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 package org.mozilla.gecko.fxa.activities;
 
-import org.mozilla.gecko.R;
-import org.mozilla.gecko.background.common.log.Logger;
-import org.mozilla.gecko.fxa.FirefoxAccounts;
-import org.mozilla.gecko.fxa.SyncStatusListener;
-import org.mozilla.gecko.fxa.authenticator.AndroidFxAccount;
-import org.mozilla.gecko.fxa.login.Engaged;
-import org.mozilla.gecko.fxa.login.State;
-import org.mozilla.gecko.fxa.login.State.Action;
-import org.mozilla.gecko.fxa.sync.FxAccountSyncStatusHelper;
-import org.mozilla.gecko.fxa.tasks.FxAccountCodeResender;
-import org.mozilla.gecko.sync.setup.activities.ActivityUtils;
-
-import android.accounts.Account;
-import android.app.Activity;
-import android.content.Context;
-import android.content.Intent;
-import android.os.Bundle;
-import android.view.View;
-import android.view.View.OnClickListener;
-import android.widget.TextView;
-
-/**
- * Activity which displays account created successfully screen to the user, and
- * starts them on the email verification path.
- */
-public class FxAccountConfirmAccountActivity extends FxAccountAbstractActivity implements OnClickListener {
-  private static final String LOG_TAG = FxAccountConfirmAccountActivity.class.getSimpleName();
-
-  // Set in onCreate.
-  protected TextView verificationLinkTextView;
-  protected View resendLink;
-  protected View changeEmail;
-
-  // Set in onResume.
-  protected AndroidFxAccount fxAccount;
-
-  protected final InnerSyncStatusDelegate syncStatusDelegate = new InnerSyncStatusDelegate();
-
-  public FxAccountConfirmAccountActivity() {
-    super(CANNOT_RESUME_WHEN_NO_ACCOUNTS_EXIST);
-  }
-
-  /**
-   * {@inheritDoc}
-   */
-  @Override
-  public void onCreate(Bundle icicle) {
-    Logger.debug(LOG_TAG, "onCreate(" + icicle + ")");
-
-    super.onCreate(icicle);
-    setContentView(R.layout.fxaccount_confirm_account);
-
-    verificationLinkTextView = (TextView) ensureFindViewById(null, R.id.verification_link_text, "verification link text");
-    resendLink = ensureFindViewById(null, R.id.resend_confirmation_email_link, "resend confirmation email link");
-    resendLink.setOnClickListener(this);
-    changeEmail = ensureFindViewById(null, R.id.change_confirmation_email_link, "change confirmation email address");
-    changeEmail.setOnClickListener(this);
-
-    View backToBrowsingButton = ensureFindViewById(null, R.id.button, "back to browsing button");
-    backToBrowsingButton.setOnClickListener(new OnClickListener() {
-      @Override
-      public void onClick(View v) {
-        ActivityUtils.openURLInFennec(v.getContext(), null);
-        setResult(Activity.RESULT_OK);
-        finish();
-      }
-    });
-  }
-
-  @Override
-  public void onResume() {
-    super.onResume();
-    this.fxAccount = getAndroidFxAccount();
-    if (fxAccount == null) {
-      Logger.warn(LOG_TAG, "Could not get Firefox Account.");
-      setResult(RESULT_CANCELED);
-      finish();
-      return;
-    }
-
-    FxAccountSyncStatusHelper.getInstance().startObserving(syncStatusDelegate);
-
-    refresh();
-
-    fxAccount.requestSync(FirefoxAccounts.NOW);
-  }
-
-  @Override
-  public void onPause() {
-    super.onPause();
-    FxAccountSyncStatusHelper.getInstance().stopObserving(syncStatusDelegate);
-
-    if (fxAccount != null) {
-      fxAccount.requestSync(FirefoxAccounts.SOON);
-    }
-  }
-
-  protected class InnerSyncStatusDelegate implements SyncStatusListener {
-    protected final Runnable refreshRunnable = new Runnable() {
-      @Override
-      public void run() {
-        refresh();
-      }
-    };
-
-    @Override
-    public Context getContext() {
-      return FxAccountConfirmAccountActivity.this;
-    }
-
-    @Override
-    public Account getAccount() {
-      return fxAccount.getAndroidAccount();
-    }
-
-    @Override
-    public void onSyncStarted() {
-      Logger.info(LOG_TAG, "Got sync started message; ignoring.");
-    }
-
-    @Override
-    public void onSyncFinished() {
-      if (fxAccount == null) {
-        return;
-      }
-      Logger.info(LOG_TAG, "Got sync finished message; refreshing.");
-      runOnUiThread(refreshRunnable);
-    }
-  }
-
-  protected void refresh() {
-    final State state = fxAccount.getState();
-    final Action neededAction = state.getNeededAction();
-    switch (neededAction) {
-    case NeedsVerification:
-      // This is what we're here to handle.
-      break;
-    default:
-      // We're not in the right place!  Redirect to status.
-      Logger.warn(LOG_TAG, "No need to verify Firefox Account that needs action " + neededAction.toString() +
-          " (in state " + state.getStateLabel() + ").");
-      setResult(RESULT_CANCELED);
-      this.redirectToActivity(FxAccountStatusActivity.class);
-      return;
-    }
-
-    final String email = fxAccount.getEmail();
-    final String text = getResources().getString(R.string.fxaccount_confirm_account_verification_link, email);
-    verificationLinkTextView.setText(text);
-
-    boolean resendLinkShouldBeEnabled = ((Engaged) state).getSessionToken() != null;
-    resendLink.setEnabled(resendLinkShouldBeEnabled);
-    resendLink.setClickable(resendLinkShouldBeEnabled);
-  }
-
-  @Override
-  public void onClick(View v) {
-    if (v.equals(resendLink)) {
-        FxAccountCodeResender.resendCode(this, fxAccount);
-    } else if (v.equals(changeEmail)) {
-      final Account account = fxAccount.getAndroidAccount();
-      Intent intent = new Intent(this, FxAccountGetStartedActivity.class);
-      FxAccountStatusActivity.maybeDeleteAndroidAccount(this, account, intent);
-    }
+public class FxAccountConfirmAccountActivityWeb extends FxAccountWebFlowActivity {
+  public FxAccountConfirmAccountActivityWeb() {
+    super(CANNOT_RESUME_WHEN_NO_ACCOUNTS_EXIST, "settings");
   }
 }
--- a/mobile/android/base/fxa/activities/FxAccountCreateAccountActivity.java
+++ b/mobile/android/base/fxa/activities/FxAccountCreateAccountActivity.java
@@ -424,17 +424,18 @@ public class FxAccountCreateAccountActiv
             : AndroidFxAccount.DEFAULT_AUTHORITIES_TO_SYNC_AUTOMATICALLY_MAP;
         if (FxAccountAgeLockoutHelper.passesAgeCheck(dayOfBirth, zeroBasedMonthOfBirth, yearEdit.getText().toString(), yearItems)) {
           FxAccountUtils.pii(LOG_TAG, "Passed age check.");
           createAccount(email, password, engines, authoritiesMap);
         } else {
           FxAccountUtils.pii(LOG_TAG, "Failed age check!");
           FxAccountAgeLockoutHelper.lockOut(SystemClock.elapsedRealtime());
           setResult(RESULT_CANCELED);
-          redirectToActivity(FxAccountCreateAccountNotAllowedActivity.class);
+          launchActivity(FxAccountCreateAccountNotAllowedActivity.class);
+          finish();
         }
       }
     });
   }
 
   /**
    * The "Choose what to sync" checkbox pops up a multi-choice dialog when it is
    * unchecked. It toggles to unchecked from checked.
copy from mobile/android/base/fxa/activities/FxAccountFinishMigratingActivity.java
copy to mobile/android/base/fxa/activities/FxAccountFinishMigratingActivityWeb.java
--- a/mobile/android/base/fxa/activities/FxAccountFinishMigratingActivity.java
+++ b/mobile/android/base/fxa/activities/FxAccountFinishMigratingActivityWeb.java
@@ -1,63 +1,11 @@
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 package org.mozilla.gecko.fxa.activities;
 
-import java.util.Map;
-
-import org.mozilla.gecko.R;
-import org.mozilla.gecko.background.common.log.Logger;
-import org.mozilla.gecko.background.fxa.FxAccountClient20.LoginResponse;
-import org.mozilla.gecko.fxa.login.State;
-import org.mozilla.gecko.fxa.login.State.StateLabel;
-
-import android.content.Intent;
-
-/**
- * Activity which displays a screen for inputting the password and finishing
- * migrating to Firefox Accounts / Sync 1.5.
- */
-public class FxAccountFinishMigratingActivity extends FxAccountAbstractUpdateCredentialsActivity {
-  protected static final String LOG_TAG = FxAccountFinishMigratingActivity.class.getSimpleName();
-
-  public FxAccountFinishMigratingActivity() {
-    super(R.layout.fxaccount_finish_migrating);
-  }
-
-  @Override
-  public void onResume() {
-    super.onResume();
-    this.fxAccount = getAndroidFxAccount();
-    if (fxAccount == null) {
-      Logger.warn(LOG_TAG, "Could not get Firefox Account.");
-      setResult(RESULT_CANCELED);
-      finish();
-      return;
-    }
-    final State state = fxAccount.getState();
-    if (state.getStateLabel() != StateLabel.MigratedFromSync11) {
-      Logger.warn(LOG_TAG, "Cannot finish migrating from Firefox Account in state: " + state.getStateLabel());
-      setResult(RESULT_CANCELED);
-      finish();
-      return;
-    }
-    emailEdit.setText(fxAccount.getEmail());
-  }
-
-  @Override
-  public Intent makeSuccessIntent(String email, LoginResponse result) {
-    final Intent successIntent = new Intent(this, FxAccountMigrationFinishedActivity.class);
-    // Per http://stackoverflow.com/a/8992365, this triggers a known bug with
-    // the soft keyboard not being shown for the started activity. Why, Android, why?
-    successIntent.setFlags(Intent.FLAG_ACTIVITY_NO_ANIMATION);
-    return successIntent;
-  }
-
-  @Override
-  protected Map<String, String> getQueryParameters() {
-    final Map<String, String> queryParameters = super.getQueryParameters();
-    queryParameters.put("migration", "sync11");
-    return queryParameters;
+public class FxAccountFinishMigratingActivityWeb extends FxAccountWebFlowActivity {
+  public FxAccountFinishMigratingActivityWeb() {
+    super(CANNOT_RESUME_WHEN_NO_ACCOUNTS_EXIST, "signin", "migration=sync11");
   }
 }
new file mode 100644
--- /dev/null
+++ b/mobile/android/base/fxa/activities/FxAccountGetStartedActivityWeb.java
@@ -0,0 +1,11 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.gecko.fxa.activities;
+
+public class FxAccountGetStartedActivityWeb extends FxAccountWebFlowActivity {
+  public FxAccountGetStartedActivityWeb() {
+    super(CANNOT_RESUME_WHEN_ACCOUNTS_EXIST, "signin");
+  }
+}
--- a/mobile/android/base/fxa/activities/FxAccountStatusActivity.java
+++ b/mobile/android/base/fxa/activities/FxAccountStatusActivity.java
@@ -1,23 +1,14 @@
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 package org.mozilla.gecko.fxa.activities;
 
-import org.mozilla.gecko.AppConstants;
-import org.mozilla.gecko.Locales.LocaleAwareFragmentActivity;
-import org.mozilla.gecko.R;
-import org.mozilla.gecko.background.common.log.Logger;
-import org.mozilla.gecko.background.fxa.FxAccountUtils;
-import org.mozilla.gecko.fxa.FirefoxAccounts;
-import org.mozilla.gecko.fxa.authenticator.AndroidFxAccount;
-import org.mozilla.gecko.sync.Utils;
-
 import android.accounts.Account;
 import android.accounts.AccountManager;
 import android.accounts.AccountManagerCallback;
 import android.accounts.AccountManagerFuture;
 import android.annotation.SuppressLint;
 import android.annotation.TargetApi;
 import android.app.ActionBar;
 import android.app.Activity;
@@ -27,16 +18,25 @@ import android.content.DialogInterface;
 import android.content.Intent;
 import android.os.Build;
 import android.os.Bundle;
 import android.util.TypedValue;
 import android.view.Menu;
 import android.view.MenuInflater;
 import android.view.MenuItem;
 import android.widget.Toast;
+import org.mozilla.gecko.AppConstants;
+import org.mozilla.gecko.Locales.LocaleAwareFragmentActivity;
+import org.mozilla.gecko.R;
+import org.mozilla.gecko.background.common.log.Logger;
+import org.mozilla.gecko.background.fxa.FxAccountUtils;
+import org.mozilla.gecko.fxa.FirefoxAccounts;
+import org.mozilla.gecko.fxa.FxAccountConstants;
+import org.mozilla.gecko.fxa.authenticator.AndroidFxAccount;
+import org.mozilla.gecko.sync.Utils;
 
 /**
  * Activity which displays account status.
  */
 public class FxAccountStatusActivity extends LocaleAwareFragmentActivity {
   private static final String LOG_TAG = FxAccountStatusActivity.class.getSimpleName();
 
   protected FxAccountStatusFragment statusFragment;
@@ -80,17 +80,17 @@ public class FxAccountStatusActivity ext
   public void onResume() {
     super.onResume();
 
     final AndroidFxAccount fxAccount = getAndroidFxAccount();
     if (fxAccount == null) {
       Logger.warn(LOG_TAG, "Could not get Firefox Account.");
 
       // Gracefully redirect to get started.
-      Intent intent = new Intent(this, FxAccountGetStartedActivity.class);
+      final Intent intent = new Intent(FxAccountConstants.ACTION_FXA_GET_STARTED);
       // Per http://stackoverflow.com/a/8992365, this triggers a known bug with
       // the soft keyboard not being shown for the started activity. Why, Android, why?
       intent.setFlags(Intent.FLAG_ACTIVITY_NO_ANIMATION);
       startActivity(intent);
 
       setResult(RESULT_CANCELED);
       finish();
       return;
--- a/mobile/android/base/fxa/activities/FxAccountStatusFragment.java
+++ b/mobile/android/base/fxa/activities/FxAccountStatusFragment.java
@@ -1,40 +1,14 @@
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 package org.mozilla.gecko.fxa.activities;
 
-import java.util.Calendar;
-import java.util.Date;
-import java.util.GregorianCalendar;
-import java.util.HashMap;
-import java.util.Map;
-import java.util.Set;
-
-import org.mozilla.gecko.AppConstants;
-import org.mozilla.gecko.R;
-import org.mozilla.gecko.background.common.log.Logger;
-import org.mozilla.gecko.background.fxa.FxAccountUtils;
-import org.mozilla.gecko.background.preferences.PreferenceFragment;
-import org.mozilla.gecko.fxa.FirefoxAccounts;
-import org.mozilla.gecko.fxa.FxAccountConstants;
-import org.mozilla.gecko.fxa.SyncStatusListener;
-import org.mozilla.gecko.fxa.authenticator.AndroidFxAccount;
-import org.mozilla.gecko.fxa.login.Married;
-import org.mozilla.gecko.fxa.login.State;
-import org.mozilla.gecko.fxa.sync.FxAccountSyncStatusHelper;
-import org.mozilla.gecko.fxa.tasks.FxAccountCodeResender;
-import org.mozilla.gecko.sync.ExtendedJSONObject;
-import org.mozilla.gecko.sync.SharedPreferencesClientsDataDelegate;
-import org.mozilla.gecko.sync.SyncConfiguration;
-import org.mozilla.gecko.util.HardwareUtils;
-import org.mozilla.gecko.util.ThreadUtils;
-
 import android.accounts.Account;
 import android.content.BroadcastReceiver;
 import android.content.ContentResolver;
 import android.content.Context;
 import android.content.Intent;
 import android.content.IntentFilter;
 import android.content.SharedPreferences;
 import android.os.Bundle;
@@ -44,19 +18,43 @@ import android.preference.EditTextPrefer
 import android.preference.Preference;
 import android.preference.Preference.OnPreferenceChangeListener;
 import android.preference.Preference.OnPreferenceClickListener;
 import android.preference.PreferenceCategory;
 import android.preference.PreferenceScreen;
 import android.support.v4.content.LocalBroadcastManager;
 import android.text.TextUtils;
 import android.text.format.DateUtils;
-
 import com.squareup.picasso.Picasso;
 import com.squareup.picasso.Target;
+import org.mozilla.gecko.AppConstants;
+import org.mozilla.gecko.R;
+import org.mozilla.gecko.background.common.log.Logger;
+import org.mozilla.gecko.background.fxa.FxAccountUtils;
+import org.mozilla.gecko.background.preferences.PreferenceFragment;
+import org.mozilla.gecko.fxa.FirefoxAccounts;
+import org.mozilla.gecko.fxa.FxAccountConstants;
+import org.mozilla.gecko.fxa.SyncStatusListener;
+import org.mozilla.gecko.fxa.authenticator.AndroidFxAccount;
+import org.mozilla.gecko.fxa.login.Married;
+import org.mozilla.gecko.fxa.login.State;
+import org.mozilla.gecko.fxa.sync.FxAccountSyncStatusHelper;
+import org.mozilla.gecko.fxa.tasks.FxAccountCodeResender;
+import org.mozilla.gecko.sync.ExtendedJSONObject;
+import org.mozilla.gecko.sync.SharedPreferencesClientsDataDelegate;
+import org.mozilla.gecko.sync.SyncConfiguration;
+import org.mozilla.gecko.util.HardwareUtils;
+import org.mozilla.gecko.util.ThreadUtils;
+
+import java.util.Calendar;
+import java.util.Date;
+import java.util.GregorianCalendar;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Set;
 
 /**
  * A fragment that displays the status of an AndroidFxAccount.
  * <p>
  * The owning activity is responsible for providing an AndroidFxAccount at
  * appropriate times.
  */
 public class FxAccountStatusFragment
@@ -232,47 +230,49 @@ public class FxAccountStatusFragment
   @Override
   public void onResume() {
     super.onResume();
   }
 
   @Override
   public boolean onPreferenceClick(Preference preference) {
     if (preference == needsPasswordPreference) {
-      Intent intent = new Intent(getActivity(), FxAccountUpdateCredentialsActivity.class);
+      final Intent intent = new Intent(FxAccountConstants.ACTION_FXA_UPDATE_CREDENTIALS);
       final Bundle extras = getExtrasForAccount();
       if (extras != null) {
         intent.putExtras(extras);
       }
       // Per http://stackoverflow.com/a/8992365, this triggers a known bug with
       // the soft keyboard not being shown for the started activity. Why, Android, why?
       intent.setFlags(Intent.FLAG_ACTIVITY_NO_ANIMATION);
       startActivity(intent);
 
       return true;
     }
 
     if (preference == needsFinishMigratingPreference) {
-      final Intent intent = new Intent(getActivity(), FxAccountFinishMigratingActivity.class);
+      final Intent intent = new Intent(FxAccountConstants.ACTION_FXA_FINISH_MIGRATING);
       final Bundle extras = getExtrasForAccount();
       if (extras != null) {
         intent.putExtras(extras);
       }
       // Per http://stackoverflow.com/a/8992365, this triggers a known bug with
       // the soft keyboard not being shown for the started activity. Why, Android, why?
       intent.setFlags(Intent.FLAG_ACTIVITY_NO_ANIMATION);
       startActivity(intent);
 
       return true;
     }
 
     if (preference == needsVerificationPreference) {
-      FxAccountCodeResender.resendCode(getActivity().getApplicationContext(), fxAccount);
+      if (AppConstants.MOZ_ANDROID_NATIVE_ACCOUNT_UI) {
+        FxAccountCodeResender.resendCode(getActivity().getApplicationContext(), fxAccount);
+      }
 
-      Intent intent = new Intent(getActivity(), FxAccountConfirmAccountActivity.class);
+      final Intent intent = new Intent(FxAccountConstants.ACTION_FXA_CONFIRM_ACCOUNT);
       // Per http://stackoverflow.com/a/8992365, this triggers a known bug with
       // the soft keyboard not being shown for the started activity. Why, Android, why?
       intent.setFlags(Intent.FLAG_ACTIVITY_NO_ANIMATION);
       startActivity(intent);
 
       return true;
     }
 
copy from mobile/android/base/fxa/activities/FxAccountUpdateCredentialsActivity.java
copy to mobile/android/base/fxa/activities/FxAccountUpdateCredentialsActivityWeb.java
--- a/mobile/android/base/fxa/activities/FxAccountUpdateCredentialsActivity.java
+++ b/mobile/android/base/fxa/activities/FxAccountUpdateCredentialsActivityWeb.java
@@ -1,52 +1,11 @@
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 package org.mozilla.gecko.fxa.activities;
 
-import org.mozilla.gecko.R;
-import org.mozilla.gecko.background.common.log.Logger;
-import org.mozilla.gecko.background.fxa.FxAccountClient20.LoginResponse;
-import org.mozilla.gecko.fxa.login.State;
-import org.mozilla.gecko.fxa.login.State.StateLabel;
-
-import android.content.Intent;
-
-/**
- * Activity which displays a screen for updating the local password.
- */
-public class FxAccountUpdateCredentialsActivity extends FxAccountAbstractUpdateCredentialsActivity {
-  protected static final String LOG_TAG = FxAccountUpdateCredentialsActivity.class.getSimpleName();
-
-  public FxAccountUpdateCredentialsActivity() {
-    super(R.layout.fxaccount_update_credentials);
-  }
-
-  @Override
-  public void onResume() {
-    super.onResume();
-    this.fxAccount = getAndroidFxAccount();
-    if (fxAccount == null) {
-      Logger.warn(LOG_TAG, "Could not get Firefox Account.");
-      setResult(RESULT_CANCELED);
-      finish();
-      return;
-    }
-    final State state = fxAccount.getState();
-    if (state.getStateLabel() != StateLabel.Separated) {
-      Logger.warn(LOG_TAG, "Cannot update credentials from Firefox Account in state: " + state.getStateLabel());
-      setResult(RESULT_CANCELED);
-      finish();
-      return;
-    }
-    emailEdit.setText(fxAccount.getEmail());
-  }
-
-  @Override
-  public Intent makeSuccessIntent(String email, LoginResponse result) {
-    // We don't show anything after updating credentials. The updating Activity
-    // sets its result to OK and the user is returned to the previous task,
-    // which is often the Status Activity.
-    return null;
+public class FxAccountUpdateCredentialsActivityWeb extends FxAccountWebFlowActivity {
+  public FxAccountUpdateCredentialsActivityWeb() {
+    super(CANNOT_RESUME_WHEN_NO_ACCOUNTS_EXIST, "force_auth");
   }
 }
new file mode 100644
--- /dev/null
+++ b/mobile/android/base/fxa/activities/FxAccountWebFlowActivity.java
@@ -0,0 +1,64 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.gecko.fxa.activities;
+
+import android.os.Bundle;
+import org.mozilla.gecko.Locales;
+import org.mozilla.gecko.background.common.log.Logger;
+import org.mozilla.gecko.fxa.FxAccountConstants;
+import org.mozilla.gecko.fxa.authenticator.AndroidFxAccount;
+import org.mozilla.gecko.sync.setup.activities.ActivityUtils;
+
+/**
+ * Activity which shows the status activity or passes through to web flow.
+ */
+public class FxAccountWebFlowActivity extends FxAccountAbstractActivity {
+    protected static final String LOG_TAG = FxAccountWebFlowActivity.class.getSimpleName();
+
+    protected static final String ABOUT_ACCOUNTS = "about:accounts";
+    private final String action;
+    private final String extras;
+
+    public FxAccountWebFlowActivity(int resume, String action) {
+        this(resume, action, null);
+    }
+
+    public FxAccountWebFlowActivity(int resume, String action, String extras) {
+        super(resume);
+        this.action = action;
+        this.extras = (extras != null) ? ("&" + extras) : "";
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public void onCreate(Bundle icicle) {
+        Logger.setThreadLogTag(FxAccountConstants.GLOBAL_LOG_TAG);
+        Logger.debug(LOG_TAG, "onCreate(" + icicle + ")");
+
+        Locales.initializeLocale(getApplicationContext());
+
+        super.onCreate(icicle);
+    }
+
+    protected boolean redirectIfAppropriate() {
+        final boolean redirected = super.redirectIfAppropriate();
+        if (redirected) {
+            return true;
+        }
+        ActivityUtils.openURLInFennec(getApplicationContext(),
+                ABOUT_ACCOUNTS + "?action=" + action + extras);
+        return true;
+    }
+
+    @Override
+    public void onResume() {
+        super.onResume();
+
+        // We are always redirected.
+        this.finish();
+    }
+}
--- a/mobile/android/base/fxa/authenticator/FxAccountAuthenticator.java
+++ b/mobile/android/base/fxa/authenticator/FxAccountAuthenticator.java
@@ -1,48 +1,46 @@
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 package org.mozilla.gecko.fxa.authenticator;
 
-import java.security.NoSuchAlgorithmException;
-import java.util.concurrent.Executor;
-import java.util.concurrent.Executors;
-
+import android.accounts.AbstractAccountAuthenticator;
+import android.accounts.Account;
+import android.accounts.AccountAuthenticatorResponse;
+import android.accounts.AccountManager;
+import android.accounts.NetworkErrorException;
+import android.content.Context;
+import android.content.Intent;
+import android.os.Bundle;
 import org.mozilla.gecko.background.common.log.Logger;
 import org.mozilla.gecko.background.fxa.FxAccountClient;
 import org.mozilla.gecko.background.fxa.FxAccountClient20;
 import org.mozilla.gecko.background.fxa.FxAccountUtils;
 import org.mozilla.gecko.background.fxa.oauth.FxAccountAbstractClient.RequestDelegate;
 import org.mozilla.gecko.background.fxa.oauth.FxAccountAbstractClientException.FxAccountAbstractClientRemoteException;
 import org.mozilla.gecko.background.fxa.oauth.FxAccountOAuthClient10;
 import org.mozilla.gecko.background.fxa.oauth.FxAccountOAuthClient10.AuthorizationResponse;
 import org.mozilla.gecko.browserid.BrowserIDKeyPair;
 import org.mozilla.gecko.browserid.JSONWebTokenUtils;
 import org.mozilla.gecko.fxa.FxAccountConstants;
-import org.mozilla.gecko.fxa.activities.FxAccountGetStartedActivity;
 import org.mozilla.gecko.fxa.login.FxAccountLoginStateMachine;
 import org.mozilla.gecko.fxa.login.FxAccountLoginStateMachine.LoginStateMachineDelegate;
 import org.mozilla.gecko.fxa.login.FxAccountLoginTransition.Transition;
 import org.mozilla.gecko.fxa.login.Married;
 import org.mozilla.gecko.fxa.login.State;
 import org.mozilla.gecko.fxa.login.State.StateLabel;
 import org.mozilla.gecko.fxa.login.StateFactory;
 import org.mozilla.gecko.fxa.sync.FxAccountNotificationManager;
 import org.mozilla.gecko.fxa.sync.FxAccountSyncAdapter;
 
-import android.accounts.AbstractAccountAuthenticator;
-import android.accounts.Account;
-import android.accounts.AccountAuthenticatorResponse;
-import android.accounts.AccountManager;
-import android.accounts.NetworkErrorException;
-import android.content.Context;
-import android.content.Intent;
-import android.os.Bundle;
+import java.security.NoSuchAlgorithmException;
+import java.util.concurrent.Executor;
+import java.util.concurrent.Executors;
 
 public class FxAccountAuthenticator extends AbstractAccountAuthenticator {
   public static final String LOG_TAG = FxAccountAuthenticator.class.getSimpleName();
   public static final int UNKNOWN_ERROR_CODE = 999;
 
   protected final Context context;
   protected final AccountManager accountManager;
 
@@ -66,17 +64,17 @@ public class FxAccountAuthenticator exte
     final Bundle res = new Bundle();
 
     if (!FxAccountConstants.ACCOUNT_TYPE.equals(accountType)) {
       res.putInt(AccountManager.KEY_ERROR_CODE, -1);
       res.putString(AccountManager.KEY_ERROR_MESSAGE, "Not adding unknown account type.");
       return res;
     }
 
-    Intent intent = new Intent(context, FxAccountGetStartedActivity.class);
+    final Intent intent = new Intent(FxAccountConstants.ACTION_FXA_GET_STARTED);
     res.putParcelable(AccountManager.KEY_INTENT, intent);
     return res;
   }
 
   @Override
   public Bundle confirmCredentials(AccountAuthenticatorResponse response, Account account, Bundle options)
       throws NetworkErrorException {
     Logger.debug(LOG_TAG, "confirmCredentials");
--- a/mobile/android/base/fxa/sync/FxAccountNotificationManager.java
+++ b/mobile/android/base/fxa/sync/FxAccountNotificationManager.java
@@ -1,32 +1,30 @@
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 package org.mozilla.gecko.fxa.sync;
 
-import org.mozilla.gecko.BrowserLocaleManager;
-import org.mozilla.gecko.R;
-import org.mozilla.gecko.background.common.log.Logger;
-import org.mozilla.gecko.background.common.telemetry.TelemetryWrapper;
-import org.mozilla.gecko.background.fxa.FxAccountUtils;
-import org.mozilla.gecko.fxa.activities.FxAccountFinishMigratingActivity;
-import org.mozilla.gecko.fxa.activities.FxAccountStatusActivity;
-import org.mozilla.gecko.fxa.authenticator.AndroidFxAccount;
-import org.mozilla.gecko.fxa.login.State;
-import org.mozilla.gecko.fxa.login.State.Action;
-import org.mozilla.gecko.sync.telemetry.TelemetryContract;
-
 import android.app.NotificationManager;
 import android.app.PendingIntent;
 import android.content.Context;
 import android.content.Intent;
 import android.support.v4.app.NotificationCompat;
 import android.support.v4.app.NotificationCompat.Builder;
+import org.mozilla.gecko.BrowserLocaleManager;
+import org.mozilla.gecko.R;
+import org.mozilla.gecko.background.common.log.Logger;
+import org.mozilla.gecko.background.common.telemetry.TelemetryWrapper;
+import org.mozilla.gecko.background.fxa.FxAccountUtils;
+import org.mozilla.gecko.fxa.FxAccountConstants;
+import org.mozilla.gecko.fxa.authenticator.AndroidFxAccount;
+import org.mozilla.gecko.fxa.login.State;
+import org.mozilla.gecko.fxa.login.State.Action;
+import org.mozilla.gecko.sync.telemetry.TelemetryContract;
 
 /**
  * Abstraction that manages notifications shown or hidden for a Firefox Account.
  * <p>
  * In future, we anticipate this tracking things like:
  * <ul>
  * <li>new engines to offer to Sync;</li>
  * <li>service interruption updates;</li>
@@ -84,21 +82,21 @@ public class FxAccountNotificationManage
     final String title;
     final String text;
     final Intent notificationIntent;
     if (action == Action.NeedsFinishMigrating) {
       TelemetryWrapper.addToHistogram(TelemetryContract.SYNC11_MIGRATION_NOTIFICATIONS_OFFERED, 1);
 
       title = context.getResources().getString(R.string.fxaccount_sync_finish_migrating_notification_title);
       text = context.getResources().getString(R.string.fxaccount_sync_finish_migrating_notification_text, state.email);
-      notificationIntent = new Intent(context, FxAccountFinishMigratingActivity.class);
+      notificationIntent = new Intent(FxAccountConstants.ACTION_FXA_FINISH_MIGRATING);
     } else {
       title = context.getResources().getString(R.string.fxaccount_sync_sign_in_error_notification_title);
       text = context.getResources().getString(R.string.fxaccount_sync_sign_in_error_notification_text, state.email);
-      notificationIntent = new Intent(context, FxAccountStatusActivity.class);
+      notificationIntent = new Intent(FxAccountConstants.ACTION_FXA_STATUS);
     }
     Logger.info(LOG_TAG, "State " + state.getStateLabel() + " needs action; offering notification with title: " + title);
     FxAccountUtils.pii(LOG_TAG, "And text: " + text);
 
     final PendingIntent pendingIntent = PendingIntent.getActivity(context, 0, notificationIntent, 0);
 
     final Builder builder = new NotificationCompat.Builder(context);
     builder
--- a/mobile/android/base/home/RemoteTabsStaticFragment.java
+++ b/mobile/android/base/home/RemoteTabsStaticFragment.java
@@ -1,30 +1,27 @@
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 package org.mozilla.gecko.home;
 
-import java.util.EnumSet;
-import java.util.Locale;
-
-import org.mozilla.gecko.R;
-import org.mozilla.gecko.fxa.FirefoxAccounts;
-import org.mozilla.gecko.fxa.activities.FxAccountCreateAccountActivity;
-import org.mozilla.gecko.fxa.activities.FxAccountFinishMigratingActivity;
-import org.mozilla.gecko.fxa.activities.FxAccountUpdateCredentialsActivity;
-import org.mozilla.gecko.home.HomePager.OnUrlOpenListener;
-
 import android.content.Intent;
 import android.os.Bundle;
 import android.view.LayoutInflater;
 import android.view.View;
 import android.view.View.OnClickListener;
 import android.view.ViewGroup;
+import org.mozilla.gecko.R;
+import org.mozilla.gecko.fxa.FirefoxAccounts;
+import org.mozilla.gecko.fxa.FxAccountConstants;
+import org.mozilla.gecko.home.HomePager.OnUrlOpenListener;
+
+import java.util.EnumSet;
+import java.util.Locale;
 
 /**
  * A <code>HomeFragment</code> which displays one of a small set of static views
  * in response to different Firefox Account states. When the Firefox Account is
  * healthy and syncing normally, these views should not be shown.
  * <p>
  * This class exists to handle view-specific actions when buttons and links
  * shown by the different static views are clicked. For example, a static view
@@ -97,37 +94,36 @@ public class RemoteTabsStaticFragment ex
         }
     }
 
     @Override
     public void onClick(final View v) {
         final int id = v.getId();
         if (id == R.id.remote_tabs_setup_get_started) {
             // This Activity will redirect to the correct Activity as needed.
-            final Intent intent = new Intent(getActivity(), FxAccountCreateAccountActivity.class);
-            intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
+            final Intent intent = new Intent(FxAccountConstants.ACTION_FXA_GET_STARTED);
             startActivity(intent);
         } else if (id == R.id.remote_tabs_setup_old_sync_link) {
             final String url = FirefoxAccounts.getOldSyncUpgradeURL(getResources(), Locale.getDefault());
             // Don't allow switch-to-tab.
             final EnumSet<OnUrlOpenListener.Flags> flags = EnumSet.noneOf(OnUrlOpenListener.Flags.class);
             mUrlOpenListener.onUrlOpen(url, flags);
         } else if (id == R.id.remote_tabs_needs_verification_resend_email) {
             // Send a fresh email; this displays a toast, so the user gets feedback.
             FirefoxAccounts.resendVerificationEmail(getActivity());
         } else if (id == R.id.remote_tabs_needs_verification_help) {
             // Don't allow switch-to-tab.
             final EnumSet<OnUrlOpenListener.Flags> flags = EnumSet.noneOf(OnUrlOpenListener.Flags.class);
             mUrlOpenListener.onUrlOpen(CONFIRM_ACCOUNT_SUPPORT_URL, flags);
         } else if (id == R.id.remote_tabs_needs_password_sign_in) {
-            final Intent intent = new Intent(getActivity(), FxAccountUpdateCredentialsActivity.class);
+            final Intent intent = new Intent(FxAccountConstants.ACTION_FXA_UPDATE_CREDENTIALS);
             intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
             startActivity(intent);
         } else if (id == R.id.remote_tabs_needs_finish_migrating_sign_in) {
-            final Intent intent = new Intent(getActivity(), FxAccountFinishMigratingActivity.class);
+            final Intent intent = new Intent(FxAccountConstants.ACTION_FXA_FINISH_MIGRATING);
             intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
             startActivity(intent);
         }
     }
 
     @Override
     protected void load() {
         // We're static, so nothing to do here!
--- a/mobile/android/base/moz.build
+++ b/mobile/android/base/moz.build
@@ -140,16 +140,17 @@ if CONFIG['MOZ_WEBRTC']:
         'gecko-util.jar',
         'gecko-mozglue.jar',
     ]
     wrjar.javac_flags += ['-Xlint:all,-deprecation,-cast']
 
 gbjar = add_java_jar('gecko-browser')
 gbjar.sources += [
     'AboutPages.java',
+    'AccountsHelper.java',
     'ActionModeCompat.java',
     'ActionModeCompatView.java',
     'ActivityHandlerHelper.java',
     'AlertNotification.java',
     'AndroidGamepadManager.java',
     'animation/AnimationUtils.java',
     'animation/AnimatorProxy.java',
     'animation/BounceAnimatorBuilder.java',
--- a/mobile/android/base/overlays/service/sharemethods/SendTab.java
+++ b/mobile/android/base/overlays/service/sharemethods/SendTab.java
@@ -1,35 +1,31 @@
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 package org.mozilla.gecko.overlays.service.sharemethods;
 
 import android.accounts.Account;
 import android.accounts.AccountManager;
-import android.app.Activity;
 import android.content.Context;
 import android.content.Intent;
 import android.content.SharedPreferences;
 import android.database.Cursor;
 import android.os.Bundle;
 import android.os.Parcelable;
 import android.support.v4.content.LocalBroadcastManager;
 import android.util.Log;
-
 import org.mozilla.gecko.GeckoProfile;
 import org.mozilla.gecko.R;
 import org.mozilla.gecko.db.BrowserDB;
 import org.mozilla.gecko.db.RemoteClient;
 import org.mozilla.gecko.db.TabsAccessor;
 import org.mozilla.gecko.fxa.FirefoxAccounts;
 import org.mozilla.gecko.fxa.FxAccountConstants;
-import org.mozilla.gecko.fxa.activities.FxAccountGetStartedActivity;
-import org.mozilla.gecko.fxa.activities.FxAccountStatusActivity;
 import org.mozilla.gecko.fxa.authenticator.AndroidFxAccount;
 import org.mozilla.gecko.fxa.login.State;
 import org.mozilla.gecko.overlays.OverlayConstants;
 import org.mozilla.gecko.overlays.service.ShareData;
 import org.mozilla.gecko.sync.CommandProcessor;
 import org.mozilla.gecko.sync.CommandRunner;
 import org.mozilla.gecko.sync.GlobalSession;
 import org.mozilla.gecko.sync.SyncConfiguration;
@@ -154,17 +150,17 @@ public class SendTab extends ShareMethod
         if (fxAccounts.length > 0) {
             final AndroidFxAccount fxAccount = new AndroidFxAccount(context, fxAccounts[0]);
             if (fxAccount.getState().getNeededAction() != State.Action.None) {
                 // We have a Firefox Account, but it's definitely not able to send a tab
                 // right now. Redirect to the status activity.
                 Log.w(LOGTAG, "Firefox Account named like " + fxAccount.getObfuscatedEmail() +
                               " needs action before it can send a tab; redirecting to status activity.");
 
-                setOverrideIntent(FxAccountStatusActivity.class);
+                setOverrideIntentAction(FxAccountConstants.ACTION_FXA_STATUS);
                 return;
             }
 
             tabSender = new FxAccountTabSender(fxAccount);
 
             updateClientList(tabSender);
 
             Log.i(LOGTAG, "Allowing tab send for Firefox Account.");
@@ -179,17 +175,17 @@ public class SendTab extends ShareMethod
             updateClientList(tabSender);
 
             Log.i(LOGTAG, "Allowing tab send for Sync account.");
             registerDisplayURICommand();
             return;
         }
 
         // Have registered UIs offer to set up a Firefox Account.
-        setOverrideIntent(FxAccountGetStartedActivity.class);
+        setOverrideIntentAction(FxAccountConstants.ACTION_FXA_GET_STARTED);
     }
 
     /**
      * Load the list of Sync clients that are not this device using the given TabSender.
      */
     private void updateClientList(TabSender tabSender) {
         Collection<RemoteClient> otherClients = getOtherClients(tabSender);
 
@@ -201,37 +197,37 @@ public class SendTab extends ShareMethod
 
         for (RemoteClient client : otherClients) {
             validGUIDs.add(client.guid);
         }
 
         if (validGUIDs.isEmpty()) {
             // Guess we'd better override. We have no clients.
             // This does the broadcast for us.
-            setOverrideIntent(FxAccountGetStartedActivity.class);
+            setOverrideIntentAction(FxAccountConstants.ACTION_FXA_GET_STARTED);
             return;
         }
 
         Intent uiStateIntent = getUIStateIntent();
         uiStateIntent.putExtra(EXTRA_REMOTE_CLIENT_RECORDS, records);
         broadcastUIState(uiStateIntent);
     }
 
     /**
      * Record our intention to redirect the user to a different activity when they attempt to share
      * with us, usually because we found something wrong with their Sync account (a need to login,
      * register, etc.)
      * This will be recorded in the OVERRIDE_INTENT field of the UI broadcast. Consumers should
      * dispatch this intent instead of attempting to share with this ShareMethod whenever it is
      * non-null.
      *
-     * @param activityClass The class of the activity we wish to launch instead of invoking a share.
+     * @param action to launch instead of invoking a share.
      */
-    protected void setOverrideIntent(Class<? extends Activity> activityClass) {
-        Intent intent = new Intent(context, activityClass);
+    protected void setOverrideIntentAction(final String action) {
+        Intent intent = new Intent(action);
         // Per http://stackoverflow.com/a/8992365, this triggers a known bug with
         // the soft keyboard not being shown for the started activity. Why, Android, why?
         intent.addFlags(Intent.FLAG_ACTIVITY_NO_ANIMATION);
         intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
 
         Intent uiStateIntent = getUIStateIntent();
         uiStateIntent.putExtra(OVERRIDE_INTENT, intent);
 
--- a/mobile/android/base/preferences/SyncPreference.java
+++ b/mobile/android/base/preferences/SyncPreference.java
@@ -1,27 +1,25 @@
 /* -*- Mode: Java; c-basic-offset: 4; tab-width: 4; indent-tabs-mode: nil; -*-
  * This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 package org.mozilla.gecko.preferences;
 
-import org.mozilla.gecko.Telemetry;
-import org.mozilla.gecko.TelemetryContract;
-import org.mozilla.gecko.TelemetryContract.Method;
-import org.mozilla.gecko.fxa.activities.FxAccountGetStartedActivity;
-import org.mozilla.gecko.sync.setup.SyncAccounts;
-import org.mozilla.gecko.sync.setup.activities.SetupSyncActivity;
-import org.mozilla.gecko.util.HardwareUtils;
-
 import android.content.Context;
 import android.content.Intent;
 import android.preference.Preference;
 import android.util.AttributeSet;
+import org.mozilla.gecko.Telemetry;
+import org.mozilla.gecko.TelemetryContract;
+import org.mozilla.gecko.TelemetryContract.Method;
+import org.mozilla.gecko.fxa.FxAccountConstants;
+import org.mozilla.gecko.sync.setup.SyncAccounts;
+import org.mozilla.gecko.sync.setup.activities.SetupSyncActivity;
 
 class SyncPreference extends Preference {
     private static final boolean DEFAULT_TO_FXA = true;
 
     private final Context mContext;
 
     public SyncPreference(Context context, AttributeSet attrs) {
         super(context, attrs);
@@ -36,23 +34,19 @@ class SyncPreference extends Preference 
             SyncAccounts.openSyncSettings(mContext);
             return;
         }
         Intent intent = new Intent(mContext, SetupSyncActivity.class);
         mContext.startActivity(intent);
     }
 
     private void launchFxASetup() {
-        Intent intent = new Intent(mContext, FxAccountGetStartedActivity.class);
+        final Intent intent = new Intent(FxAccountConstants.ACTION_FXA_GET_STARTED);
         intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
-
-        if (HardwareUtils.IS_KINDLE_DEVICE) {
-            intent.setFlags(Intent.FLAG_ACTIVITY_NO_ANIMATION);
-        }
-
+        intent.setFlags(Intent.FLAG_ACTIVITY_NO_ANIMATION);
         mContext.startActivity(intent);
     }
 
     @Override
     protected void onClick() {
         // If we're not defaulting to FxA, just do what we've always done.
         if (!DEFAULT_TO_FXA) {
             openSync11Settings();
--- a/mobile/android/base/tabs/TabsPanelThumbnailView.java
+++ b/mobile/android/base/tabs/TabsPanelThumbnailView.java
@@ -1,15 +1,16 @@
 /* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
  * This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 package org.mozilla.gecko.tabs;
 
+import org.mozilla.gecko.AppConstants;
 import org.mozilla.gecko.R;
 import org.mozilla.gecko.ThumbnailHelper;
 import org.mozilla.gecko.widget.CropImageView;
 
 import android.content.Context;
 import android.graphics.drawable.Drawable;
 import android.util.AttributeSet;
 
@@ -29,17 +30,21 @@ public class TabsPanelThumbnailView exte
     }
 
     public TabsPanelThumbnailView(Context context, AttributeSet attrs, int defStyle) {
         super(context, attrs, defStyle);
     }
 
     @Override
     protected float getAspectRatio() {
-        return ThumbnailHelper.TABS_PANEL_THUMBNAIL_ASPECT_RATIO;
+        if (AppConstants.NIGHTLY_BUILD) {
+            return ThumbnailHelper.TABS_PANEL_THUMBNAIL_ASPECT_RATIO;
+        } else {
+            return ThumbnailHelper.TOP_SITES_THUMBNAIL_ASPECT_RATIO;
+        }
     }
 
     @Override
     public void setImageDrawable(Drawable drawable) {
         boolean resize = true;
 
         if (drawable == null) {
             drawable = getResources().getDrawable(R.drawable.tab_panel_tab_background);
--- a/mobile/android/chrome/content/browser.js
+++ b/mobile/android/chrome/content/browser.js
@@ -541,16 +541,26 @@ var BrowserApp = {
       let mm = window.getGroupMessageManager("browsers");
       mm.loadFrameScript("chrome://browser/content/content.js", true);
     });
 
     if (AppConstants.ACCESSIBILITY) {
       InitLater(() => AccessFu.attach(window), window, "AccessFu");
     }
 
+    if (!AppConstants.MOZ_ANDROID_NATIVE_ACCOUNT_UI) {
+      // We can't delay registering WebChannel listeners: if the first page is
+      // about:accounts, which can happen when starting the Firefox Account flow
+      // from the first run experience, or via the Firefox Account Status
+      // Activity, we can and do miss messages from the fxa-content-server.
+      console.log("browser.js: loading Firefox Accounts WebChannel");
+      Cu.import("resource://gre/modules/FxAccountsWebChannel.jsm");
+      EnsureFxAccountsWebChannel();
+    }
+
     // Notify Java that Gecko has loaded.
     Messaging.sendRequest({ type: "Gecko:Ready" });
 
     this.deck.addEventListener("DOMContentLoaded", function BrowserApp_delayedStartup() {
       BrowserApp.deck.removeEventListener("DOMContentLoaded", BrowserApp_delayedStartup, false);
 
       InitLater(() => Cu.import("resource://gre/modules/NotificationDB.jsm"));
       InitLater(() => Cu.import("resource://gre/modules/Payment.jsm"));
--- a/mobile/android/confvars.sh
+++ b/mobile/android/confvars.sh
@@ -112,8 +112,11 @@ MOZ_ANDROID_FIREFOX_ACCOUNT_PROFILES=1
 MOZ_ADDON_SIGNING=1
 
 # Enable the Switchboard A/B framework code.
 # Note: The framework is always included in the app. This flag controls
 # usage of the framework.
 if test "$NIGHTLY_BUILD"; then
   MOZ_SWITCHBOARD=1
 fi
+
+# Use native Firefox Accounts UI regardless of channel.
+MOZ_ANDROID_NATIVE_ACCOUNT_UI=1
new file mode 100644
--- /dev/null
+++ b/mobile/android/locales/en-US/chrome/aboutAccounts.properties
@@ -0,0 +1,16 @@
+# 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/.
+
+# LOCALIZATION NOTE (relinkDenied.message): Ideally, this string is short (it's
+# a toast message).
+relinkDenied.message = Already signed in to Sync!
+# LOCALIZATION NOTE (relinkDenied.openPrefs): Ideally, this string is short (it's a
+# button label) and upper-case, to match Google and Android's convention.
+relinkDenied.openPrefs = PREFS
+
+relinkVerify.title = Are you sure you want to sign in to Sync?
+# LOCALIZATION NOTE (relinkVerify.message): Email address of a user previously signed in to Sync.
+relinkVerify.message = You were previously signed in to Sync with a different email address. Signing in will merge this browser’s bookmarks, passwords and other settings with %S
+relinkVerify.continue = Continue
+relinkVerify.cancel = Cancel
--- a/mobile/android/locales/jar.mn
+++ b/mobile/android/locales/jar.mn
@@ -2,16 +2,19 @@
 # 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/.
 
 
 @AB_CD@.jar:
 % locale browser @AB_CD@ %locale/@AB_CD@/browser/
   locale/@AB_CD@/browser/about.dtd                (%chrome/about.dtd)
+#ifndef MOZ_ANDROID_NATIVE_ACCOUNT_UI
+  locale/@AB_CD@/browser/aboutAccounts.properties (%chrome/aboutAccounts.properties)
+#endif
   locale/@AB_CD@/browser/aboutAddons.dtd          (%chrome/aboutAddons.dtd)
   locale/@AB_CD@/browser/aboutAddons.properties   (%chrome/aboutAddons.properties)
 #ifdef MOZ_DEVICES
   locale/@AB_CD@/browser/aboutDevices.dtd         (%chrome/aboutDevices.dtd)
 #endif
   locale/@AB_CD@/browser/aboutCertError.dtd       (%chrome/aboutCertError.dtd)
   locale/@AB_CD@/browser/aboutDownloads.dtd       (%chrome/aboutDownloads.dtd)
   locale/@AB_CD@/browser/aboutDownloads.properties (%chrome/aboutDownloads.properties)
--- a/mobile/android/modules/Accounts.jsm
+++ b/mobile/android/modules/Accounts.jsm
@@ -3,19 +3,19 @@
  * You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 "use strict";
 
 this.EXPORTED_SYMBOLS = ["Accounts"];
 
 const { utils: Cu } = Components;
 
-Cu.import("resource://gre/modules/Messaging.jsm");
-Cu.import("resource://gre/modules/Services.jsm");
-Cu.import("resource://gre/modules/Promise.jsm");
+Cu.import("resource://gre/modules/Messaging.jsm"); /*global Messaging */
+Cu.import("resource://gre/modules/Promise.jsm"); /*global Promise */
+Cu.import("resource://gre/modules/Services.jsm"); /*global Services */
 
 /**
  * A promise-based API for querying the existence of Sync accounts,
  * and accessing the Sync setup wizard.
  *
  * Usage:
  *
  *   Cu.import("resource://gre/modules/Accounts.jsm");
@@ -63,24 +63,86 @@ var Accounts = Object.freeze({
    */
   launchSetup: function (extras) {
     Messaging.sendRequest({
       type: "Accounts:Create",
       extras: extras
     });
   },
 
+  _addDefaultEndpoints: function (json) {
+    let newData = Cu.cloneInto(json, {}, { cloneFunctions: false });
+    let associations = {
+      authServerEndpoint: 'identity.fxaccounts.auth.uri',
+      profileServerEndpoint: 'identity.fxaccounts.remote.profile.uri',
+      tokenServerEndpoint: 'identity.sync.tokenserver.uri'
+    };
+    for (let key in associations) {
+      newData[key] = newData[key] || Services.urlFormatter.formatURLPref(associations[key]);
+    }
+    return newData;
+  },
+
   /**
    * Create a new Android Account corresponding to the given
    * fxa-content-server "login" JSON datum.  The new account will be
    * in the "Engaged" state, and will start syncing immediately.
    *
    * It is an error if an Android Account already exists.
    *
    * Returns a Promise that resolves to a boolean indicating success.
    */
   createFirefoxAccountFromJSON: function (json) {
-    return Messaging.sendRequestForResponse({
+    return Messaging.sendRequestForResult({
       type: "Accounts:CreateFirefoxAccountFromJSON",
-      json: json
+      json: this._addDefaultEndpoints(json)
+    });
+  },
+
+  /**
+   * Move an existing Android Account to the "Engaged" state with the given
+   * fxa-content-server "login" JSON datum.  The account will (re)start
+   * syncing immediately, unless the user has manually configured the account
+   * to not Sync.
+   *
+   * It is an error if no Android Account exists.
+   *
+   * Returns a Promise that resolves to a boolean indicating success.
+   */
+  updateFirefoxAccountFromJSON: function (json) {
+    return Messaging.sendRequestForResult({
+      type: "Accounts:UpdateFirefoxAccountFromJSON",
+      json: this._addDefaultEndpoints(json)
+    });
+  },
+
+  /**
+   * Fetch information about an existing Android Firefox Account.
+   *
+   * Returns a Promise that resolves to null if no Android Firefox Account
+   * exists, or an object including at least a string-valued 'email' key.
+   */
+  getFirefoxAccount: function () {
+    return Messaging.sendRequestForResult({
+      type: "Accounts:Exist",
+      kind: "fxa",
+    }).then(data => {
+      if (!data || !data.exists) {
+        return null;
+      }
+      delete data.exists;
+      return data;
+    });
+  },
+
+  /**
+   * Delete an existing Android Firefox Account.
+   *
+   * It is an error if no Android Account exists.
+   *
+   * Returns a Promise that resolves to a boolean indicating success.
+   */
+  deleteFirefoxAccount: function () {
+    return Messaging.sendRequestForResult({
+      type: "Accounts:DeleteFirefoxAccount",
     });
   }
 });
new file mode 100644
--- /dev/null
+++ b/mobile/android/modules/FxAccountsWebChannel.jsm
@@ -0,0 +1,368 @@
+// -*- Mode: js2; tab-width: 2; indent-tabs-mode: nil; js2-basic-offset: 2; js2-skip-preprocessor-directives: t; -*-
+/* 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/. */
+
+/**
+ * Firefox Accounts Web Channel.
+ *
+ * Use the WebChannel component to receive messages about account
+ * state changes.
+ */
+this.EXPORTED_SYMBOLS = ["EnsureFxAccountsWebChannel"];
+
+const {classes: Cc, interfaces: Ci, utils: Cu, results: Cr} = Components; /*global Components */
+
+Cu.import("resource://gre/modules/Accounts.jsm"); /*global Accounts */
+Cu.import("resource://gre/modules/Notifications.jsm"); /*global Notifications */
+Cu.import("resource://gre/modules/Prompt.jsm"); /*global Prompt */
+Cu.import("resource://gre/modules/Services.jsm"); /*global Services */
+Cu.import("resource://gre/modules/WebChannel.jsm"); /*global WebChannel */
+Cu.import("resource://gre/modules/XPCOMUtils.jsm"); /*global XPCOMUtils */
+
+const log = Cu.import("resource://gre/modules/AndroidLog.jsm", {}).AndroidLog.bind("FxAccounts");
+
+const WEBCHANNEL_ID = "account_updates";
+
+const COMMAND_LOADED               = "fxaccounts:loaded";
+const COMMAND_CAN_LINK_ACCOUNT     = "fxaccounts:can_link_account";
+const COMMAND_LOGIN                = "fxaccounts:login";
+const COMMAND_CHANGE_PASSWORD      = "fxaccounts:change_password";
+const COMMAND_DELETE_ACCOUNT       = "fxaccounts:delete_account";
+
+const PREF_LAST_FXA_USER           = "identity.fxaccounts.lastSignedInUserHash";
+
+XPCOMUtils.defineLazyGetter(this, "strings",
+                            () => Services.strings.createBundle("chrome://browser/locale/aboutAccounts.properties")); /*global strings */
+
+Object.defineProperty(this, "NativeWindow",
+                      { get: () => Services.wm.getMostRecentWindow("navigator:browser").NativeWindow }); /*global NativeWindow */
+
+this.FxAccountsWebChannelHelpers = function() {
+};
+
+this.FxAccountsWebChannelHelpers.prototype = {
+  /**
+   * Get the hash of account name of the previously signed in account.
+   */
+  getPreviousAccountNameHashPref() {
+    try {
+      return Services.prefs.getComplexValue(PREF_LAST_FXA_USER, Ci.nsISupportsString).data;
+    } catch (_) {
+      return "";
+    }
+  },
+
+  /**
+   * Given an account name, set the hash of the previously signed in account.
+   *
+   * @param acctName the account name of the user's account.
+   */
+  setPreviousAccountNameHashPref(acctName) {
+    let string = Cc["@mozilla.org/supports-string;1"]
+                 .createInstance(Ci.nsISupportsString);
+    string.data = this.sha256(acctName);
+    Services.prefs.setComplexValue(PREF_LAST_FXA_USER, Ci.nsISupportsString, string);
+  },
+
+  /**
+   * Given a string, returns the SHA265 hash in base64.
+   */
+  sha256(str) {
+    let converter = Cc["@mozilla.org/intl/scriptableunicodeconverter"]
+                      .createInstance(Ci.nsIScriptableUnicodeConverter);
+    converter.charset = "UTF-8";
+    // Data is an array of bytes.
+    let data = converter.convertToByteArray(str, {});
+    let hasher = Cc["@mozilla.org/security/hash;1"]
+                   .createInstance(Ci.nsICryptoHash);
+    hasher.init(hasher.SHA256);
+    hasher.update(data, data.length);
+
+    return hasher.finish(true);
+  },
+};
+
+/**
+ * Create a new FxAccountsWebChannel to listen for account updates.
+ *
+ * @param {Object} options Options
+ *   @param {Object} options
+ *     @param {String} options.content_uri
+ *     The FxA Content server uri
+ *     @param {String} options.channel_id
+ *     The ID of the WebChannel
+ *     @param {String} options.helpers
+ *     Helpers functions. Should only be passed in for testing.
+ * @constructor
+ */
+this.FxAccountsWebChannel = function(options) {
+  if (!options) {
+    throw new Error("Missing configuration options");
+  }
+  if (!options["content_uri"]) {
+    throw new Error("Missing 'content_uri' option");
+  }
+  this._contentUri = options.content_uri;
+
+  if (!options["channel_id"]) {
+    throw new Error("Missing 'channel_id' option");
+  }
+  this._webChannelId = options.channel_id;
+
+  // options.helpers is only specified by tests.
+  this._helpers = options.helpers || new FxAccountsWebChannelHelpers(options);
+
+  this._setupChannel();
+};
+
+this.FxAccountsWebChannel.prototype = {
+  /**
+   * WebChannel that is used to communicate with content page
+   */
+  _channel: null,
+
+  /**
+   * WebChannel ID.
+   */
+  _webChannelId: null,
+  /**
+   * WebChannel origin, used to validate origin of messages
+   */
+  _webChannelOrigin: null,
+
+  /**
+   * Release all resources that are in use.
+   */
+  tearDown() {
+    this._channel.stopListening();
+    this._channel = null;
+    this._channelCallback = null;
+  },
+
+  /**
+   * Configures and registers a new WebChannel
+   *
+   * @private
+   */
+  _setupChannel() {
+    // if this.contentUri is present but not a valid URI, then this will throw an error.
+    try {
+      this._webChannelOrigin = Services.io.newURI(this._contentUri, null, null);
+      this._registerChannel();
+    } catch (e) {
+      log.e(e.toString());
+      throw e;
+    }
+  },
+
+  /**
+   * Create a new channel with the WebChannelBroker, setup a callback listener
+   * @private
+   */
+  _registerChannel() {
+    /**
+     * Processes messages that are called back from the FxAccountsChannel
+     *
+     * @param webChannelId {String}
+     *        Command webChannelId
+     * @param message {Object}
+     *        Command message
+     * @param sendingContext {Object}
+     *        Message sending context.
+     *        @param sendingContext.browser {browser}
+     *               The <browser> object that captured the
+     *               WebChannelMessageToChrome.
+     *        @param sendingContext.eventTarget {EventTarget}
+     *               The <EventTarget> where the message was sent.
+     *        @param sendingContext.principal {Principal}
+     *               The <Principal> of the EventTarget where the message was sent.
+     * @private
+     *
+     */
+    let listener = (webChannelId, message, sendingContext) => {
+      if (message) {
+        let command = message.command;
+        let data = message.data;
+        log.d("FxAccountsWebChannel message received, command: " + command);
+
+        // Respond to the message with true or false.
+        let respond = (data) => {
+          let response = {
+            command: command,
+            messageId: message.messageId,
+            data: data
+          };
+          log.d("Sending response to command: " + command);
+          this._channel.send(response, sendingContext);
+        };
+
+        switch (command) {
+          case COMMAND_LOADED:
+            let mm = sendingContext.browser.docShell
+              .QueryInterface(Ci.nsIInterfaceRequestor)
+              .getInterface(Ci.nsIContentFrameMessageManager);
+            mm.sendAsyncMessage(COMMAND_LOADED);
+            break;
+
+          case COMMAND_CAN_LINK_ACCOUNT:
+            Accounts.getFirefoxAccount().then(account => {
+              if (account) {
+                // If we /have/ an Android Account, we never allow the user to
+                // login to a different account.  They need to manually delete
+                // the first Android Account and then create a new one.
+                if (account.email == data.email) {
+                  // In future, we should use a UID for this comparison.
+                  log.d("Relinking existing Android Account: email addresses agree.");
+                  respond({ok: true});
+                } else {
+                  log.w("Not relinking existing Android Account: email addresses disagree!");
+                  let message = strings.GetStringFromName("relinkDenied.message");
+                  let buttonLabel = strings.GetStringFromName("relinkDenied.openPrefs");
+                  NativeWindow.toast.show(message, "long", {
+                    button: {
+                      icon: "drawable://switch_button_icon",
+                      label: buttonLabel,
+                      callback: () => {
+                        // We have an account, so this opens Sync native preferences.
+                        Accounts.launchSetup();
+                      },
+                    }
+                  });
+                  respond({ok: false});
+                }
+              } else {
+                // If we /don't have/ an Android Account, we warn if we're
+                // connecting to a new Account.  This is to minimize surprise;
+                // we never did this when changing accounts via the native UI.
+                let prevAcctHash = this._helpers.getPreviousAccountNameHashPref();
+                let shouldShowWarning = prevAcctHash && (prevAcctHash != this._helpers.sha256(data.email));
+
+                if (shouldShowWarning) {
+                  log.w("Warning about creating a new Android Account: previously linked to different email address!");
+                  let message = strings.formatStringFromName("relinkVerify.message", [data.email], 1);
+                  new Prompt({
+                    title: strings.GetStringFromName("relinkVerify.title"),
+                    message: message,
+                    buttons: [
+                      // This puts Cancel on the right.
+                      strings.GetStringFromName("relinkVerify.cancel"),
+                      strings.GetStringFromName("relinkVerify.continue"),
+                    ],
+                  }).show(result => respond({ok: result && result.button == 1}));
+                } else {
+                  log.d("Not warning about creating a new Android Account: no previously linked email address.");
+                  respond({ok: true});
+                }
+              }
+            }).catch(e => {
+              log.e(e.toString());
+              respond({ok: false});
+            });
+            break;
+
+          case COMMAND_LOGIN:
+            // Either create a new Android Account or re-connect an existing
+            // Android Account here.  There's not much to be done if we don't
+            // succeed or get an error.
+            Accounts.getFirefoxAccount().then(account => {
+              if (!account) {
+                return Accounts.createFirefoxAccountFromJSON(data).then(success => {
+                  if (!success) {
+                    throw new Error("Could not create Firefox Account!");
+                  }
+                  return success;
+                });
+              } else {
+                return Accounts.updateFirefoxAccountFromJSON(data).then(success => {
+                  if (!success) {
+                    throw new Error("Could not update Firefox Account!");
+                  }
+                  return success;
+                });
+              }
+            })
+            .then(success => {
+              if (!success) {
+                throw new Error("Could not create or update Firefox Account!");
+              }
+
+              // Remember who it is so we can show a relink warning when appropriate.
+              this._helpers.setPreviousAccountNameHashPref(data.email);
+
+              log.i("Created or updated Firefox Account.");
+            })
+            .catch(e => {
+              log.e(e.toString());
+            });
+            break;
+
+          case COMMAND_CHANGE_PASSWORD:
+            // Only update an existing Android Account.
+            Accounts.getFirefoxAccount().then(account => {
+              if (!account) {
+                throw new Error("Can't change password of non-existent Firefox Account!");
+              }
+              return Accounts.updateFirefoxAccountFromJSON(data);
+            })
+            .then(success => {
+              if (!success) {
+                throw new Error("Could not change Firefox Account password!");
+              }
+              log.i("Changed Firefox Account password.");
+            })
+            .catch(e => {
+              log.e(e.toString());
+            });
+            break;
+
+          case COMMAND_DELETE_ACCOUNT:
+            // The fxa-content-server has already confirmed the user's intent.
+            // Bombs away.  There's no recovery from failure, and not even a
+            // real need to check an account exists (although we do, for error
+            // messaging only).
+            Accounts.getFirefoxAccount().then(account => {
+              if (!account) {
+                throw new Error("Can't delete non-existent Firefox Account!");
+              }
+              return Accounts.deleteFirefoxAccount().then(success => {
+                if (!success) {
+                  throw new Error("Could not delete Firefox Account!");
+                }
+                log.i("Firefox Account deleted.");
+              });
+            }).catch(e => {
+              log.e(e.toString());
+            });
+            break;
+
+          default:
+            log.w("Ignoring unrecognized FxAccountsWebChannel command: " + JSON.stringify(command));
+            break;
+        }
+      }
+    };
+
+    this._channelCallback = listener;
+    this._channel = new WebChannel(this._webChannelId, this._webChannelOrigin);
+    this._channel.listen(listener);
+
+    log.d("FxAccountsWebChannel registered: " + this._webChannelId + " with origin " + this._webChannelOrigin.prePath);
+  }
+};
+
+let singleton;
+// The entry-point for this module, which ensures only one of our channels is
+// ever created - we require this because the WebChannel is global in scope and
+// allowing multiple channels would cause such notifications to be sent multiple
+// times.
+this.EnsureFxAccountsWebChannel = function() {
+  if (!singleton) {
+    let contentUri = Services.urlFormatter.formatURLPref("identity.fxaccounts.remote.webchannel.uri");
+    // The FxAccountsWebChannel listens for events and updates the Java layer.
+    singleton = new this.FxAccountsWebChannel({
+      content_uri: contentUri,
+      channel_id: WEBCHANNEL_ID,
+    });
+  }
+};
--- a/mobile/android/modules/moz.build
+++ b/mobile/android/modules/moz.build
@@ -6,16 +6,17 @@
 
 EXTRA_JS_MODULES += [
     'Accounts.jsm',
     'AndroidLog.jsm',
     'ContactService.jsm',
     'dbg-browser-actors.js',
     'DelayedInit.jsm',
     'DownloadNotifications.jsm',
+    'FxAccountsWebChannel.jsm',
     'HelperApps.jsm',
     'Home.jsm',
     'HomeProvider.jsm',
     'JavaAddonManager.jsm',
     'JNI.jsm',
     'LightweightThemeConsumer.jsm',
     'MediaPlayerApp.jsm',
     'Messaging.jsm',
--- a/mobile/android/services/manifests/FxAccountAndroidManifest_activities.xml.in
+++ b/mobile/android/services/manifests/FxAccountAndroidManifest_activities.xml.in
@@ -7,47 +7,63 @@
             android:configChanges="locale|layoutDirection"
             android:windowSoftInputMode="adjustResize">
             <!-- Adding a launcher will make this activity appear on the
                  Apps screen, which we only want when testing. -->
             <intent-filter>
                 <action android:name="android.intent.action.MAIN" />
                 <!-- <category android:name="android.intent.category.LAUNCHER" /> -->
             </intent-filter>
+            <intent-filter>
+                <action android:name="@ANDROID_PACKAGE_NAME@.ACTION_FXA_STATUS"/>
+                <category android:name="android.intent.category.DEFAULT"/>
+            </intent-filter>
         </activity>
 
         <activity
             android:theme="@style/FxAccountTheme"
             android:label="@string/sync_app_name"
             android:clearTaskOnLaunch="true"
             android:taskAffinity="@ANDROID_PACKAGE_NAME@.FXA"
             android:name="org.mozilla.gecko.fxa.activities.FxAccountGetStartedActivity"
             android:configChanges="locale|layoutDirection"
             android:windowSoftInputMode="adjustResize">
             <!-- Adding a launcher will make this activity appear on the
                  Apps screen, which we only want when testing. -->
             <intent-filter>
                 <action android:name="android.intent.action.MAIN" />
                 <!-- <category android:name="android.intent.category.LAUNCHER" /> -->
             </intent-filter>
+#ifdef MOZ_ANDROID_NATIVE_ACCOUNT_UI
+            <intent-filter>
+                <action android:name="@ANDROID_PACKAGE_NAME@.ACTION_FXA_GET_STARTED"/>
+                <category android:name="android.intent.category.DEFAULT"/>
+            </intent-filter>
+#endif
         </activity>
 
         <activity
             android:theme="@style/FxAccountTheme"
             android:name="org.mozilla.gecko.fxa.activities.FxAccountCreateAccountActivity"
             android:configChanges="locale|layoutDirection"
             android:windowSoftInputMode="adjustResize">
         </activity>
 
         <activity
             android:theme="@style/FxAccountTheme"
             android:name="org.mozilla.gecko.fxa.activities.FxAccountConfirmAccountActivity"
             android:configChanges="locale|layoutDirection"
             android:noHistory="true"
             android:windowSoftInputMode="adjustResize">
+#ifdef MOZ_ANDROID_NATIVE_ACCOUNT_UI
+            <intent-filter>
+                <action android:name="@ANDROID_PACKAGE_NAME@.ACTION_FXA_CONFIRM_ACCOUNT"/>
+                <category android:name="android.intent.category.DEFAULT"/>
+            </intent-filter>
+#endif
         </activity>
 
         <activity
             android:theme="@style/FxAccountTheme"
             android:name="org.mozilla.gecko.fxa.activities.FxAccountSignInActivity"
             android:configChanges="locale|layoutDirection"
             android:windowSoftInputMode="adjustResize">
         </activity>
@@ -60,23 +76,35 @@
             android:windowSoftInputMode="adjustResize">
         </activity>
 
         <activity
             android:theme="@style/FxAccountTheme"
             android:name="org.mozilla.gecko.fxa.activities.FxAccountUpdateCredentialsActivity"
             android:configChanges="locale|layoutDirection"
             android:windowSoftInputMode="adjustResize">
+#ifdef MOZ_ANDROID_NATIVE_ACCOUNT_UI
+            <intent-filter>
+                <action android:name="@ANDROID_PACKAGE_NAME@.ACTION_FXA_UPDATE_CREDENTIALS"/>
+                <category android:name="android.intent.category.DEFAULT"/>
+            </intent-filter>
+#endif
         </activity>
 
         <activity
             android:theme="@style/FxAccountTheme"
             android:name="org.mozilla.gecko.fxa.activities.FxAccountFinishMigratingActivity"
             android:configChanges="locale|layoutDirection"
             android:windowSoftInputMode="adjustResize">
+#ifdef MOZ_ANDROID_NATIVE_ACCOUNT_UI
+            <intent-filter>
+                <action android:name="@ANDROID_PACKAGE_NAME@.ACTION_FXA_FINISH_MIGRATING"/>
+                <category android:name="android.intent.category.DEFAULT"/>
+            </intent-filter>
+#endif
         </activity>
 
         <activity
             android:theme="@style/FxAccountTheme"
             android:name="org.mozilla.gecko.fxa.activities.FxAccountMigrationFinishedActivity"
             android:configChanges="locale|layoutDirection"
             android:noHistory="true"
             android:windowSoftInputMode="adjustResize">
@@ -100,8 +128,46 @@
 
         <receiver
             android:name="org.mozilla.gecko.fxa.receivers.FxAccountUpgradeReceiver">
             <intent-filter>
                 <action android:name="android.intent.action.PACKAGE_REPLACED" />
                 <data android:scheme="package"/>
             </intent-filter>
         </receiver>
+
+#ifndef MOZ_ANDROID_NATIVE_ACCOUNT_UI
+        <activity
+            android:exported="false"
+            android:name="org.mozilla.gecko.fxa.activities.FxAccountGetStartedActivityWeb">
+            <intent-filter>
+                <action android:name="@ANDROID_PACKAGE_NAME@.ACTION_FXA_GET_STARTED"/>
+                <category android:name="android.intent.category.DEFAULT"/>
+            </intent-filter>
+        </activity>
+
+        <activity
+            android:exported="false"
+            android:name="org.mozilla.gecko.fxa.activities.FxAccountUpdateCredentialsActivityWeb">
+            <intent-filter>
+                <action android:name="@ANDROID_PACKAGE_NAME@.ACTION_FXA_UPDATE_CREDENTIALS"/>
+                <category android:name="android.intent.category.DEFAULT"/>
+            </intent-filter>
+        </activity>
+
+        <activity
+            android:exported="false"
+            android:name="org.mozilla.gecko.fxa.activities.FxAccountFinishMigratingActivityWeb">
+            <intent-filter>
+                <action android:name="@ANDROID_PACKAGE_NAME@.ACTION_FXA_FINISH_MIGRATING"/>
+                <category android:name="android.intent.category.DEFAULT"/>
+            </intent-filter>
+        </activity>
+
+        <activity
+            android:exported="false"
+            android:name="org.mozilla.gecko.fxa.activities.FxAccountConfirmAccountActivityWeb">
+            <intent-filter>
+                <action android:name="@ANDROID_PACKAGE_NAME@.ACTION_FXA_CONFIRM_ACCOUNT"/>
+                <category android:name="android.intent.category.DEFAULT"/>
+            </intent-filter>
+        </activity>
+#endif
--- a/toolkit/components/search/nsSearchService.js
+++ b/toolkit/components/search/nsSearchService.js
@@ -2069,67 +2069,64 @@ Engine.prototype = {
         if (aWidth && aHeight) {
           this._addIconToMap(aWidth, aHeight, aIconURL)
         }
         break;
       case "http":
       case "https":
       case "ftp":
         // No use downloading the icon if the engine file is read-only
-        if (!this._readOnly ||
-            getBoolPref(BROWSER_SEARCH_PREF + "cache.enabled", true)) {
-          LOG("_setIcon: Downloading icon: \"" + uri.spec +
-              "\" for engine: \"" + this.name + "\"");
-          var chan = NetUtil.ioService.newChannelFromURI2(uri,
-                                                          null,      // aLoadingNode
-                                                          Services.scriptSecurityManager.getSystemPrincipal(),
-                                                          null,      // aTriggeringPrincipal
-                                                          Ci.nsILoadInfo.SEC_NORMAL,
-                                                          Ci.nsIContentPolicy.TYPE_IMAGE);
-
-          let iconLoadCallback = function (aByteArray, aEngine) {
-            // This callback may run after we've already set a preferred icon,
-            // so check again.
-            if (aEngine._hasPreferredIcon && !aIsPreferred)
-              return;
-
-            if (!aByteArray || aByteArray.length > MAX_ICON_SIZE) {
-              LOG("iconLoadCallback: load failed, or the icon was too large!");
-              return;
-            }
-
-            var str = btoa(String.fromCharCode.apply(null, aByteArray));
-            let dataURL = ICON_DATAURL_PREFIX + str;
-            aEngine._iconURI = makeURI(dataURL);
-
-            if (aWidth && aHeight) {
-              aEngine._addIconToMap(aWidth, aHeight, dataURL)
-            }
-
-            // The engine might not have a file yet, if it's being downloaded,
-            // because the request for the engine file itself (_onLoad) may not
-            // yet be complete. In that case, this change will be written to
-            // file when _onLoad is called. For readonly engines, we'll store
-            // the changes in the cache once notified below.
-            if (aEngine._file && !aEngine._readOnly)
-              aEngine._serializeToFile();
-
-            notifyAction(aEngine, SEARCH_ENGINE_CHANGED);
-            aEngine._hasPreferredIcon = aIsPreferred;
+        LOG("_setIcon: Downloading icon: \"" + uri.spec +
+            "\" for engine: \"" + this.name + "\"");
+        var chan = NetUtil.ioService.newChannelFromURI2(uri,
+                                                        null,      // aLoadingNode
+                                                        Services.scriptSecurityManager.getSystemPrincipal(),
+                                                        null,      // aTriggeringPrincipal
+                                                        Ci.nsILoadInfo.SEC_NORMAL,
+                                                        Ci.nsIContentPolicy.TYPE_IMAGE);
+
+        let iconLoadCallback = function (aByteArray, aEngine) {
+          // This callback may run after we've already set a preferred icon,
+          // so check again.
+          if (aEngine._hasPreferredIcon && !aIsPreferred)
+            return;
+
+          if (!aByteArray || aByteArray.length > MAX_ICON_SIZE) {
+            LOG("iconLoadCallback: load failed, or the icon was too large!");
+            return;
           }
 
-          // If we're currently acting as an "update engine", then the callback
-          // should set the icon on the engine we're updating and not us, since
-          // |this| might be gone by the time the callback runs.
-          var engineToSet = this._engineToUpdate || this;
-
-          var listener = new loadListener(chan, engineToSet, iconLoadCallback);
-          chan.notificationCallbacks = listener;
-          chan.asyncOpen(listener, null);
+          var str = btoa(String.fromCharCode.apply(null, aByteArray));
+          let dataURL = ICON_DATAURL_PREFIX + str;
+          aEngine._iconURI = makeURI(dataURL);
+
+          if (aWidth && aHeight) {
+            aEngine._addIconToMap(aWidth, aHeight, dataURL)
+          }
+
+          // The engine might not have a file yet, if it's being downloaded,
+          // because the request for the engine file itself (_onLoad) may not
+          // yet be complete. In that case, this change will be written to
+          // file when _onLoad is called. For readonly engines, we'll store
+          // the changes in the cache once notified below.
+          if (aEngine._file && !aEngine._readOnly)
+            aEngine._serializeToFile();
+
+          notifyAction(aEngine, SEARCH_ENGINE_CHANGED);
+          aEngine._hasPreferredIcon = aIsPreferred;
         }
+
+        // If we're currently acting as an "update engine", then the callback
+        // should set the icon on the engine we're updating and not us, since
+        // |this| might be gone by the time the callback runs.
+        var engineToSet = this._engineToUpdate || this;
+
+        var listener = new loadListener(chan, engineToSet, iconLoadCallback);
+        chan.notificationCallbacks = listener;
+        chan.asyncOpen(listener, null);
         break;
     }
   },
 
   /**
    * Initialize this Engine object from the collected data.
    */
   _initFromData: function SRCH_ENG_initFromData() {
@@ -3609,19 +3606,16 @@ SearchService.prototype = {
     return this.getEngineByName(defaultEngine);
   },
 
   resetToOriginalDefaultEngine: function SRCH_SVC__resetToOriginalDefaultEngine() {
     this.currentEngine = this._originalDefaultEngine;
   },
 
   _buildCache: function SRCH_SVC__buildCache() {
-    if (!getBoolPref(BROWSER_SEARCH_PREF + "cache.enabled", true))
-      return;
-
     TelemetryStopwatch.start("SEARCH_SERVICE_BUILD_CACHE_MS");
     let cache = {};
     let locale = getLocale();
     let buildID = Services.appinfo.platformBuildID;
 
     // Allows us to force a cache refresh should the cache format change.
     cache.version = CACHE_VERSION;
     // We don't want to incur the costs of stat()ing each plugin on every
@@ -3692,23 +3686,20 @@ SearchService.prototype = {
     }
     TelemetryStopwatch.finish("SEARCH_SERVICE_BUILD_CACHE_MS");
   },
 
   _syncLoadEngines: function SRCH_SVC__syncLoadEngines() {
     LOG("_syncLoadEngines: start");
     // See if we have a cache file so we don't have to parse a bunch of XML.
     let cache = {};
-    let cacheEnabled = getBoolPref(BROWSER_SEARCH_PREF + "cache.enabled", true);
-    if (cacheEnabled) {
-      let cacheFile = getDir(NS_APP_USER_PROFILE_50_DIR);
-      cacheFile.append("search.json");
-      if (cacheFile.exists())
-        cache = this._readCacheFile(cacheFile);
-    }
+    let cacheFile = getDir(NS_APP_USER_PROFILE_50_DIR);
+    cacheFile.append("search.json");
+    if (cacheFile.exists())
+      cache = this._readCacheFile(cacheFile);
 
     let [chromeFiles, chromeURIs] = this._findJAREngines();
 
     let distDirs = [];
     let locations;
     try {
       locations = getDir(NS_APP_DISTRIBUTION_SEARCH_DIR_LIST,
                          Ci.nsISimpleEnumerator);
@@ -3751,26 +3742,25 @@ SearchService.prototype = {
                        cache.locale != getLocale() ||
                        cache.buildID != buildID ||
                        cachePaths.length != toLoad.length ||
                        toLoad.some(notInCachePath) ||
                        cache.visibleDefaultEngines.length != this._visibleDefaultEngines.length ||
                        this._visibleDefaultEngines.some(notInCacheVisibleEngines) ||
                        toLoad.some(modifiedDir);
 
-    if (!cacheEnabled || rebuildCache) {
+    if (rebuildCache) {
       LOG("_loadEngines: Absent or outdated cache. Loading engines from disk.");
       distDirs.forEach(this._loadEnginesFromDir, this);
 
       this._loadFromChromeURLs(chromeURIs);
 
       otherDirs.forEach(this._loadEnginesFromDir, this);
 
-      if (cacheEnabled)
-        this._buildCache();
+      this._buildCache();
       return;
     }
 
     LOG("_loadEngines: loading from cache directories");
     for each (let dir in cache.directories)
       this._loadEnginesFromCache(dir);
 
     LOG("_loadEngines: done");
@@ -3782,21 +3772,18 @@ SearchService.prototype = {
    * @returns {Promise} A promise, resolved successfully if loading data
    * succeeds.
    */
   _asyncLoadEngines: function SRCH_SVC__asyncLoadEngines() {
     return Task.spawn(function() {
       LOG("_asyncLoadEngines: start");
       // See if we have a cache file so we don't have to parse a bunch of XML.
       let cache = {};
-      let cacheEnabled = getBoolPref(BROWSER_SEARCH_PREF + "cache.enabled", true);
-      if (cacheEnabled) {
-        let cacheFilePath = OS.Path.join(OS.Constants.Path.profileDir, "search.json");
-        cache = yield checkForSyncCompletion(this._asyncReadCacheFile(cacheFilePath));
-      }
+      let cacheFilePath = OS.Path.join(OS.Constants.Path.profileDir, "search.json");
+      cache = yield checkForSyncCompletion(this._asyncReadCacheFile(cacheFilePath));
 
       Services.obs.notifyObservers(null, SEARCH_SERVICE_TOPIC, "find-jar-engines");
       let [chromeFiles, chromeURIs] =
         yield checkForSyncCompletion(this._asyncFindJAREngines());
 
       // Get the non-empty distribution directories into distDirs...
       let distDirs = [];
       let locations;
@@ -3877,17 +3864,17 @@ SearchService.prototype = {
                          cache.locale != getLocale() ||
                          cache.buildID != buildID ||
                          cachePaths.length != toLoad.length ||
                          toLoad.some(notInCachePath) ||
                          cache.visibleDefaultEngines.length != this._visibleDefaultEngines.length ||
                          this._visibleDefaultEngines.some(notInCacheVisibleEngines) ||
                          (yield checkForSyncCompletion(hasModifiedDir(toLoad)));
 
-      if (!cacheEnabled || rebuildCache) {
+      if (rebuildCache) {
         LOG("_asyncLoadEngines: Absent or outdated cache. Loading engines from disk.");
         let engines = [];
         for (let loadDir of distDirs) {
           let enginesFromDir =
             yield checkForSyncCompletion(this._asyncLoadEnginesFromDir(loadDir));
           engines = engines.concat(enginesFromDir);
         }
         let enginesFromURLs =
@@ -3897,18 +3884,17 @@ SearchService.prototype = {
           let enginesFromDir =
             yield checkForSyncCompletion(this._asyncLoadEnginesFromDir(loadDir));
           engines = engines.concat(enginesFromDir);
         }
 
         for (let engine of engines) {
           this._addEngineToStore(engine);
         }
-        if (cacheEnabled)
-          this._buildCache();
+        this._buildCache();
         return;
       }
 
       LOG("_asyncLoadEngines: loading from cache directories");
       for each (let dir in cache.directories)
         this._loadEnginesFromCache(dir);
 
       LOG("_asyncLoadEngines: done");
@@ -5540,22 +5526,16 @@ var engineUpdateService = {
   },
 
   update: function eus_Update(aEngine) {
     let engine = aEngine.wrappedJSObject;
     ULOG("update called for " + aEngine._name);
     if (!getBoolPref(BROWSER_SEARCH_PREF + "update", true) || !engine._hasUpdates)
       return;
 
-    // We use the cache to store updated app engines, so refuse to update if the
-    // cache is disabled.
-    if (engine._readOnly &&
-        !getBoolPref(BROWSER_SEARCH_PREF + "cache.enabled", true))
-      return;
-
     let testEngine = null;
     let updateURL = engine._getURLOfType(URLTYPE_OPENSEARCH);
     let updateURI = (updateURL && updateURL._hasRelation("self")) ?
                      updateURL.getSubmission("", engine).uri :
                      makeURI(engine._updateURL);
     if (updateURI) {
       if (engine._isDefault && !updateURI.schemeIs("https")) {
         ULOG("Invalid scheme for default engine update");
--- a/toolkit/components/telemetry/Histograms.json
+++ b/toolkit/components/telemetry/Histograms.json
@@ -7347,16 +7347,34 @@
   "DEVTOOLS_PERFTOOLS_SELECTED_VIEW_MS": {
     "expires_in_version": "never",
     "kind": "exponential",
     "keyed": true,
     "high": "600000",
     "n_buckets": 20,
     "description": "The amount of time spent in a specific performance tool view, keyed by view name (waterfall, js-calltree, js-flamegraph, etc)."
   },
+  "VIEW_SOURCE_IN_BROWSER_OPENED_BOOLEAN": {
+    "alert_emails": ["mozilla-dev-developer-tools@lists.mozilla.org", "jryans@mozilla.com"],
+    "expires_in_version": "53",
+    "kind": "boolean",
+    "description": "How many times has view source in browser / tab been opened?"
+  },
+  "VIEW_SOURCE_IN_WINDOW_OPENED_BOOLEAN": {
+    "alert_emails": ["mozilla-dev-developer-tools@lists.mozilla.org", "jryans@mozilla.com"],
+    "expires_in_version": "53",
+    "kind": "boolean",
+    "description": "How many times has view source in a new window been opened?"
+  },
+  "VIEW_SOURCE_EXTERNAL_RESULT_BOOLEAN": {
+    "alert_emails": ["mozilla-dev-developer-tools@lists.mozilla.org", "jryans@mozilla.com"],
+    "expires_in_version": "53",
+    "kind": "boolean",
+    "description": "How many times has view source in an external editor been opened, and did it succeed?"
+  },
   "BROWSER_IS_USER_DEFAULT": {
     "expires_in_version": "never",
     "kind": "boolean",
     "releaseChannelCollection": "opt-out",
     "description": "The result of the startup default desktop browser check."
   },
   "BROWSER_IS_USER_DEFAULT_ERROR": {
     "expires_in_version": "never",
@@ -8750,45 +8768,45 @@
     "expires_in_version": "never",
     "alert_emails": ["gfx-telemetry-alerts@mozilla.com"],
     "kind": "enumerated",
     "n_values": 20,
     "releaseChannelCollection": "opt-out",
     "description": "Reports results from the graphics sanity test to track which drivers are having problems (0=TEST_PASSED, 1=TEST_FAILED_RENDER, 2=TEST_FAILED_VIDEO, 3=TEST_CRASHED)"
   },
   "READER_MODE_SERIALIZE_DOM_MS": {
-    "expires_in_version": "42",
+    "expires_in_version": "50",
     "kind": "exponential",
     "high": "5000",
     "n_buckets": 15,
     "description": "Time (ms) to serialize a DOM to send to the reader worker"
   },
   "READER_MODE_WORKER_PARSE_MS": {
-    "expires_in_version": "42",
+    "expires_in_version": "50",
     "kind": "exponential",
     "high": "10000",
     "n_buckets": 30,
     "description": "Time (ms) for the reader worker to parse a document"
   },
   "READER_MODE_DOWNLOAD_MS": {
-    "expires_in_version": "42",
+    "expires_in_version": "50",
     "kind": "exponential",
     "low": 50,
     "high": "40000",
     "n_buckets": 60,
     "description": "Time (ms) to download a document to show in reader mode"
   },
   "READER_MODE_PARSE_RESULT" : {
-    "expires_in_version": "42",
+    "expires_in_version": "50",
     "kind": "enumerated",
     "n_values": 5,
     "description": "The result of trying to parse a document to show in reader view (0=Success, 1=Error too many elements, 2=Error in worker, 3=Error no article)"
   },
   "READER_MODE_DOWNLOAD_RESULT" : {
-    "expires_in_version": "42",
+    "expires_in_version": "50",
     "kind": "enumerated",
     "n_values": 5,
     "description": "The result of trying to download a document to show in reader view (0=Success, 1=Error XHR, 2=Error no document)"
   },
   "PERMISSIONS_SQL_CORRUPTED": {
     "expires_in_version": "never",
     "kind": "count",
     "description": "Record the permissions.sqlite init failure"
--- a/toolkit/components/viewsource/content/viewSourceUtils.js
+++ b/toolkit/components/viewsource/content/viewSourceUtils.js
@@ -83,16 +83,19 @@ var gViewSourceUtils = {
    *        outerWindowID (optional):
    *          The outerWindowID of the content window containing the document that
    *          we want to view the source of. Pass this if you want to attempt to
    *          load the document source out of the network cache.
    *        lineNumber (optional):
    *          The line number to focus on once the source is loaded.
    */
   viewSourceInBrowser: function(aArgs) {
+    Services.telemetry
+            .getHistogramById("VIEW_SOURCE_IN_BROWSER_OPENED_BOOLEAN")
+            .add(true);
     let viewSourceBrowser = new ViewSourceBrowser(aArgs.viewSourceBrowser);
     viewSourceBrowser.loadViewSource(aArgs);
   },
 
   /**
    * Displays view source for a selection from some document in the provided
    * <browser>.  This allows for non-window display methods, such as a tab from
    * Firefox.
@@ -151,16 +154,19 @@ var gViewSourceUtils = {
         isForcedCharset =
           aDocument.defaultView
                    .QueryInterface(Components.interfaces.nsIInterfaceRequestor)
                    .getInterface(Components.interfaces.nsIDOMWindowUtils)
                    .docCharsetIsForced;
       } catch (ex) {
       }
     }
+    Services.telemetry
+            .getHistogramById("VIEW_SOURCE_IN_WINDOW_OPENED_BOOLEAN")
+            .add(true);
     openDialog("chrome://global/content/viewSource.xul",
                "_blank",
                "all,dialog=no",
                aArgsOrURL, charset, aPageDescriptor, aLineNumber, isForcedCharset);
   },
 
   buildEditorArgs: function(aPath, aLineNumber) {
     // Determine the command line arguments to pass to the editor.
@@ -335,17 +341,20 @@ var gViewSourceUtils = {
     if (!result) {
       this._openInInternalViewer(data.url, data.pageDescriptor, data.doc, data.lineNumber);
     }
   },
 
   // Calls the callback, keeping in mind undefined or null values.
   handleCallBack: function(aCallBack, result, data)
   {
-    // ifcallback is undefined, default to the internal viewer
+    Services.telemetry
+            .getHistogramById("VIEW_SOURCE_EXTERNAL_RESULT_BOOLEAN")
+            .add(result);
+    // if callback is undefined, default to the internal viewer
     if (aCallBack === undefined) {
       this.internalViewerFallback(result, data);
     } else if (aCallBack) {
       aCallBack(result, data);
     }
   },
 
   // Returns nsIProcess of the external view source editor or null
--- a/toolkit/mozapps/extensions/test/xpcshell/test_system_reset.js
+++ b/toolkit/mozapps/extensions/test/xpcshell/test_system_reset.js
@@ -34,16 +34,18 @@ function* check_installed(inProfile, ...
     let addon = yield promiseAddonByID(id);
 
     if (versions[i]) {
       // Add-on should be installed
       do_check_neq(addon, null);
       do_check_eq(addon.version, versions[i]);
       do_check_true(addon.isActive);
       do_check_false(addon.foreignInstall);
+      do_check_false(hasFlag(addon.permissions, AddonManager.PERM_CAN_UPGRADE));
+      do_check_false(hasFlag(addon.permissions, AddonManager.PERM_CAN_UNINSTALL));
 
       // Verify the add-ons file is in the right place
       let file = expectedDir.clone();
       file.append(id + ".xpi");
       do_check_true(file.exists());
       do_check_true(file.isFile());
 
       let uri = addon.getResourceURI(null);