Merge mozilla-central to mozilla-inbound
authorCarsten "Tomcat" Book <cbook@mozilla.com>
Fri, 06 Nov 2015 13:58:23 +0100
changeset 271593 516b30eec70daeec8259e3f84b13fe8f308d6a85
parent 271592 a93a69859594569b51d6c8a792983d95d22115f0 (current diff)
parent 271496 512caeeb5565bb819a24ebda6a37b44fdf71b6c3 (diff)
child 271594 23f47084749fb6bba5723996c7f2fd11aa65b352
push id16135
push usercbook@mozilla.com
push dateMon, 09 Nov 2015 13:59:56 +0000
treeherderfx-team@5898d8162f44 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
milestone45.0a1
Merge mozilla-central to mozilla-inbound
--- 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="f39a7a827c0c0f48087ff3ead94f61ae22523919"/>
+  <project name="gaia" path="gaia" remote="mozillaorg" revision="cec4c1d3729137a24163756d15f98b0d37803966"/>
   <project name="fake-libdvm" path="dalvik" remote="b2g" revision="d50ae982b19f42f0b66d08b9eb306be81687869f"/>
   <project name="fake-qemu-kernel" path="prebuilts/qemu-kernel" remote="b2g" revision="939b377d55a2f081d94029a30a75d05e5a20daf3"/>
   <project name="gonk-misc" path="gonk-misc" remote="b2g" revision="956700d9754349b630a34551750ae6353614b6aa"/>
   <project name="librecovery" path="librecovery" remote="b2g" revision="1b3591a50ed352fc6ddb77462b7b35d0bfa555a3"/>
   <project name="moztt" path="external/moztt" remote="b2g" revision="aa5b7b7f6ed207ea1adc4df11d1d8bdaeabadd85"/>
   <project name="rilproxy" path="rilproxy" remote="b2g" revision="5ef30994f4778b4052e58a4383dbe7890048c87e"/>
   <project name="valgrind" path="external/valgrind" remote="b2g" revision="5f931350fbc87c3df9db8b0ceb37734b8b471593"/>
   <project name="vex" path="external/VEX" remote="b2g" revision="48d8c7c950745f1b166b42125e6f0d3293d71636"/>
--- 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="f39a7a827c0c0f48087ff3ead94f61ae22523919"/>
+  <project name="gaia" path="gaia" remote="mozillaorg" revision="cec4c1d3729137a24163756d15f98b0d37803966"/>
   <project name="fake-libdvm" path="dalvik" remote="b2g" revision="d50ae982b19f42f0b66d08b9eb306be81687869f"/>
   <project name="fake-qemu-kernel" path="prebuilts/qemu-kernel" remote="b2g" revision="939b377d55a2f081d94029a30a75d05e5a20daf3"/>
   <project name="gonk-misc" path="gonk-misc" remote="b2g" revision="956700d9754349b630a34551750ae6353614b6aa"/>
   <project name="librecovery" path="librecovery" remote="b2g" revision="1b3591a50ed352fc6ddb77462b7b35d0bfa555a3"/>
   <project name="moztt" path="external/moztt" remote="b2g" revision="aa5b7b7f6ed207ea1adc4df11d1d8bdaeabadd85"/>
   <project name="rilproxy" path="rilproxy" remote="b2g" revision="5ef30994f4778b4052e58a4383dbe7890048c87e"/>
   <project name="valgrind" path="external/valgrind" remote="b2g" revision="5f931350fbc87c3df9db8b0ceb37734b8b471593"/>
   <project name="vex" path="external/VEX" remote="b2g" revision="48d8c7c950745f1b166b42125e6f0d3293d71636"/>
--- 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="f39a7a827c0c0f48087ff3ead94f61ae22523919"/>
+  <project name="gaia.git" path="gaia" remote="mozillaorg" revision="cec4c1d3729137a24163756d15f98b0d37803966"/>
   <project name="gonk-misc" path="gonk-misc" remote="b2g" revision="956700d9754349b630a34551750ae6353614b6aa"/>
   <project name="rilproxy" path="rilproxy" remote="b2g" revision="5ef30994f4778b4052e58a4383dbe7890048c87e"/>
   <project name="platform_hardware_ril" path="hardware/ril" remote="b2g" revision="4ace9aaee0e048dfda11bb787646c59982a3dc80"/>
   <project name="platform_external_qemu" path="external/qemu" remote="b2g" revision="c72c9278ddc2f442d193474993d36e7f2cfb08c4"/>
   <project name="moztt" path="external/moztt" remote="b2g" revision="aa5b7b7f6ed207ea1adc4df11d1d8bdaeabadd85"/>
   <project name="apitrace" path="external/apitrace" remote="apitrace" revision="34ea6163f9f0e0122fb0bb03607eccdca31ced7a"/>
   <project name="platform_hardware_libhardware_moz" path="hardware/libhardware_moz" remote="b2g" revision="fdf3a143dc777e5f9d33a88373af7ea161d3b440"/>
   <!-- Stock Android things -->
--- 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="f39a7a827c0c0f48087ff3ead94f61ae22523919"/>
+  <project name="gaia" path="gaia" remote="mozillaorg" revision="cec4c1d3729137a24163756d15f98b0d37803966"/>
   <project name="gonk-misc" path="gonk-misc" remote="b2g" revision="956700d9754349b630a34551750ae6353614b6aa"/>
   <project name="moztt" path="external/moztt" remote="b2g" revision="aa5b7b7f6ed207ea1adc4df11d1d8bdaeabadd85"/>
   <project name="apitrace" path="external/apitrace" remote="apitrace" revision="638ec448619fda80fcb439b1747af62169d05548"/>
   <project name="valgrind" path="external/valgrind" remote="b2g" revision="5f931350fbc87c3df9db8b0ceb37734b8b471593"/>
   <project name="vex" path="external/VEX" remote="b2g" revision="48d8c7c950745f1b166b42125e6f0d3293d71636"/>
   <project name="platform_hardware_libhardware_moz" path="hardware/libhardware_moz" remote="b2g" revision="fdf3a143dc777e5f9d33a88373af7ea161d3b440"/>
   <!-- Stock Android things -->
   <project groups="linux" name="platform/prebuilts/clang/linux-x86/3.1" path="prebuilts/clang/linux-x86/3.1" revision="5c45f43419d5582949284eee9cef0c43d866e03b"/>
--- 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="f39a7a827c0c0f48087ff3ead94f61ae22523919"/>
+  <project name="gaia" path="gaia" remote="mozillaorg" revision="cec4c1d3729137a24163756d15f98b0d37803966"/>
   <project name="fake-libdvm" path="dalvik" remote="b2g" revision="d50ae982b19f42f0b66d08b9eb306be81687869f"/>
   <project name="gonk-misc" path="gonk-misc" remote="b2g" revision="956700d9754349b630a34551750ae6353614b6aa"/>
   <project name="librecovery" path="librecovery" remote="b2g" revision="1b3591a50ed352fc6ddb77462b7b35d0bfa555a3"/>
   <project name="moztt" path="external/moztt" remote="b2g" revision="aa5b7b7f6ed207ea1adc4df11d1d8bdaeabadd85"/>
   <project name="rilproxy" path="rilproxy" remote="b2g" revision="5ef30994f4778b4052e58a4383dbe7890048c87e"/>
   <project name="valgrind" path="external/valgrind" remote="b2g" revision="5f931350fbc87c3df9db8b0ceb37734b8b471593"/>
   <project name="vex" path="external/VEX" remote="b2g" revision="48d8c7c950745f1b166b42125e6f0d3293d71636"/>
   <project name="apitrace" path="external/apitrace" remote="apitrace" revision="638ec448619fda80fcb439b1747af62169d05548"/>
--- 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="f39a7a827c0c0f48087ff3ead94f61ae22523919"/>
+  <project name="gaia" path="gaia" remote="mozillaorg" revision="cec4c1d3729137a24163756d15f98b0d37803966"/>
   <project name="fake-libdvm" path="dalvik" remote="b2g" revision="d50ae982b19f42f0b66d08b9eb306be81687869f"/>
   <project name="gonk-misc" path="gonk-misc" remote="b2g" revision="956700d9754349b630a34551750ae6353614b6aa"/>
   <project name="librecovery" path="librecovery" remote="b2g" revision="1b3591a50ed352fc6ddb77462b7b35d0bfa555a3"/>
   <project name="moztt" path="external/moztt" remote="b2g" revision="aa5b7b7f6ed207ea1adc4df11d1d8bdaeabadd85"/>
   <project name="rilproxy" path="rilproxy" remote="b2g" revision="5ef30994f4778b4052e58a4383dbe7890048c87e"/>
   <project name="valgrind" path="external/valgrind" remote="b2g" revision="5f931350fbc87c3df9db8b0ceb37734b8b471593"/>
   <project name="vex" path="external/VEX" remote="b2g" revision="48d8c7c950745f1b166b42125e6f0d3293d71636"/>
   <project name="apitrace" path="external/apitrace" remote="apitrace" revision="638ec448619fda80fcb439b1747af62169d05548"/>
--- 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="f39a7a827c0c0f48087ff3ead94f61ae22523919"/>
+  <project name="gaia.git" path="gaia" remote="mozillaorg" revision="cec4c1d3729137a24163756d15f98b0d37803966"/>
   <project name="gonk-misc" path="gonk-misc" remote="b2g" revision="956700d9754349b630a34551750ae6353614b6aa"/>
   <project name="rilproxy" path="rilproxy" remote="b2g" revision="5ef30994f4778b4052e58a4383dbe7890048c87e"/>
   <project name="platform_hardware_ril" path="hardware/ril" remote="b2g" revision="4ace9aaee0e048dfda11bb787646c59982a3dc80"/>
   <project name="platform_external_qemu" path="external/qemu" remote="b2g" revision="c72c9278ddc2f442d193474993d36e7f2cfb08c4"/>
   <project name="moztt" path="external/moztt" remote="b2g" revision="aa5b7b7f6ed207ea1adc4df11d1d8bdaeabadd85"/>
   <project name="apitrace" path="external/apitrace" remote="apitrace" revision="34ea6163f9f0e0122fb0bb03607eccdca31ced7a"/>
   <project name="platform_hardware_libhardware_moz" path="hardware/libhardware_moz" remote="b2g" revision="fdf3a143dc777e5f9d33a88373af7ea161d3b440"/>
   <!-- Stock Android things -->
--- 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="f39a7a827c0c0f48087ff3ead94f61ae22523919"/>
+  <project name="gaia" path="gaia" remote="mozillaorg" revision="cec4c1d3729137a24163756d15f98b0d37803966"/>
   <project name="fake-libdvm" path="dalvik" remote="b2g" revision="d50ae982b19f42f0b66d08b9eb306be81687869f"/>
   <project name="fake-qemu-kernel" path="prebuilts/qemu-kernel" remote="b2g" revision="939b377d55a2f081d94029a30a75d05e5a20daf3"/>
   <project name="gonk-misc" path="gonk-misc" remote="b2g" revision="956700d9754349b630a34551750ae6353614b6aa"/>
   <project name="librecovery" path="librecovery" remote="b2g" revision="1b3591a50ed352fc6ddb77462b7b35d0bfa555a3"/>
   <project name="moztt" path="external/moztt" remote="b2g" revision="aa5b7b7f6ed207ea1adc4df11d1d8bdaeabadd85"/>
   <project name="rilproxy" path="rilproxy" remote="b2g" revision="5ef30994f4778b4052e58a4383dbe7890048c87e"/>
   <project name="valgrind" path="external/valgrind" remote="b2g" revision="5f931350fbc87c3df9db8b0ceb37734b8b471593"/>
   <project name="vex" path="external/VEX" remote="b2g" revision="48d8c7c950745f1b166b42125e6f0d3293d71636"/>
--- a/b2g/config/gaia.json
+++ b/b2g/config/gaia.json
@@ -1,9 +1,9 @@
 {
     "git": {
-        "git_revision": "f39a7a827c0c0f48087ff3ead94f61ae22523919", 
+        "git_revision": "cec4c1d3729137a24163756d15f98b0d37803966", 
         "remote": "https://git.mozilla.org/releases/gaia.git", 
         "branch": ""
     }, 
-    "revision": "a49674a0739948d3a32741ed77e9345c2ee91883", 
+    "revision": "18c5a983482c343e6c08638eece0cdd6d336887e", 
     "repo_path": "integration/gaia-central"
 }
--- a/b2g/config/nexus-4-kk/sources.xml
+++ b/b2g/config/nexus-4-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="f39a7a827c0c0f48087ff3ead94f61ae22523919"/>
+  <project name="gaia" path="gaia" remote="mozillaorg" revision="cec4c1d3729137a24163756d15f98b0d37803966"/>
   <project name="fake-libdvm" path="dalvik" remote="b2g" revision="d50ae982b19f42f0b66d08b9eb306be81687869f"/>
   <project name="fake-qemu-kernel" path="prebuilts/qemu-kernel" remote="b2g" revision="939b377d55a2f081d94029a30a75d05e5a20daf3"/>
   <project name="gonk-misc" path="gonk-misc" remote="b2g" revision="956700d9754349b630a34551750ae6353614b6aa"/>
   <project name="librecovery" path="librecovery" remote="b2g" revision="1b3591a50ed352fc6ddb77462b7b35d0bfa555a3"/>
   <project name="moztt" path="external/moztt" remote="b2g" revision="aa5b7b7f6ed207ea1adc4df11d1d8bdaeabadd85"/>
   <project name="rilproxy" path="rilproxy" remote="b2g" revision="5ef30994f4778b4052e58a4383dbe7890048c87e"/>
   <project name="valgrind" path="external/valgrind" remote="b2g" revision="5f931350fbc87c3df9db8b0ceb37734b8b471593"/>
   <project name="vex" path="external/VEX" remote="b2g" revision="48d8c7c950745f1b166b42125e6f0d3293d71636"/>
--- a/b2g/config/nexus-4/sources.xml
+++ b/b2g/config/nexus-4/sources.xml
@@ -13,17 +13,17 @@
   <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="fake-qemu-kernel" path="prebuilts/qemu-kernel" remote="b2g" revision="939b377d55a2f081d94029a30a75d05e5a20daf3"/>
-  <project name="gaia" path="gaia" remote="mozillaorg" revision="f39a7a827c0c0f48087ff3ead94f61ae22523919"/>
+  <project name="gaia" path="gaia" remote="mozillaorg" revision="cec4c1d3729137a24163756d15f98b0d37803966"/>
   <project name="gonk-misc" path="gonk-misc" remote="b2g" revision="956700d9754349b630a34551750ae6353614b6aa"/>
   <project name="moztt" path="external/moztt" remote="b2g" revision="aa5b7b7f6ed207ea1adc4df11d1d8bdaeabadd85"/>
   <project name="apitrace" path="external/apitrace" remote="apitrace" revision="638ec448619fda80fcb439b1747af62169d05548"/>
   <project name="valgrind" path="external/valgrind" remote="b2g" revision="5f931350fbc87c3df9db8b0ceb37734b8b471593"/>
   <project name="vex" path="external/VEX" remote="b2g" revision="48d8c7c950745f1b166b42125e6f0d3293d71636"/>
   <project name="platform_hardware_libhardware_moz" path="hardware/libhardware_moz" remote="b2g" revision="fdf3a143dc777e5f9d33a88373af7ea161d3b440"/>
   <!-- Stock Android things -->
   <project groups="linux" name="platform/prebuilts/clang/linux-x86/3.1" path="prebuilts/clang/linux-x86/3.1" revision="5c45f43419d5582949284eee9cef0c43d866e03b"/>
--- 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="f39a7a827c0c0f48087ff3ead94f61ae22523919"/>
+  <project name="gaia" path="gaia" remote="mozillaorg" revision="cec4c1d3729137a24163756d15f98b0d37803966"/>
   <project name="fake-libdvm" path="dalvik" remote="b2g" revision="d50ae982b19f42f0b66d08b9eb306be81687869f"/>
   <project name="fake-qemu-kernel" path="prebuilts/qemu-kernel" remote="b2g" revision="939b377d55a2f081d94029a30a75d05e5a20daf3"/>
   <project name="gonk-misc" path="gonk-misc" remote="b2g" revision="956700d9754349b630a34551750ae6353614b6aa"/>
   <project name="librecovery" path="librecovery" remote="b2g" revision="1b3591a50ed352fc6ddb77462b7b35d0bfa555a3"/>
   <project name="moztt" path="external/moztt" remote="b2g" revision="aa5b7b7f6ed207ea1adc4df11d1d8bdaeabadd85"/>
   <project name="rilproxy" path="rilproxy" remote="b2g" revision="5ef30994f4778b4052e58a4383dbe7890048c87e"/>
   <project name="valgrind" path="external/valgrind" remote="b2g" revision="5f931350fbc87c3df9db8b0ceb37734b8b471593"/>
   <project name="vex" path="external/VEX" remote="b2g" revision="48d8c7c950745f1b166b42125e6f0d3293d71636"/>
--- a/browser/components/loop/.eslintrc
+++ b/browser/components/loop/.eslintrc
@@ -12,16 +12,17 @@
     "browser": true,
     "mocha": true
   },
   "extends": "eslint:recommended",
   "globals": {
     "_": false,
     "Backbone": false,
     "chai": false,
+    "classNames": false,
     "console": false,
     "loop": true,
     "MozActivity": false,
     "RTCSessionDescription": false,
     "OT": false,
     "performance": false,
     "Promise": false,
     "React": false,
--- a/browser/components/loop/content/conversation.html
+++ b/browser/components/loop/content/conversation.html
@@ -18,16 +18,17 @@
     <div id="main"></div>
 
     <script type="text/javascript" src="loop/libs/l10n.js"></script>
     <script type="text/javascript" src="loop/js/otconfig.js"></script>
     <script type="text/javascript" src="loop/libs/sdk.js"></script>
     <script type="text/javascript" src="loop/shared/libs/react-0.12.2.js"></script>
     <script type="text/javascript" src="loop/shared/libs/lodash-3.9.3.js"></script>
     <script type="text/javascript" src="loop/shared/libs/backbone-1.2.1.js"></script>
+    <script type="text/javascript" src="loop/shared/libs/classnames-2.2.0.js"></script>
 
     <script type="text/javascript" src="loop/shared/js/utils.js"></script>
     <script type="text/javascript" src="loop/shared/js/mixins.js"></script>
     <script type="text/javascript" src="loop/shared/js/actions.js"></script>
     <script type="text/javascript" src="loop/shared/js/validate.js"></script>
     <script type="text/javascript" src="loop/shared/js/dispatcher.js"></script>
     <script type="text/javascript" src="loop/shared/js/otSdkDriver.js"></script>
     <script type="text/javascript" src="loop/shared/js/store.js"></script>
--- a/browser/components/loop/content/js/panel.js
+++ b/browser/components/loop/content/js/panel.js
@@ -160,17 +160,17 @@ loop.panel = (function(_, mozL10n) {
       onClick: React.PropTypes.func.isRequired
     },
 
     getDefaultProps: function() {
       return { displayed: true };
     },
 
     render: function() {
-      var cx = React.addons.classSet;
+      var cx = classNames;
 
       if (!this.props.displayed) {
         return null;
       }
 
       var extraCSSClass = {
         "dropdown-menu-item": true
       };
@@ -240,17 +240,17 @@ loop.panel = (function(_, mozL10n) {
     },
 
     openGettingStartedTour: function() {
       this.props.mozLoop.openGettingStartedTour("settings-menu");
       this.closeWindow();
     },
 
     render: function() {
-      var cx = React.addons.classSet;
+      var cx = classNames;
       var accountEntryCSSClass = this._isSignedIn() ? "entry-settings-signout" :
                                                       "entry-settings-signin";
       var notificationsLabel = this.props.mozLoop.doNotDisturb ? "settings_menu_item_turnnotificationson" :
                                                                  "settings_menu_item_turnnotificationsoff";
 
       return (
         React.createElement("div", {className: "settings-menu dropdown"}, 
           React.createElement("button", {className: "button-settings", 
@@ -431,17 +431,17 @@ loop.panel = (function(_, mozL10n) {
      */
     _handleMouseOut: function() {
       if (this.state.showMenu) {
         this.toggleDropdownMenu();
       }
     },
 
     render: function() {
-      var roomClasses = React.addons.classSet({
+      var roomClasses = classNames({
         "room-entry": true,
         "room-active": this._isActive(),
         "room-opened": this.props.isOpenedRoom
       });
 
       var roomTitle = this.props.room.decryptedContext.roomName ||
         this.props.room.decryptedContext.urls[0].description ||
         this.props.room.decryptedContext.urls[0].location;
@@ -583,17 +583,17 @@ loop.panel = (function(_, mozL10n) {
       } else {
         // Position below click area.
         menuNode.style.top = this.props.eventPosY - listNodeRect.top +
                              offset + "px";
       }
     },
 
     render: function() {
-      var dropdownClasses = React.addons.classSet({
+      var dropdownClasses = classNames({
         "dropdown-menu": true,
         "dropdown-menu-up": this.state.openDirUp
       });
 
       return (
         React.createElement("ul", {className: dropdownClasses}, 
           React.createElement("li", {
             className: "dropdown-menu-item", 
--- a/browser/components/loop/content/js/panel.jsx
+++ b/browser/components/loop/content/js/panel.jsx
@@ -160,17 +160,17 @@ loop.panel = (function(_, mozL10n) {
       onClick: React.PropTypes.func.isRequired
     },
 
     getDefaultProps: function() {
       return { displayed: true };
     },
 
     render: function() {
-      var cx = React.addons.classSet;
+      var cx = classNames;
 
       if (!this.props.displayed) {
         return null;
       }
 
       var extraCSSClass = {
         "dropdown-menu-item": true
       };
@@ -240,17 +240,17 @@ loop.panel = (function(_, mozL10n) {
     },
 
     openGettingStartedTour: function() {
       this.props.mozLoop.openGettingStartedTour("settings-menu");
       this.closeWindow();
     },
 
     render: function() {
-      var cx = React.addons.classSet;
+      var cx = classNames;
       var accountEntryCSSClass = this._isSignedIn() ? "entry-settings-signout" :
                                                       "entry-settings-signin";
       var notificationsLabel = this.props.mozLoop.doNotDisturb ? "settings_menu_item_turnnotificationson" :
                                                                  "settings_menu_item_turnnotificationsoff";
 
       return (
         <div className="settings-menu dropdown">
           <button className="button-settings"
@@ -431,17 +431,17 @@ loop.panel = (function(_, mozL10n) {
      */
     _handleMouseOut: function() {
       if (this.state.showMenu) {
         this.toggleDropdownMenu();
       }
     },
 
     render: function() {
-      var roomClasses = React.addons.classSet({
+      var roomClasses = classNames({
         "room-entry": true,
         "room-active": this._isActive(),
         "room-opened": this.props.isOpenedRoom
       });
 
       var roomTitle = this.props.room.decryptedContext.roomName ||
         this.props.room.decryptedContext.urls[0].description ||
         this.props.room.decryptedContext.urls[0].location;
@@ -583,17 +583,17 @@ loop.panel = (function(_, mozL10n) {
       } else {
         // Position below click area.
         menuNode.style.top = this.props.eventPosY - listNodeRect.top +
                              offset + "px";
       }
     },
 
     render: function() {
-      var dropdownClasses = React.addons.classSet({
+      var dropdownClasses = classNames({
         "dropdown-menu": true,
         "dropdown-menu-up": this.state.openDirUp
       });
 
       return (
         <ul className={dropdownClasses}>
           <li
             className="dropdown-menu-item"
--- a/browser/components/loop/content/js/roomViews.js
+++ b/browser/components/loop/content/js/roomViews.js
@@ -194,17 +194,17 @@ loop.roomViews = (function(mozL10n) {
     },
 
     render: function() {
       // Don't render a thing when no data has been fetched yet.
       if (!this.props.socialShareProviders) {
         return null;
       }
 
-      var cx = React.addons.classSet;
+      var cx = classNames;
       var shareDropdown = cx({
         "share-service-dropdown": true,
         "dropdown-menu": true,
         "visually-hidden": true,
         "hide": !this.props.show
       });
 
       return (
@@ -325,17 +325,17 @@ loop.roomViews = (function(mozL10n) {
       }
     },
 
     render: function() {
       if (!this.props.show) {
         return null;
       }
 
-      var cx = React.addons.classSet;
+      var cx = classNames;
       return (
         React.createElement("div", {className: "room-invitation-overlay"}, 
           React.createElement("div", {className: "room-invitation-content"}, 
             React.createElement("p", {className: cx({ hide: this.props.showEditContext })}, 
               mozL10n.get("invite_header_text2")
             )
           ), 
           React.createElement("div", {className: cx({
@@ -541,17 +541,17 @@ loop.roomViews = (function(mozL10n) {
         return null;
       }
 
       var url = this._getURL();
       var thumbnail = url && url.thumbnail || "loop/shared/img/icons-16x16.svg#globe";
       var urlDescription = url && url.description || "";
       var location = url && url.location || "";
 
-      var cx = React.addons.classSet;
+      var cx = classNames;
       var availableContext = this.state.availableContext;
       return (
         React.createElement("div", {className: "room-context"}, 
           React.createElement("p", {className: cx({ "error": !!this.props.error,
                             "error-display-area": true })}, 
             mozL10n.get("rooms_change_failed_label")
           ), 
           React.createElement("h2", {className: "room-context-header"}, mozL10n.get("context_inroom_header")), 
--- a/browser/components/loop/content/js/roomViews.jsx
+++ b/browser/components/loop/content/js/roomViews.jsx
@@ -194,17 +194,17 @@ loop.roomViews = (function(mozL10n) {
     },
 
     render: function() {
       // Don't render a thing when no data has been fetched yet.
       if (!this.props.socialShareProviders) {
         return null;
       }
 
-      var cx = React.addons.classSet;
+      var cx = classNames;
       var shareDropdown = cx({
         "share-service-dropdown": true,
         "dropdown-menu": true,
         "visually-hidden": true,
         "hide": !this.props.show
       });
 
       return (
@@ -325,17 +325,17 @@ loop.roomViews = (function(mozL10n) {
       }
     },
 
     render: function() {
       if (!this.props.show) {
         return null;
       }
 
-      var cx = React.addons.classSet;
+      var cx = classNames;
       return (
         <div className="room-invitation-overlay">
           <div className="room-invitation-content">
             <p className={cx({ hide: this.props.showEditContext })}>
               {mozL10n.get("invite_header_text2")}
             </p>
           </div>
           <div className={cx({
@@ -541,17 +541,17 @@ loop.roomViews = (function(mozL10n) {
         return null;
       }
 
       var url = this._getURL();
       var thumbnail = url && url.thumbnail || "loop/shared/img/icons-16x16.svg#globe";
       var urlDescription = url && url.description || "";
       var location = url && url.location || "";
 
-      var cx = React.addons.classSet;
+      var cx = classNames;
       var availableContext = this.state.availableContext;
       return (
         <div className="room-context">
           <p className={cx({ "error": !!this.props.error,
                             "error-display-area": true })}>
             {mozL10n.get("rooms_change_failed_label")}
           </p>
           <h2 className="room-context-header">{mozL10n.get("context_inroom_header")}</h2>
--- a/browser/components/loop/content/panel.html
+++ b/browser/components/loop/content/panel.html
@@ -12,16 +12,17 @@
   <body class="panel">
 
     <div id="main"></div>
 
     <script type="text/javascript" src="loop/shared/libs/react-0.12.2.js"></script>
     <script type="text/javascript" src="loop/libs/l10n.js"></script>
     <script type="text/javascript" src="loop/shared/libs/lodash-3.9.3.js"></script>
     <script type="text/javascript" src="loop/shared/libs/backbone-1.2.1.js"></script>
+    <script type="text/javascript" src="loop/shared/libs/classnames-2.2.0.js"></script>
 
     <script type="text/javascript" src="loop/shared/js/utils.js"></script>
     <script type="text/javascript" src="loop/shared/js/models.js"></script>
     <script type="text/javascript" src="loop/shared/js/mixins.js"></script>
     <script type="text/javascript" src="loop/shared/js/views.js"></script>
     <script type="text/javascript" src="loop/shared/js/validate.js"></script>
     <script type="text/javascript" src="loop/shared/js/actions.js"></script>
     <script type="text/javascript" src="loop/shared/js/dispatcher.js"></script>
--- a/browser/components/loop/content/shared/js/textChatView.js
+++ b/browser/components/loop/content/shared/js/textChatView.js
@@ -43,17 +43,17 @@ loop.shared.views.chat = (function(mozL1
           date.toLocaleTimeString(language,
                                    { hour: "numeric", minute: "numeric",
                                    hour12: false })
         )
       );
     },
 
     render: function() {
-      var classes = React.addons.classSet({
+      var classes = classNames({
         "text-chat-entry": true,
         "received": this.props.type === CHAT_MESSAGE_TYPES.RECEIVED,
         "sent": this.props.type === CHAT_MESSAGE_TYPES.SENT,
         "special": this.props.type === CHAT_MESSAGE_TYPES.SPECIAL,
         "room-name": this.props.contentType === CHAT_CONTENT_TYPES.ROOM_NAME
       });
 
       var optionalProps = {};
@@ -160,17 +160,17 @@ loop.shared.views.chat = (function(mozL1
         }.bind(this));
       }
     },
 
     render: function() {
       /* Keep track of the last printed timestamp. */
       var lastTimestamp = 0;
 
-      var entriesClasses = React.addons.classSet({
+      var entriesClasses = classNames({
         "text-chat-entries": true
       });
 
       return (
         React.createElement("div", {className: entriesClasses}, 
           React.createElement("div", {className: "text-chat-scroller"}, 
             
               this.props.messageList.map(function(entry, i) {
@@ -390,17 +390,17 @@ loop.shared.views.chat = (function(mozL1
         });
       }
 
       // Only show the placeholder if we've sent messages.
       var hasSentMessages = messageList.some(function(item) {
         return item.type === CHAT_MESSAGE_TYPES.SENT;
       });
 
-      var textChatViewClasses = React.addons.classSet({
+      var textChatViewClasses = classNames({
         "text-chat-view": true,
         "text-chat-entries-empty": !messageList.length,
         "text-chat-disabled": !this.state.textChatEnabled
       });
 
       return (
         React.createElement("div", {className: textChatViewClasses}, 
           React.createElement(TextChatEntriesView, {
--- a/browser/components/loop/content/shared/js/textChatView.jsx
+++ b/browser/components/loop/content/shared/js/textChatView.jsx
@@ -43,17 +43,17 @@ loop.shared.views.chat = (function(mozL1
           {date.toLocaleTimeString(language,
                                    { hour: "numeric", minute: "numeric",
                                    hour12: false })}
         </span>
       );
     },
 
     render: function() {
-      var classes = React.addons.classSet({
+      var classes = classNames({
         "text-chat-entry": true,
         "received": this.props.type === CHAT_MESSAGE_TYPES.RECEIVED,
         "sent": this.props.type === CHAT_MESSAGE_TYPES.SENT,
         "special": this.props.type === CHAT_MESSAGE_TYPES.SPECIAL,
         "room-name": this.props.contentType === CHAT_CONTENT_TYPES.ROOM_NAME
       });
 
       var optionalProps = {};
@@ -160,17 +160,17 @@ loop.shared.views.chat = (function(mozL1
         }.bind(this));
       }
     },
 
     render: function() {
       /* Keep track of the last printed timestamp. */
       var lastTimestamp = 0;
 
-      var entriesClasses = React.addons.classSet({
+      var entriesClasses = classNames({
         "text-chat-entries": true
       });
 
       return (
         <div className={entriesClasses}>
           <div className="text-chat-scroller">
             {
               this.props.messageList.map(function(entry, i) {
@@ -390,17 +390,17 @@ loop.shared.views.chat = (function(mozL1
         });
       }
 
       // Only show the placeholder if we've sent messages.
       var hasSentMessages = messageList.some(function(item) {
         return item.type === CHAT_MESSAGE_TYPES.SENT;
       });
 
-      var textChatViewClasses = React.addons.classSet({
+      var textChatViewClasses = classNames({
         "text-chat-view": true,
         "text-chat-entries-empty": !messageList.length,
         "text-chat-disabled": !this.state.textChatEnabled
       });
 
       return (
         <div className={textChatViewClasses}>
           <TextChatEntriesView
--- a/browser/components/loop/content/shared/js/views.js
+++ b/browser/components/loop/content/shared/js/views.js
@@ -65,17 +65,17 @@ loop.shared.views = (function(_, mozL10n
       return { enabled: true, visible: true };
     },
 
     handleClick: function() {
       this.props.action();
     },
 
     _getClasses: function() {
-      var cx = React.addons.classSet;
+      var cx = classNames;
       // classes
       var classesObj = {
         "btn": true,
         "media-control": true,
         "transparent-button": true,
         "local-media": this.props.scope === "local",
         "muted": !this.props.enabled,
         "hide": !this.props.visible
@@ -165,17 +165,17 @@ loop.shared.views = (function(_, mozL10n
       return mozL10n.get(prefix + "_screenshare_button_title");
     },
 
     render: function() {
       if (!this.props.visible) {
         return null;
       }
 
-      var cx = React.addons.classSet;
+      var cx = classNames;
 
       var isActive = this.props.state === SCREEN_SHARE_STATES.ACTIVE;
       var screenShareClasses = cx({
         "btn": true,
         "btn-screen-share": true,
         "transparent-button": true,
         "menu-showing": this.state.showMenu,
         "active": isActive,
@@ -299,17 +299,17 @@ loop.shared.views = (function(_, mozL10n
       var helloSupportUrl = this.props.mozLoop.getLoopPref("support_url");
       this.props.mozLoop.openURL(helloSupportUrl);
     },
 
     /**
      * Recover the needed info for generating an specific menu Item
      */
     getItemInfo: function(menuItem) {
-      var cx = React.addons.classSet;
+      var cx = classNames;
       switch (menuItem.id) {
         case "help":
           return {
             cssClasses: "dropdown-menu-item",
             handler: this.handleHelpEntry,
             label: mozL10n.get("help_label")
           };
         case "edit":
@@ -357,17 +357,17 @@ loop.shared.views = (function(_, mozL10n
       }
       var menuItemRows = this.props.menuItems.map(this.generateMenuItem)
         .filter(function(item) { return item; });
 
       if (!menuItemRows || !menuItemRows.length) {
         return null;
       }
 
-      var cx = React.addons.classSet;
+      var cx = classNames;
       var settingsDropdownMenuClasses = cx({
         "settings-menu": true,
         "dropdown-menu": true,
         "menu-below": this.props.menuBelow,
         "hide": !this.state.showMenu
       });
       return (
         React.createElement("div", {className: "settings-control"}, 
@@ -489,17 +489,17 @@ loop.shared.views = (function(_, mozL10n
       }.bind(this), 6000);
     },
 
     render: function() {
       if (!this.props.show) {
         return null;
       }
 
-      var cx = React.addons.classSet;
+      var cx = classNames;
       var conversationToolbarCssClasses = cx({
         "conversation-toolbar": true,
         "idle": this.state.idle
       });
       var showButtons = this.props.video.visible || this.props.audio.visible;
       var mediaButtonGroupCssClasses = cx({
         "conversation-toolbar-media-btn-group-box": true,
         "hide": !showButtons
@@ -637,17 +637,17 @@ loop.shared.views = (function(_, mozL10n
       return {
         disabled: false,
         additionalClass: "",
         htmlId: ""
       };
     },
 
     render: function() {
-      var cx = React.addons.classSet;
+      var cx = classNames;
       var classObject = { button: true, disabled: this.props.disabled };
       if (this.props.additionalClass) {
         classObject[this.props.additionalClass] = true;
       }
       return (
         React.createElement("button", {className: cx(classObject), 
                 disabled: this.props.disabled, 
                 id: this.props.htmlId, 
@@ -670,17 +670,17 @@ loop.shared.views = (function(_, mozL10n
 
     getDefaultProps: function() {
       return {
         additionalClass: ""
       };
     },
 
     render: function() {
-      var cx = React.addons.classSet;
+      var cx = classNames;
       var classObject = { "button-group": true };
       if (this.props.additionalClass) {
         classObject[this.props.additionalClass] = true;
       }
       return (
         React.createElement("div", {className: cx(classObject)}, 
           this.props.children
         )
@@ -737,17 +737,17 @@ loop.shared.views = (function(_, mozL10n
         checked: !this.state.checked,
         value: this.state.checked ? "" : this.props.value
       };
       this.setState(newState);
       this.props.onChange(newState);
     },
 
     render: function() {
-      var cx = React.addons.classSet;
+      var cx = classNames;
       var wrapperClasses = {
         "checkbox-wrapper": true,
         disabled: this.props.disabled
       };
       var checkClasses = {
         checkbox: true,
         checked: this.state.checked,
         disabled: this.props.disabled
@@ -855,17 +855,17 @@ loop.shared.views = (function(_, mozL10n
       var thumbnail = this.props.thumbnail;
 
       if (!thumbnail) {
         thumbnail = this.props.useDesktopPaths ?
           "loop/shared/img/icons-16x16.svg#globe" :
           "shared/img/icons-16x16.svg#globe";
       }
 
-      var wrapperClasses = React.addons.classSet({
+      var wrapperClasses = classNames({
         "context-wrapper": true,
         "clicks-allowed": this.props.allowClick
       });
 
       return (
         React.createElement("div", {className: "context-content"}, 
           React.createElement("a", {className: wrapperClasses, 
              href: this.props.allowClick ? this.props.url : null, 
@@ -1071,27 +1071,27 @@ loop.shared.views = (function(_, mozL10n
             mediaType: "local", 
             posterUrl: this.props.localPosterUrl, 
             srcMediaElement: this.props.localSrcMediaElement})
         )
       );
     },
 
     render: function() {
-      var remoteStreamClasses = React.addons.classSet({
+      var remoteStreamClasses = classNames({
         "remote": true,
         "focus-stream": !this.props.displayScreenShare
       });
 
-      var screenShareStreamClasses = React.addons.classSet({
+      var screenShareStreamClasses = classNames({
         "screen": true,
         "focus-stream": this.props.displayScreenShare
       });
 
-      var mediaWrapperClasses = React.addons.classSet({
+      var mediaWrapperClasses = classNames({
         "media-wrapper": true,
         "receiving-screen-share": this.props.displayScreenShare,
         "showing-local-streams": this.props.localSrcMediaElement ||
           this.props.localPosterUrl,
         "showing-remote-streams": this.props.remoteSrcMediaElement ||
           this.props.remotePosterUrl || this.props.isRemoteLoading
       });
 
--- a/browser/components/loop/content/shared/js/views.jsx
+++ b/browser/components/loop/content/shared/js/views.jsx
@@ -65,17 +65,17 @@ loop.shared.views = (function(_, mozL10n
       return { enabled: true, visible: true };
     },
 
     handleClick: function() {
       this.props.action();
     },
 
     _getClasses: function() {
-      var cx = React.addons.classSet;
+      var cx = classNames;
       // classes
       var classesObj = {
         "btn": true,
         "media-control": true,
         "transparent-button": true,
         "local-media": this.props.scope === "local",
         "muted": !this.props.enabled,
         "hide": !this.props.visible
@@ -165,17 +165,17 @@ loop.shared.views = (function(_, mozL10n
       return mozL10n.get(prefix + "_screenshare_button_title");
     },
 
     render: function() {
       if (!this.props.visible) {
         return null;
       }
 
-      var cx = React.addons.classSet;
+      var cx = classNames;
 
       var isActive = this.props.state === SCREEN_SHARE_STATES.ACTIVE;
       var screenShareClasses = cx({
         "btn": true,
         "btn-screen-share": true,
         "transparent-button": true,
         "menu-showing": this.state.showMenu,
         "active": isActive,
@@ -299,17 +299,17 @@ loop.shared.views = (function(_, mozL10n
       var helloSupportUrl = this.props.mozLoop.getLoopPref("support_url");
       this.props.mozLoop.openURL(helloSupportUrl);
     },
 
     /**
      * Recover the needed info for generating an specific menu Item
      */
     getItemInfo: function(menuItem) {
-      var cx = React.addons.classSet;
+      var cx = classNames;
       switch (menuItem.id) {
         case "help":
           return {
             cssClasses: "dropdown-menu-item",
             handler: this.handleHelpEntry,
             label: mozL10n.get("help_label")
           };
         case "edit":
@@ -357,17 +357,17 @@ loop.shared.views = (function(_, mozL10n
       }
       var menuItemRows = this.props.menuItems.map(this.generateMenuItem)
         .filter(function(item) { return item; });
 
       if (!menuItemRows || !menuItemRows.length) {
         return null;
       }
 
-      var cx = React.addons.classSet;
+      var cx = classNames;
       var settingsDropdownMenuClasses = cx({
         "settings-menu": true,
         "dropdown-menu": true,
         "menu-below": this.props.menuBelow,
         "hide": !this.state.showMenu
       });
       return (
         <div className="settings-control">
@@ -489,17 +489,17 @@ loop.shared.views = (function(_, mozL10n
       }.bind(this), 6000);
     },
 
     render: function() {
       if (!this.props.show) {
         return null;
       }
 
-      var cx = React.addons.classSet;
+      var cx = classNames;
       var conversationToolbarCssClasses = cx({
         "conversation-toolbar": true,
         "idle": this.state.idle
       });
       var showButtons = this.props.video.visible || this.props.audio.visible;
       var mediaButtonGroupCssClasses = cx({
         "conversation-toolbar-media-btn-group-box": true,
         "hide": !showButtons
@@ -637,17 +637,17 @@ loop.shared.views = (function(_, mozL10n
       return {
         disabled: false,
         additionalClass: "",
         htmlId: ""
       };
     },
 
     render: function() {
-      var cx = React.addons.classSet;
+      var cx = classNames;
       var classObject = { button: true, disabled: this.props.disabled };
       if (this.props.additionalClass) {
         classObject[this.props.additionalClass] = true;
       }
       return (
         <button className={cx(classObject)}
                 disabled={this.props.disabled}
                 id={this.props.htmlId}
@@ -670,17 +670,17 @@ loop.shared.views = (function(_, mozL10n
 
     getDefaultProps: function() {
       return {
         additionalClass: ""
       };
     },
 
     render: function() {
-      var cx = React.addons.classSet;
+      var cx = classNames;
       var classObject = { "button-group": true };
       if (this.props.additionalClass) {
         classObject[this.props.additionalClass] = true;
       }
       return (
         <div className={cx(classObject)}>
           {this.props.children}
         </div>
@@ -737,17 +737,17 @@ loop.shared.views = (function(_, mozL10n
         checked: !this.state.checked,
         value: this.state.checked ? "" : this.props.value
       };
       this.setState(newState);
       this.props.onChange(newState);
     },
 
     render: function() {
-      var cx = React.addons.classSet;
+      var cx = classNames;
       var wrapperClasses = {
         "checkbox-wrapper": true,
         disabled: this.props.disabled
       };
       var checkClasses = {
         checkbox: true,
         checked: this.state.checked,
         disabled: this.props.disabled
@@ -855,17 +855,17 @@ loop.shared.views = (function(_, mozL10n
       var thumbnail = this.props.thumbnail;
 
       if (!thumbnail) {
         thumbnail = this.props.useDesktopPaths ?
           "loop/shared/img/icons-16x16.svg#globe" :
           "shared/img/icons-16x16.svg#globe";
       }
 
-      var wrapperClasses = React.addons.classSet({
+      var wrapperClasses = classNames({
         "context-wrapper": true,
         "clicks-allowed": this.props.allowClick
       });
 
       return (
         <div className="context-content">
           <a className={wrapperClasses}
              href={this.props.allowClick ? this.props.url : null}
@@ -1071,27 +1071,27 @@ loop.shared.views = (function(_, mozL10n
             mediaType="local"
             posterUrl={this.props.localPosterUrl}
             srcMediaElement={this.props.localSrcMediaElement} />
         </div>
       );
     },
 
     render: function() {
-      var remoteStreamClasses = React.addons.classSet({
+      var remoteStreamClasses = classNames({
         "remote": true,
         "focus-stream": !this.props.displayScreenShare
       });
 
-      var screenShareStreamClasses = React.addons.classSet({
+      var screenShareStreamClasses = classNames({
         "screen": true,
         "focus-stream": this.props.displayScreenShare
       });
 
-      var mediaWrapperClasses = React.addons.classSet({
+      var mediaWrapperClasses = classNames({
         "media-wrapper": true,
         "receiving-screen-share": this.props.displayScreenShare,
         "showing-local-streams": this.props.localSrcMediaElement ||
           this.props.localPosterUrl,
         "showing-remote-streams": this.props.remoteSrcMediaElement ||
           this.props.remotePosterUrl || this.props.isRemoteLoading
       });
 
new file mode 100644
--- /dev/null
+++ b/browser/components/loop/content/shared/libs/classnames-2.2.0.js
@@ -0,0 +1,48 @@
+/*!
+  Copyright (c) 2015 Jed Watson.
+  Licensed under the MIT License (MIT), see
+  http://jedwatson.github.io/classnames
+*/
+/* global define */
+
+(function () {
+	'use strict';
+
+	var hasOwn = {}.hasOwnProperty;
+
+	function classNames () {
+		var classes = '';
+
+		for (var i = 0; i < arguments.length; i++) {
+			var arg = arguments[i];
+			if (!arg) continue;
+
+			var argType = typeof arg;
+
+			if (argType === 'string' || argType === 'number') {
+				classes += ' ' + arg;
+			} else if (Array.isArray(arg)) {
+				classes += ' ' + classNames.apply(null, arg);
+			} else if (argType === 'object') {
+				for (var key in arg) {
+					if (hasOwn.call(arg, key) && arg[key]) {
+						classes += ' ' + key;
+					}
+				}
+			}
+		}
+
+		return classes.substr(1);
+	}
+
+	if (typeof module !== 'undefined' && module.exports) {
+		module.exports = classNames;
+	} else if (typeof define === 'function' && typeof define.amd === 'object' && define.amd) {
+		// register as 'classnames', consistent with npm package name
+		define('classnames', function () {
+			return classNames;
+		});
+	} else {
+		window.classNames = classNames;
+	}
+}());
--- a/browser/components/loop/jar.mn
+++ b/browser/components/loop/jar.mn
@@ -109,16 +109,17 @@ browser.jar:
   # Shared libs
 #ifdef DEBUG
   content/browser/loop/shared/libs/react-0.12.2.js    (content/shared/libs/react-0.12.2.js)
 #else
   content/browser/loop/shared/libs/react-0.12.2.js    (content/shared/libs/react-0.12.2-prod.js)
 #endif
   content/browser/loop/shared/libs/lodash-3.9.3.js    (content/shared/libs/lodash-3.9.3.js)
   content/browser/loop/shared/libs/backbone-1.2.1.js  (content/shared/libs/backbone-1.2.1.js)
+  content/browser/loop/shared/libs/classnames-2.2.0.js      (content/shared/libs/classnames-2.2.0.js)
 
   # Shared sounds
   content/browser/loop/shared/sounds/ringtone.ogg       (content/shared/sounds/ringtone.ogg)
   content/browser/loop/shared/sounds/connecting.ogg     (content/shared/sounds/connecting.ogg)
   content/browser/loop/shared/sounds/connected.ogg      (content/shared/sounds/connected.ogg)
   content/browser/loop/shared/sounds/terminated.ogg     (content/shared/sounds/terminated.ogg)
   content/browser/loop/shared/sounds/room-joined.ogg    (content/shared/sounds/room-joined.ogg)
   content/browser/loop/shared/sounds/room-joined-in.ogg (content/shared/sounds/room-joined-in.ogg)
--- a/browser/components/loop/standalone/content/js/standaloneRoomViews.js
+++ b/browser/components/loop/standalone/content/js/standaloneRoomViews.js
@@ -75,17 +75,17 @@ loop.standaloneRoomViews = (function(moz
       this.props.dispatcher.dispatch(new sharedActions.JoinRoom());
     },
 
     _renderJoinButton: function() {
       var buttonMessage = this.state.roomState === ROOM_STATES.JOINED ?
         mozL10n.get("rooms_room_joined_own_conversation_label") :
         mozL10n.get("rooms_room_join_label");
 
-      var buttonClasses = React.addons.classSet({
+      var buttonClasses = classNames({
         btn: true,
         "btn-info": true,
         disabled: this.state.roomState === ROOM_STATES.JOINED
       });
 
       return (
         React.createElement("button", {
           className: buttonClasses, 
@@ -320,17 +320,17 @@ loop.standaloneRoomViews = (function(moz
         }
         case ROOM_STATES.MEDIA_WAIT: {
           var msg = mozL10n.get("call_progress_getting_media_description",
                                 { clientShortname: mozL10n.get("clientShortname2") });
           var utils = loop.shared.utils;
           var isChrome = utils.isChrome(navigator.userAgent);
           var isFirefox = utils.isFirefox(navigator.userAgent);
           var isOpera = utils.isOpera(navigator.userAgent);
-          var promptMediaMessageClasses = React.addons.classSet({
+          var promptMediaMessageClasses = classNames({
             "prompt-media-message": true,
             "chrome": isChrome,
             "firefox": isFirefox,
             "opera": isOpera,
             "other": !isChrome && !isFirefox && !isOpera
           });
           return (
             React.createElement("div", {className: "room-inner-info-area"}, 
--- a/browser/components/loop/standalone/content/js/standaloneRoomViews.jsx
+++ b/browser/components/loop/standalone/content/js/standaloneRoomViews.jsx
@@ -75,17 +75,17 @@ loop.standaloneRoomViews = (function(moz
       this.props.dispatcher.dispatch(new sharedActions.JoinRoom());
     },
 
     _renderJoinButton: function() {
       var buttonMessage = this.state.roomState === ROOM_STATES.JOINED ?
         mozL10n.get("rooms_room_joined_own_conversation_label") :
         mozL10n.get("rooms_room_join_label");
 
-      var buttonClasses = React.addons.classSet({
+      var buttonClasses = classNames({
         btn: true,
         "btn-info": true,
         disabled: this.state.roomState === ROOM_STATES.JOINED
       });
 
       return (
         <button
           className={buttonClasses}
@@ -320,17 +320,17 @@ loop.standaloneRoomViews = (function(moz
         }
         case ROOM_STATES.MEDIA_WAIT: {
           var msg = mozL10n.get("call_progress_getting_media_description",
                                 { clientShortname: mozL10n.get("clientShortname2") });
           var utils = loop.shared.utils;
           var isChrome = utils.isChrome(navigator.userAgent);
           var isFirefox = utils.isFirefox(navigator.userAgent);
           var isOpera = utils.isOpera(navigator.userAgent);
-          var promptMediaMessageClasses = React.addons.classSet({
+          var promptMediaMessageClasses = classNames({
             "prompt-media-message": true,
             "chrome": isChrome,
             "firefox": isFirefox,
             "opera": isOpera,
             "other": !isChrome && !isFirefox && !isOpera
           });
           return (
             <div className="room-inner-info-area">
--- a/browser/components/loop/standalone/content/webappEntryPoint.js
+++ b/browser/components/loop/standalone/content/webappEntryPoint.js
@@ -33,19 +33,21 @@ require("exports?_!shared/libs/lodash-3.
 require("expose?Backbone!imports?define=>false!shared/libs/backbone-1.2.1.js");
 
 /* global: __PROD__ */
 if (typeof __PROD__ !== "undefined") {
   // webpack warns if we try to minify some prebuilt libraries, so we
   // pull in the unbuilt version from node_modules
   require("expose?React!react");
   require("expose?React!react/addons");
+  require("expose?classNames!classnames");
 } else {
   // our development server setup doesn't yet handle real modules, so for now...
   require("shared/libs/react-0.12.2.js");
+  require("shared/libs/classnames-2.2.0.js");
 }
 
 
 // Someday, these will be real modules.  For now, we're chaining loaders
 // to teach webpack how to treat them like modules anyway.
 //
 // We do it in this file rather than globally in webpack.config.js
 // because:
--- a/browser/components/loop/standalone/package.json
+++ b/browser/components/loop/standalone/package.json
@@ -7,16 +7,17 @@
     "url": "git@github.com:mozilla/loop-client.git"
   },
   "engines": {
     "node": "0.10.x",
     "npm": "2.14.x"
   },
   "dependencies": {},
   "devDependencies": {
+    "classnames": "2.2.x",
     "compression": "1.5.x",
     "eslint": "1.6.x",
     "eslint-plugin-mozilla": "../../../../testing/eslint-plugin-mozilla",
     "eslint-plugin-react": "3.5.x",
     "exports-loader": "0.6.x",
     "expose-loader": "0.7.x",
     "express": "4.x",
     "imports-loader": "0.6.x",
--- a/browser/components/loop/test/desktop-local/index.html
+++ b/browser/components/loop/test/desktop-local/index.html
@@ -26,16 +26,17 @@
       caughtWarnings.push(args);
       consoleWarn.apply(console, args);
     };
   </script>
 
   <!-- libs -->
   <script src="../../content/libs/l10n.js"></script>
   <script src="../../content/shared/libs/react-0.12.2.js"></script>
+  <script src="../../content/shared/libs/classnames-2.2.0.js"></script>
   <script src="../../content/shared/libs/lodash-3.9.3.js"></script>
   <script src="../../content/shared/libs/backbone-1.2.1.js"></script>
 
   <!-- test dependencies -->
   <script src="../shared/vendor/mocha-2.2.5.js"></script>
   <script src="../shared/vendor/chai-3.0.0.js"></script>
   <script src="../shared/vendor/sinon-1.16.1.js"></script>
   <script>
--- a/browser/components/loop/test/karma/karma.coverage.desktop.js
+++ b/browser/components/loop/test/karma/karma.coverage.desktop.js
@@ -7,16 +7,17 @@ module.exports = function(config) {
   "use strict";
 
   var baseConfig = require("./karma.conf.base.js")(config);
 
   // List of files / patterns to load in the browser.
   baseConfig.files = baseConfig.files.concat([
     "content/libs/l10n.js",
     "content/shared/libs/react-0.12.2.js",
+    "content/shared/libs/classnames-2.2.0.js",
     "content/shared/libs/lodash-3.9.3.js",
     "content/shared/libs/backbone-1.2.1.js",
     "test/shared/vendor/*.js",
     "test/karma/head.js", // Stub out DOM event listener due to races.
     "content/shared/js/utils.js",
     "content/shared/js/models.js",
     "content/shared/js/mixins.js",
     "content/shared/js/actions.js",
--- a/browser/components/loop/test/karma/karma.coverage.shared_standalone.js
+++ b/browser/components/loop/test/karma/karma.coverage.shared_standalone.js
@@ -9,16 +9,17 @@ module.exports = function(config) {
   var baseConfig = require("./karma.conf.base.js")(config);
 
   // List of files / patterns to load in the browser.
   baseConfig.files = baseConfig.files.concat([
     "standalone/content/libs/l10n-gaia-02ca67948fe8.js",
     "content/shared/libs/lodash-3.9.3.js",
     "content/shared/libs/backbone-1.2.1.js",
     "content/shared/libs/react-0.12.2.js",
+    "content/shared/libs/classnames-2.2.0.js",
     "content/shared/libs/sdk.js",
     "test/shared/vendor/*.js",
     "test/karma/head.js", // Add test fixture container
     "content/shared/js/utils.js",
     "content/shared/js/store.js",
     "content/shared/js/models.js",
     "content/shared/js/mixins.js",
     "content/shared/js/crypto.js",
--- a/browser/components/loop/test/shared/index.html
+++ b/browser/components/loop/test/shared/index.html
@@ -25,16 +25,17 @@
       var args = Array.slice(arguments);
       caughtWarnings.push(args);
       consoleWarn.apply(console, args);
     };
   </script>
 
   <!-- libs -->
   <script src="../../content/shared/libs/react-0.12.2.js"></script>
+  <script src="../../content/shared/libs/classnames-2.2.0.js"></script>
   <script src="../../content/shared/libs/lodash-3.9.3.js"></script>
   <script src="../../content/shared/libs/backbone-1.2.1.js"></script>
   <script src="../../standalone/content/libs/l10n-gaia-02ca67948fe8.js"></script>
 
   <!-- test dependencies -->
   <script src="vendor/mocha-2.2.5.js"></script>
   <script src="vendor/chai-3.0.0.js"></script>
   <script src="vendor/chai-as-promised-5.1.0.js"></script>
--- a/browser/components/loop/test/standalone/index.html
+++ b/browser/components/loop/test/standalone/index.html
@@ -26,16 +26,17 @@
       var args = Array.slice(arguments);
       caughtWarnings.push(args);
       consoleWarn.apply(console, args);
     };
   </script>
 
   <!-- libs -->
   <script src="../../content/shared/libs/react-0.12.2.js"></script>
+  <script src="../../content/shared/libs/classnames-2.2.0.js"></script>
   <script src="../../content/shared/libs/lodash-3.9.3.js"></script>
   <script src="../../content/shared/libs/backbone-1.2.1.js"></script>
   <script src="../../standalone/content/libs/l10n-gaia-02ca67948fe8.js"></script>
   <!-- test dependencies -->
   <script src="../shared/vendor/mocha-2.2.5.js"></script>
   <script src="../shared/vendor/chai-3.0.0.js"></script>
   <script src="../shared/vendor/sinon-1.16.1.js"></script>
   <script src="../shared/sdk_mock.js"></script>
--- a/browser/components/loop/ui/index.html
+++ b/browser/components/loop/ui/index.html
@@ -23,16 +23,17 @@
       });
     </script>
 
     <div id="main"></div>
     <div id="results"></div>
     <script src="fake-mozLoop.js"></script>
     <script src="fake-l10n.js"></script>
     <script src="../content/shared/libs/react-0.12.2.js"></script>
+    <script src="../content/shared/libs/classnames-2.2.0.js"></script>
     <script src="../content/shared/libs/lodash-3.9.3.js"></script>
     <script src="../content/shared/libs/backbone-1.2.1.js"></script>
     <script src="../content/shared/js/actions.js"></script>
     <script src="../content/shared/js/utils.js"></script>
     <script src="../content/shared/js/models.js"></script>
     <script src="../content/shared/js/mixins.js"></script>
     <script src="../content/shared/js/validate.js"></script>
     <script src="../content/shared/js/dispatcher.js"></script>
--- a/browser/components/loop/ui/ui-showcase.js
+++ b/browser/components/loop/ui/ui-showcase.js
@@ -551,17 +551,17 @@
       var width = this.props.width;
 
       // make room for a 1-pixel border on each edge
       if (this.props.dashed) {
         height += 2;
         width += 2;
       }
 
-      var cx = React.addons.classSet;
+      var cx = classNames;
       return (
         React.createElement("div", {className: "example"}, 
           React.createElement("h3", {id: this.makeId()}, 
             this.props.summary, 
             React.createElement("a", {href: this.makeId("#")}, " ¶")
           ), 
           React.createElement("div", {className: "comp"}, 
             React.createElement(Frame, {className: cx({ dashed: this.props.dashed }), 
--- a/browser/components/loop/ui/ui-showcase.jsx
+++ b/browser/components/loop/ui/ui-showcase.jsx
@@ -551,17 +551,17 @@
       var width = this.props.width;
 
       // make room for a 1-pixel border on each edge
       if (this.props.dashed) {
         height += 2;
         width += 2;
       }
 
-      var cx = React.addons.classSet;
+      var cx = classNames;
       return (
         <div className="example">
           <h3 id={this.makeId()}>
             {this.props.summary}
             <a href={this.makeId("#")}>&nbsp;¶</a>
           </h3>
           <div className="comp">
             <Frame className={cx({ dashed: this.props.dashed })}
--- a/build.gradle
+++ b/build.gradle
@@ -110,17 +110,16 @@ idea {
             .findAll({it.startsWith('obj') && !it.startsWith('.') && !it.equals('mobile/')}))
 
         // If topobjdir is below topsrcdir, hide only some portions of that tree.
         def topobjdirURI = file(topobjdir).toURI()
         if (!topsrcdirURI.relativize(topobjdirURI).isAbsolute()) {
             excludeDirs -= file(topobjdir)
             excludeDirs += files(file(topobjdir).listFiles())
             excludeDirs -= file("${topobjdir}/gradle")
-            excludeDirs -= file("${topobjdir}/mobile")
         }
 
         if (!mozconfig.substs.MOZ_INSTALL_TRACKING) {
             excludeDirs += file("${topsrcdir}/mobile/android/thirdparty/com/adjust")
         }
     }
 }
 
new file mode 100644
--- /dev/null
+++ b/mobile/android/base/AndroidManifest.xml
@@ -0,0 +1,6 @@
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+    package="org.mozilla.gecko">
+<!-- THIS IS NOT THE REAL MANIFEST!  This is for Gradle only.  See
+     AndroidManifest.xml.in. -->
+
+</manifest>
--- a/mobile/android/base/DataReportingNotification.java
+++ b/mobile/android/base/DataReportingNotification.java
@@ -73,17 +73,17 @@ public class DataReportingNotification {
      */
     private static void notifyDataPolicy(Context context, SharedPreferences sharedPrefs) {
         boolean result = false;
         try {
             // Launch main App to launch Data choices when notification is clicked.
             Intent prefIntent = new Intent(GeckoApp.ACTION_LAUNCH_SETTINGS);
             prefIntent.setClassName(AppConstants.ANDROID_PACKAGE_NAME, AppConstants.MOZ_ANDROID_BROWSER_INTENT_CLASS);
 
-            GeckoPreferences.setResourceToOpen(prefIntent, "preferences_vendor");
+            GeckoPreferences.setResourceToOpen(prefIntent, "preferences_privacy");
             prefIntent.putExtra(ALERT_NAME_DATAREPORTING_NOTIFICATION, true);
 
             PendingIntent contentIntent = PendingIntent.getActivity(context, 0, prefIntent, PendingIntent.FLAG_UPDATE_CURRENT);
             final Resources resources = context.getResources();
 
             // Create and send notification.
             String notificationTitle = resources.getString(R.string.datareporting_notification_title);
             String notificationSummary;
--- a/mobile/android/base/android-services.mozbuild
+++ b/mobile/android/base/android-services.mozbuild
@@ -845,16 +845,17 @@ sync_java_files = [
     'browserid/verifier/AbstractBrowserIDRemoteVerifierClient.java',
     'browserid/verifier/BrowserIDRemoteVerifierClient10.java',
     'browserid/verifier/BrowserIDRemoteVerifierClient20.java',
     'browserid/verifier/BrowserIDVerifierClient.java',
     'browserid/verifier/BrowserIDVerifierDelegate.java',
     'browserid/verifier/BrowserIDVerifierException.java',
     'browserid/VerifyingPublicKey.java',
     'fxa/AccountLoader.java',
+    'fxa/activities/CustomColorPreference.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',
new file mode 100644
--- /dev/null
+++ b/mobile/android/base/build.gradle
@@ -0,0 +1,131 @@
+buildDir "${topobjdir}/gradle/build/mobile/android/base"
+
+apply plugin: 'com.android.library'
+
+android {
+    compileSdkVersion 23
+    buildToolsVersion "23.0.1"
+
+    defaultConfig {
+        targetSdkVersion 22
+        minSdkVersion 9
+    }
+
+    compileOptions {
+        sourceCompatibility JavaVersion.VERSION_1_7
+        targetCompatibility JavaVersion.VERSION_1_7
+    }
+
+    lintOptions {
+        abortOnError false
+    }
+
+    sourceSets {
+        main {
+            manifest.srcFile 'AndroidManifest.xml'
+            java {
+                srcDir "${topobjdir}/gradle/base/src"
+                exclude 'org/mozilla/gecko/resources/**'
+
+                srcDir "${topsrcdir}/mobile/android/search/java"
+                srcDir "${topsrcdir}/mobile/android/javaaddons/java"
+
+                if (mozconfig.substs.MOZ_ANDROID_MLS_STUMBLER) {
+                    srcDir "${topsrcdir}/mobile/android/stumbler/java"
+                }
+
+                if (!mozconfig.substs.MOZ_CRASHREPORTER) {
+                    exclude 'org/mozilla/gecko/CrashReporter.java'
+                }
+
+                if (!mozconfig.substs.MOZ_NATIVE_DEVICES) {
+                    exclude 'org/mozilla/gecko/ChromeCast.java'
+                    exclude 'org/mozilla/gecko/GeckoMediaPlayer.java'
+                    exclude 'org/mozilla/gecko/MediaPlayerManager.java'
+                }
+
+                if (mozconfig.substs.MOZ_WEBRTC) {
+                    srcDir "${topsrcdir}/media/webrtc/trunk/webrtc/modules/audio_device/android/java/src"
+                    srcDir "${topsrcdir}/media/webrtc/trunk/webrtc/modules/video_capture/android/java/src"
+                    srcDir "${topsrcdir}/media/webrtc/trunk/webrtc/modules/video_render/android/java/src"
+                }
+
+                if (mozconfig.substs.MOZ_INSTALL_TRACKING) {
+                    exclude 'org/mozilla/gecko/adjust/StubAdjustHelper.java'
+                } else {
+                    exclude 'org/mozilla/gecko/adjust/AdjustHelper.java'
+                }
+
+                srcDir "${project.buildDir}/generated/source/preprocessed_code" // See syncPreprocessedCode.
+            }
+
+            res {
+                srcDir "${topsrcdir}/${mozconfig.substs.MOZ_BRANDING_DIRECTORY}/res"
+                srcDir "${project.buildDir}/generated/source/preprocessed_resources" // See syncPreprocessedResources.
+                srcDir 'resources'
+                if (mozconfig.substs.MOZ_CRASHREPORTER) {
+                    srcDir 'crashreporter/res'
+                }
+            }
+        }
+
+        test {
+            java {
+                srcDir "${topsrcdir}/mobile/android/tests/background/junit4/src"
+            }
+
+            resources {
+                srcDir "${topsrcdir}/mobile/android/tests/background/junit4/resources"
+            }
+        }
+    }
+}
+
+task syncPreprocessedCode(type: Sync, dependsOn: rootProject.generateCodeAndResources) {
+    into("${project.buildDir}/generated/source/preprocessed_code")
+    from("${topobjdir}/mobile/android/base/generated/preprocessed")
+}
+
+task syncPreprocessedResources(type: Sync, dependsOn: rootProject.generateCodeAndResources) {
+    into("${project.buildDir}/generated/source/preprocessed_resources")
+    from("${topobjdir}/mobile/android/base/res")
+}
+
+android.libraryVariants.all { variant ->
+    variant.preBuild.dependsOn syncPreprocessedCode
+    variant.preBuild.dependsOn syncPreprocessedResources
+}
+
+dependencies {
+    compile 'com.android.support:support-v4:23.0.1'
+    compile 'com.android.support:appcompat-v7:23.0.1'
+    compile 'com.android.support:recyclerview-v7:23.0.1'
+    compile 'com.android.support:design:23.0.1'
+
+    if (mozconfig.substs.MOZ_NATIVE_DEVICES) {
+        compile 'com.android.support:mediarouter-v7:23.0.1'
+        compile 'com.google.android.gms:play-services-basement:8.1.0'
+        compile 'com.google.android.gms:play-services-base:8.1.0'
+        compile 'com.google.android.gms:play-services-cast:8.1.0'
+    }
+
+    if (mozconfig.substs.MOZ_ANDROID_GCM) {
+        compile 'com.google.android.gms:play-services-basement:8.1.0'
+        compile 'com.google.android.gms:play-services-base:8.1.0'
+        compile 'com.google.android.gms:play-services-gcm:8.1.0'
+    }
+
+    compile project(':thirdparty')
+
+    testCompile 'junit:junit:4.12'
+    testCompile 'org.robolectric:robolectric:3.0'
+    testCompile 'org.simpleframework:simple-http:4.1.13'
+}
+
+apply plugin: 'idea'
+
+idea {
+    module {
+        excludeDirs += file("${topobjdir}/gradle/base/src/org/mozilla/gecko/resources")
+    }
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/base/fxa/activities/CustomColorPreference.java
@@ -0,0 +1,52 @@
+/* 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 android.content.Context;
+import android.content.res.TypedArray;
+import android.preference.Preference;
+import android.util.AttributeSet;
+import android.view.View;
+import android.widget.TextView;
+
+ /**
+  * This preference is used to define custom  colors for both title and summary texts.
+  * Color code #777777 (placeholder_grey) is used as the fallback color for both title and summary.
+  */
+public class CustomColorPreference extends Preference {
+    private int mTitleColor;
+    private int mSummaryColor;
+
+    public CustomColorPreference(Context context) {
+        super(context);
+    }
+
+    public CustomColorPreference(Context context, AttributeSet attrs) {
+        super(context, attrs);
+        init(context, attrs);
+    }
+
+    public CustomColorPreference(Context context, AttributeSet attrs, int defStyle) {
+        super(context, attrs, defStyle);
+        init(context, attrs);
+    }
+
+    public void init(Context context, AttributeSet attrs) {
+        TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.CustomColorPreference);
+        mTitleColor = a.getColor(R.styleable.CustomColorPreference_titleColor, R.color.placeholder_grey);
+        mSummaryColor = a.getColor(R.styleable.CustomColorPreference_summaryColor, R.color.placeholder_grey);
+        a.recycle();
+    }
+
+    @Override
+    protected void onBindView(View view) {
+        super.onBindView(view);
+        final TextView title = (TextView) view.findViewById(android.R.id.title);
+        final TextView summary = (TextView) view.findViewById(android.R.id.summary);
+        title.setTextColor(mTitleColor);
+        summary.setTextColor(mSummaryColor);
+    }
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/base/lint.xml
@@ -0,0 +1,8 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<lint>
+    <!-- Enable relevant checks disabled by default -->
+    <issue id="NegativeMargin" severity="warning" />
+
+    <!-- We have a custom menu and don't conform to the recommended styles. -->
+    <issue id="IconColors" severity="ignore" />
+</lint>
--- a/mobile/android/base/preferences/GeckoPreferenceFragment.java
+++ b/mobile/android/base/preferences/GeckoPreferenceFragment.java
@@ -93,18 +93,18 @@ public class GeckoPreferenceFragment ext
     private String getTitle() {
         final int res = getResource();
         if (res == R.xml.preferences) {
             return getString(R.string.settings_title);
         }
 
         // We need this because we can launch straight into this category
         // from the Data Reporting notification.
-        if (res == R.xml.preferences_vendor) {
-            return getString(R.string.pref_category_vendor);
+        if (res == R.xml.preferences_privacy) {
+            return getString(R.string.pref_category_privacy_short);
         }
 
         // from the Awesomescreen with the magnifying glass.
         if (res == R.xml.preferences_search) {
             return getString(R.string.pref_category_search);
         }
 
         return null;
--- a/mobile/android/base/resources/values/attrs.xml
+++ b/mobile/android/base/resources/values/attrs.xml
@@ -163,16 +163,21 @@
     <declare-styleable name="TabMenuStrip">
         <attr name="strip" format="reference"/>
         <attr name="tabsMarginLeft" format="dimension" />
         <attr name="activeTextColor" format="color" />
         <attr name="inactiveTextColor" format="color" />
         <attr name="titlebarFill" format="boolean" />
     </declare-styleable>
 
+    <declare-styleable name="CustomColorPreference">
+        <attr name="titleColor" format="color" />
+        <attr name="summaryColor" format="color" />
+    </declare-styleable>
+
     <declare-styleable name="TabPanelBackButton">
         <attr name="rightDivider" format="reference"/>
         <attr name="dividerVerticalPadding" format="dimension"/>
     </declare-styleable>
 
     <declare-styleable name="EllipsisTextView">
         <attr name="ellipsizeAtLine" format="integer"/>
     </declare-styleable>
--- a/mobile/android/base/resources/xml/fxaccount_status_prefscreen.xml
+++ b/mobile/android/base/resources/xml/fxaccount_status_prefscreen.xml
@@ -1,11 +1,12 @@
 <?xml version="1.0" encoding="utf-8"?>
 <PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android"
-    android:key="status_screen" >
+                  xmlns:gecko="http://schemas.android.com/apk/res-auto"
+                  android:key="status_screen">
 
     <PreferenceCategory
         android:key="signed_in_as_category"
         android:title="@string/fxaccount_status_signed_in_as" >
         <Preference
             android:editable="false"
             android:key="profile"
             android:icon="@drawable/sync_avatar_default"
@@ -97,20 +98,21 @@
             android:persistent="false"
             android:title="@string/fxaccount_status_device_name" />
 
         <Preference
             android:editable="false"
             android:key="sync_server"
             android:persistent="false"
             android:title="@string/fxaccount_status_sync_server" />
-        <Preference
+        <org.mozilla.gecko.fxa.activities.CustomColorPreference
             android:editable="false"
             android:key="remove_account"
             android:persistent="false"
+            gecko:titleColor="@color/rejection_red"
             android:title="@string/fxaccount_remove_account" />
         <Preference
             android:editable="false"
             android:key="more"
             android:persistent="false"
             android:title="@string/fxaccount_status_more" />
 
     </PreferenceCategory>
--- a/mobile/android/chrome/content/Reader.js
+++ b/mobile/android/chrome/content/Reader.js
@@ -15,16 +15,30 @@ var Reader = {
   STATUS_FETCH_FAILED_UNSUPPORTED_FORMAT: 3,
   STATUS_FETCHED_ARTICLE: 4,
 
   get _hasUsedToolbar() {
     delete this._hasUsedToolbar;
     return this._hasUsedToolbar = Services.prefs.getBoolPref("reader.has_used_toolbar");
   },
 
+  get _buttonHistogram() {
+    delete this._buttonHistogram;
+    return this._buttonHistogram = Services.telemetry.getHistogramById("FENNEC_READER_VIEW_BUTTON");
+  },
+
+  // Values for "FENNEC_READER_VIEW_BUTTON" histogram.
+  _buttonHistogramValues: {
+    HIDDEN: 0,
+    SHOWN: 1,
+    TAP_ENTER: 2,
+    TAP_EXIT: 3,
+    LONG_TAP: 4
+  },
+
   /**
    * BackPressListener (listeners / ReaderView Ids).
    */
   _backPressListeners: [],
   _backPressViewIds: [],
 
   /**
    * Set a backPressListener for this tabId / ReaderView Id pair.
@@ -197,24 +211,27 @@ var Reader = {
       let url = browser.currentURI.spec;
       if (url.startsWith("about:reader")) {
         let originalURL = ReaderMode.getOriginalUrl(url);
         if (!originalURL) {
           Cu.reportError("Error finding original URL for about:reader URL: " + url);
         } else {
           browser.loadURI(originalURL);
         }
+        Reader._buttonHistogram.add(Reader._buttonHistogramValues.TAP_EXIT);
       } else {
         browser.messageManager.sendAsyncMessage("Reader:ParseDocument", { url: url });
+        Reader._buttonHistogram.add(Reader._buttonHistogramValues.TAP_ENTER);
       }
     },
 
     readerModeActiveCallback: function(tabID) {
       Reader._addTabToReadingList(tabID).catch(e => Cu.reportError("Error adding tab to reading list: " + e));
       UITelemetry.addEvent("save.1", "pageaction", null, "reader");
+      Reader._buttonHistogram.add(Reader._buttonHistogramValues.LONG_TAP);
     },
   },
 
   updatePageAction: function(tab) {
     if (!tab.getActive()) {
       return;
     }
 
@@ -242,16 +259,19 @@ var Reader = {
       return;
     }
 
     // Only stop a reader session if the foreground viewer is not visible.
     UITelemetry.stopSession("reader.1", "", null);
 
     if (browser.isArticle) {
       showPageAction("drawable://reader", Strings.reader.GetStringFromName("readerView.enter"));
+      this._buttonHistogram.add(this._buttonHistogramValues.SHOWN);
+    } else {
+      this._buttonHistogram.add(this._buttonHistogramValues.HIDDEN);
     }
   },
 
   /**
    * Downloads and caches content for a reading list item with a given URL and id.
    */
   _fetchContent: function(url, id) {
     this._downloadAndCacheArticle(url).then(article => {
--- a/settings.gradle
+++ b/settings.gradle
@@ -16,24 +16,42 @@ if (proc.exitValue() != 0) {
 import groovy.json.JsonSlurper
 def slurper = new JsonSlurper()
 def json = slurper.parseText(standardOutput.toString())
 
 if (json.substs.MOZ_BUILD_APP != 'mobile/android') {
     throw new GradleException("Building with Gradle is only supported for Fennec, i.e., MOZ_BUILD_APP == 'mobile/android'.");
 }
 
+def srcdir = { dst, src ->
+    def d = java.nio.file.Paths.get("${json.topobjdir}/gradle/${dst}")
+    def s = java.nio.file.Paths.get("${json.topsrcdir}/${src}")
+    try {
+        java.nio.file.Files.createDirectories(d.getParent())
+    } catch (java.nio.file.FileAlreadyExistsException e) {
+        // Do nothing.
+    }
+    try {
+        java.nio.file.Files.createSymbolicLink(d, s)
+    } catch (java.nio.file.FileAlreadyExistsException e) {
+        // Do nothing.
+    }
+}
+
+// Since base/ doesn't have the correct package prefix directory structure, we
+// still need to symlink.
+srcdir('base/src/org/mozilla/gecko', 'mobile/android/base')
+
 include ':app'
 include ':base'
 include ':omnijar'
 include ':thirdparty'
 
-def gradleRoot = new File("${json.topobjdir}/mobile/android/gradle")
 project(':app').projectDir = new File("${json.topsrcdir}/mobile/android/app")
-project(':base').projectDir = new File(gradleRoot, 'base')
+project(':base').projectDir = new File("${json.topsrcdir}/mobile/android/base")
 project(':omnijar').projectDir = new File("${json.topsrcdir}/mobile/android/app/omnijar")
 project(':thirdparty').projectDir = new File("${json.topsrcdir}/mobile/android/thirdparty")
 
 // The Gradle instance is shared between settings.gradle and all the
 // other build.gradle files (see
 // http://forums.gradle.org/gradle/topics/define_extension_properties_from_settings_xml).
 // We use this ext property to pass the per-object-directory mozconfig
 // between scripts.  This lets us execute set-up code before we gradle
--- a/toolkit/components/extensions/Extension.jsm
+++ b/toolkit/components/extensions/Extension.jsm
@@ -1,15 +1,15 @@
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 "use strict";
 
-this.EXPORTED_SYMBOLS = ["Extension"];
+this.EXPORTED_SYMBOLS = ["Extension", "ExtensionData"];
 
 /*
  * This file is the main entry point for extensions. When an extension
  * loads, its bootstrap.js file creates a Extension instance
  * and calls .startup() on it. It calls .shutdown() when the extension
  * unloads. Extension manages any extension-specific state in
  * the chrome process.
  */
@@ -28,18 +28,22 @@ XPCOMUtils.defineLazyModuleGetter(this, 
 XPCOMUtils.defineLazyModuleGetter(this, "Log",
                                   "resource://gre/modules/Log.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "MatchPattern",
                                   "resource://gre/modules/MatchPattern.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "NetUtil",
                                   "resource://gre/modules/NetUtil.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "FileUtils",
                                   "resource://gre/modules/FileUtils.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "OS",
+                                  "resource://gre/modules/osfile.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "PrivateBrowsingUtils",
                                   "resource://gre/modules/PrivateBrowsingUtils.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "Task",
+                                  "resource://gre/modules/Task.jsm");
 
 Cu.import("resource://gre/modules/ExtensionManagement.jsm");
 
 // Register built-in parts of the API. Other parts may be registered
 // in browser/, mobile/, or b2g/.
 ExtensionManagement.registerScript("chrome://extensions/content/ext-alarms.js");
 ExtensionManagement.registerScript("chrome://extensions/content/ext-backgroundPage.js");
 ExtensionManagement.registerScript("chrome://extensions/content/ext-cookies.js");
@@ -53,16 +57,17 @@ ExtensionManagement.registerScript("chro
 ExtensionManagement.registerScript("chrome://extensions/content/ext-storage.js");
 ExtensionManagement.registerScript("chrome://extensions/content/ext-test.js");
 
 Cu.import("resource://gre/modules/ExtensionUtils.jsm");
 var {
   MessageBroker,
   Messenger,
   injectAPI,
+  extend,
   flushJarCache,
 } = ExtensionUtils;
 
 const LOGGER_ID_BASE = "addons.webextension.";
 
 var scriptScope = this;
 
 // This object loads the ext-*.js scripts that define the extension API.
@@ -317,38 +322,357 @@ var GlobalManager = {
     let listener = event => {
       eventHandler.removeEventListener("unload", listener);
       context.unload();
     };
     eventHandler.addEventListener("unload", listener, true);
   },
 };
 
+// Represents the data contained in an extension, contained either
+// in a directory or a zip file, which may or may not be installed.
+// This class implements the functionality of the Extension class,
+// primarily related to manifest parsing and localization, which is
+// useful prior to extension installation or initialization.
+//
+// No functionality of this class is guaranteed to work before
+// |readManifest| has been called, and completed.
+this.ExtensionData = function(rootURI)
+{
+  this.rootURI = rootURI;
+
+  this.manifest = null;
+  this.id = null;
+  // Map(locale-name -> message-map)
+  // Contains a key for each loaded locale, each of which is a
+  // JSON-compatible object with a property for each message
+  // in that locale.
+  this.localeMessages = new Map();
+  this.selectedLocale = null;
+  this._promiseLocales = null;
+
+  this.errors = [];
+}
+
+ExtensionData.prototype = {
+  get logger() {
+    let id = this.id || "<unknown>";
+    return Log.repository.getLogger(LOGGER_ID_BASE + id);
+  },
+
+  // Report an error about the extension's manifest file.
+  manifestError(message) {
+    this.packagingError(`Reading manifest: ${message}`);
+  },
+
+  // Report an error about the extension's general packaging.
+  packagingError(message) {
+    this.errors.push(message);
+    this.logger.error(`Loading extension '${this.id}': ${message}`);
+  },
+
+  // https://developer.chrome.com/extensions/i18n
+  localizeMessage(message, substitutions, locale = this.selectedLocale) {
+    let messages = {};
+    if (this.localeMessages.has(locale)) {
+      messages = this.localeMessages.get(locale);
+    }
+
+    if (message in messages) {
+      let str = messages[message].message;
+
+      if (!substitutions) {
+        substitutions = [];
+      } else if (!Array.isArray(substitutions)) {
+        substitutions = [substitutions];
+      }
+
+      // https://developer.chrome.com/extensions/i18n-messages
+      // |str| may contain substrings of the form $1 or $PLACEHOLDER$.
+      // In the former case, we replace $n with substitutions[n - 1].
+      // In the latter case, we consult the placeholders array.
+      // The placeholder may itself use $n to refer to substitutions.
+      let replacer = (matched, name) => {
+        if (name.length == 1 && name[0] >= '1' && name[0] <= '9') {
+          return substitutions[parseInt(name) - 1];
+        } else {
+          let content = messages[message].placeholders[name].content;
+          if (content[0] == '$') {
+            return replacer(matched, content[1]);
+          } else {
+            return content;
+          }
+        }
+      };
+      return str.replace(/\$([A-Za-z_@]+)\$/, replacer)
+                .replace(/\$([0-9]+)/, replacer)
+                .replace(/\$\$/, "$");
+    }
+
+    // Check for certain pre-defined messages.
+    if (message == "@@extension_id") {
+      if ("uuid" in this) {
+        // Per Chrome, this isn't available before an ID is guaranteed
+        // to have been assigned, namely, in manifest files.
+        // This should only be present in instances of the |Extension|
+        // subclass.
+        return this.uuid;
+      }
+    } else if (message == "@@ui_locale") {
+      return Locale.getLocale();
+    } else if (message == "@@bidi_dir") {
+      return "ltr"; // FIXME
+    }
+
+    Cu.reportError(`Unknown localization message ${message}`);
+    return "??";
+  },
+
+  // Localize a string, replacing all |__MSG_(.*)__| tokens with the
+  // matching string from the current locale, as determined by
+  // |this.selectedLocale|.
+  //
+  // This may not be called before calling either |initLocale| or
+  // |initAllLocales|.
+  localize(str, locale = this.selectedLocale) {
+    if (!str) {
+      return str;
+    }
+
+    return str.replace(/__MSG_([A-Za-z0-9@_]+?)__/g, (matched, message) => {
+      return this.localizeMessage(message, [], locale);
+    });
+  },
+
+  // If a "default_locale" is specified in that manifest, returns it
+  // as a Gecko-compatible locale string. Otherwise, returns null.
+  get defaultLocale() {
+    if ("default_locale" in this.manifest) {
+      return this.normalizeLocaleCode(this.manifest.default_locale);
+    }
+
+    return null;
+  },
+
+  // Normalizes a Chrome-compatible locale code to the appropriate
+  // Gecko-compatible variant. Currently, this means simply
+  // replacing underscores with hyphens.
+  normalizeLocaleCode(locale) {
+    return String.replace(locale, /_/g, "-");
+  },
+
+  readDirectory: Task.async(function* (path) {
+    if (this.rootURI instanceof Ci.nsIFileURL) {
+      let uri = NetUtil.newURI(this.rootURI.resolve("./" + path));
+      let fullPath = uri.QueryInterface(Ci.nsIFileURL).file.path;
+
+      let iter = new OS.File.DirectoryIterator(fullPath);
+      let results = [];
+
+      try {
+        yield iter.forEach(entry => {
+          results.push(entry);
+        });
+      } catch (e) {}
+      iter.close();
+
+      // Always return a list, even if the directory does not exist (or is
+      // not a directory) for symmetry with the ZipReader behavior.
+      return results;
+    }
+
+    if (!(this.rootURI instanceof Ci.nsIJARURI &&
+          this.rootURI.JARFile instanceof Ci.nsIFileURL)) {
+      throw Error("Invalid extension root URL");
+    }
+
+    // FIXME: We need a way to do this without main thread IO.
+
+    let file = this.rootURI.JARFile.file;
+    let zipReader = Cc["@mozilla.org/libjar/zip-reader;1"].createInstance(Ci.nsIZipReader);
+    try {
+      zipReader.open(file);
+
+      let results = [];
+
+      // Normalize the directory path.
+      path = path.replace(/\/\/+/g, "/").replace(/^\/|\/$/g, "") + "/";
+
+      // Escape pattern metacharacters.
+      let pattern = path.replace(/[[\]()?*~|$\\]/g, "\\$&");
+
+      let enumerator = zipReader.findEntries(pattern + "*");
+      while (enumerator.hasMore()) {
+        let name = enumerator.getNext();
+        if (!name.startsWith(path)) {
+          throw new Error("Unexpected ZipReader entry");
+        }
+
+        // The enumerator returns the full path of all entries.
+        // Trim off the leading path, and filter out entries from
+        // subdirectories.
+        name = name.slice(path.length);
+        if (name && !/\/./.test(name)) {
+          results.push({
+            name: name.replace("/", ""),
+            isDir: name.endsWith("/"),
+          });
+        }
+      }
+
+      return results;
+    } finally {
+      zipReader.close();
+    }
+  }),
+
+  readJSON(path) {
+    return new Promise((resolve, reject) => {
+      let uri = this.rootURI.resolve(`./${path}`);
+
+      NetUtil.asyncFetch({uri, loadUsingSystemPrincipal: true}, (inputStream, status) => {
+        if (!Components.isSuccessCode(status)) {
+          reject(new Error(status));
+          return;
+        }
+        try {
+          let text = NetUtil.readInputStreamToString(inputStream, inputStream.available());
+          resolve(JSON.parse(text));
+        } catch (e) {
+          reject(e);
+        }
+      });
+    });
+  },
+
+  // Reads the extension's |manifest.json| file, and stores its
+  // parsed contents in |this.manifest|.
+  readManifest() {
+    return this.readJSON("manifest.json").then(manifest => {
+      this.manifest = manifest;
+
+      try {
+        this.id = this.manifest.applications.gecko.id;
+      } catch (e) {}
+
+      if (typeof this.id != "string") {
+        this.manifestError("Missing required `applications.gecko.id` property");
+      }
+
+      return manifest;
+    });
+  },
+
+  // Reads the locale file for the given Gecko-compatible locale code, and
+  // stores its parsed contents in |this.localeMessages.get(locale)|.
+  readLocaleFile: Task.async(function* (locale) {
+    let locales = yield this.promiseLocales();
+    let dir = locales.get(locale);
+    let file = `_locales/${dir}/messages.json`;
+
+    let messages = {};
+    try {
+      messages = yield this.readJSON(file);
+    } catch (e) {
+      this.packagingError(`Loading locale file ${file}: ${e}`);
+    }
+
+    this.localeMessages.set(locale, messages);
+    return messages;
+  }),
+
+  // Reads the list of locales available in the extension, and returns a
+  // Promise which resolves to a Map upon completion.
+  // Each map key is a Gecko-compatible locale code, and each value is the
+  // "_locales" subdirectory containing that locale:
+  //
+  // Map(gecko-locale-code -> locale-directory-name)
+  promiseLocales() {
+    if (!this._promiseLocales) {
+      this._promiseLocales = Task.spawn(function* () {
+        let locales = new Map();
+
+        let entries = yield this.readDirectory("_locales");
+        for (let file of entries) {
+          if (file.isDir) {
+            let locale = this.normalizeLocaleCode(file.name);
+            locales.set(locale, file.name);
+          }
+        }
+
+        return locales;
+      }.bind(this));
+    }
+
+    return this._promiseLocales;
+  },
+
+  // Reads the locale messages for all locales, and returns a promise which
+  // resolves to a Map of locale messages upon completion. Each key in the map
+  // is a Gecko-compatible locale code, and each value is a locale data object
+  // as returned by |readLocaleFile|.
+  initAllLocales: Task.async(function* () {
+    let locales = yield this.promiseLocales();
+
+    yield Promise.all(Array.from(locales.keys(),
+                                 locale => this.readLocaleFile(locale)));
+
+    let defaultLocale = this.defaultLocale;
+    if (defaultLocale) {
+      if (!locales.has(defaultLocale)) {
+        this.manifestError('Value for "default_locale" property must correspond to ' +
+                           'a directory in "_locales/". Not found: ' +
+                           JSON.stringify(`_locales/${default_locale}/`));
+      }
+    } else if (this.localeMessages.size) {
+      this.manifestError('The "default_locale" property is required when a ' +
+                         '"_locales/" directory is present.');
+    }
+
+    return this.localeMessages;
+  }),
+
+  // Reads the locale file for the given Gecko-compatible locale code, or the
+  // default locale if no locale code is given, and sets it as the currently
+  // selected locale on success.
+  //
+  // If no locales are unavailable, resolves to |null|.
+  initLocale: Task.async(function* (locale = this.defaultLocale) {
+    if (locale == null) {
+      return null;
+    }
+
+    let localeData = yield this.readLocaleFile(locale);
+
+    this.selectedLocale = locale;
+    return localeData;
+  }),
+};
+
 // We create one instance of this class per extension. |addonData|
 // comes directly from bootstrap.js when initializing.
 this.Extension = function(addonData)
 {
+  ExtensionData.call(this, addonData.resourceURI);
+
   let uuidGenerator = Cc["@mozilla.org/uuid-generator;1"].getService(Ci.nsIUUIDGenerator);
   let uuid = uuidGenerator.generateUUID().number;
-  uuid = uuid.substring(1, uuid.length - 1); // Strip of { and } off the UUID.
+  uuid = uuid.slice(1, -1); // Strip of { and } off the UUID.
   this.uuid = uuid;
 
   if (addonData.cleanupFile) {
     Services.obs.addObserver(this, "xpcom-shutdown", false);
     this.cleanupFile = addonData.cleanupFile || null;
     delete addonData.cleanupFile;
   }
 
   this.addonData = addonData;
   this.id = addonData.id;
   this.baseURI = Services.io.newURI("moz-extension://" + uuid, null, null);
   this.baseURI.QueryInterface(Ci.nsIURL);
-  this.manifest = null;
-  this.localeMessages = null;
-  this.logger = Log.repository.getLogger(LOGGER_ID_BASE + this.id.replace(/\./g, "-"));
   this.principal = this.createPrincipal();
 
   this.views = new Set();
 
   this.onStartup = null;
 
   this.hasShutdown = false;
   this.onShutdown = new Set();
@@ -470,17 +794,17 @@ this.Extension.generate = function(id, d
 
   return new Extension({
     id,
     resourceURI: jarURI,
     cleanupFile: file
   });
 }
 
-Extension.prototype = {
+Extension.prototype = extend(Object.create(ExtensionData.prototype), {
   on(hook, f) {
     return this.emitter.on(hook, f);
   },
 
   off(hook, f) {
     return this.emitter.off(hook, f);
   },
 
@@ -500,178 +824,32 @@ Extension.prototype = {
   // Checks that the given URL is a child of our baseURI.
   isExtensionURL(url) {
     let uri = Services.io.newURI(url, null, null);
 
     let common = this.baseURI.getCommonBaseSpec(uri);
     return common == this.baseURI.spec;
   },
 
-  // Report an error about the extension's manifest file.
-  manifestError(message) {
-    this.logger.error(`Loading extension '${this.id}': ${message}`);
-  },
-
   // Representation of the extension to send to content
   // processes. This should include anything the content process might
   // need.
   serialize() {
     return {
       id: this.id,
       uuid: this.uuid,
       manifest: this.manifest,
       resourceURL: this.addonData.resourceURI.spec,
       baseURL: this.baseURI.spec,
       content_scripts: this.manifest.content_scripts || [],
       webAccessibleResources: this.webAccessibleResources,
       whiteListedHosts: this.whiteListedHosts.serialize(),
     };
   },
 
-  // https://developer.chrome.com/extensions/i18n
-  localizeMessage(message, substitutions) {
-    if (message in this.localeMessages) {
-      let str = this.localeMessages[message].message;
-
-      if (!substitutions) {
-        substitutions = [];
-      }
-      if (!Array.isArray(substitutions)) {
-        substitutions = [substitutions];
-      }
-
-      // https://developer.chrome.com/extensions/i18n-messages
-      // |str| may contain substrings of the form $1 or $PLACEHOLDER$.
-      // In the former case, we replace $n with substitutions[n - 1].
-      // In the latter case, we consult the placeholders array.
-      // The placeholder may itself use $n to refer to substitutions.
-      let replacer = (matched, name) => {
-        if (name.length == 1 && name[0] >= '1' && name[0] <= '9') {
-          return substitutions[parseInt(name) - 1];
-        } else {
-          let content = this.localeMessages[message].placeholders[name].content;
-          if (content[0] == '$') {
-            return replacer(matched, content[1]);
-          } else {
-            return content;
-          }
-        }
-      };
-      return str.replace(/\$([A-Za-z_@]+)\$/, replacer)
-                .replace(/\$([0-9]+)/, replacer)
-                .replace(/\$\$/, "$");
-    }
-
-    // Check for certain pre-defined messages.
-    if (message == "@@extension_id") {
-      return this.id;
-    } else if (message == "@@ui_locale") {
-      return Locale.getLocale();
-    } else if (message == "@@bidi_dir") {
-      return "ltr"; // FIXME
-    }
-
-    Cu.reportError(`Unknown localization message ${message}`);
-    return "??";
-  },
-
-  localize(str) {
-    if (!str) {
-      return str;
-    }
-
-    if (str.startsWith("__MSG_") && str.endsWith("__")) {
-      let message = str.substring("__MSG_".length, str.length - "__".length);
-      return this.localizeMessage(message);
-    }
-
-    return str;
-  },
-
-  readJSON(uri) {
-    return new Promise((resolve, reject) => {
-      NetUtil.asyncFetch({uri, loadUsingSystemPrincipal: true}, (inputStream, status) => {
-        if (!Components.isSuccessCode(status)) {
-          reject(status);
-          return;
-        }
-        let text = NetUtil.readInputStreamToString(inputStream, inputStream.available());
-        try {
-          resolve(JSON.parse(text));
-        } catch (e) {
-          reject(e);
-        }
-      });
-    });
-  },
-
-  readManifest() {
-    let manifestURI = Services.io.newURI("manifest.json", null, this.baseURI);
-    return this.readJSON(manifestURI);
-  },
-
-  readLocaleFile(locale) {
-    let dir = locale.replace("-", "_");
-    let url = `_locales/${dir}/messages.json`;
-    let uri = Services.io.newURI(url, null, this.baseURI);
-    return this.readJSON(uri);
-  },
-
-  readLocaleMessages() {
-    let locales = [];
-
-    // We need to base this off of this.addonData.resourceURI rather
-    // than baseURI since baseURI is a moz-extension URI, which always
-    // QIs to nsIFileURL.
-    let uri = Services.io.newURI("_locales", null, this.addonData.resourceURI);
-    if (uri instanceof Ci.nsIFileURL) {
-      let file = uri.file;
-      let enumerator;
-      try {
-        enumerator = file.directoryEntries;
-      } catch (e) {
-        return {};
-      }
-      while (enumerator.hasMoreElements()) {
-        let file = enumerator.getNext().QueryInterface(Ci.nsIFile);
-        locales.push({
-          name: file.leafName,
-          locales: [file.leafName.replace("_", "-")]
-        });
-      }
-    }
-
-    if (uri instanceof Ci.nsIJARURI && uri.JARFile instanceof Ci.nsIFileURL) {
-      let file = uri.JARFile.file;
-      let zipReader = Cc["@mozilla.org/libjar/zip-reader;1"].createInstance(Ci.nsIZipReader);
-      try {
-        zipReader.open(file);
-        let enumerator = zipReader.findEntries("_locales/*");
-        while (enumerator.hasMore()) {
-          let name = enumerator.getNext();
-          let match = name.match(new RegExp("_locales\/([^/]*)"));
-          if (match && match[1]) {
-            locales.push({
-              name: match[1],
-              locales: [match[1].replace("_", "-")]
-            });
-          }
-        }
-      } finally {
-        zipReader.close();
-      }
-    }
-
-    let locale = Locale.findClosestLocale(locales);
-    if (locale) {
-      return this.readLocaleFile(locale.name).catch(() => {});
-    }
-    return {};
-  },
-
   broadcast(msg, data) {
     return new Promise(resolve => {
       let count = Services.ppmm.childCount;
       Services.ppmm.addMessageListener(msg + "Complete", function listener() {
         count--;
         if (count == 0) {
           Services.ppmm.removeMessageListener(msg + "Complete", listener);
           resolve();
@@ -718,38 +896,55 @@ Extension.prototype = {
   callOnClose(obj) {
     this.onShutdown.add(obj);
   },
 
   forgetOnClose(obj) {
     this.onShutdown.delete(obj);
   },
 
+  // Reads the locale file for the given Gecko-compatible locale code, or if
+  // no locale is given, the available locale closest to the UI locale.
+  // Sets the currently selected locale on success.
+  initLocale: Task.async(function* (locale = undefined) {
+    if (locale === undefined) {
+      let locales = yield this.promiseLocales();
+
+      let localeList = Object.keys(locales).map(locale => {
+        return { name: locale, locales: [locale] };
+      });
+
+      let match = Locale.findClosestLocale(localeList);
+      locale = match ? match.name : this.defaultLocale;
+    }
+
+    return ExtensionData.prototype.initLocale.call(this, locale);
+  }),
+
   startup() {
     try {
       ExtensionManagement.startupExtension(this.uuid, this.addonData.resourceURI, this);
     } catch (e) {
       return Promise.reject(e);
     }
 
-    return Promise.all([this.readManifest(), this.readLocaleMessages()]).then(([manifest, messages]) => {
+    return this.readManifest().then(() => {
+      return this.initLocale();
+    }).then(() => {
       if (this.hasShutdown) {
         return;
       }
 
       GlobalManager.init(this);
 
-      this.manifest = manifest;
-      this.localeMessages = messages;
-
       Management.emit("startup", this);
 
-      return this.runManifest(manifest);
+      return this.runManifest(this.manifest);
     }).catch(e => {
-      dump(`Extension error: ${e} ${e.fileName}:${e.lineNumber}\n`);
+      dump(`Extension error: ${e} ${e.filename}:${e.lineNumber}\n`);
       Cu.reportError(e);
       throw e;
     });
   },
 
   cleanupGeneratedFile() {
     if (!this.cleanupFile) {
       return;
@@ -803,10 +998,10 @@ Extension.prototype = {
 
   hasPermission(perm) {
     return this.permissions.has(perm);
   },
 
   get name() {
     return this.localize(this.manifest.name);
   },
-};
+});
 
--- a/toolkit/components/extensions/ExtensionUtils.jsm
+++ b/toolkit/components/extensions/ExtensionUtils.jsm
@@ -59,16 +59,31 @@ function runSafe(context, f, ...args)
     args = Cu.cloneInto(args, context.cloneScope);
   } catch (e) {
     Cu.reportError(e);
     dump(`runSafe failure: cloning into ${context.cloneScope}: ${e}\n\n${Error().stack}`);
   }
   return runSafeWithoutClone(f, ...args);
 }
 
+// Extend the object |obj| with the property descriptors of each object in
+// |args|.
+function extend(obj, ...args) {
+  for (let arg of args) {
+    let props = [...Object.getOwnPropertyNames(arg),
+                 ...Object.getOwnPropertySymbols(arg)];
+    for (let prop of props) {
+      let descriptor = Object.getOwnPropertyDescriptor(arg, prop);
+      Object.defineProperty(obj, prop, descriptor);
+    }
+  }
+
+  return obj;
+}
+
 // Similar to a WeakMap, but returns a particular default value for
 // |get| if a key is not present.
 function DefaultWeakMap(defaultValue)
 {
   this.defaultValue = defaultValue;
   this.weakmap = new WeakMap();
 }
 
@@ -597,10 +612,11 @@ this.ExtensionUtils = {
   runSafeSync,
   DefaultWeakMap,
   EventManager,
   SingletonEventManager,
   ignoreEvent,
   injectAPI,
   MessageBroker,
   Messenger,
+  extend,
   flushJarCache,
 };
--- a/toolkit/components/telemetry/Histograms.json
+++ b/toolkit/components/telemetry/Histograms.json
@@ -9088,48 +9088,60 @@
     "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": "50",
+    "alert_emails": ["mleibovic@mozilla.com"],
     "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": "50",
+    "alert_emails": ["mleibovic@mozilla.com"],
     "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": "50",
+    "alert_emails": ["mleibovic@mozilla.com"],
     "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": "50",
+    "alert_emails": ["mleibovic@mozilla.com"],
     "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": "50",
+    "alert_emails": ["mleibovic@mozilla.com"],
     "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)"
   },
+  "FENNEC_READER_VIEW_BUTTON" : {
+    "expires_in_version": "50",
+    "alert_emails": ["mleibovic@mozilla.com"],
+    "kind": "enumerated",
+    "n_values": 10,
+    "description": "Bug 1219240: Measures user interaction with the reader view button (0=Button hidden, 1=Button shown, 2=Tap to enter reader view, 3=Tap to exit reader view, 4=Long tap)"
+  },
   "PERMISSIONS_SQL_CORRUPTED": {
     "expires_in_version": "never",
     "kind": "count",
     "description": "Record the permissions.sqlite init failure"
   },
   "DEFECTIVE_PERMISSIONS_SQL_REMOVED": {
     "expires_in_version": "never",
     "kind": "count",
--- a/toolkit/content/license.html
+++ b/toolkit/content/license.html
@@ -76,16 +76,17 @@
       <li><a href="about:license#apache">Apache License 2.0</a></li>
       <li><a href="about:license#apple">Apple License</a></li>
       <li><a href="about:license#apple-mozilla">Apple/Mozilla NPRuntime License</a></li>
       <li><a href="about:license#arm">ARM License</a></li>
       <li><a href="about:license#backbone">Backbone License</a></li>
       <li><a href="about:license#bspatch">bspatch License</a></li>
       <li><a href="about:license#cairo">Cairo Component Licenses</a></li>
       <li><a href="about:license#chromium">Chromium License</a></li>
+      <li><a href="about:license#classnames">classnames License</a></li>
       <li><a href="about:license#codemirror">CodeMirror License</a></li>
       <li><a href="about:license#cubic-bezier">cubic-bezier License</a></li>
       <li><a href="about:license#d3">D3 License</a></li>
       <li><a href="about:license#dagre-d3">Dagre-D3 License</a></li>
       <li><a href="about:license#dtoa">dtoa License</a></li>
       <li><a href="about:license#hunspell-nl">Dutch Spellchecking Dictionary License</a></li>
       <li><a href="about:license#hunspell-ee">Estonian Spellchecking Dictionary License</a></li>
       <li><a href="about:license#expat">Expat License</a></li>
@@ -2715,16 +2716,46 @@ DATA, OR PROFITS; OR BUSINESS INTERRUPTI
 THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
 (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
 OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 </pre>
 
 
     <hr>
 
+    <h1><a id="classnames"></a>classnames License</h1>
+
+    <p>This license applies to the file <span class="path">
+      browser/components/loop/content/shared/libs/classnames-*.js</span>.
+    </p>
+
+<pre>
+Copyright (c) 2015 Jed Watson
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
+</pre>
+
+    <hr>
+
     <h1><a id="codemirror"></a>CodeMirror License</h1>
 
     <p>This license applies to all files in
       <span class="path">devtools/client/sourceeditor/codemirror</span> and
       to specified files in the <span class="path">devtools/client/sourceeditor/test/</span>:
     </p>
     <ul>
       <li><span class="path">cm_comment_test.js</span></li>
--- a/toolkit/mozapps/extensions/internal/XPIProvider.jsm
+++ b/toolkit/mozapps/extensions/internal/XPIProvider.jsm
@@ -17,16 +17,18 @@ Components.utils.import("resource://gre/
 Components.utils.import("resource://gre/modules/Preferences.jsm");
 
 XPCOMUtils.defineLazyModuleGetter(this, "AddonRepository",
                                   "resource://gre/modules/addons/AddonRepository.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "ChromeManifestParser",
                                   "resource://gre/modules/ChromeManifestParser.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "LightweightThemeManager",
                                   "resource://gre/modules/LightweightThemeManager.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "ExtensionData",
+                                  "resource://gre/modules/Extension.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "Locale",
                                   "resource://gre/modules/Locale.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "FileUtils",
                                   "resource://gre/modules/FileUtils.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "ZipUtils",
                                   "resource://gre/modules/ZipUtils.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "NetUtil",
                                   "resource://gre/modules/NetUtil.jsm");
@@ -823,25 +825,30 @@ function getRDFValue(aLiteral) {
  */
 function getRDFProperty(aDs, aResource, aProperty) {
   return getRDFValue(aDs.GetTarget(aResource, EM_R(aProperty), true));
 }
 
 /**
  * Reads an AddonInternal object from a manifest stream.
  *
- * @param  aStream
- *         An open stream to read the manifest from
+ * @param  aUri
+ *         A |file:| or |jar:| URL for the manifest
  * @return an AddonInternal object
  * @throws if the install manifest in the stream is corrupt or could not
  *         be read
  */
-function loadManifestFromWebManifest(aStream) {
-  let decoder = Cc["@mozilla.org/dom/json;1"].createInstance(Ci.nsIJSON);
-  let manifest = decoder.decodeFromStream(aStream, aStream.available());
+var loadManifestFromWebManifest = Task.async(function* loadManifestFromWebManifest(aUri) {
+  // We're passed the URI for the manifest file. Get the URI for its
+  // parent directory.
+  let uri = NetUtil.newURI("./", null, aUri);
+
+  let extension = new ExtensionData(uri);
+
+  let manifest = yield extension.readManifest();
 
   function findProp(obj, current, properties) {
     if (properties.length == 0)
       return obj;
 
     let field = properties[0];
     current += "." + field;
     if (!obj || !(field in obj)) {
@@ -898,40 +905,56 @@ function loadManifestFromWebManifest(aSt
     Object.keys(icons)
           .map((size) => parseInt(size, 10))
           .filter((size) => !isNaN(size))
           .forEach((size) => addon.icons[size] = icons[size]);
   }
 
   addon.applyBackgroundUpdates = AddonManager.AUTOUPDATE_DEFAULT;
 
-  addon.defaultLocale = {
-    name: getProp("name"),
-    description: getOptionalProp("description"),
-    creator: null,
-    homepageURL: null,
-
-    developers: null,
-    translators: null,
-    contributors: null,
+  function getLocale(aLocale) {
+    let result = {
+      name: extension.localize(getProp("name"), aLocale),
+      description: extension.localize(getOptionalProp("description"), aLocale),
+      creator: null,
+      homepageURL: null,
+
+      developers: null,
+      translators: null,
+      contributors: null,
+      locales: [aLocale],
+    };
+    return result;
   }
 
+  // Read the list of available locales, and pre-load messages for
+  // all locales.
+  let locales = yield extension.initAllLocales();
+
+  // If there were any errors loading the extension, bail out now.
+  if (extension.errors.length)
+    throw new Error("Extension is invalid");
+
+  addon.defaultLocale = getLocale(extension.defaultLocale);
+  addon.locales = Array.from(locales.keys(), getLocale);
+
+  delete addon.defaultLocale.locales;
+
   addon.targetApplications = [{
     id: TOOLKIT_ID,
     minVersion: AddonManagerPrivate.webExtensionsMinPlatformVersion,
     maxVersion: "*",
   }];
 
-  addon.locales = [];
   addon.targetPlatforms = [];
   addon.userDisabled = false;
   addon.softDisabled = addon.blocklistState == Blocklist.STATE_SOFTBLOCKED;
 
   return addon;
-}
+});
 
 /**
  * Reads an AddonInternal object from an RDF stream.
  *
  * @param  aUri
  *         The URI that the manifest is being read from
  * @param  aStream
  *         An open stream to read the RDF from
@@ -1246,18 +1269,29 @@ var loadManifestFromDir = Task.async(fun
     let entries = aFile.directoryEntries.QueryInterface(Ci.nsIDirectoryEnumerator);
     let entry;
     while ((entry = entries.nextFile))
       size += getFileSize(entry);
     entries.close();
     return size;
   }
 
-  function loadFromRDF(aFile, aStream) {
-    let addon = loadManifestFromRDF(Services.io.newFileURI(aFile), aStream);
+  function loadFromRDF(aUri) {
+    let fis = Cc["@mozilla.org/network/file-input-stream;1"].
+              createInstance(Ci.nsIFileInputStream);
+    fis.init(aUri.file, -1, -1, false);
+    let bis = Cc["@mozilla.org/network/buffered-input-stream;1"].
+              createInstance(Ci.nsIBufferedInputStream);
+    bis.init(fis, 4096);
+    try {
+      var addon = loadManifestFromRDF(aUri, bis);
+    } finally {
+      bis.close();
+      fis.close();
+    }
 
     let iconFile = aDir.clone();
     iconFile.append("icon.png");
 
     if (iconFile.exists()) {
       addon.icons[32] = "icon.png";
       addon.icons[48] = "icon.png";
     }
@@ -1278,114 +1312,102 @@ var loadManifestFromDir = Task.async(fun
   }
 
   let file = getManifestFileForDir(aDir);
   if (!file) {
     throw new Error("Directory " + aDir.path + " does not contain a valid " +
                     "install manifest");
   }
 
-  let fis = Cc["@mozilla.org/network/file-input-stream;1"].
-            createInstance(Ci.nsIFileInputStream);
-  fis.init(file, -1, -1, false);
-  let bis = Cc["@mozilla.org/network/buffered-input-stream;1"].
-            createInstance(Ci.nsIBufferedInputStream);
-  bis.init(fis, 4096);
-
-  try {
-    let addon = file.leafName == FILE_WEB_MANIFEST ?
-                loadManifestFromWebManifest(bis) :
-                loadFromRDF(file, bis);
-
-    addon._sourceBundle = aDir.clone();
-    addon._installLocation = aInstallLocation;
-    addon.size = getFileSize(aDir);
-    addon.signedState = yield verifyDirSignedState(aDir, addon);
-    addon.appDisabled = !isUsableAddon(addon);
-
-    defineSyncGUID(addon);
-
-    return addon;
-  }
-  finally {
-    bis.close();
-    fis.close();
-  }
+  let uri = Services.io.newFileURI(file).QueryInterface(Ci.nsIFileURL);
+
+  let addon = file.leafName == FILE_WEB_MANIFEST ?
+              yield loadManifestFromWebManifest(uri) :
+              loadFromRDF(uri);
+
+  addon._sourceBundle = aDir.clone();
+  addon._installLocation = aInstallLocation;
+  addon.size = getFileSize(aDir);
+  addon.signedState = yield verifyDirSignedState(aDir, addon);
+  addon.appDisabled = !isUsableAddon(addon);
+
+  defineSyncGUID(addon);
+
+  return addon;
 });
 
 /**
  * Loads an AddonInternal object from an nsIZipReader for an add-on.
  *
  * @param  aZipReader
  *         An open nsIZipReader for the add-on's files
  * @return an AddonInternal object
  * @throws if the XPI file does not contain a valid install manifest
  */
 var loadManifestFromZipReader = Task.async(function* loadManifestFromZipReader(aZipReader, aInstallLocation) {
-  function loadFromRDF(aStream) {
-    let uri = buildJarURI(aZipReader.file, FILE_RDF_MANIFEST);
-    let addon = loadManifestFromRDF(uri, aStream);
+  function loadFromRDF(aUri) {
+    let zis = aZipReader.getInputStream(entry);
+    let bis = Cc["@mozilla.org/network/buffered-input-stream;1"].
+              createInstance(Ci.nsIBufferedInputStream);
+    bis.init(zis, 4096);
+    try {
+      var addon = loadManifestFromRDF(aUri, bis);
+    } finally {
+      bis.close();
+      zis.close();
+    }
 
     if (aZipReader.hasEntry("icon.png")) {
       addon.icons[32] = "icon.png";
       addon.icons[48] = "icon.png";
     }
 
     if (aZipReader.hasEntry("icon64.png")) {
       addon.icons[64] = "icon64.png";
     }
 
     // Binary components can only be loaded from unpacked addons.
     if (addon.unpack) {
-      uri = buildJarURI(aZipReader.file, "chrome.manifest");
+      let uri = buildJarURI(aZipReader.file, "chrome.manifest");
       let chromeManifest = ChromeManifestParser.parseSync(uri);
       addon.hasBinaryComponents = ChromeManifestParser.hasType(chromeManifest,
                                                                "binary-component");
     } else {
       addon.hasBinaryComponents = false;
     }
 
     return addon;
   }
 
   let entry = getManifestEntryForZipReader(aZipReader);
   if (!entry) {
     throw new Error("File " + aZipReader.file.path + " does not contain a valid " +
                     "install manifest");
   }
 
-  let zis = aZipReader.getInputStream(entry);
-  let bis = Cc["@mozilla.org/network/buffered-input-stream;1"].
-            createInstance(Ci.nsIBufferedInputStream);
-  bis.init(zis, 4096);
-
-  try {
-    let addon = entry == FILE_WEB_MANIFEST ?
-                loadManifestFromWebManifest(bis) :
-                loadFromRDF(bis);
-
-    addon._sourceBundle = aZipReader.file;
-    addon._installLocation = aInstallLocation;
-
-    addon.size = 0;
-    let entries = aZipReader.findEntries(null);
-    while (entries.hasMore())
-      addon.size += aZipReader.getEntry(entries.getNext()).realSize;
-
-    addon.signedState = yield verifyZipSignedState(aZipReader.file, addon);
-    addon.appDisabled = !isUsableAddon(addon);
-
-    defineSyncGUID(addon);
-
-    return addon;
-  }
-  finally {
-    bis.close();
-    zis.close();
-  }
+  let uri = buildJarURI(aZipReader.file, entry);
+
+  let addon = entry == FILE_WEB_MANIFEST ?
+              yield loadManifestFromWebManifest(uri) :
+              loadFromRDF(uri);
+
+  addon._sourceBundle = aZipReader.file;
+  addon._installLocation = aInstallLocation;
+
+  addon.size = 0;
+  let entries = aZipReader.findEntries(null);
+  while (entries.hasMore())
+    addon.size += aZipReader.getEntry(entries.getNext()).realSize;
+
+  addon.signedState = yield verifyZipSignedState(aZipReader.file, addon);
+  addon.appDisabled = !isUsableAddon(addon);
+
+  defineSyncGUID(addon);
+
+  return addon;
 });
 
 /**
  * Loads an AddonInternal object from an add-on in an XPI file.
  *
  * @param  aXPIFile
  *         An nsIFile pointing to the add-on's XPI file
  * @return an AddonInternal object
new file mode 100644
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/addons/webextension_3/_locales/en/messages.json
@@ -0,0 +1,10 @@
+{
+  "name": {
+    "message": "foo",
+    "description": "foo"
+  },
+  "desc": {
+    "message": "bar",
+    "description": "bar"
+  }
+}
new file mode 100644
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/addons/webextension_3/_locales/fr/messages.json
@@ -0,0 +1,10 @@
+{
+  "name": {
+    "message": "le foo",
+    "description": "foo"
+  },
+  "desc": {
+    "message": "le bar",
+    "description": "bar"
+  }
+}
new file mode 100644
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/addons/webextension_3/manifest.json
@@ -0,0 +1,12 @@
+{
+  "name": "Web Extension __MSG_name__",
+  "description": "Descripton __MSG_desc__ of add-on",
+  "version": "1.0",
+  "manifest_version": 2,
+  "default_locale": "en",
+  "applications": {
+    "gecko": {
+      "id": "webextension3@tests.mozilla.org"
+    }
+  }
+}
--- a/toolkit/mozapps/extensions/test/xpcshell/test_webextension.js
+++ b/toolkit/mozapps/extensions/test/xpcshell/test_webextension.js
@@ -1,14 +1,16 @@
 /* Any copyright is dedicated to the Public Domain.
  * http://creativecommons.org/publicdomain/zero/1.0/
  */
 
 const ID = "webextension1@tests.mozilla.org";
 
+const PREF_SELECTED_LOCALE = "general.useragent.locale";
+
 const profileDir = gProfD.clone();
 profileDir.append("extensions");
 
 createAppInfo("xpcshell@tests.mozilla.org", "XPCShell", "1", "42");
 startupManager();
 
 const { GlobalManager, Management } = Components.utils.import("resource://gre/modules/Extension.jsm", {});
 
@@ -137,16 +139,43 @@ add_task(function*() {
   let file = getFileForAddon(profileDir, ID);
   do_check_true(file.exists());
 
   addon.uninstall();
 
   yield promiseRestartManager();
 });
 
+add_task(function* test_manifest_localization() {
+  const ID = "webextension3@tests.mozilla.org";
+
+  yield promiseInstallAllFiles([do_get_addon("webextension_3")], true);
+
+  let addon = yield promiseAddonByID(ID);
+
+  equal(addon.name, "Web Extension foo");
+  equal(addon.description, "Descripton bar of add-on");
+
+  Services.prefs.setCharPref(PREF_SELECTED_LOCALE, "fr-FR");
+  yield promiseRestartManager();
+
+  addon = yield promiseAddonByID(ID);
+
+  equal(addon.name, "Web Extension le foo");
+  equal(addon.description, "Descripton le bar of add-on");
+
+  Services.prefs.setCharPref(PREF_SELECTED_LOCALE, "de");
+  yield promiseRestartManager();
+
+  addon = yield promiseAddonByID(ID);
+
+  equal(addon.name, "Web Extension foo");
+  equal(addon.description, "Descripton bar of add-on");
+});
+
 // Missing ID should cause a failure
 add_task(function*() {
   writeWebManifestForExtension({
     name: "Web Extension Name",
     version: "1.0",
     manifest_version: 2,
   }, profileDir, ID);