Merge m-c to inbound a=merge
authorWes Kocher <wkocher@mozilla.com>
Wed, 19 Nov 2014 19:04:29 -0800
changeset 240897 46a5a5a5f9125fe67ca80e296380e78a4232c6ef
parent 240896 8aad622123672568fa9c90c2a69a17507892016d (current diff)
parent 240855 d1adecc7adad0beee45eeea0d7f774adbfe03dee (diff)
child 240898 9986a68aa635ee7845a23cb4adb14d48d0edb013
push id4311
push userraliiev@mozilla.com
push dateMon, 12 Jan 2015 19:37:41 +0000
treeherdermozilla-beta@150c9fed433b [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersmerge
milestone36.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Merge m-c to inbound a=merge
widget/android/bindings/mediacodec-classes.txt
widget/android/bindings/surfacetexture-classes.txt
--- 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="3ab0d9c70f0b2e1ededc679112c392303f037361">
     <copyfile dest="Makefile" src="core/root.mk"/>
   </project>
-  <project name="gaia" path="gaia" remote="mozillaorg" revision="e64428c5b2dce5db90b75a5055077a04f4bd4819"/>
+  <project name="gaia" path="gaia" remote="mozillaorg" revision="8e09627d75acd4abced0ab81983b5b5de6d15881"/>
   <project name="fake-libdvm" path="dalvik" remote="b2g" revision="d50ae982b19f42f0b66d08b9eb306be81687869f"/>
   <project name="gonk-misc" path="gonk-misc" remote="b2g" revision="45c54a55e31758f7e54e5eafe0d01d387f35897a"/>
   <project name="librecovery" path="librecovery" remote="b2g" revision="891e5069c0ad330d8191bf8c7b879c814258c89f"/>
   <project name="moztt" path="external/moztt" remote="b2g" revision="fe893bb760a3bb64375f62fdf4762a58c59df9ef"/>
   <project name="rilproxy" path="rilproxy" remote="b2g" revision="827214fcf38d6569aeb5c6d6f31cb296d1f09272"/>
   <project name="valgrind" path="external/valgrind" remote="b2g" revision="daa61633c32b9606f58799a3186395fd2bbb8d8c"/>
   <project name="vex" path="external/VEX" remote="b2g" revision="47f031c320888fe9f3e656602588565b52d43010"/>
   <project name="apitrace" path="external/apitrace" remote="apitrace" revision="b6f8267794b8c7f2a33236d46a857a84388b8485"/>
--- 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="df362ace56338da8173d30d3e09e08c42c1accfa">
     <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="e64428c5b2dce5db90b75a5055077a04f4bd4819"/>
+  <project name="gaia.git" path="gaia" remote="mozillaorg" revision="8e09627d75acd4abced0ab81983b5b5de6d15881"/>
   <project name="gonk-misc" path="gonk-misc" remote="b2g" revision="45c54a55e31758f7e54e5eafe0d01d387f35897a"/>
   <project name="rilproxy" path="rilproxy" remote="b2g" revision="827214fcf38d6569aeb5c6d6f31cb296d1f09272"/>
   <project name="platform_hardware_ril" path="hardware/ril" remote="b2g" revision="d5d3f93914558b6f168447b805cd799c8233e300"/>
   <project name="platform_external_qemu" path="external/qemu" remote="b2g" revision="a47dd04f8f66e42fd331711140f2c3e2fed0767d"/>
   <project name="moztt" path="external/moztt" remote="b2g" revision="fe893bb760a3bb64375f62fdf4762a58c59df9ef"/>
   <project name="apitrace" path="external/apitrace" remote="apitrace" revision="b6f8267794b8c7f2a33236d46a857a84388b8485"/>
   <!-- Stock Android things -->
   <project name="platform/abi/cpp" path="abi/cpp" revision="dd924f92906085b831bf1cbbc7484d3c043d613c"/>
--- a/b2g/config/emulator-jb/sources.xml
+++ b/b2g/config/emulator-jb/sources.xml
@@ -12,17 +12,17 @@
   <!--original fetch url was https://git.mozilla.org/releases-->
   <remote fetch="https://git.mozilla.org/releases" name="mozillaorg"/>
   <!-- B2G specific things. -->
   <project name="platform_build" path="build" remote="b2g" revision="0e94c080bee081a50aa2097527b0b40852f9143f">
     <copyfile dest="Makefile" src="core/root.mk"/>
   </project>
   <project name="rilproxy" path="rilproxy" remote="b2g" revision="827214fcf38d6569aeb5c6d6f31cb296d1f09272"/>
   <project name="fake-libdvm" path="dalvik" remote="b2g" revision="d50ae982b19f42f0b66d08b9eb306be81687869f"/>
-  <project name="gaia" path="gaia" remote="mozillaorg" revision="e64428c5b2dce5db90b75a5055077a04f4bd4819"/>
+  <project name="gaia" path="gaia" remote="mozillaorg" revision="8e09627d75acd4abced0ab81983b5b5de6d15881"/>
   <project name="gonk-misc" path="gonk-misc" remote="b2g" revision="45c54a55e31758f7e54e5eafe0d01d387f35897a"/>
   <project name="moztt" path="external/moztt" remote="b2g" revision="fe893bb760a3bb64375f62fdf4762a58c59df9ef"/>
   <project name="apitrace" path="external/apitrace" remote="apitrace" revision="b6f8267794b8c7f2a33236d46a857a84388b8485"/>
   <project name="valgrind" path="external/valgrind" remote="b2g" revision="daa61633c32b9606f58799a3186395fd2bbb8d8c"/>
   <project name="vex" path="external/VEX" remote="b2g" revision="47f031c320888fe9f3e656602588565b52d43010"/>
   <!-- Stock Android things -->
   <project groups="linux" name="platform/prebuilts/clang/linux-x86/3.1" path="prebuilts/clang/linux-x86/3.1" revision="5c45f43419d5582949284eee9cef0c43d866e03b"/>
   <project groups="linux" name="platform/prebuilts/clang/linux-x86/3.2" path="prebuilts/clang/linux-x86/3.2" revision="3748b4168e7bd8d46457d4b6786003bc6a5223ce"/>
--- a/b2g/config/emulator-kk/sources.xml
+++ b/b2g/config/emulator-kk/sources.xml
@@ -10,17 +10,17 @@
   <!--original fetch url was git://codeaurora.org/-->
   <remote fetch="https://git.mozilla.org/external/caf" name="caf"/>
   <!--original fetch url was https://git.mozilla.org/releases-->
   <remote fetch="https://git.mozilla.org/releases" name="mozillaorg"/>
   <!-- B2G specific things. -->
   <project name="platform_build" path="build" remote="b2g" revision="3ab0d9c70f0b2e1ededc679112c392303f037361">
     <copyfile dest="Makefile" src="core/root.mk"/>
   </project>
-  <project name="gaia" path="gaia" remote="mozillaorg" revision="e64428c5b2dce5db90b75a5055077a04f4bd4819"/>
+  <project name="gaia" path="gaia" remote="mozillaorg" revision="8e09627d75acd4abced0ab81983b5b5de6d15881"/>
   <project name="fake-libdvm" path="dalvik" remote="b2g" revision="d50ae982b19f42f0b66d08b9eb306be81687869f"/>
   <project name="gonk-misc" path="gonk-misc" remote="b2g" revision="45c54a55e31758f7e54e5eafe0d01d387f35897a"/>
   <project name="librecovery" path="librecovery" remote="b2g" revision="891e5069c0ad330d8191bf8c7b879c814258c89f"/>
   <project name="moztt" path="external/moztt" remote="b2g" revision="fe893bb760a3bb64375f62fdf4762a58c59df9ef"/>
   <project name="rilproxy" path="rilproxy" remote="b2g" revision="827214fcf38d6569aeb5c6d6f31cb296d1f09272"/>
   <project name="valgrind" path="external/valgrind" remote="b2g" revision="daa61633c32b9606f58799a3186395fd2bbb8d8c"/>
   <project name="vex" path="external/VEX" remote="b2g" revision="47f031c320888fe9f3e656602588565b52d43010"/>
   <project name="apitrace" path="external/apitrace" remote="apitrace" revision="b6f8267794b8c7f2a33236d46a857a84388b8485"/>
--- 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="df362ace56338da8173d30d3e09e08c42c1accfa">
     <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="e64428c5b2dce5db90b75a5055077a04f4bd4819"/>
+  <project name="gaia.git" path="gaia" remote="mozillaorg" revision="8e09627d75acd4abced0ab81983b5b5de6d15881"/>
   <project name="gonk-misc" path="gonk-misc" remote="b2g" revision="45c54a55e31758f7e54e5eafe0d01d387f35897a"/>
   <project name="rilproxy" path="rilproxy" remote="b2g" revision="827214fcf38d6569aeb5c6d6f31cb296d1f09272"/>
   <project name="platform_hardware_ril" path="hardware/ril" remote="b2g" revision="d5d3f93914558b6f168447b805cd799c8233e300"/>
   <project name="platform_external_qemu" path="external/qemu" remote="b2g" revision="a47dd04f8f66e42fd331711140f2c3e2fed0767d"/>
   <project name="moztt" path="external/moztt" remote="b2g" revision="fe893bb760a3bb64375f62fdf4762a58c59df9ef"/>
   <project name="apitrace" path="external/apitrace" remote="apitrace" revision="b6f8267794b8c7f2a33236d46a857a84388b8485"/>
   <!-- Stock Android things -->
   <project name="platform/abi/cpp" path="abi/cpp" revision="dd924f92906085b831bf1cbbc7484d3c043d613c"/>
--- a/b2g/config/flame-kk/sources.xml
+++ b/b2g/config/flame-kk/sources.xml
@@ -10,17 +10,17 @@
   <!--original fetch url was git://codeaurora.org/-->
   <remote fetch="https://git.mozilla.org/external/caf" name="caf"/>
   <!--original fetch url was https://git.mozilla.org/releases-->
   <remote fetch="https://git.mozilla.org/releases" name="mozillaorg"/>
   <!-- B2G specific things. -->
   <project name="platform_build" path="build" remote="b2g" revision="3ab0d9c70f0b2e1ededc679112c392303f037361">
     <copyfile dest="Makefile" src="core/root.mk"/>
   </project>
-  <project name="gaia" path="gaia" remote="mozillaorg" revision="e64428c5b2dce5db90b75a5055077a04f4bd4819"/>
+  <project name="gaia" path="gaia" remote="mozillaorg" revision="8e09627d75acd4abced0ab81983b5b5de6d15881"/>
   <project name="fake-libdvm" path="dalvik" remote="b2g" revision="d50ae982b19f42f0b66d08b9eb306be81687869f"/>
   <project name="gonk-misc" path="gonk-misc" remote="b2g" revision="45c54a55e31758f7e54e5eafe0d01d387f35897a"/>
   <project name="librecovery" path="librecovery" remote="b2g" revision="891e5069c0ad330d8191bf8c7b879c814258c89f"/>
   <project name="moztt" path="external/moztt" remote="b2g" revision="fe893bb760a3bb64375f62fdf4762a58c59df9ef"/>
   <project name="rilproxy" path="rilproxy" remote="b2g" revision="827214fcf38d6569aeb5c6d6f31cb296d1f09272"/>
   <project name="valgrind" path="external/valgrind" remote="b2g" revision="daa61633c32b9606f58799a3186395fd2bbb8d8c"/>
   <project name="vex" path="external/VEX" remote="b2g" revision="47f031c320888fe9f3e656602588565b52d43010"/>
   <project name="apitrace" path="external/apitrace" remote="apitrace" revision="b6f8267794b8c7f2a33236d46a857a84388b8485"/>
--- a/b2g/config/flame/sources.xml
+++ b/b2g/config/flame/sources.xml
@@ -12,17 +12,17 @@
   <!--original fetch url was https://git.mozilla.org/releases-->
   <remote fetch="https://git.mozilla.org/releases" name="mozillaorg"/>
   <!-- B2G specific things. -->
   <project name="platform_build" path="build" remote="b2g" revision="0e94c080bee081a50aa2097527b0b40852f9143f">
     <copyfile dest="Makefile" src="core/root.mk"/>
   </project>
   <project name="librecovery" path="librecovery" remote="b2g" revision="891e5069c0ad330d8191bf8c7b879c814258c89f"/>
   <project name="fake-libdvm" path="dalvik" remote="b2g" revision="d50ae982b19f42f0b66d08b9eb306be81687869f"/>
-  <project name="gaia" path="gaia" remote="mozillaorg" revision="e64428c5b2dce5db90b75a5055077a04f4bd4819"/>
+  <project name="gaia" path="gaia" remote="mozillaorg" revision="8e09627d75acd4abced0ab81983b5b5de6d15881"/>
   <project name="gonk-misc" path="gonk-misc" remote="b2g" revision="45c54a55e31758f7e54e5eafe0d01d387f35897a"/>
   <project name="moztt" path="external/moztt" remote="b2g" revision="fe893bb760a3bb64375f62fdf4762a58c59df9ef"/>
   <project name="apitrace" path="external/apitrace" remote="apitrace" revision="b6f8267794b8c7f2a33236d46a857a84388b8485"/>
   <project name="valgrind" path="external/valgrind" remote="b2g" revision="daa61633c32b9606f58799a3186395fd2bbb8d8c"/>
   <project name="vex" path="external/VEX" remote="b2g" revision="47f031c320888fe9f3e656602588565b52d43010"/>
   <!-- Stock Android things -->
   <project groups="linux" name="platform/prebuilts/clang/linux-x86/3.1" path="prebuilts/clang/linux-x86/3.1" revision="e95b4ce22c825da44d14299e1190ea39a5260bde"/>
   <project groups="linux" name="platform/prebuilts/clang/linux-x86/3.2" path="prebuilts/clang/linux-x86/3.2" revision="471afab478649078ad7c75ec6b252481a59e19b8"/>
--- a/b2g/config/gaia.json
+++ b/b2g/config/gaia.json
@@ -1,9 +1,9 @@
 {
     "git": {
         "git_revision": "", 
         "remote": "", 
         "branch": ""
     }, 
-    "revision": "f155f8ab67a65c066730976e0b7cef0d39579a24", 
+    "revision": "db25ffb7b2e825b20123fc11ccc8a5ebe597f378", 
     "repo_path": "integration/gaia-central"
 }
--- a/b2g/config/hamachi/sources.xml
+++ b/b2g/config/hamachi/sources.xml
@@ -12,17 +12,17 @@
   <!--original fetch url was git://github.com/apitrace/-->
   <remote fetch="https://git.mozilla.org/external/apitrace" name="apitrace"/>
   <default remote="caf" revision="b2g/ics_strawberry" sync-j="4"/>
   <!-- Gonk specific things and forks -->
   <project name="platform_build" path="build" remote="b2g" revision="df362ace56338da8173d30d3e09e08c42c1accfa">
     <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="e64428c5b2dce5db90b75a5055077a04f4bd4819"/>
+  <project name="gaia.git" path="gaia" remote="mozillaorg" revision="8e09627d75acd4abced0ab81983b5b5de6d15881"/>
   <project name="gonk-misc" path="gonk-misc" remote="b2g" revision="45c54a55e31758f7e54e5eafe0d01d387f35897a"/>
   <project name="librecovery" path="librecovery" remote="b2g" revision="891e5069c0ad330d8191bf8c7b879c814258c89f"/>
   <project name="moztt" path="external/moztt" remote="b2g" revision="fe893bb760a3bb64375f62fdf4762a58c59df9ef"/>
   <project name="apitrace" path="external/apitrace" remote="apitrace" revision="b6f8267794b8c7f2a33236d46a857a84388b8485"/>
   <!-- Stock Android things -->
   <project name="platform/abi/cpp" path="abi/cpp" revision="6426040f1be4a844082c9769171ce7f5341a5528"/>
   <project name="platform/bionic" path="bionic" revision="d2eb6c7b6e1bc7643c17df2d9d9bcb1704d0b9ab"/>
   <project name="platform/bootable/recovery" path="bootable/recovery" revision="746bc48f34f5060f90801925dcdd964030c1ab6d"/>
--- a/b2g/config/helix/sources.xml
+++ b/b2g/config/helix/sources.xml
@@ -10,17 +10,17 @@
   <!--original fetch url was https://git.mozilla.org/releases-->
   <remote fetch="https://git.mozilla.org/releases" name="mozillaorg"/>
   <default remote="caf" revision="b2g/ics_strawberry" sync-j="4"/>
   <!-- Gonk specific things and forks -->
   <project name="platform_build" path="build" remote="b2g" revision="df362ace56338da8173d30d3e09e08c42c1accfa">
     <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="e64428c5b2dce5db90b75a5055077a04f4bd4819"/>
+  <project name="gaia.git" path="gaia" remote="mozillaorg" revision="8e09627d75acd4abced0ab81983b5b5de6d15881"/>
   <project name="gonk-misc" path="gonk-misc" remote="b2g" revision="45c54a55e31758f7e54e5eafe0d01d387f35897a"/>
   <project name="rilproxy" path="rilproxy" remote="b2g" revision="827214fcf38d6569aeb5c6d6f31cb296d1f09272"/>
   <project name="librecovery" path="librecovery" remote="b2g" revision="891e5069c0ad330d8191bf8c7b879c814258c89f"/>
   <project name="moztt" path="external/moztt" remote="b2g" revision="fe893bb760a3bb64375f62fdf4762a58c59df9ef"/>
   <project name="gonk-patches" path="patches" remote="b2g" revision="223a2421006e8f5da33f516f6891c87cae86b0f6"/>
   <!-- Stock Android things -->
   <project name="platform/abi/cpp" path="abi/cpp" revision="6426040f1be4a844082c9769171ce7f5341a5528"/>
   <project name="platform/bionic" path="bionic" revision="d2eb6c7b6e1bc7643c17df2d9d9bcb1704d0b9ab"/>
--- a/b2g/config/nexus-4/sources.xml
+++ b/b2g/config/nexus-4/sources.xml
@@ -12,17 +12,17 @@
   <!--original fetch url was https://git.mozilla.org/releases-->
   <remote fetch="https://git.mozilla.org/releases" name="mozillaorg"/>
   <!-- B2G specific things. -->
   <project name="platform_build" path="build" remote="b2g" revision="0e94c080bee081a50aa2097527b0b40852f9143f">
     <copyfile dest="Makefile" src="core/root.mk"/>
   </project>
   <project name="rilproxy" path="rilproxy" remote="b2g" revision="827214fcf38d6569aeb5c6d6f31cb296d1f09272"/>
   <project name="fake-libdvm" path="dalvik" remote="b2g" revision="d50ae982b19f42f0b66d08b9eb306be81687869f"/>
-  <project name="gaia" path="gaia" remote="mozillaorg" revision="e64428c5b2dce5db90b75a5055077a04f4bd4819"/>
+  <project name="gaia" path="gaia" remote="mozillaorg" revision="8e09627d75acd4abced0ab81983b5b5de6d15881"/>
   <project name="gonk-misc" path="gonk-misc" remote="b2g" revision="45c54a55e31758f7e54e5eafe0d01d387f35897a"/>
   <project name="moztt" path="external/moztt" remote="b2g" revision="fe893bb760a3bb64375f62fdf4762a58c59df9ef"/>
   <project name="apitrace" path="external/apitrace" remote="apitrace" revision="b6f8267794b8c7f2a33236d46a857a84388b8485"/>
   <project name="valgrind" path="external/valgrind" remote="b2g" revision="daa61633c32b9606f58799a3186395fd2bbb8d8c"/>
   <project name="vex" path="external/VEX" remote="b2g" revision="47f031c320888fe9f3e656602588565b52d43010"/>
   <!-- Stock Android things -->
   <project groups="linux" name="platform/prebuilts/clang/linux-x86/3.1" path="prebuilts/clang/linux-x86/3.1" revision="5c45f43419d5582949284eee9cef0c43d866e03b"/>
   <project groups="linux" name="platform/prebuilts/clang/linux-x86/3.2" path="prebuilts/clang/linux-x86/3.2" revision="3748b4168e7bd8d46457d4b6786003bc6a5223ce"/>
--- a/b2g/config/wasabi/sources.xml
+++ b/b2g/config/wasabi/sources.xml
@@ -12,17 +12,17 @@
   <!--original fetch url was git://github.com/apitrace/-->
   <remote fetch="https://git.mozilla.org/external/apitrace" name="apitrace"/>
   <default remote="caf" revision="ics_chocolate_rb4.2" sync-j="4"/>
   <!-- Gonk specific things and forks -->
   <project name="platform_build" path="build" remote="b2g" revision="df362ace56338da8173d30d3e09e08c42c1accfa">
     <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="e64428c5b2dce5db90b75a5055077a04f4bd4819"/>
+  <project name="gaia.git" path="gaia" remote="mozillaorg" revision="8e09627d75acd4abced0ab81983b5b5de6d15881"/>
   <project name="gonk-misc" path="gonk-misc" remote="b2g" revision="45c54a55e31758f7e54e5eafe0d01d387f35897a"/>
   <project name="rilproxy" path="rilproxy" remote="b2g" revision="827214fcf38d6569aeb5c6d6f31cb296d1f09272"/>
   <project name="librecovery" path="librecovery" remote="b2g" revision="891e5069c0ad330d8191bf8c7b879c814258c89f"/>
   <project name="moztt" path="external/moztt" remote="b2g" revision="fe893bb760a3bb64375f62fdf4762a58c59df9ef"/>
   <project name="apitrace" path="external/apitrace" remote="apitrace" revision="b6f8267794b8c7f2a33236d46a857a84388b8485"/>
   <project name="gonk-patches" path="patches" remote="b2g" revision="223a2421006e8f5da33f516f6891c87cae86b0f6"/>
   <!-- Stock Android things -->
   <project name="platform/abi/cpp" path="abi/cpp" revision="6426040f1be4a844082c9769171ce7f5341a5528"/>
--- a/browser/components/loop/LoopRooms.jsm
+++ b/browser/components/loop/LoopRooms.jsm
@@ -1,25 +1,29 @@
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this file,
  * You can obtain one at http://mozilla.org/MPL/2.0/. */
 "use strict";
 
 const {classes: Cc, interfaces: Ci, utils: Cu} = Components;
 
 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+Cu.import("resource://gre/modules/Services.jsm");
 const {MozLoopService, LOOP_SESSION_TYPE} = Cu.import("resource:///modules/loop/MozLoopService.jsm", {});
 XPCOMUtils.defineLazyModuleGetter(this, "Promise",
                                   "resource://gre/modules/Promise.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "Task",
                                   "resource://gre/modules/Task.jsm");
 XPCOMUtils.defineLazyGetter(this, "eventEmitter", function() {
   const {EventEmitter} = Cu.import("resource://gre/modules/devtools/event-emitter.js", {});
   return new EventEmitter();
 });
+XPCOMUtils.defineLazyGetter(this, "gLoopBundle", function() {
+  return Services.strings.createBundle('chrome://browser/locale/loop/loop.properties');
+});
 
 this.EXPORTED_SYMBOLS = ["LoopRooms", "roomsPushNotification"];
 
 // The maximum number of clients that we support currently.
 const CLIENT_MAX_SIZE = 2;
 
 const roomsPushNotification = function(version, channelID) {
   return LoopRoomsInternal.onNotification(version, channelID);
@@ -322,19 +326,26 @@ let LoopRoomsInternal = {
    * Joins a room
    *
    * @param {String} roomToken  The room token.
    * @param {Function} callback Function that will be invoked once the operation
    *                            finished. The first argument passed will be an
    *                            `Error` object or `null`.
    */
   join: function(roomToken, callback) {
+    let displayName;
+    if (MozLoopService.userProfile && MozLoopService.userProfile.email) {
+      displayName = MozLoopService.userProfile.email;
+    } else {
+      displayName = gLoopBundle.GetStringFromName("display_name_guest");
+    }
+
     this._postToRoom(roomToken, {
       action: "join",
-      displayName: MozLoopService.userProfile.email,
+      displayName: displayName,
       clientMaxSize: CLIENT_MAX_SIZE
     }, callback);
   },
 
   /**
    * Refreshes a room
    *
    * @param {String} roomToken    The room token.
--- a/browser/components/loop/MozLoopAPI.jsm
+++ b/browser/components/loop/MozLoopAPI.jsm
@@ -409,88 +409,51 @@ function injectLoopAPI(targetWindow) {
       enumerable: true,
       writable: true,
       value: function(expiryTimeSeconds) {
         MozLoopService.noteCallUrlExpiry(expiryTimeSeconds);
       }
     },
 
     /**
-     * Set any character preference under "loop."
+     * Set any preference under "loop."
      *
      * @param {String} prefName The name of the pref without the preceding "loop."
-     * @param {String} stringValue The value to set.
+     * @param {*} value The value to set.
+     * @param {Enum} prefType Type of preference, defined at Ci.nsIPrefBranch. Optional.
      *
      * Any errors thrown by the Mozilla pref API are logged to the console
      * and cause false to be returned.
      */
-    setLoopCharPref: {
+    setLoopPref: {
       enumerable: true,
       writable: true,
-      value: function(prefName, value) {
-        MozLoopService.setLoopCharPref(prefName, value);
+      value: function(prefName, value, prefType) {
+        MozLoopService.setLoopPref(prefName, value, prefType);
       }
     },
 
     /**
-     * Return any preference under "loop." that's coercible to a character
-     * preference.
+     * Return any preference under "loop.".
      *
      * @param {String} prefName The name of the pref without the preceding
      * "loop."
+     * @param {Enum} prefType Type of preference, defined at Ci.nsIPrefBranch. Optional.
      *
      * Any errors thrown by the Mozilla pref API are logged to the console
      * and cause null to be returned. This includes the case of the preference
      * not being found.
      *
-     * @return {String} on success, null on error
+     * @return {*} on success, null on error
      */
-    getLoopCharPref: {
-      enumerable: true,
-      writable: true,
-      value: function(prefName) {
-        return MozLoopService.getLoopCharPref(prefName);
-      }
-    },
-
-    /**
-     * Set any boolean preference under "loop."
-     *
-     * @param {String} prefName The name of the pref without the preceding "loop."
-     * @param {bool} value The value to set.
-     *
-     * Any errors thrown by the Mozilla pref API are logged to the console
-     * and cause false to be returned.
-     */
-    setLoopBoolPref: {
+    getLoopPref: {
       enumerable: true,
       writable: true,
-      value: function(prefName, value) {
-        MozLoopService.setLoopBoolPref(prefName, value);
-      }
-    },
-
-    /**
-     * Return any preference under "loop." that's coercible to a boolean
-     * preference.
-     *
-     * @param {String} prefName The name of the pref without the preceding
-     * "loop."
-     *
-     * Any errors thrown by the Mozilla pref API are logged to the console
-     * and cause null to be returned. This includes the case of the preference
-     * not being found.
-     *
-     * @return {String} on success, null on error
-     */
-    getLoopBoolPref: {
-      enumerable: true,
-      writable: true,
-      value: function(prefName) {
-        return MozLoopService.getLoopBoolPref(prefName);
+      value: function(prefName, prefType) {
+        return MozLoopService.getLoopPref(prefName);
       }
     },
 
     /**
      * Starts alerting the user about an incoming call
      */
     startAlerting: {
       enumerable: true,
@@ -610,22 +573,25 @@ function injectLoopAPI(targetWindow) {
       writable: true,
       value: function() {
         return MozLoopService.openFxASettings();
       },
     },
 
     /**
      * Opens the Getting Started tour in the browser.
+     *
+     * @param {String} aSrc
+     *   - The UI element that the user used to begin the tour, optional.
      */
     openGettingStartedTour: {
       enumerable: true,
       writable: true,
-      value: function() {
-        return MozLoopService.openGettingStartedTour();
+      value: function(aSrc) {
+        return MozLoopService.openGettingStartedTour(aSrc);
       },
     },
 
     /**
      * Copies passed string onto the system clipboard.
      *
      * @param {String} str The string to copy
      */
--- a/browser/components/loop/MozLoopService.jsm
+++ b/browser/components/loop/MozLoopService.jsm
@@ -1215,90 +1215,85 @@ this.MozLoopService = {
       return Services.prefs.getComplexValue("general.useragent.locale",
         Ci.nsISupportsString).data;
     } catch (ex) {
       return "en-US";
     }
   },
 
   /**
-   * Set any character preference under "loop.".
+   * Set any preference under "loop.".
    *
-   * @param {String} prefName The name of the pref without the preceding "loop."
-   * @param {String} value The value to set.
+   * @param {String} prefSuffix The name of the pref without the preceding "loop."
+   * @param {*} value The value to set.
+   * @param {Enum} prefType Type of preference, defined at Ci.nsIPrefBranch. Optional.
    *
    * Any errors thrown by the Mozilla pref API are logged to the console.
    */
-  setLoopCharPref: function(prefName, value) {
+  setLoopPref: function(prefSuffix, value, prefType) {
+    let prefName = "loop." + prefSuffix;
     try {
-      Services.prefs.setCharPref("loop." + prefName, value);
+      if (!prefType) {
+        prefType = Services.prefs.getPrefType(prefName);
+      }
+      switch (prefType) {
+        case Ci.nsIPrefBranch.PREF_STRING:
+          Services.prefs.setCharPref(prefName, value);
+          break;
+        case Ci.nsIPrefBranch.PREF_INT:
+          Services.prefs.setIntPref(prefName, value);
+          break;
+        case Ci.nsIPrefBranch.PREF_BOOL:
+          Services.prefs.setBoolPref(prefName, value);
+          break;
+        default:
+          log.error("invalid preference type setting " + prefName);
+          break;
+      }
     } catch (ex) {
-      log.error("setLoopCharPref had trouble setting " + prefName +
+      log.error("setLoopPref had trouble setting " + prefName +
         "; exception: " + ex);
     }
   },
 
   /**
-   * Return any preference under "loop." that's coercible to a character
-   * preference.
+   * Return any preference under "loop.".
    *
    * @param {String} prefName The name of the pref without the preceding
    * "loop."
+   * @param {Enum} prefType Type of preference, defined at Ci.nsIPrefBranch. Optional.
    *
    * Any errors thrown by the Mozilla pref API are logged to the console
    * and cause null to be returned. This includes the case of the preference
    * not being found.
    *
-   * @return {String} on success, null on error
+   * @return {*} on success, null on error
    */
-  getLoopCharPref: function(prefName) {
+  getLoopPref: function(prefSuffix, prefType) {
+    let prefName = "loop." + prefSuffix;
     try {
-      return Services.prefs.getCharPref("loop." + prefName);
-    } catch (ex) {
-      log.error("getLoopCharPref had trouble getting " + prefName +
-        "; exception: " + ex);
-      return null;
-    }
-  },
-
-  /**
-   * Set any boolean preference under "loop.".
-   *
-   * @param {String} prefName The name of the pref without the preceding "loop."
-   * @param {boolean} value The value to set.
-   *
-   * Any errors thrown by the Mozilla pref API are logged to the console.
-   */
-  setLoopBoolPref: function(prefName, value) {
-    try {
-      Services.prefs.setBoolPref("loop." + prefName, value);
+      if (!prefType) {
+        prefType = Services.prefs.getPrefType(prefName);
+      } else if (prefType != Services.prefs.getPrefType(prefName)) {
+        log.error("invalid type specified for preference");
+        return null;
+      }
+      switch (prefType) {
+        case Ci.nsIPrefBranch.PREF_STRING:
+          return Services.prefs.getCharPref(prefName);
+        case Ci.nsIPrefBranch.PREF_INT:
+          return Services.prefs.getIntPref(prefName);
+        case Ci.nsIPrefBranch.PREF_BOOL:
+          return Services.prefs.getBoolPref(prefName);
+        default:
+          log.error("invalid preference type getting " + prefName);
+          return null;
+      }
     } catch (ex) {
-      log.error("setLoopCharPref had trouble setting " + prefName +
-        "; exception: " + ex);
-    }
-  },
-
-  /**
-   * Return any preference under "loop." that's coercible to a character
-   * preference.
-   *
-   * @param {String} prefName The name of the pref without the preceding
-   * "loop."
-   *
-   * Any errors thrown by the Mozilla pref API are logged to the console
-   * and cause null to be returned. This includes the case of the preference
-   * not being found.
-   *
-   * @return {String} on success, null on error
-   */
-  getLoopBoolPref: function(prefName) {
-    try {
-      return Services.prefs.getBoolPref("loop." + prefName);
-    } catch (ex) {
-      log.error("getLoopBoolPref had trouble getting " + prefName +
+      log.error("getLoopPref had trouble getting " + prefName +
         "; exception: " + ex);
       return null;
     }
   },
 
   /**
    * Start the FxA login flow using the OAuth client and params from the Loop server.
    *
@@ -1401,22 +1396,28 @@ this.MozLoopService = {
       win.switchToTabHavingURI(url.toString(), true);
     } catch (ex) {
       log.error("Error opening FxA settings", ex);
     }
   }),
 
   /**
    * Opens the Getting Started tour in the browser.
+   *
+   * @param {String} aSrc
+   *   - The UI element that the user used to begin the tour, optional.
    */
-  openGettingStartedTour: Task.async(function() {
+  openGettingStartedTour: Task.async(function(aSrc = null) {
     try {
-      let url = Services.prefs.getCharPref("loop.gettingStarted.url");
+      let url = new URL(Services.prefs.getCharPref("loop.gettingStarted.url"));
+      if (aSrc) {
+        url.searchParams.set("source", aSrc);
+      }
       let win = Services.wm.getMostRecentWindow("navigator:browser");
-      win.switchToTabHavingURI(url, true);
+      win.switchToTabHavingURI(url, true, {replaceQueryString: true});
     } catch (ex) {
       log.error("Error opening Getting Started tour", ex);
     }
   }),
 
   /**
    * Performs a hawk based request to the loop server.
    *
--- a/browser/components/loop/content/conversation.html
+++ b/browser/components/loop/content/conversation.html
@@ -29,18 +29,19 @@
     <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/feedbackApiClient.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>
+    <script type="text/javascript" src="loop/shared/js/roomStore.js"></script>
     <script type="text/javascript" src="loop/shared/js/conversationStore.js"></script>
-    <script type="text/javascript" src="loop/shared/js/roomStore.js"></script>
     <script type="text/javascript" src="loop/shared/js/activeRoomStore.js"></script>
     <script type="text/javascript" src="loop/js/conversationViews.js"></script>
     <script type="text/javascript" src="loop/shared/js/websocket.js"></script>
     <script type="text/javascript" src="loop/js/conversationAppStore.js"></script>
     <script type="text/javascript" src="loop/js/client.js"></script>
     <script type="text/javascript" src="loop/js/conversationViews.js"></script>
     <script type="text/javascript" src="loop/js/roomViews.js"></script>
     <script type="text/javascript" src="loop/js/conversation.js"></script>
--- a/browser/components/loop/content/js/conversation.js
+++ b/browser/components/loop/content/js/conversation.js
@@ -293,23 +293,23 @@ loop.conversation = (function(mozL10n) {
           if (this.state.callFailed) {
             return GenericFailureView({
               cancelCall: this.closeWindow.bind(this)}
             );
           }
 
           document.title = mozL10n.get("conversation_has_ended");
 
-          var feebackAPIBaseUrl = navigator.mozLoop.getLoopCharPref(
+          var feebackAPIBaseUrl = navigator.mozLoop.getLoopPref(
             "feedback.baseUrl");
 
           var appVersionInfo = navigator.mozLoop.appVersionInfo;
 
           var feedbackClient = new loop.FeedbackAPIClient(feebackAPIBaseUrl, {
-            product: navigator.mozLoop.getLoopCharPref("feedback.product"),
+            product: navigator.mozLoop.getLoopPref("feedback.product"),
             platform: appVersionInfo.OS,
             channel: appVersionInfo.channel,
             version: appVersionInfo.version
           });
 
           return (
             sharedViews.FeedbackView({
               feedbackApiClient: feedbackClient, 
@@ -611,20 +611,20 @@ loop.conversation = (function(mozL10n) {
     // Do the initial L10n setup, we do this before anything
     // else to ensure the L10n environment is setup correctly.
     mozL10n.initialize(navigator.mozLoop);
 
     // Plug in an alternate client ID mechanism, as localStorage and cookies
     // don't work in the conversation window
     window.OT.overrideGuidStorage({
       get: function(callback) {
-        callback(null, navigator.mozLoop.getLoopCharPref("ot.guid"));
+        callback(null, navigator.mozLoop.getLoopPref("ot.guid"));
       },
       set: function(guid, callback) {
-        navigator.mozLoop.setLoopCharPref("ot.guid", guid);
+        navigator.mozLoop.setLoopPref("ot.guid", guid);
         callback(null);
       }
     });
 
     var dispatcher = new loop.Dispatcher();
     var client = new loop.Client();
     var sdkDriver = new loop.OTSdkDriver({
       dispatcher: dispatcher,
@@ -636,23 +636,21 @@ loop.conversation = (function(mozL10n) {
       dispatcher: dispatcher,
       mozLoop: navigator.mozLoop
     });
     var conversationStore = new loop.store.ConversationStore({}, {
       client: client,
       dispatcher: dispatcher,
       sdkDriver: sdkDriver
     });
-    var activeRoomStore = new loop.store.ActiveRoomStore({
-      dispatcher: dispatcher,
+    var activeRoomStore = new loop.store.ActiveRoomStore(dispatcher, {
       mozLoop: navigator.mozLoop,
       sdkDriver: sdkDriver
     });
-    var roomStore = new loop.store.RoomStore({
-      dispatcher: dispatcher,
+    var roomStore = new loop.store.RoomStore(dispatcher, {
       mozLoop: navigator.mozLoop,
       activeRoomStore: activeRoomStore
     });
 
     // XXX Old class creation for the incoming conversation view, whilst
     // we transition across (bug 1072323).
     var conversation = new sharedModels.ConversationModel(
       {},                // Model attributes
--- a/browser/components/loop/content/js/conversation.jsx
+++ b/browser/components/loop/content/js/conversation.jsx
@@ -293,23 +293,23 @@ loop.conversation = (function(mozL10n) {
           if (this.state.callFailed) {
             return <GenericFailureView
               cancelCall={this.closeWindow.bind(this)}
             />;
           }
 
           document.title = mozL10n.get("conversation_has_ended");
 
-          var feebackAPIBaseUrl = navigator.mozLoop.getLoopCharPref(
+          var feebackAPIBaseUrl = navigator.mozLoop.getLoopPref(
             "feedback.baseUrl");
 
           var appVersionInfo = navigator.mozLoop.appVersionInfo;
 
           var feedbackClient = new loop.FeedbackAPIClient(feebackAPIBaseUrl, {
-            product: navigator.mozLoop.getLoopCharPref("feedback.product"),
+            product: navigator.mozLoop.getLoopPref("feedback.product"),
             platform: appVersionInfo.OS,
             channel: appVersionInfo.channel,
             version: appVersionInfo.version
           });
 
           return (
             <sharedViews.FeedbackView
               feedbackApiClient={feedbackClient}
@@ -611,20 +611,20 @@ loop.conversation = (function(mozL10n) {
     // Do the initial L10n setup, we do this before anything
     // else to ensure the L10n environment is setup correctly.
     mozL10n.initialize(navigator.mozLoop);
 
     // Plug in an alternate client ID mechanism, as localStorage and cookies
     // don't work in the conversation window
     window.OT.overrideGuidStorage({
       get: function(callback) {
-        callback(null, navigator.mozLoop.getLoopCharPref("ot.guid"));
+        callback(null, navigator.mozLoop.getLoopPref("ot.guid"));
       },
       set: function(guid, callback) {
-        navigator.mozLoop.setLoopCharPref("ot.guid", guid);
+        navigator.mozLoop.setLoopPref("ot.guid", guid);
         callback(null);
       }
     });
 
     var dispatcher = new loop.Dispatcher();
     var client = new loop.Client();
     var sdkDriver = new loop.OTSdkDriver({
       dispatcher: dispatcher,
@@ -636,23 +636,21 @@ loop.conversation = (function(mozL10n) {
       dispatcher: dispatcher,
       mozLoop: navigator.mozLoop
     });
     var conversationStore = new loop.store.ConversationStore({}, {
       client: client,
       dispatcher: dispatcher,
       sdkDriver: sdkDriver
     });
-    var activeRoomStore = new loop.store.ActiveRoomStore({
-      dispatcher: dispatcher,
+    var activeRoomStore = new loop.store.ActiveRoomStore(dispatcher, {
       mozLoop: navigator.mozLoop,
       sdkDriver: sdkDriver
     });
-    var roomStore = new loop.store.RoomStore({
-      dispatcher: dispatcher,
+    var roomStore = new loop.store.RoomStore(dispatcher, {
       mozLoop: navigator.mozLoop,
       activeRoomStore: activeRoomStore
     });
 
     // XXX Old class creation for the incoming conversation view, whilst
     // we transition across (bug 1072323).
     var conversation = new sharedModels.ConversationModel(
       {},                // Model attributes
--- a/browser/components/loop/content/js/conversationViews.js
+++ b/browser/components/loop/content/js/conversationViews.js
@@ -458,23 +458,23 @@ loop.conversationViews = (function(mozL1
 
     /**
      * Used to setup and render the feedback view.
      */
     _renderFeedbackView: function() {
       document.title = mozL10n.get("conversation_has_ended");
 
       // XXX Bug 1076754 Feedback view should be redone in the Flux style.
-      var feebackAPIBaseUrl = navigator.mozLoop.getLoopCharPref(
+      var feebackAPIBaseUrl = navigator.mozLoop.getLoopPref(
         "feedback.baseUrl");
 
       var appVersionInfo = navigator.mozLoop.appVersionInfo;
 
       var feedbackClient = new loop.FeedbackAPIClient(feebackAPIBaseUrl, {
-        product: navigator.mozLoop.getLoopCharPref("feedback.product"),
+        product: navigator.mozLoop.getLoopPref("feedback.product"),
         platform: appVersionInfo.OS,
         channel: appVersionInfo.channel,
         version: appVersionInfo.version
       });
 
       return (
         sharedViews.FeedbackView({
           feedbackApiClient: feedbackClient, 
--- a/browser/components/loop/content/js/conversationViews.jsx
+++ b/browser/components/loop/content/js/conversationViews.jsx
@@ -458,23 +458,23 @@ loop.conversationViews = (function(mozL1
 
     /**
      * Used to setup and render the feedback view.
      */
     _renderFeedbackView: function() {
       document.title = mozL10n.get("conversation_has_ended");
 
       // XXX Bug 1076754 Feedback view should be redone in the Flux style.
-      var feebackAPIBaseUrl = navigator.mozLoop.getLoopCharPref(
+      var feebackAPIBaseUrl = navigator.mozLoop.getLoopPref(
         "feedback.baseUrl");
 
       var appVersionInfo = navigator.mozLoop.appVersionInfo;
 
       var feedbackClient = new loop.FeedbackAPIClient(feebackAPIBaseUrl, {
-        product: navigator.mozLoop.getLoopCharPref("feedback.product"),
+        product: navigator.mozLoop.getLoopPref("feedback.product"),
         platform: appVersionInfo.OS,
         channel: appVersionInfo.channel,
         version: appVersionInfo.version
       });
 
       return (
         <sharedViews.FeedbackView
           feedbackApiClient={feedbackClient}
--- a/browser/components/loop/content/js/panel.js
+++ b/browser/components/loop/content/js/panel.js
@@ -35,17 +35,17 @@ loop.panel = (function(_, mozL10n) {
     },
 
     getInitialState: function() {
       // XXX Work around props.selectedTab being undefined initially.
       // When we don't need to rely on the pref, this can move back to
       // getDefaultProps (bug 1100258).
       return {
         selectedTab: this.props.selectedTab ||
-          (navigator.mozLoop.getLoopBoolPref("rooms.enabled") ?
+          (navigator.mozLoop.getLoopPref("rooms.enabled") ?
             "rooms" : "call")
       };
     },
 
     handleSelectTab: function(event) {
       var tabName = event.target.dataset.tabName;
       this.setState({selectedTab: tabName});
     },
@@ -161,25 +161,25 @@ loop.panel = (function(_, mozL10n) {
           )
         )
       );
     }
   });
 
   var GettingStartedView = React.createClass({displayName: 'GettingStartedView',
     componentDidMount: function() {
-      navigator.mozLoop.setLoopBoolPref("gettingStarted.seen", true);
+      navigator.mozLoop.setLoopPref("gettingStarted.seen", true);
     },
 
     handleButtonClick: function() {
       navigator.mozLoop.openGettingStartedTour();
     },
 
     render: function() {
-      if (navigator.mozLoop.getLoopBoolPref("gettingStarted.seen")) {
+      if (navigator.mozLoop.getLoopPref("gettingStarted.seen")) {
         return null;
       }
       return (
         React.DOM.div({id: "fte-getstarted"}, 
           React.DOM.header({id: "fte-title"}, 
             mozL10n.get("first_time_experience_title", {
               "clientShortname": mozL10n.get("clientShortname2")
             })
@@ -189,24 +189,24 @@ loop.panel = (function(_, mozL10n) {
                   caption: mozL10n.get("first_time_experience_button_label")})
         )
       );
     }
   });
 
   var ToSView = React.createClass({displayName: 'ToSView',
     getInitialState: function() {
-      return {seenToS: navigator.mozLoop.getLoopCharPref('seenToS')};
+      return {seenToS: navigator.mozLoop.getLoopPref("seenToS")};
     },
 
     render: function() {
       if (this.state.seenToS == "unseen") {
         var locale = mozL10n.getLanguage();
-        var terms_of_use_url = navigator.mozLoop.getLoopCharPref('legal.ToS_url');
-        var privacy_notice_url = navigator.mozLoop.getLoopCharPref('legal.privacy_url');
+        var terms_of_use_url = navigator.mozLoop.getLoopPref('legal.ToS_url');
+        var privacy_notice_url = navigator.mozLoop.getLoopPref('legal.privacy_url');
         var tosHTML = mozL10n.get("legal_text_and_links3", {
           "clientShortname": mozL10n.get("clientShortname2"),
           "terms_of_use": React.renderComponentToStaticMarkup(
             React.DOM.a({href: terms_of_use_url, target: "_blank"}, 
               mozL10n.get("legal_text_tos")
             )
           ),
           "privacy_notice": React.renderComponentToStaticMarkup(
@@ -281,16 +281,20 @@ loop.panel = (function(_, mozL10n) {
         navigator.mozLoop.logInToFxA();
       }
     },
 
     _isSignedIn: function() {
       return !!navigator.mozLoop.userProfile;
     },
 
+    openGettingStartedTour: function() {
+      navigator.mozLoop.openGettingStartedTour("settingsMenu");
+    },
+
     render: function() {
       var cx = React.addons.classSet;
 
       // For now all of the menu entries require FxA so hide the whole gear if FxA is disabled.
       if (!navigator.mozLoop.fxAEnabled) {
         return null;
       }
 
@@ -303,16 +307,18 @@ loop.panel = (function(_, mozL10n) {
             SettingsDropdownEntry({label: mozL10n.get("settings_menu_item_settings"), 
                                    onClick: this.handleClickSettingsEntry, 
                                    displayed: false, 
                                    icon: "settings"}), 
             SettingsDropdownEntry({label: mozL10n.get("settings_menu_item_account"), 
                                    onClick: this.handleClickAccountEntry, 
                                    icon: "account", 
                                    displayed: this._isSignedIn()}), 
+            SettingsDropdownEntry({label: mozL10n.get("tour_label"), 
+                                   onClick: this.openGettingStartedTour}), 
             SettingsDropdownEntry({label: this._isSignedIn() ?
                                           mozL10n.get("settings_menu_item_signout") :
                                           mozL10n.get("settings_menu_item_signin"), 
                                    onClick: this.handleClickAuthEntry, 
                                    displayed: navigator.mozLoop.fxAEnabled, 
                                    icon: this._isSignedIn() ? "signout" : "signin"})
           )
         )
@@ -715,17 +721,17 @@ loop.panel = (function(_, mozL10n) {
           detailsButtonCallback: serviceError.error.friendlyDetailsButtonCallback,
         });
       } else {
         this.props.notifications.remove(this.props.notifications.get("service-error"));
       }
     },
 
     _roomsEnabled: function() {
-      return navigator.mozLoop.getLoopBoolPref("rooms.enabled");
+      return navigator.mozLoop.getLoopPref("rooms.enabled");
     },
 
     _onStatusChanged: function() {
       var profile = navigator.mozLoop.userProfile;
       var currUid = this.state.userProfile ? this.state.userProfile.uid : null;
       var newUid = profile ? profile.uid : null;
       if (currUid != newUid) {
         // On profile change (login, logout), switch back to the default tab.
@@ -840,19 +846,18 @@ loop.panel = (function(_, mozL10n) {
   function init() {
     // Do the initial L10n setup, we do this before anything
     // else to ensure the L10n environment is setup correctly.
     mozL10n.initialize(navigator.mozLoop);
 
     var client = new loop.Client();
     var notifications = new sharedModels.NotificationCollection();
     var dispatcher = new loop.Dispatcher();
-    var roomStore = new loop.store.RoomStore({
-      mozLoop: navigator.mozLoop,
-      dispatcher: dispatcher
+    var roomStore = new loop.store.RoomStore(dispatcher, {
+      mozLoop: navigator.mozLoop
     });
 
     React.renderComponent(PanelView({
       client: client, 
       notifications: notifications, 
       roomStore: roomStore, 
       dispatcher: dispatcher}
     ), document.querySelector("#main"));
--- a/browser/components/loop/content/js/panel.jsx
+++ b/browser/components/loop/content/js/panel.jsx
@@ -35,17 +35,17 @@ loop.panel = (function(_, mozL10n) {
     },
 
     getInitialState: function() {
       // XXX Work around props.selectedTab being undefined initially.
       // When we don't need to rely on the pref, this can move back to
       // getDefaultProps (bug 1100258).
       return {
         selectedTab: this.props.selectedTab ||
-          (navigator.mozLoop.getLoopBoolPref("rooms.enabled") ?
+          (navigator.mozLoop.getLoopPref("rooms.enabled") ?
             "rooms" : "call")
       };
     },
 
     handleSelectTab: function(event) {
       var tabName = event.target.dataset.tabName;
       this.setState({selectedTab: tabName});
     },
@@ -161,25 +161,25 @@ loop.panel = (function(_, mozL10n) {
           </ul>
         </div>
       );
     }
   });
 
   var GettingStartedView = React.createClass({
     componentDidMount: function() {
-      navigator.mozLoop.setLoopBoolPref("gettingStarted.seen", true);
+      navigator.mozLoop.setLoopPref("gettingStarted.seen", true);
     },
 
     handleButtonClick: function() {
       navigator.mozLoop.openGettingStartedTour();
     },
 
     render: function() {
-      if (navigator.mozLoop.getLoopBoolPref("gettingStarted.seen")) {
+      if (navigator.mozLoop.getLoopPref("gettingStarted.seen")) {
         return null;
       }
       return (
         <div id="fte-getstarted">
           <header id="fte-title">
             {mozL10n.get("first_time_experience_title", {
               "clientShortname": mozL10n.get("clientShortname2")
             })}
@@ -189,24 +189,24 @@ loop.panel = (function(_, mozL10n) {
                   caption={mozL10n.get("first_time_experience_button_label")} />
         </div>
       );
     }
   });
 
   var ToSView = React.createClass({
     getInitialState: function() {
-      return {seenToS: navigator.mozLoop.getLoopCharPref('seenToS')};
+      return {seenToS: navigator.mozLoop.getLoopPref("seenToS")};
     },
 
     render: function() {
       if (this.state.seenToS == "unseen") {
         var locale = mozL10n.getLanguage();
-        var terms_of_use_url = navigator.mozLoop.getLoopCharPref('legal.ToS_url');
-        var privacy_notice_url = navigator.mozLoop.getLoopCharPref('legal.privacy_url');
+        var terms_of_use_url = navigator.mozLoop.getLoopPref('legal.ToS_url');
+        var privacy_notice_url = navigator.mozLoop.getLoopPref('legal.privacy_url');
         var tosHTML = mozL10n.get("legal_text_and_links3", {
           "clientShortname": mozL10n.get("clientShortname2"),
           "terms_of_use": React.renderComponentToStaticMarkup(
             <a href={terms_of_use_url} target="_blank">
               {mozL10n.get("legal_text_tos")}
             </a>
           ),
           "privacy_notice": React.renderComponentToStaticMarkup(
@@ -281,16 +281,20 @@ loop.panel = (function(_, mozL10n) {
         navigator.mozLoop.logInToFxA();
       }
     },
 
     _isSignedIn: function() {
       return !!navigator.mozLoop.userProfile;
     },
 
+    openGettingStartedTour: function() {
+      navigator.mozLoop.openGettingStartedTour("settingsMenu");
+    },
+
     render: function() {
       var cx = React.addons.classSet;
 
       // For now all of the menu entries require FxA so hide the whole gear if FxA is disabled.
       if (!navigator.mozLoop.fxAEnabled) {
         return null;
       }
 
@@ -303,16 +307,18 @@ loop.panel = (function(_, mozL10n) {
             <SettingsDropdownEntry label={mozL10n.get("settings_menu_item_settings")}
                                    onClick={this.handleClickSettingsEntry}
                                    displayed={false}
                                    icon="settings" />
             <SettingsDropdownEntry label={mozL10n.get("settings_menu_item_account")}
                                    onClick={this.handleClickAccountEntry}
                                    icon="account"
                                    displayed={this._isSignedIn()} />
+            <SettingsDropdownEntry label={mozL10n.get("tour_label")}
+                                   onClick={this.openGettingStartedTour} />
             <SettingsDropdownEntry label={this._isSignedIn() ?
                                           mozL10n.get("settings_menu_item_signout") :
                                           mozL10n.get("settings_menu_item_signin")}
                                    onClick={this.handleClickAuthEntry}
                                    displayed={navigator.mozLoop.fxAEnabled}
                                    icon={this._isSignedIn() ? "signout" : "signin"} />
           </ul>
         </div>
@@ -715,17 +721,17 @@ loop.panel = (function(_, mozL10n) {
           detailsButtonCallback: serviceError.error.friendlyDetailsButtonCallback,
         });
       } else {
         this.props.notifications.remove(this.props.notifications.get("service-error"));
       }
     },
 
     _roomsEnabled: function() {
-      return navigator.mozLoop.getLoopBoolPref("rooms.enabled");
+      return navigator.mozLoop.getLoopPref("rooms.enabled");
     },
 
     _onStatusChanged: function() {
       var profile = navigator.mozLoop.userProfile;
       var currUid = this.state.userProfile ? this.state.userProfile.uid : null;
       var newUid = profile ? profile.uid : null;
       if (currUid != newUid) {
         // On profile change (login, logout), switch back to the default tab.
@@ -840,19 +846,18 @@ loop.panel = (function(_, mozL10n) {
   function init() {
     // Do the initial L10n setup, we do this before anything
     // else to ensure the L10n environment is setup correctly.
     mozL10n.initialize(navigator.mozLoop);
 
     var client = new loop.Client();
     var notifications = new sharedModels.NotificationCollection();
     var dispatcher = new loop.Dispatcher();
-    var roomStore = new loop.store.RoomStore({
-      mozLoop: navigator.mozLoop,
-      dispatcher: dispatcher
+    var roomStore = new loop.store.RoomStore(dispatcher, {
+      mozLoop: navigator.mozLoop
     });
 
     React.renderComponent(<PanelView
       client={client}
       notifications={notifications}
       roomStore={roomStore}
       dispatcher={dispatcher}
     />, document.querySelector("#main"));
--- a/browser/components/loop/content/js/roomViews.js
+++ b/browser/components/loop/content/js/roomViews.js
@@ -65,17 +65,17 @@ loop.roomViews = (function(mozL10n) {
     propTypes: {
       dispatcher: React.PropTypes.instanceOf(loop.Dispatcher).isRequired
     },
 
     getInitialState: function() {
       return {
         copiedUrl: false,
         newRoomName: ""
-      }
+      };
     },
 
     handleFormSubmit: function(event) {
       event.preventDefault();
 
       var newRoomName = this.state.newRoomName;
 
       if (newRoomName && this.state.roomName != newRoomName) {
--- a/browser/components/loop/content/js/roomViews.jsx
+++ b/browser/components/loop/content/js/roomViews.jsx
@@ -65,17 +65,17 @@ loop.roomViews = (function(mozL10n) {
     propTypes: {
       dispatcher: React.PropTypes.instanceOf(loop.Dispatcher).isRequired
     },
 
     getInitialState: function() {
       return {
         copiedUrl: false,
         newRoomName: ""
-      }
+      };
     },
 
     handleFormSubmit: function(event) {
       event.preventDefault();
 
       var newRoomName = this.state.newRoomName;
 
       if (newRoomName && this.state.roomName != newRoomName) {
--- a/browser/components/loop/content/panel.html
+++ b/browser/components/loop/content/panel.html
@@ -22,15 +22,16 @@
 
     <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>
+    <script type="text/javascript" src="loop/shared/js/store.js"></script>
     <script type="text/javascript" src="loop/shared/js/roomStore.js"></script>
     <script type="text/javascript" src="loop/shared/js/activeRoomStore.js"></script>
     <script type="text/javascript" src="loop/js/client.js"></script>
     <script type="text/javascript;version=1.8" src="loop/js/contacts.js"></script>
     <script type="text/javascript" src="loop/js/panel.js"></script>
  </body>
 </html>
--- a/browser/components/loop/content/shared/js/actions.js
+++ b/browser/components/loop/content/shared/js/actions.js
@@ -25,16 +25,18 @@ loop.shared.actions = (function() {
     this.name = name;
   }
 
   Action.define = function(name, schema) {
     return Action.bind(null, name, schema);
   };
 
   return {
+    Action: Action,
+
     /**
      * Get the window data for the provided window id
      */
     GetWindowData: Action.define("getWindowData", {
       windowId: String
     }),
 
     /**
--- a/browser/components/loop/content/shared/js/activeRoomStore.js
+++ b/browser/components/loop/content/shared/js/activeRoomStore.js
@@ -1,16 +1,17 @@
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this file,
  * You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 /* global loop:true */
 
 var loop = loop || {};
 loop.store = loop.store || {};
+
 loop.store.ActiveRoomStore = (function() {
   "use strict";
 
   var sharedActions = loop.shared.actions;
   var FAILURE_REASONS = loop.shared.utils.FAILURE_REASONS;
 
   // Error numbers taken from
   // https://github.com/mozilla-services/loop-server/blob/master/loop/errno.json
@@ -35,90 +36,62 @@ loop.store.ActiveRoomStore = (function()
     HAS_PARTICIPANTS: "room-has-participants",
     // There was an issue with the room
     FAILED: "room-failed",
     // The room is full
     FULL: "room-full"
   };
 
   /**
-   * Store for things that are local to this instance (in this profile, on
-   * this machine) of this roomRoom store, in addition to a mirror of some
-   * remote-state.
+   * Active room store.
    *
-   * @extends {Backbone.Events}
-   *
-   * @param {Object}          options - Options object
-   * @param {loop.Dispatcher} options.dispatch - The dispatcher for dispatching
-   *                            actions and registering to consume them.
-   * @param {MozLoop}         options.mozLoop - MozLoop API provider object
+   * @param {loop.Dispatcher} dispatcher  The dispatcher for dispatching actions
+   *                                      and registering to consume actions.
+   * @param {Object} options Options object:
+   * - {mozLoop}     mozLoop    The MozLoop API object.
+   * - {OTSdkDriver} sdkDriver  The SDK driver instance.
    */
-  function ActiveRoomStore(options) {
-    options = options || {};
-
-    if (!options.dispatcher) {
-      throw new Error("Missing option dispatcher");
-    }
-    this._dispatcher = options.dispatcher;
-
-    if (!options.mozLoop) {
-      throw new Error("Missing option mozLoop");
-    }
-    this._mozLoop = options.mozLoop;
-
-    if (!options.sdkDriver) {
-      throw new Error("Missing option sdkDriver");
-    }
-    this._sdkDriver = options.sdkDriver;
-
-    // XXX Further actions are registered in setupWindowData and
-    // fetchServerData when we know what window type this is. At some stage,
-    // we might want to consider store mixins or some alternative which
-    // means the stores would only be created when we want them.
-    this._dispatcher.register(this, [
-      "setupWindowData",
-      "fetchServerData"
-    ]);
-
-    /**
-     * Stored data reflecting the local state of a given room, used to drive
-     * the room's views.
-     *
-     * @see https://wiki.mozilla.org/Loop/Architecture/Rooms#GET_.2Frooms.2F.7Btoken.7D
-     *      for the main data. Additional properties below.
-     *
-     * @property {ROOM_STATES} roomState - the state of the room.
-     * @property {Error=} error - if the room is an error state, this will be
-     *                            set to an Error object reflecting the problem;
-     *                            otherwise it will be unset.
-     */
-    this._storeState = {
-      roomState: ROOM_STATES.INIT,
-      audioMuted: false,
-      videoMuted: false,
-      failureReason: undefined
-    };
-  }
-
-  ActiveRoomStore.prototype = _.extend({
+  var ActiveRoomStore = loop.store.createStore({
     /**
      * The time factor to adjust the expires time to ensure that we send a refresh
      * before the expiry. Currently set as 90%.
      */
     expiresTimeFactor: 0.9,
 
-    getStoreState: function() {
-      return this._storeState;
+    // XXX Further actions are registered in setupWindowData and
+    // fetchServerData when we know what window type this is. At some stage,
+    // we might want to consider store mixins or some alternative which
+    // means the stores would only be created when we want them.
+    actions: [
+      "setupWindowData",
+      "fetchServerData"
+    ],
+
+    initialize: function(options) {
+      if (!options.mozLoop) {
+        throw new Error("Missing option mozLoop");
+      }
+      this._mozLoop = options.mozLoop;
+
+      if (!options.sdkDriver) {
+        throw new Error("Missing option sdkDriver");
+      }
+      this._sdkDriver = options.sdkDriver;
     },
 
-    setStoreState: function(newState) {
-      for (var key in newState) {
-        this._storeState[key] = newState[key];
-      }
-      this.trigger("change");
+    /**
+     * Returns initial state data for this active room.
+     */
+    getInitialStoreState: function() {
+      return {
+        roomState: ROOM_STATES.INIT,
+        audioMuted: false,
+        videoMuted: false,
+        failureReason: undefined
+      };
     },
 
     /**
      * Handles a room failure.
      *
      * @param {sharedActions.RoomFailure} actionData
      */
     roomFailure: function(actionData) {
@@ -140,20 +113,20 @@ loop.store.ActiveRoomStore = (function()
         failureReason: getReason(actionData.error.errno),
         roomState: actionData.error.errno === SERVER_CODES.ROOM_FULL ?
           ROOM_STATES.FULL : ROOM_STATES.FAILED
       });
     },
 
     /**
      * Registers the actions with the dispatcher that this store is interested
-     * in.
+     * in after the initial setup has been performed.
      */
-    _registerActions: function() {
-      this._dispatcher.register(this, [
+    _registerPostSetupActions: function() {
+      this.dispatcher.register(this, [
         "roomFailure",
         "setupRoomInfo",
         "updateRoomInfo",
         "joinRoom",
         "joinedRoom",
         "connectedToSdkServers",
         "connectionFailure",
         "setMute",
@@ -173,43 +146,40 @@ loop.store.ActiveRoomStore = (function()
      * @param {sharedActions.SetupWindowData} actionData
      */
     setupWindowData: function(actionData) {
       if (actionData.type !== "room") {
         // Nothing for us to do here, leave it to other stores.
         return;
       }
 
-      this._registerActions();
+      this._registerPostSetupActions();
 
       this.setStoreState({
         roomState: ROOM_STATES.GATHER
       });
 
       // Get the window data from the mozLoop api.
       this._mozLoop.rooms.get(actionData.roomToken,
         function(error, roomData) {
           if (error) {
-            this._dispatcher.dispatch(new sharedActions.RoomFailure({
-              error: error
-            }));
+            this.dispatchAction(new sharedActions.RoomFailure({error: error}));
             return;
           }
 
-          this._dispatcher.dispatch(
-            new sharedActions.SetupRoomInfo({
-              roomToken: actionData.roomToken,
-              roomName: roomData.roomName,
-              roomOwner: roomData.roomOwner,
-              roomUrl: roomData.roomUrl
-            }));
+          this.dispatchAction(new sharedActions.SetupRoomInfo({
+            roomToken: actionData.roomToken,
+            roomName: roomData.roomName,
+            roomOwner: roomData.roomOwner,
+            roomUrl: roomData.roomUrl
+          }));
 
           // For the conversation window, we need to automatically
           // join the room.
-          this._dispatcher.dispatch(new sharedActions.JoinRoom());
+          this.dispatchAction(new sharedActions.JoinRoom());
         }.bind(this));
     },
 
     /**
      * Execute fetchServerData event action from the dispatcher. Although
      * this is to fetch the server data - for rooms on the standalone client,
      * we don't actually need to get any data. Therefore we just save the
      * data that is given to us for when the user chooses to join the room.
@@ -217,17 +187,17 @@ loop.store.ActiveRoomStore = (function()
      * @param {sharedActions.FetchServerData} actionData
      */
     fetchServerData: function(actionData) {
       if (actionData.windowType !== "room") {
         // Nothing for us to do here, leave it to other stores.
         return;
       }
 
-      this._registerActions();
+      this._registerPostSetupActions();
 
       this.setStoreState({
         roomToken: actionData.token,
         roomState: ROOM_STATES.READY
       });
 
       this._mozLoop.rooms.on("update:" + actionData.roomToken,
         this._handleRoomUpdate.bind(this));
@@ -267,17 +237,17 @@ loop.store.ActiveRoomStore = (function()
 
     /**
      * Handles room updates notified by the mozLoop rooms API.
      *
      * @param {String} eventName The name of the event
      * @param {Object} roomData  The new roomData.
      */
     _handleRoomUpdate: function(eventName, roomData) {
-      this._dispatcher.dispatch(new sharedActions.UpdateRoomInfo({
+      this.dispatchAction(new sharedActions.UpdateRoomInfo({
         roomName: roomData.roomName,
         roomOwner: roomData.roomOwner,
         roomUrl: roomData.roomUrl
       }));
     },
 
     /**
      * Handles the action to join to a room.
@@ -286,22 +256,21 @@ loop.store.ActiveRoomStore = (function()
       // Reset the failure reason if necessary.
       if (this.getStoreState().failureReason) {
         this.setStoreState({failureReason: undefined});
       }
 
       this._mozLoop.rooms.join(this._storeState.roomToken,
         function(error, responseData) {
           if (error) {
-            this._dispatcher.dispatch(
-              new sharedActions.RoomFailure({error: error}));
+            this.dispatchAction(new sharedActions.RoomFailure({error: error}));
             return;
           }
 
-          this._dispatcher.dispatch(new sharedActions.JoinedRoom({
+          this.dispatchAction(new sharedActions.JoinedRoom({
             apiKey: responseData.apiKey,
             sessionToken: responseData.sessionToken,
             sessionId: responseData.sessionId,
             expires: responseData.expires
           }));
         }.bind(this));
     },
 
@@ -364,17 +333,17 @@ loop.store.ActiveRoomStore = (function()
      * Handles recording when a remote peer has connected to the servers.
      */
     remotePeerConnected: function() {
       this.setStoreState({
         roomState: ROOM_STATES.HAS_PARTICIPANTS
       });
 
       // We've connected with a third-party, therefore stop displaying the ToS etc.
-      this._mozLoop.setLoopCharPref("seenToS", "seen");
+      this._mozLoop.setLoopPref("seenToS", "seen");
     },
 
     /**
      * Handles a remote peer disconnecting from the session.
      */
     remotePeerDisconnected: function() {
       // As we only support two users at the moment, we just set this
       // back to joined.
@@ -415,18 +384,17 @@ loop.store.ActiveRoomStore = (function()
      * Refreshes the membership of the room with the server, and then
      * sets up the refresh for the next cycle.
      */
     _refreshMembership: function() {
       this._mozLoop.rooms.refreshMembership(this._storeState.roomToken,
         this._storeState.sessionToken,
         function(error, responseData) {
           if (error) {
-            this._dispatcher.dispatch(
-              new sharedActions.RoomFailure({error: error}));
+            this.dispatchAction(new sharedActions.RoomFailure({error: error}));
             return;
           }
 
           this._setRefreshTimeout(responseData.expires);
         }.bind(this));
     },
 
     /**
@@ -454,14 +422,12 @@ loop.store.ActiveRoomStore = (function()
         this._mozLoop.rooms.leave(this._storeState.roomToken,
           this._storeState.sessionToken);
       }
 
       this.setStoreState({
         roomState: nextState ? nextState : ROOM_STATES.READY
       });
     }
-
-  }, Backbone.Events);
+  });
 
   return ActiveRoomStore;
-
 })();
--- a/browser/components/loop/content/shared/js/conversationStore.js
+++ b/browser/components/loop/content/shared/js/conversationStore.js
@@ -48,16 +48,17 @@ loop.store.ConversationStore = (function
     // The call ended successfully.
     FINISHED: "cs-finished",
     // The user has finished with the window.
     CLOSE: "cs-close",
     // The call was terminated due to an issue during connection.
     TERMINATED: "cs-terminated"
   };
 
+  // XXX this needs to migrate to use loop.store.createStore
   var ConversationStore = Backbone.Model.extend({
     defaults: {
       // The id of the window. Currently used for getting the window id.
       windowId: undefined,
       // The current state of the call
       callState: CALL_STATES.INIT,
       // The reason if a call was terminated
       callStateReason: undefined,
--- a/browser/components/loop/content/shared/js/roomStore.js
+++ b/browser/components/loop/content/shared/js/roomStore.js
@@ -42,129 +42,77 @@ loop.store = loop.store || {};
     }
   }
 
   loop.store.Room = Room;
 
   /**
    * Room store.
    *
-   * Options:
-   * - {loop.Dispatcher} dispatcher       The dispatcher for dispatching actions
+   * @param {loop.Dispatcher} dispatcher  The dispatcher for dispatching actions
    *                                      and registering to consume actions.
+   * @param {Object} options Options object:
    * - {mozLoop}         mozLoop          The MozLoop API object.
    * - {ActiveRoomStore} activeRoomStore  An optional substore for active room
    *                                      state.
-   *
-   * @extends {Backbone.Events}
-   * @param {Object} options Options object.
    */
-  function RoomStore(options) {
-    options = options || {};
-
-    if (!options.dispatcher) {
-      throw new Error("Missing option dispatcher");
-    }
-    this._dispatcher = options.dispatcher;
-
-    if (!options.mozLoop) {
-      throw new Error("Missing option mozLoop");
-    }
-    this._mozLoop = options.mozLoop;
-
-    if (options.activeRoomStore) {
-      this.activeRoomStore = options.activeRoomStore;
-      this.setStoreState({activeRoom: this.activeRoomStore.getStoreState()});
-      this.activeRoomStore.on("change",
-                              this._onActiveRoomStoreChange.bind(this));
-    }
-
-    this._dispatcher.register(this, [
-      "createRoom",
-      "createRoomError",
-      "copyRoomUrl",
-      "deleteRoom",
-      "deleteRoomError",
-      "emailRoomUrl",
-      "getAllRooms",
-      "getAllRoomsError",
-      "openRoom",
-      "renameRoom",
-      "updateRoomList"
-    ]);
-  }
-
-  RoomStore.prototype = _.extend({
+  loop.store.RoomStore = loop.store.createStore({
     /**
      * Maximum size given to createRoom; only 2 is supported (and is
      * always passed) because that's what the user-experience is currently
      * designed and tested to handle.
      * @type {Number}
      */
     maxRoomCreationSize: 2,
 
     /**
      * The number of hours for which the room will exist - default 8 weeks
      * @type {Number}
      */
     defaultExpiresIn: 24 * 7 * 8,
 
     /**
-     * Internal store state representation.
-     * @type {Object}
-     * @see  #getStoreState
+     * Registered actions.
+     * @type {Array}
      */
-    _storeState: {
-      activeRoom: {},
-      error: null,
-      pendingCreation: false,
-      pendingInitialRetrieval: false,
-      rooms: []
+    actions: [
+      "createRoom",
+      "createRoomError",
+      "copyRoomUrl",
+      "deleteRoom",
+      "deleteRoomError",
+      "emailRoomUrl",
+      "getAllRooms",
+      "getAllRoomsError",
+      "openRoom",
+      "renameRoom",
+      "updateRoomList"
+    ],
+
+    initialize: function(options) {
+      if (!options.mozLoop) {
+        throw new Error("Missing option mozLoop");
+      }
+      this._mozLoop = options.mozLoop;
+
+      if (options.activeRoomStore) {
+        this.activeRoomStore = options.activeRoomStore;
+        this.activeRoomStore.on("change",
+                                this._onActiveRoomStoreChange.bind(this));
+      }
     },
 
-    /**
-     * Retrieves current store state. The returned state object holds the
-     * following properties:
-     *
-     * - {Boolean} pendingCreation         Pending room creation flag.
-     * - {Boolean} pendingInitialRetrieval Pending initial list retrieval flag.
-     * - {Array}   rooms                   The current room list.
-     * - {Error}   error                   Latest error encountered, if any.
-     * - {Object}  activeRoom              Active room data, if any.
-     *
-     * You can request a given state property by providing the `key` argument.
-     *
-     * @param  {String|undefined} key An optional state property name.
-     * @return {Object}
-     */
-    getStoreState: function(key) {
-      if (key) {
-        return this._storeState[key];
-      }
-      return this._storeState;
-    },
-
-    /**
-     * Updates store state and trigger a global "change" event, plus one for
-     * each provided newState property:
-     *
-     * - change:rooms
-     * - change:pendingInitialRetrieval
-     * - change:pendingCreation
-     * - change:error
-     * - change:activeRoom
-     *
-     * @param {Object} newState The new store state object.
-     */
-    setStoreState: function(newState) {
-      for (var key in newState) {
-        this._storeState[key] = newState[key];
-        this.trigger("change:" + key);
-      }
-      this.trigger("change");
+    getInitialStoreState: function() {
+      return {
+        activeRoom: this.activeRoomStore ? this.activeRoomStore.getStoreState() : {},
+        error: null,
+        pendingCreation: false,
+        pendingInitialRetrieval: false,
+        rooms: []
+      };
     },
 
     /**
      * Registers mozLoop.rooms events.
      */
     startListeningToRoomEvents: function() {
       // Rooms event registration
       this._mozLoop.rooms.on("add", this._onRoomAdded.bind(this));
@@ -175,61 +123,52 @@ loop.store = loop.store || {};
     /**
      * Updates active room store state.
      */
     _onActiveRoomStoreChange: function() {
       this.setStoreState({activeRoom: this.activeRoomStore.getStoreState()});
     },
 
     /**
-     * Local proxy helper to dispatch an action.
-     *
-     * @param {Action} action The action to dispatch.
-     */
-    _dispatchAction: function(action) {
-      this._dispatcher.dispatch(action);
-    },
-
-    /**
      * Updates current room list when a new room is available.
      *
      * @param {String} eventName     The event name (unused).
      * @param {Object} addedRoomData The added room data.
      */
     _onRoomAdded: function(eventName, addedRoomData) {
       addedRoomData.participants = [];
       addedRoomData.ctime = new Date().getTime();
-      this._dispatchAction(new sharedActions.UpdateRoomList({
+      this.dispatchAction(new sharedActions.UpdateRoomList({
         roomList: this._storeState.rooms.concat(new Room(addedRoomData))
       }));
     },
 
     /**
      * Executed when a room is updated.
      *
      * @param {String} eventName       The event name (unused).
      * @param {Object} updatedRoomData The updated room data.
      */
     _onRoomUpdated: function(eventName, updatedRoomData) {
-      this._dispatchAction(new sharedActions.UpdateRoomList({
+      this.dispatchAction(new sharedActions.UpdateRoomList({
         roomList: this._storeState.rooms.map(function(room) {
           return room.roomToken === updatedRoomData.roomToken ?
                  updatedRoomData : room;
         })
       }));
     },
 
     /**
      * Executed when a room is deleted.
      *
      * @param {String} eventName       The event name (unused).
      * @param {Object} removedRoomData The removed room data.
      */
     _onRoomRemoved: function(eventName, removedRoomData) {
-      this._dispatchAction(new sharedActions.UpdateRoomList({
+      this.dispatchAction(new sharedActions.UpdateRoomList({
         roomList: this._storeState.rooms.filter(function(room) {
           return room.roomToken !== removedRoomData.roomToken;
         })
       }));
     },
 
     /**
      * Maps and sorts the raw room list received from the mozLoop API.
@@ -298,17 +237,17 @@ loop.store = loop.store || {};
         roomOwner: actionData.roomOwner,
         maxSize:   this.maxRoomCreationSize,
         expiresIn: this.defaultExpiresIn
       };
 
       this._mozLoop.rooms.create(roomCreationData, function(err) {
         this.setStoreState({pendingCreation: false});
         if (err) {
-          this._dispatchAction(new sharedActions.CreateRoomError({error: err}));
+          this.dispatchAction(new sharedActions.CreateRoomError({error: err}));
         }
       }.bind(this));
     },
 
     /**
      * Executed when a room creation error occurs.
      *
      * @param {sharedActions.CreateRoomError} actionData The action data.
@@ -341,17 +280,17 @@ loop.store = loop.store || {};
     /**
      * Creates a new room.
      *
      * @param {sharedActions.DeleteRoom} actionData The action data.
      */
     deleteRoom: function(actionData) {
       this._mozLoop.rooms.delete(actionData.roomToken, function(err) {
         if (err) {
-         this._dispatchAction(new sharedActions.DeleteRoomError({error: err}));
+         this.dispatchAction(new sharedActions.DeleteRoomError({error: err}));
         }
       }.bind(this));
     },
 
     /**
      * Executed when a room deletion error occurs.
      *
      * @param {sharedActions.DeleteRoomError} actionData The action data.
@@ -371,17 +310,17 @@ loop.store = loop.store || {};
         this.setStoreState({pendingInitialRetrieval: false});
 
         if (err) {
           action = new sharedActions.GetAllRoomsError({error: err});
         } else {
           action = new sharedActions.UpdateRoomList({roomList: rawRoomList});
         }
 
-        this._dispatchAction(action);
+        this.dispatchAction(action);
 
         // We can only start listening to room events after getAll() has been
         // called executed first.
         this.startListeningToRoomEvents();
       }.bind(this));
     },
 
     /**
@@ -423,12 +362,10 @@ loop.store = loop.store || {};
       this._mozLoop.rooms.rename(actionData.roomToken, actionData.newRoomName,
         function(err) {
           if (err) {
             // XXX Give this a proper UI - bug 1100595.
             console.error("Failed to rename the room", err);
           }
         });
     }
-  }, Backbone.Events);
-
-  loop.store.RoomStore = RoomStore;
+  });
 })();
new file mode 100644
--- /dev/null
+++ b/browser/components/loop/content/shared/js/store.js
@@ -0,0 +1,96 @@
+/* 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/. */
+
+/* global loop:true */
+
+var loop = loop || {};
+loop.store = loop.store || {};
+
+loop.store.createStore = (function() {
+  "use strict";
+
+  var baseStorePrototype = {
+    __registerActions: function(actions) {
+      // check that store methods are implemented
+      actions.forEach(function(handler) {
+        if (typeof this[handler] !== "function") {
+          throw new Error("Store should implement an action handler for " +
+                          handler);
+        }
+      }, this);
+      this.dispatcher.register(this, actions);
+    },
+
+    /**
+     * Proxy helper for dispatching an action from this store.
+     *
+     * @param  {sharedAction.Action} action The action to dispatch.
+     */
+    dispatchAction: function(action) {
+      this.dispatcher.dispatch(action);
+    },
+
+    /**
+     * Returns current store state. You can request a given state property by
+     * providing the `key` argument.
+     *
+     * @param  {String|undefined} key An optional state property name.
+     * @return {Mixed}
+     */
+    getStoreState: function(key) {
+      return key ? this._storeState[key] : this._storeState;
+    },
+
+    /**
+     * Updates store state and trigger a global "change" event, plus one for
+     * each provided newState property.
+     *
+     * @param {Object} newState The new store state object.
+     */
+    setStoreState: function(newState) {
+      for (var key in newState) {
+        this._storeState[key] = newState[key];
+        this.trigger("change:" + key);
+      }
+      this.trigger("change");
+    }
+  };
+
+  /**
+   * Creates a new Store constructor.
+   *
+   * @param  {Object}   storeProto The store prototype.
+   * @return {Function}            A store constructor.
+   */
+  function createStore(storeProto) {
+    var BaseStore = function(dispatcher, options) {
+      options = options || {};
+
+      if (!dispatcher) {
+        throw new Error("Missing required dispatcher");
+      }
+      this.dispatcher = dispatcher;
+      if (Array.isArray(this.actions)) {
+        this.__registerActions(this.actions);
+      }
+
+      if (typeof this.initialize === "function") {
+        this.initialize(options);
+      }
+
+      if (typeof this.getInitialStoreState === "function") {
+        this._storeState = this.getInitialStoreState();
+      } else {
+        this._storeState = {};
+      }
+    };
+    BaseStore.prototype = _.extend({}, // destination object
+                                   Backbone.Events,
+                                   baseStorePrototype,
+                                   storeProto);
+    return BaseStore;
+  }
+
+  return createStore;
+})();
--- a/browser/components/loop/content/shared/js/utils.js
+++ b/browser/components/loop/content/shared/js/utils.js
@@ -43,17 +43,17 @@ loop.shared.utils = (function(mozL10n) {
    *
    * @param {String} prefName The name of the preference. Note that mozLoop adds
    *                          'loop.' to the start of the string.
    *
    * @return The value of the preference, or false if not available.
    */
   function getBoolPreference(prefName) {
     if (navigator.mozLoop) {
-      return !!navigator.mozLoop.getLoopBoolPref(prefName);
+      return !!navigator.mozLoop.getLoopPref(prefName);
     }
 
     return !!localStorage.getItem(prefName);
   }
 
   /**
    * Helper for general things
    */
@@ -105,17 +105,17 @@ loop.shared.utils = (function(mozL10n) {
     }
     navigator.mozLoop.composeEmail(
       mozL10n.get("share_email_subject4", {
         clientShortname: mozL10n.get("clientShortname2")
       }),
       mozL10n.get("share_email_body4", {
         callUrl: callUrl,
         clientShortname: mozL10n.get("clientShortname2"),
-        learnMoreUrl: navigator.mozLoop.getLoopCharPref("learnMoreUrl")
+        learnMoreUrl: navigator.mozLoop.getLoopPref("learnMoreUrl")
       }),
       recipient
     );
   }
 
   return {
     CALL_TYPES: CALL_TYPES,
     FAILURE_REASONS: FAILURE_REASONS,
--- a/browser/components/loop/jar.mn
+++ b/browser/components/loop/jar.mn
@@ -62,16 +62,17 @@ browser.jar:
   content/browser/loop/shared/img/02.png                        (content/shared/img/02.png)
   content/browser/loop/shared/img/02@2x.png                     (content/shared/img/02@2x.png)
   content/browser/loop/shared/img/telefonica.png                (content/shared/img/telefonica.png)
   content/browser/loop/shared/img/telefonica@2x.png             (content/shared/img/telefonica@2x.png)
 
   # Shared scripts
   content/browser/loop/shared/js/actions.js           (content/shared/js/actions.js)
   content/browser/loop/shared/js/conversationStore.js (content/shared/js/conversationStore.js)
+  content/browser/loop/shared/js/store.js             (content/shared/js/store.js)
   content/browser/loop/shared/js/roomStore.js         (content/shared/js/roomStore.js)
   content/browser/loop/shared/js/activeRoomStore.js   (content/shared/js/activeRoomStore.js)
   content/browser/loop/shared/js/dispatcher.js        (content/shared/js/dispatcher.js)
   content/browser/loop/shared/js/feedbackApiClient.js (content/shared/js/feedbackApiClient.js)
   content/browser/loop/shared/js/models.js            (content/shared/js/models.js)
   content/browser/loop/shared/js/mixins.js            (content/shared/js/mixins.js)
   content/browser/loop/shared/js/otSdkDriver.js       (content/shared/js/otSdkDriver.js)
   content/browser/loop/shared/js/views.js             (content/shared/js/views.js)
--- a/browser/components/loop/standalone/content/index.html
+++ b/browser/components/loop/standalone/content/index.html
@@ -37,17 +37,17 @@
           var evt = document.createEvent("CustomEvent");
           evt.initCustomEvent(event, params.bubbles, params.cancelable, params.detail);
           return evt;
         };
 
         myCustomEvent.prototype = window.Event.prototype;
         window.CustomEvent = myCustomEvent;
       }
- 
+
       // To support IE for the l10n-gaia library on IE <= 10.
       if (!"language" in navigator) {
         navigator.language = navigator.browserLanguage;
       }
 
       // To support IE <= 10.
       if (!window.MutationObserver) {
         // Define a dummy MutationObserver object if one doesn't exist
@@ -92,16 +92,17 @@
     <script type="text/javascript" src="shared/js/mixins.js"></script>
     <script type="text/javascript" src="shared/js/views.js"></script>
     <script type="text/javascript" src="shared/js/feedbackApiClient.js"></script>
     <script type="text/javascript" src="shared/js/actions.js"></script>
     <script type="text/javascript" src="shared/js/validate.js"></script>
     <script type="text/javascript" src="shared/js/dispatcher.js"></script>
     <script type="text/javascript" src="shared/js/websocket.js"></script>
     <script type="text/javascript" src="shared/js/otSdkDriver.js"></script>
+    <script type="text/javascript" src="shared/js/store.js"></script>
     <script type="text/javascript" src="shared/js/activeRoomStore.js"></script>
     <script type="text/javascript" src="js/standaloneAppStore.js"></script>
     <script type="text/javascript" src="js/standaloneClient.js"></script>
     <script type="text/javascript" src="js/standaloneMozLoop.js"></script>
     <script type="text/javascript" src="js/standaloneRoomViews.js"></script>
     <script type="text/javascript" src="js/webapp.js"></script>
 
     <script>
--- a/browser/components/loop/standalone/content/js/standaloneMozLoop.js
+++ b/browser/components/loop/standalone/content/js/standaloneMozLoop.js
@@ -194,29 +194,29 @@ loop.StandaloneMozLoop = (function(mozL1
     /**
      * Stores a preference in the local storage for standalone.
      * Note: Some prefs are filtered out as they are not applicable
      * to the standalone UI.
      *
      * @param {String} prefName The name of the pref
      * @param {String} value The value to set.
      */
-    setLoopCharPref: function(prefName, value) {
+    setLoopPref: function(prefName, value) {
       if (prefName === "seenToS") {
         return;
       }
 
       localStorage.setItem(prefName, value);
     },
 
     /**
      * Gets a preference from the local storage for standalone.
      *
      * @param {String} prefName The name of the pref
      * @param {String} value The value to set.
      */
-    getLoopCharPref: function(prefName) {
+    getLoopPref: function(prefName) {
       return localStorage.getItem(prefName);
     }
   };
 
   return StandaloneMozLoop;
 })(navigator.mozL10n);
--- a/browser/components/loop/standalone/content/js/webapp.js
+++ b/browser/components/loop/standalone/content/js/webapp.js
@@ -994,18 +994,17 @@ loop.webapp = (function($, _, OT, mozL10
     });
 
     var standaloneAppStore = new loop.store.StandaloneAppStore({
       conversation: conversation,
       dispatcher: dispatcher,
       helper: helper,
       sdk: OT
     });
-    var activeRoomStore = new loop.store.ActiveRoomStore({
-      dispatcher: dispatcher,
+    var activeRoomStore = new loop.store.ActiveRoomStore(dispatcher, {
       mozLoop: standaloneMozLoop,
       sdkDriver: sdkDriver
     });
 
     window.addEventListener("unload", function() {
       dispatcher.dispatch(new sharedActions.WindowUnload());
     });
 
--- a/browser/components/loop/standalone/content/js/webapp.jsx
+++ b/browser/components/loop/standalone/content/js/webapp.jsx
@@ -994,18 +994,17 @@ loop.webapp = (function($, _, OT, mozL10
     });
 
     var standaloneAppStore = new loop.store.StandaloneAppStore({
       conversation: conversation,
       dispatcher: dispatcher,
       helper: helper,
       sdk: OT
     });
-    var activeRoomStore = new loop.store.ActiveRoomStore({
-      dispatcher: dispatcher,
+    var activeRoomStore = new loop.store.ActiveRoomStore(dispatcher, {
       mozLoop: standaloneMozLoop,
       sdkDriver: sdkDriver
     });
 
     window.addEventListener("unload", function() {
       dispatcher.dispatch(new sharedActions.WindowUnload());
     });
 
--- a/browser/components/loop/test/desktop-local/client_test.js
+++ b/browser/components/loop/test/desktop-local/client_test.js
@@ -23,17 +23,17 @@ describe("loop.Client", function() {
       message: "invalid token"
     };
 
   beforeEach(function() {
     sandbox = sinon.sandbox.create();
     callback = sinon.spy();
     fakeToken = "fakeTokenText";
     mozLoop = {
-      getLoopCharPref: sandbox.stub()
+      getLoopPref: sandbox.stub()
         .returns(null)
         .withArgs("hawk-session-token")
         .returns(fakeToken),
       ensureRegistered: sinon.stub().callsArgWith(1, null),
       noteCallUrlExpiry: sinon.spy(),
       hawkRequest: sinon.stub(),
       LOOP_SESSION_TYPE: {
         GUEST: 1,
--- a/browser/components/loop/test/desktop-local/conversationViews_test.js
+++ b/browser/components/loop/test/desktop-local/conversationViews_test.js
@@ -39,17 +39,17 @@ describe("loop.conversationViews", funct
           return "audio/ogg";
       },
       responseType: null,
       response: new ArrayBuffer(10),
       onload: null
     };
 
     navigator.mozLoop = {
-      getLoopCharPref: sinon.stub().returns("http://fakeurl"),
+      getLoopPref: sinon.stub().returns("http://fakeurl"),
       composeEmail: sinon.spy(),
       get appVersionInfo() {
         return {
           version: "42",
           channel: "test",
           platform: "test"
         };
       },
--- a/browser/components/loop/test/desktop-local/conversation_test.js
+++ b/browser/components/loop/test/desktop-local/conversation_test.js
@@ -33,19 +33,24 @@ describe("loop.conversation", function()
     navigator.mozLoop = {
       doNotDisturb: true,
       getStrings: function() {
         return JSON.stringify({textContent: "fakeText"});
       },
       get locale() {
         return "en-US";
       },
-      setLoopCharPref: sinon.stub(),
-      getLoopCharPref: sinon.stub().returns("http://fakeurl"),
-      getLoopBoolPref: sinon.stub(),
+      setLoopPref: sinon.stub(),
+      getLoopPref: function(prefName) {
+        if (prefName == "debug.sdk") {
+          return false;
+        }
+
+        return "http://fake";
+      },
       calls: {
         clearCallInProgress: sinon.stub()
       },
       LOOP_SESSION_TYPE: {
         GUEST: 1,
         FXA: 2
       },
       startAlerting: sinon.stub(),
@@ -164,19 +169,18 @@ describe("loop.conversation", function()
             pref: true
           }]
         }
       }, {
         client: client,
         dispatcher: dispatcher,
         sdkDriver: {}
       });
-      roomStore = new loop.store.RoomStore({
+      roomStore = new loop.store.RoomStore(dispatcher, {
         mozLoop: navigator.mozLoop,
-        dispatcher: dispatcher
       });
       conversationAppStore = new loop.store.ConversationAppStore({
         dispatcher: dispatcher,
         mozLoop: navigator.mozLoop
       });
     });
 
     afterEach(function() {
@@ -642,17 +646,16 @@ describe("loop.conversation", function()
             return new Promise(function() {});
           },
           on: sandbox.spy()
         });
 
         icView = mountTestComponent();
 
         conversation.set("loopToken", "fakeToken");
-        navigator.mozLoop.getLoopCharPref.returns("http://fake");
         stubComponent(sharedView, "ConversationView");
       });
 
       describe("call:accepted", function() {
         it("should display the ConversationView",
           function() {
             conversation.accepted();
 
--- a/browser/components/loop/test/desktop-local/index.html
+++ b/browser/components/loop/test/desktop-local/index.html
@@ -38,16 +38,17 @@
   <script src="../../content/shared/js/models.js"></script>
   <script src="../../content/shared/js/mixins.js"></script>
   <script src="../../content/shared/js/views.js"></script>
   <script src="../../content/shared/js/websocket.js"></script>
   <script src="../../content/shared/js/actions.js"></script>
   <script src="../../content/shared/js/validate.js"></script>
   <script src="../../content/shared/js/dispatcher.js"></script>
   <script src="../../content/shared/js/otSdkDriver.js"></script>
+  <script src="../../content/shared/js/store.js"></script>
   <script src="../../content/shared/js/roomStore.js"></script>
   <script src="../../content/shared/js/activeRoomStore.js"></script>
   <script src="../../content/js/client.js"></script>
   <script src="../../content/js/conversationAppStore.js"></script>
   <script src="../../content/js/roomViews.js"></script>
   <script src="../../content/js/conversationViews.js"></script>
   <script src="../../content/js/conversation.js"></script>
   <script type="text/javascript;version=1.8" src="../../content/js/contacts.js"></script>
--- a/browser/components/loop/test/desktop-local/panel_test.js
+++ b/browser/components/loop/test/desktop-local/panel_test.js
@@ -29,20 +29,18 @@ describe("loop.panel", function() {
       doNotDisturb: true,
       fxAEnabled: true,
       getStrings: function() {
         return JSON.stringify({textContent: "fakeText"});
       },
       get locale() {
         return "en-US";
       },
-      setLoopCharPref: sandbox.stub(),
-      getLoopCharPref: sandbox.stub().returns("unseen"),
-      getLoopBoolPref: sandbox.stub(),
-      setLoopBoolPref: sandbox.stub(),
+      setLoopPref: sandbox.stub(),
+      getLoopPref: sandbox.stub().returns("unseen"),
       getPluralForm: function() {
         return "fakeText";
       },
       copyString: sandbox.stub(),
       noteCallUrlExpiry: sinon.spy(),
       composeEmail: sinon.spy(),
       telemetryAdd: sinon.spy(),
       contacts: {
@@ -151,18 +149,17 @@ describe("loop.panel", function() {
 
       fakeClient = {
         requestCallUrl: function(_, cb) {
           cb(null, callUrlData);
         }
       };
 
       dispatcher = new loop.Dispatcher();
-      roomStore = new loop.store.RoomStore({
-        dispatcher: dispatcher,
+      roomStore = new loop.store.RoomStore(dispatcher, {
         mozLoop: navigator.mozLoop
       });
     });
 
     function createTestPanelView() {
       return TestUtils.renderIntoDocument(loop.panel.PanelView({
         notifications: notifications,
         client: fakeClient,
@@ -172,17 +169,17 @@ describe("loop.panel", function() {
       }));
     }
 
     describe('TabView', function() {
       var view, callTab, roomsTab, contactsTab;
 
       describe("loop.rooms.enabled on", function() {
         beforeEach(function() {
-          navigator.mozLoop.getLoopBoolPref = function(pref) {
+          navigator.mozLoop.getLoopPref = function(pref) {
             if (pref === "rooms.enabled") {
               return true;
             }
           };
 
           view = createTestPanelView();
 
           [roomsTab, contactsTab] =
@@ -203,17 +200,17 @@ describe("loop.panel", function() {
 
           expect(roomsTab.getDOMNode().classList.contains("selected"))
             .to.be.true;
         });
       });
 
       describe("loop.rooms.enabled off", function() {
         beforeEach(function() {
-          navigator.mozLoop.getLoopBoolPref = function(pref) {
+          navigator.mozLoop.getLoopPref = function(pref) {
             if (pref === "rooms.enabled") {
               return false;
             }
           };
 
           view = createTestPanelView();
 
           [callTab, contactsTab] =
@@ -359,17 +356,17 @@ describe("loop.panel", function() {
     describe("#render", function() {
       it("should render a ToSView", function() {
         var view = createTestPanelView();
 
         TestUtils.findRenderedComponentWithType(view, loop.panel.ToSView);
       });
 
       it("should not render a ToSView when the view has been 'seen'", function() {
-        navigator.mozLoop.getLoopCharPref = function() {
+        navigator.mozLoop.getLoopPref = function() {
           return "seen";
         };
         var view = createTestPanelView();
 
         try {
           TestUtils.findRenderedComponentWithType(view, loop.panel.ToSView);
           sinon.assert.fail("Should not find the ToSView if it has been 'seen'");
         } catch (ex) {}
@@ -377,17 +374,17 @@ describe("loop.panel", function() {
 
       it("should render a GettingStarted view", function() {
         var view = createTestPanelView();
 
         TestUtils.findRenderedComponentWithType(view, loop.panel.GettingStartedView);
       });
 
       it("should not render a GettingStartedView when the view has been seen", function() {
-        navigator.mozLoop.getLoopBoolPref = function() {
+        navigator.mozLoop.getLoopPref = function() {
           return true;
         };
         var view = createTestPanelView();
 
         try {
           TestUtils.findRenderedComponentWithType(view, loop.panel.GettingStartedView);
           sinon.assert.fail("Should not find the GettingStartedView if it has been seen");
         } catch (ex) {}
@@ -783,18 +780,17 @@ describe("loop.panel", function() {
   });
 
   describe("loop.panel.RoomList", function() {
     var roomStore, dispatcher, fakeEmail;
 
     beforeEach(function() {
       fakeEmail = "fakeEmail@example.com";
       dispatcher = new loop.Dispatcher();
-      roomStore = new loop.store.RoomStore({
-        dispatcher: dispatcher,
+      roomStore = new loop.store.RoomStore(dispatcher, {
         mozLoop: navigator.mozLoop
       });
       roomStore.setStoreState({
         pendingCreation: false,
         pendingInitialRetrieval: false,
         rooms: [],
         error: undefined
       });
@@ -860,17 +856,17 @@ describe("loop.panel", function() {
     it("should render when the value of loop.seenToS is not set", function() {
       var view = TestUtils.renderIntoDocument(loop.panel.ToSView());
 
       TestUtils.findRenderedDOMComponentWithClass(view, "terms-service");
     });
 
     it("should not render when the value of loop.seenToS is set to 'seen'",
       function(done) {
-        navigator.mozLoop.getLoopCharPref = function() {
+        navigator.mozLoop.getLoopPref = function() {
           return "seen";
         };
 
         try {
           TestUtils.findRenderedDOMComponentWithClass(view, "tos");
         } catch (err) {
           done();
         }
--- a/browser/components/loop/test/desktop-local/roomViews_test.js
+++ b/browser/components/loop/test/desktop-local/roomViews_test.js
@@ -25,23 +25,21 @@ describe("loop.roomViews", function () {
     loop.shared.mixins.setRootObject(fakeWindow);
 
     // XXX These stubs should be hoisted in a common file
     // Bug 1040968
     sandbox.stub(document.mozL10n, "get", function(x) {
       return x;
     });
 
-    activeRoomStore = new loop.store.ActiveRoomStore({
-      dispatcher: dispatcher,
+    activeRoomStore = new loop.store.ActiveRoomStore(dispatcher, {
       mozLoop: {},
       sdkDriver: {}
     });
-    roomStore = new loop.store.RoomStore({
-      dispatcher: dispatcher,
+    roomStore = new loop.store.RoomStore(dispatcher, {
       mozLoop: {},
       activeRoomStore: activeRoomStore
     });
   });
 
   afterEach(function() {
     sandbox.restore();
     loop.shared.mixins.setRootObject(window);
--- a/browser/components/loop/test/mochitest/browser_mozLoop_prefs.js
+++ b/browser/components/loop/test/mochitest/browser_mozLoop_prefs.js
@@ -12,31 +12,31 @@ add_task(loadLoopPanel);
 
 add_task(function* test_mozLoop_charPref() {
   registerCleanupFunction(function () {
     Services.prefs.clearUserPref("loop.test");
   });
 
   Assert.ok(gMozLoopAPI, "mozLoop should exist");
 
-  // Test setLoopCharPref
-  gMozLoopAPI.setLoopCharPref("test", "foo");
+  // Test setLoopPref
+  gMozLoopAPI.setLoopPref("test", "foo", Ci.nsIPrefBranch.PREF_STRING);
   Assert.equal(Services.prefs.getCharPref("loop.test"), "foo",
                "should set loop pref value correctly");
 
-  // Test getLoopCharPref
-  Assert.equal(gMozLoopAPI.getLoopCharPref("test"), "foo",
+  // Test getLoopPref
+  Assert.equal(gMozLoopAPI.getLoopPref("test"), "foo",
                "should get loop pref value correctly");
 });
 
 add_task(function* test_mozLoop_boolPref() {
   registerCleanupFunction(function () {
     Services.prefs.clearUserPref("loop.testBool");
   });
 
   Assert.ok(gMozLoopAPI, "mozLoop should exist");
 
   Services.prefs.setBoolPref("loop.testBool", true);
 
-  // Test getLoopCharPref
-  Assert.equal(gMozLoopAPI.getLoopBoolPref("testBool"), true,
+  // Test getLoopPref
+  Assert.equal(gMozLoopAPI.getLoopPref("testBool"), true,
                "should get loop pref value correctly");
 });
--- a/browser/components/loop/test/shared/activeRoomStore_test.js
+++ b/browser/components/loop/test/shared/activeRoomStore_test.js
@@ -15,17 +15,17 @@ describe("loop.store.ActiveRoomStore", f
   beforeEach(function() {
     sandbox = sinon.sandbox.create();
     sandbox.useFakeTimers();
 
     dispatcher = new loop.Dispatcher();
     sandbox.stub(dispatcher, "dispatch");
 
     fakeMozLoop = {
-      setLoopCharPref: sandbox.stub(),
+      setLoopPref: sandbox.stub(),
       rooms: {
         get: sinon.stub(),
         join: sinon.stub(),
         refreshMembership: sinon.stub(),
         leave: sinon.stub(),
         on: sinon.stub(),
         off: sinon.stub()
       }
@@ -39,43 +39,36 @@ describe("loop.store.ActiveRoomStore", f
     fakeMultiplexGum = {
         reset: sandbox.spy()
     };
 
     loop.standaloneMedia = {
       multiplexGum: fakeMultiplexGum
     };
 
-    store = new loop.store.ActiveRoomStore({
-      dispatcher: dispatcher,
+    store = new loop.store.ActiveRoomStore(dispatcher, {
       mozLoop: fakeMozLoop,
       sdkDriver: fakeSdkDriver
     });
   });
 
   afterEach(function() {
     sandbox.restore();
   });
 
   describe("#constructor", function() {
-    it("should throw an error if the dispatcher is missing", function() {
-      expect(function() {
-        new loop.store.ActiveRoomStore({mozLoop: {}});
-      }).to.Throw(/dispatcher/);
-    });
-
     it("should throw an error if mozLoop is missing", function() {
       expect(function() {
-        new loop.store.ActiveRoomStore({dispatcher: dispatcher});
+        new loop.store.ActiveRoomStore(dispatcher);
       }).to.Throw(/mozLoop/);
     });
 
     it("should throw an error if sdkDriver is missing", function() {
       expect(function() {
-        new loop.store.ActiveRoomStore({dispatcher: dispatcher, mozLoop: {}});
+        new loop.store.ActiveRoomStore(dispatcher, {mozLoop: {}});
       }).to.Throw(/sdkDriver/);
     });
   });
 
   describe("#roomFailure", function() {
     var fakeError;
 
     beforeEach(function() {
@@ -138,16 +131,20 @@ describe("loop.store.ActiveRoomStore", f
     beforeEach(function() {
       fakeToken = "337-ff-54";
       fakeRoomData = {
         roomName: "Monkeys",
         roomOwner: "Alfred",
         roomUrl: "http://invalid"
       };
 
+      store = new loop.store.ActiveRoomStore(dispatcher, {
+        mozLoop: fakeMozLoop,
+        sdkDriver: {}
+      });
       fakeMozLoop.rooms.get.
         withArgs(fakeToken).
         callsArgOnWith(1, // index of callback argument
         store, // |this| to call it on
         null, // args to call the callback with...
         fakeRoomData
       );
     });
@@ -520,18 +517,18 @@ describe("loop.store.ActiveRoomStore", f
       store.remotePeerConnected();
 
       expect(store.getStoreState().roomState).eql(ROOM_STATES.HAS_PARTICIPANTS);
     });
 
     it("should set the pref for ToS to `seen`", function() {
       store.remotePeerConnected();
 
-      sinon.assert.calledOnce(fakeMozLoop.setLoopCharPref);
-      sinon.assert.calledWithExactly(fakeMozLoop.setLoopCharPref,
+      sinon.assert.calledOnce(fakeMozLoop.setLoopPref);
+      sinon.assert.calledWithExactly(fakeMozLoop.setLoopPref,
         "seenToS", "seen");
     });
   });
 
   describe("#remotePeerDisconnected", function() {
     it("should set the state to `SESSION_CONNECTED`", function() {
       store.remotePeerDisconnected();
 
--- a/browser/components/loop/test/shared/conversationStore_test.js
+++ b/browser/components/loop/test/shared/conversationStore_test.js
@@ -32,17 +32,17 @@ describe("loop.store.ConversationStore",
       email: [{
         type: "home",
         value: "fakeEmail",
         pref: true
       }]
     };
 
     navigator.mozLoop = {
-      getLoopBoolPref: sandbox.stub(),
+      getLoopPref: sandbox.stub(),
       calls: {
         setCallInProgress: sandbox.stub(),
         clearCallInProgress: sandbox.stub()
       }
     };
 
     dispatcher = new loop.Dispatcher();
     client = {
--- a/browser/components/loop/test/shared/index.html
+++ b/browser/components/loop/test/shared/index.html
@@ -38,32 +38,34 @@
   <script src="../../content/shared/js/mixins.js"></script>
   <script src="../../content/shared/js/views.js"></script>
   <script src="../../content/shared/js/websocket.js"></script>
   <script src="../../content/shared/js/feedbackApiClient.js"></script>
   <script src="../../content/shared/js/validate.js"></script>
   <script src="../../content/shared/js/actions.js"></script>
   <script src="../../content/shared/js/dispatcher.js"></script>
   <script src="../../content/shared/js/otSdkDriver.js"></script>
+  <script src="../../content/shared/js/store.js"></script>
   <script src="../../content/shared/js/activeRoomStore.js"></script>
+  <script src="../../content/shared/js/roomStore.js"></script>
   <script src="../../content/shared/js/conversationStore.js"></script>
-  <script src="../../content/shared/js/roomStore.js"></script>
 
   <!-- Test scripts -->
   <script src="models_test.js"></script>
   <script src="mixins_test.js"></script>
   <script src="utils_test.js"></script>
   <script src="views_test.js"></script>
   <script src="websocket_test.js"></script>
   <script src="feedbackApiClient_test.js"></script>
   <script src="validate_test.js"></script>
   <script src="dispatcher_test.js"></script>
   <script src="activeRoomStore_test.js"></script>
   <script src="conversationStore_test.js"></script>
   <script src="otSdkDriver_test.js"></script>
+  <script src="store_test.js"></script>
   <script src="roomStore_test.js"></script>
   <script>
     mocha.run(function () {
       $("#mocha").append("<p id='complete'>Complete.</p>");
     });
   </script>
 </body>
 </html>
--- a/browser/components/loop/test/shared/roomStore_test.js
+++ b/browser/components/loop/test/shared/roomStore_test.js
@@ -51,25 +51,19 @@ describe("loop.store.RoomStore", functio
     dispatcher = new loop.Dispatcher();
   });
 
   afterEach(function() {
     sandbox.restore();
   });
 
   describe("#constructor", function() {
-    it("should throw an error if the dispatcher is missing", function() {
-      expect(function() {
-        new loop.store.RoomStore({mozLoop: {}});
-      }).to.Throw(/dispatcher/);
-    });
-
     it("should throw an error if mozLoop is missing", function() {
       expect(function() {
-        new loop.store.RoomStore({dispatcher: dispatcher});
+        new loop.store.RoomStore(dispatcher);
       }).to.Throw(/mozLoop/);
     });
   });
 
   describe("constructed", function() {
     var fakeMozLoop, store;
 
     var defaultStoreState = {
@@ -84,20 +78,17 @@ describe("loop.store.RoomStore", functio
       fakeMozLoop = {
         copyString: function() {},
         rooms: {
           create: function() {},
           getAll: function() {},
           on: sandbox.stub()
         }
       };
-      store = new loop.store.RoomStore({
-        dispatcher: dispatcher,
-        mozLoop: fakeMozLoop
-      });
+      store = new loop.store.RoomStore(dispatcher, {mozLoop: fakeMozLoop});
       store.setStoreState(defaultStoreState);
     });
 
     describe("MozLoop rooms event listeners", function() {
       beforeEach(function() {
         _.extend(fakeMozLoop.rooms, Backbone.Events);
 
         fakeMozLoop.rooms.getAll = function(version, cb) {
@@ -149,29 +140,16 @@ describe("loop.store.RoomStore", functio
           expect(store.getStoreState().rooms).to.have.length.of(2);
           expect(store.getStoreState().rooms.some(function(room) {
             return room.roomToken === "_nxD4V4FflQ";
           })).eql(false);
         });
       });
     });
 
-    describe("#getStoreState", function() {
-      it("should retrieve the whole state by default", function() {
-        expect(store.getStoreState()).eql(defaultStoreState);
-      });
-
-      it("should retrieve a given property state", function() {
-        var fakeActiveRoom = {fake: true};
-        store.setStoreState({activeRoom: fakeActiveRoom});
-
-        expect(store.getStoreState().activeRoom).eql(fakeActiveRoom);
-      });
-    });
-
     describe("#findNextAvailableRoomNumber", function() {
       var fakeNameTemplate = "RoomWord {{conversationLabel}}";
 
       it("should find next available room number from an empty room list",
         function() {
           store.setStoreState({rooms: []});
 
           expect(store.findNextAvailableRoomNumber(fakeNameTemplate)).eql(1);
@@ -378,23 +356,21 @@ describe("loop.store.RoomStore", functio
         expect(store.getStoreState().pendingInitialRetrieval).eql(false);
       });
     });
 
     describe("ActiveRoomStore substore", function() {
       var store, activeRoomStore;
 
       beforeEach(function() {
-        activeRoomStore = new loop.store.ActiveRoomStore({
-          dispatcher: dispatcher,
+        activeRoomStore = new loop.store.ActiveRoomStore(dispatcher, {
           mozLoop: fakeMozLoop,
           sdkDriver: {}
         });
-        store = new loop.store.RoomStore({
-          dispatcher: dispatcher,
+        store = new loop.store.RoomStore(dispatcher, {
           mozLoop: fakeMozLoop,
           activeRoomStore: activeRoomStore
         });
       });
 
       it("should subscribe to substore changes", function() {
         var fakeServerData = {fake: true};
 
@@ -419,20 +395,17 @@ describe("loop.store.RoomStore", functio
     var store, fakeMozLoop;
 
     beforeEach(function() {
       fakeMozLoop = {
         rooms: {
           open: sinon.spy()
         }
       };
-      store = new loop.store.RoomStore({
-        dispatcher: dispatcher,
-        mozLoop: fakeMozLoop
-      });
+      store = new loop.store.RoomStore(dispatcher, {mozLoop: fakeMozLoop});
     });
 
     it("should open the room via mozLoop", function() {
       dispatcher.dispatch(new sharedActions.OpenRoom({roomToken: "42abc"}));
 
       sinon.assert.calledOnce(fakeMozLoop.rooms.open);
       sinon.assert.calledWithExactly(fakeMozLoop.rooms.open, "42abc");
     });
@@ -442,20 +415,17 @@ describe("loop.store.RoomStore", functio
     var store, fakeMozLoop;
 
     beforeEach(function() {
       fakeMozLoop = {
         rooms: {
           rename: sinon.spy()
         }
       };
-      store = new loop.store.RoomStore({
-        dispatcher: dispatcher,
-        mozLoop: fakeMozLoop
-      });
+      store = new loop.store.RoomStore(dispatcher, {mozLoop: fakeMozLoop});
     });
 
     it("should rename the room via mozLoop", function() {
       dispatcher.dispatch(new sharedActions.RenameRoom({
         roomToken: "42abc",
         newRoomName: "silly name"
       }));
 
new file mode 100644
--- /dev/null
+++ b/browser/components/loop/test/shared/store_test.js
@@ -0,0 +1,160 @@
+/* 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/. */
+
+var expect = chai.expect;
+
+describe("loop.store.createStore", function () {
+  "use strict";
+
+  var sandbox;
+  var sharedActions = loop.shared.actions;
+
+  beforeEach(function() {
+    sandbox = sinon.sandbox.create();
+  });
+
+  afterEach(function() {
+    sandbox.restore();
+  });
+
+  it("should create a store constructor", function() {
+    expect(loop.store.createStore({})).to.be.a("function");
+  });
+
+  it("should implement Backbone.Events", function() {
+    expect(loop.store.createStore({}).prototype).to.include.keys(["on", "off"])
+  });
+
+  describe("Store API", function() {
+    var dispatcher;
+
+    beforeEach(function() {
+      dispatcher = new loop.Dispatcher();
+    });
+
+    describe("#constructor", function() {
+      it("should require a dispatcher", function() {
+        var TestStore = loop.store.createStore({});
+        expect(function() {
+          new TestStore();
+        }).to.Throw(/required dispatcher/);
+      });
+
+      it("should call initialize() when constructed, if defined", function() {
+        var initialize = sandbox.spy();
+        var TestStore = loop.store.createStore({initialize: initialize});
+        var options = {fake: true};
+
+        new TestStore(dispatcher, options);
+
+        sinon.assert.calledOnce(initialize);
+        sinon.assert.calledWithExactly(initialize, options);
+      });
+
+      it("should register actions", function() {
+        sandbox.stub(dispatcher, "register");
+        var TestStore = loop.store.createStore({
+          actions: ["a", "b"],
+          a: function() {},
+          b: function() {}
+        });
+
+        var store = new TestStore(dispatcher);
+
+        sinon.assert.calledOnce(dispatcher.register);
+        sinon.assert.calledWithExactly(dispatcher.register, store, ["a", "b"]);
+      });
+
+      it("should throw if a registered action isn't implemented", function() {
+        var TestStore = loop.store.createStore({
+          actions: ["a", "b"],
+          a: function() {} // missing b
+        });
+
+        expect(function() {
+          new TestStore(dispatcher);
+        }).to.Throw(/should implement an action handler for b/);
+      });
+    });
+
+    describe("#getInitialStoreState", function() {
+      it("should set initial store state if provided", function() {
+        var TestStore = loop.store.createStore({
+          getInitialStoreState: function() {
+            return {foo: "bar"};
+          }
+        });
+
+        var store = new TestStore(dispatcher);
+
+        expect(store.getStoreState()).eql({foo: "bar"});
+      });
+    });
+
+    describe("#dispatchAction", function() {
+      it("should dispatch an action", function() {
+        sandbox.stub(dispatcher, "dispatch");
+        var TestStore = loop.store.createStore({});
+        var TestAction = sharedActions.Action.define("TestAction", {});
+        var action = new TestAction({});
+        var store = new TestStore(dispatcher);
+
+        store.dispatchAction(action);
+
+        sinon.assert.calledOnce(dispatcher.dispatch);
+        sinon.assert.calledWithExactly(dispatcher.dispatch, action);
+      });
+    });
+
+    describe("#getStoreState", function() {
+      var TestStore = loop.store.createStore({});
+      var store;
+
+      beforeEach(function() {
+        store = new TestStore(dispatcher);
+        store.setStoreState({foo: "bar", bar: "baz"});
+      });
+
+      it("should retrieve the whole state by default", function() {
+        expect(store.getStoreState()).eql({foo: "bar", bar: "baz"});
+      });
+
+      it("should retrieve a given property state", function() {
+        expect(store.getStoreState("bar")).eql("baz");
+      });
+    });
+
+    describe("#setStoreState", function() {
+      var TestStore = loop.store.createStore({});
+      var store;
+
+      beforeEach(function() {
+        store = new TestStore(dispatcher);
+        store.setStoreState({foo: "bar"});
+      });
+
+      it("should update store state data", function() {
+        store.setStoreState({foo: "baz"});
+
+        expect(store.getStoreState("foo")).eql("baz");
+      });
+
+      it("should trigger a `change` event", function(done) {
+        store.once("change", function() {
+          done();
+        });
+
+        store.setStoreState({foo: "baz"});
+      });
+
+      it("should trigger a `change:<prop>` event", function(done) {
+        store.once("change:foo", function() {
+          done();
+        });
+
+        store.setStoreState({foo: "baz"});
+      });
+    });
+  });
+});
--- a/browser/components/loop/test/shared/utils_test.js
+++ b/browser/components/loop/test/shared/utils_test.js
@@ -112,17 +112,17 @@ describe("loop.shared.utils", function()
   describe("#getBoolPreference", function() {
     afterEach(function() {
       localStorage.removeItem("test.true");
     });
 
     describe("mozLoop set", function() {
       beforeEach(function() {
         navigator.mozLoop = {
-          getLoopBoolPref: function(prefName) {
+          getLoopPref: function(prefName) {
             return prefName === "test.true";
           }
         };
       });
 
       it("should return the mozLoop preference", function() {
         expect(sharedUtils.getBoolPreference("test.true")).eql(true);
       });
@@ -151,17 +151,17 @@ describe("loop.shared.utils", function()
       sandbox.stub(navigator.mozL10n, "get", function(id) {
         switch(id) {
           case "share_email_subject4": return "subject";
           case "share_email_body4":    return "body";
         }
       });
       composeEmail = sandbox.spy();
       navigator.mozLoop = {
-        getLoopCharPref: sandbox.spy(),
+        getLoopPref: sandbox.spy(),
         composeEmail: composeEmail
       };
     });
 
     it("should compose a call url email", function() {
       sharedUtils.composeCallUrlEmail("http://invalid", "fake@invalid.tld");
 
       sinon.assert.calledOnce(composeEmail);
--- a/browser/components/loop/test/standalone/index.html
+++ b/browser/components/loop/test/standalone/index.html
@@ -35,16 +35,17 @@
   <script src="../../content/shared/js/models.js"></script>
   <script src="../../content/shared/js/mixins.js"></script>
   <script src="../../content/shared/js/views.js"></script>
   <script src="../../content/shared/js/websocket.js"></script>
   <script src="../../content/shared/js/feedbackApiClient.js"></script>
   <script src="../../content/shared/js/actions.js"></script>
   <script src="../../content/shared/js/validate.js"></script>
   <script src="../../content/shared/js/dispatcher.js"></script>
+  <script src="../../content/shared/js/store.js"></script>
   <script src="../../content/shared/js/activeRoomStore.js"></script>
   <script src="../../content/shared/js/otSdkDriver.js"></script>
   <script src="../../standalone/content/js/multiplexGum.js"></script>
   <script src="../../standalone/content/js/standaloneAppStore.js"></script>
   <script src="../../standalone/content/js/standaloneClient.js"></script>
   <script src="../../standalone/content/js/standaloneMozLoop.js"></script>
   <script src="../../standalone/content/js/standaloneRoomViews.js"></script>
   <script src="../../standalone/content/js/webapp.js"></script>
--- a/browser/components/loop/test/standalone/standaloneMozLoop_test.js
+++ b/browser/components/loop/test/standalone/standaloneMozLoop_test.js
@@ -41,43 +41,43 @@ describe("loop.StandaloneMozLoop", funct
   describe("#constructor", function() {
     it("should require a baseServerUrl setting", function() {
       expect(function() {
         new loop.StandaloneMozLoop();
       }).to.Throw(Error, /required/);
     });
   });
 
-  describe("#setLoopCharPref", function() {
+  describe("#setLoopPref", function() {
     afterEach(function() {
       localStorage.removeItem("fakePref");
     });
 
     it("should store the value of the preference", function() {
-      mozLoop.setLoopCharPref("fakePref", "fakeValue");
+      mozLoop.setLoopPref("fakePref", "fakeValue");
 
       expect(localStorage.getItem("fakePref")).eql("fakeValue");
     });
 
     it("should not store the value of seenToS", function() {
-      mozLoop.setLoopCharPref("seenToS", "fakeValue1");
+      mozLoop.setLoopPref("seenToS", "fakeValue1");
 
       expect(localStorage.getItem("seenToS")).eql(null);
     });
   });
 
-  describe("#getLoopCharPref", function() {
+  describe("#getLoopPref", function() {
     afterEach(function() {
       localStorage.removeItem("fakePref");
     });
 
     it("should return the value of the preference", function() {
       localStorage.setItem("fakePref", "fakeValue");
 
-      expect(mozLoop.getLoopCharPref("fakePref")).eql("fakeValue");
+      expect(mozLoop.getLoopPref("fakePref")).eql("fakeValue");
     });
   });
 
   describe("#rooms.join", function() {
     it("should POST to the server", function() {
       mozLoop.rooms.join("fakeToken", callback);
 
       expect(requests).to.have.length.of(1);
--- a/browser/components/loop/test/standalone/standaloneRoomViews_test.js
+++ b/browser/components/loop/test/standalone/standaloneRoomViews_test.js
@@ -13,18 +13,17 @@ describe("loop.standaloneRoomViews", fun
   var sharedActions = loop.shared.actions;
 
   var sandbox, dispatcher, activeRoomStore, dispatch;
 
   beforeEach(function() {
     sandbox = sinon.sandbox.create();
     dispatcher = new loop.Dispatcher();
     dispatch = sandbox.stub(dispatcher, "dispatch");
-    activeRoomStore = new loop.store.ActiveRoomStore({
-      dispatcher: dispatcher,
+    activeRoomStore = new loop.store.ActiveRoomStore(dispatcher, {
       mozLoop: {},
       sdkDriver: {}
     });
   });
 
   afterEach(function() {
     sandbox.restore();
   });
--- a/browser/components/loop/test/standalone/webapp_test.js
+++ b/browser/components/loop/test/standalone/webapp_test.js
@@ -605,18 +605,17 @@ describe("loop.webapp", function() {
       };
       conversationModel = new sharedModels.ConversationModel({}, {
         sdk: sdk
       });
       client = new loop.StandaloneClient({
         baseServerUrl: "fakeUrl"
       });
       dispatcher = new loop.Dispatcher();
-      activeRoomStore = new loop.store.ActiveRoomStore({
-        dispatcher: dispatcher,
+      activeRoomStore = new loop.store.ActiveRoomStore(dispatcher, {
         mozLoop: {},
         sdkDriver: {}
       });
       standaloneAppStore = new loop.store.StandaloneAppStore({
         dispatcher: dispatcher,
         sdk: sdk,
         helper: helper,
         conversation: conversationModel
--- a/browser/components/loop/test/xpcshell/test_looprooms.js
+++ b/browser/components/loop/test/xpcshell/test_looprooms.js
@@ -332,29 +332,39 @@ add_task(function* test_roomUpdates() {
   gExpectedJoins["_nxD4V4FflQ"] = [
     "2a1787a6-4a73-43b5-ae3e-906ec1e763cb",
     "5de6281c-6568-455f-af08-c0b0a973100e"];
   roomsPushNotification("4");
   yield waitForCondition(() => Object.getOwnPropertyNames(gExpectedJoins).length === 0);
 });
 
 // Test if joining a room works as expected.
+add_task(function* test_joinRoomGuest() {
+  // We need these set up for getting the email address.
+  let roomToken = "_nxD4V4FflQ";
+  let joinedData = yield LoopRooms.promise("join", roomToken);
+  Assert.equal(joinedData.action, "join");
+});
+
 add_task(function* test_joinRoom() {
   // We need these set up for getting the email address.
   Services.prefs.setCharPref("loop.fxa_oauth.profile", JSON.stringify({
     email: "fake@invalid.com"
   }));
   Services.prefs.setCharPref("loop.fxa_oauth.tokendata", JSON.stringify({
     token_type: "bearer"
   }));
 
   let roomToken = "_nxD4V4FflQ";
   let joinedData = yield LoopRooms.promise("join", roomToken);
   Assert.equal(joinedData.action, "join");
   Assert.equal(joinedData.displayName, "fake@invalid.com");
+
+  Services.prefs.clearUserPref("loop.fxa_oauth.profile");
+  Services.prefs.clearUserPref("loop.fxa_oauth.tokendata");
 });
 
 // Test if refreshing a room works as expected.
 add_task(function* test_refreshMembership() {
   let roomToken = "_nxD4V4FflQ";
   let refreshedData = yield LoopRooms.promise("refreshMembership", roomToken,
     "fakeSessionToken");
   Assert.equal(refreshedData.action, "refresh");
--- a/browser/components/loop/test/xpcshell/test_loopservice_loop_prefs.js
+++ b/browser/components/loop/test/xpcshell/test_loopservice_loop_prefs.js
@@ -1,108 +1,108 @@
 /* Any copyright is dedicated to the Public Domain.
  http://creativecommons.org/publicdomain/zero/1.0/ */
 /*global XPCOMUtils, Services, Assert */
 
 var fakeCharPrefName = "color";
 var fakeBoolPrefName = "boolean";
 var fakePrefValue = "green";
 
-function test_getLoopCharPref()
+function test_getLoopPref()
 {
   Services.prefs.setCharPref("loop." + fakeCharPrefName, fakePrefValue);
 
-  var returnedPref = MozLoopService.getLoopCharPref(fakeCharPrefName);
+  var returnedPref = MozLoopService.getLoopPref(fakeCharPrefName, Ci.nsIPrefBranch.PREF_STRING);
 
   Assert.equal(returnedPref, fakePrefValue,
     "Should return a char pref under the loop. branch");
   Services.prefs.clearUserPref("loop." + fakeCharPrefName);
 }
 
-function test_getLoopCharPref_not_found()
+function test_getLoopPref_not_found()
 {
-  var returnedPref = MozLoopService.getLoopCharPref(fakeCharPrefName);
+  var returnedPref = MozLoopService.getLoopPref(fakeCharPrefName);
 
   Assert.equal(returnedPref, null,
     "Should return null if a preference is not found");
 }
 
-function test_getLoopCharPref_non_coercible_type()
+function test_getLoopPref_non_coercible_type()
 {
   Services.prefs.setBoolPref("loop." + fakeCharPrefName, false);
 
-  var returnedPref = MozLoopService.getLoopCharPref(fakeCharPrefName);
+  var returnedPref = MozLoopService.getLoopPref(fakeCharPrefName, Ci.nsIPrefBranch.PREF_STRING);
 
   Assert.equal(returnedPref, null,
     "Should return null if the preference exists & is of a non-coercible type");
 }
 
-function test_setLoopCharPref()
+function test_setLoopPref()
 {
   Services.prefs.setCharPref("loop." + fakeCharPrefName, "red");
-  MozLoopService.setLoopCharPref(fakeCharPrefName, fakePrefValue);
+  MozLoopService.setLoopPref(fakeCharPrefName, fakePrefValue);
 
   var returnedPref = Services.prefs.getCharPref("loop." + fakeCharPrefName);
 
   Assert.equal(returnedPref, fakePrefValue,
     "Should set a char pref under the loop. branch");
   Services.prefs.clearUserPref("loop." + fakeCharPrefName);
 }
 
-function test_setLoopCharPref_new()
+function test_setLoopPref_new()
 {
   Services.prefs.clearUserPref("loop." + fakeCharPrefName);
-  MozLoopService.setLoopCharPref(fakeCharPrefName, fakePrefValue);
+  MozLoopService.setLoopPref(fakeCharPrefName, fakePrefValue, Ci.nsIPrefBranch.PREF_STRING);
 
   var returnedPref = Services.prefs.getCharPref("loop." + fakeCharPrefName);
 
   Assert.equal(returnedPref, fakePrefValue,
                "Should set a new char pref under the loop. branch");
   Services.prefs.clearUserPref("loop." + fakeCharPrefName);
 }
 
-function test_setLoopCharPref_non_coercible_type()
+function test_setLoopPref_non_coercible_type()
 {
-  MozLoopService.setLoopCharPref(fakeCharPrefName, true);
+  MozLoopService.setLoopPref(fakeCharPrefName, true);
 
   ok(true, "Setting non-coercible type should not fail");
 }
 
 
-function test_getLoopBoolPref()
+function test_getLoopPref_bool()
 {
   Services.prefs.setBoolPref("loop." + fakeBoolPrefName, true);
 
-  var returnedPref = MozLoopService.getLoopBoolPref(fakeBoolPrefName);
+  var returnedPref = MozLoopService.getLoopPref(fakeBoolPrefName);
 
   Assert.equal(returnedPref, true,
     "Should return a bool pref under the loop. branch");
   Services.prefs.clearUserPref("loop." + fakeBoolPrefName);
 }
 
-function test_getLoopBoolPref_not_found()
+function test_getLoopPref_not_found_bool()
 {
-  var returnedPref = MozLoopService.getLoopBoolPref(fakeBoolPrefName);
+  var returnedPref = MozLoopService.getLoopPref(fakeBoolPrefName);
 
   Assert.equal(returnedPref, null,
     "Should return null if a preference is not found");
 }
 
 
 function run_test()
 {
   setupFakeLoopServer();
 
-  test_getLoopCharPref();
-  test_getLoopCharPref_not_found();
-  test_getLoopCharPref_non_coercible_type();
-  test_setLoopCharPref();
-  test_setLoopCharPref_new();
-  test_setLoopCharPref_non_coercible_type();
+  test_getLoopPref();
+  test_getLoopPref_not_found();
+  test_getLoopPref_non_coercible_type();
+  test_setLoopPref();
+  test_setLoopPref_new();
+  test_setLoopPref_non_coercible_type();
 
-  test_getLoopBoolPref();
-  test_getLoopBoolPref_not_found();
+  test_getLoopPref_bool();
+  test_getLoopPref_not_found_bool();
 
   do_register_cleanup(function() {
     Services.prefs.clearUserPref("loop." + fakeCharPrefName);
     Services.prefs.clearUserPref("loop." + fakeBoolPrefName);
   });
 }
--- a/browser/components/loop/ui/fake-mozLoop.js
+++ b/browser/components/loop/ui/fake-mozLoop.js
@@ -44,18 +44,17 @@ var fakeRooms = [
 ];
 
 /**
  * Faking the mozLoop object which doesn't exist in regular web pages.
  * @type {Object}
  */
 navigator.mozLoop = {
   ensureRegistered: function() {},
-  getLoopCharPref: function() {},
-  getLoopBoolPref: function(pref) {
+  getLoopPref: function(pref) {
     // Ensure UI for rooms is displayed in the showcase.
     if (pref === "rooms.enabled") {
       return true;
     }
   },
   releaseCallData: function() {},
   copyString: function() {},
   contacts: {
--- a/browser/components/loop/ui/index.html
+++ b/browser/components/loop/ui/index.html
@@ -36,18 +36,19 @@
     <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/views.js"></script>
     <script src="../content/shared/js/websocket.js"></script>
     <script src="../content/shared/js/validate.js"></script>
     <script src="../content/shared/js/dispatcher.js"></script>
+    <script src="../content/shared/js/store.js"></script>
+    <script src="../content/shared/js/roomStore.js"></script>
     <script src="../content/shared/js/conversationStore.js"></script>
-    <script src="../content/shared/js/roomStore.js"></script>
     <script src="../content/shared/js/activeRoomStore.js"></script>
     <script src="../content/js/roomViews.js"></script>
     <script src="../content/js/conversationViews.js"></script>
     <script src="../content/js/client.js"></script>
     <script src="../content/js/webapp.js"></script>
     <script src="../content/js/standaloneRoomViews.js"></script>
     <script type="text/javascript;version=1.8" src="../content/js/contacts.js"></script>
     <script>
--- a/browser/components/loop/ui/ui-showcase.js
+++ b/browser/components/loop/ui/ui-showcase.js
@@ -57,23 +57,21 @@
   // which is available at https://input.allizom.org
   var stageFeedbackApiClient = new loop.FeedbackAPIClient(
     "https://input.allizom.org/api/v1/feedback", {
       product: "Loop"
     }
   );
 
   var dispatcher = new loop.Dispatcher();
-  var activeRoomStore = new loop.store.ActiveRoomStore({
-    dispatcher: dispatcher,
+  var activeRoomStore = new loop.store.ActiveRoomStore(dispatcher, {
     mozLoop: navigator.mozLoop,
     sdkDriver: {}
   });
-  var roomStore = new loop.store.RoomStore({
-    dispatcher: dispatcher,
+  var roomStore = new loop.store.RoomStore(dispatcher, {
     mozLoop: navigator.mozLoop
   });
 
   // Local mocks
 
   var mockContact = {
     name: ["Mr Smith"],
     email: [{
--- a/browser/components/loop/ui/ui-showcase.jsx
+++ b/browser/components/loop/ui/ui-showcase.jsx
@@ -57,23 +57,21 @@
   // which is available at https://input.allizom.org
   var stageFeedbackApiClient = new loop.FeedbackAPIClient(
     "https://input.allizom.org/api/v1/feedback", {
       product: "Loop"
     }
   );
 
   var dispatcher = new loop.Dispatcher();
-  var activeRoomStore = new loop.store.ActiveRoomStore({
-    dispatcher: dispatcher,
+  var activeRoomStore = new loop.store.ActiveRoomStore(dispatcher, {
     mozLoop: navigator.mozLoop,
     sdkDriver: {}
   });
-  var roomStore = new loop.store.RoomStore({
-    dispatcher: dispatcher,
+  var roomStore = new loop.store.RoomStore(dispatcher, {
     mozLoop: navigator.mozLoop
   });
 
   // Local mocks
 
   var mockContact = {
     name: ["Mr Smith"],
     email: [{
--- a/browser/devtools/inspector/test/browser.ini
+++ b/browser/devtools/inspector/test/browser.ini
@@ -6,16 +6,18 @@ support-files =
   doc_inspector_breadcrumbs.html
   doc_inspector_delete-selected-node-01.html
   doc_inspector_delete-selected-node-02.html
   doc_inspector_gcli-inspect-command.html
   doc_inspector_highlight_after_transition.html
   doc_inspector_highlighter-comments.html
   doc_inspector_highlighter_csstransform.html
   doc_inspector_highlighter.html
+  doc_inspector_highlighter_rect.html
+  doc_inspector_highlighter_rect_iframe.html
   doc_inspector_infobar_01.html
   doc_inspector_infobar_02.html
   doc_inspector_menu.html
   doc_inspector_remove-iframe-during-load.html
   doc_inspector_search.html
   doc_inspector_search-suggestions.html
   doc_inspector_select-last-selected-01.html
   doc_inspector_select-last-selected-02.html
@@ -36,16 +38,18 @@ support-files =
 [browser_inspector_highlighter-comments.js]
 [browser_inspector_highlighter-csstransform_01.js]
 [browser_inspector_highlighter-csstransform_02.js]
 [browser_inspector_highlighter-hover_01.js]
 [browser_inspector_highlighter-hover_02.js]
 [browser_inspector_highlighter-hover_03.js]
 [browser_inspector_highlighter-iframes.js]
 [browser_inspector_highlighter-options.js]
+[browser_inspector_highlighter-rect_01.js]
+[browser_inspector_highlighter-rect_02.js]
 [browser_inspector_highlighter-selector_01.js]
 [browser_inspector_highlighter-selector_02.js]
 [browser_inspector_iframe-navigation.js]
 [browser_inspector_infobar_01.js]
 [browser_inspector_initialization.js]
 [browser_inspector_inspect-object-element.js]
 [browser_inspector_invalidate.js]
 [browser_inspector_keyboard-shortcuts.js]
new file mode 100644
--- /dev/null
+++ b/browser/devtools/inspector/test/browser_inspector_highlighter-rect_01.js
@@ -0,0 +1,118 @@
+/* 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";
+
+// Test that the custom rect highlighter provides the right API, ensures that
+// the input is valid and that it does create a box with the right dimensions,
+// at the right position.
+
+const TEST_URL = "data:text/html;charset=utf-8,Rect Highlighter Test";
+
+add_task(function*() {
+  let {inspector, toolbox} = yield openInspectorForURL(TEST_URL);
+  let front = inspector.inspector;
+  let highlighter = yield front.getHighlighterByType("RectHighlighter");
+  let body = yield getNodeFront("body", inspector);
+
+  info("Make sure the highlighter returned is correct");
+
+  ok(highlighter, "The RectHighlighter custom type was created");
+  is(highlighter.typeName, "customhighlighter",
+    "The RectHighlighter has the right type");
+  ok(highlighter.show && highlighter.hide,
+    "The RectHighlighter has the expected show/hide methods");
+
+  info("Check that the highlighter is hidden by default");
+
+  let hidden = yield getAttribute(highlighter, "hidden");
+  is(hidden, "true", "The highlighter is hidden by default");
+
+  info("Check that nothing is shown if no rect is passed");
+
+  yield highlighter.show(body);
+  hidden = yield getAttribute(highlighter, "hidden");
+  is(hidden, "true", "The highlighter is hidden when no rect is passed");
+
+  info("Check that nothing is shown if rect is incomplete or invalid");
+
+  yield highlighter.show(body, {
+    rect: {x: 0, y: 0}
+  });
+  hidden = yield getAttribute(highlighter, "hidden");
+  is(hidden, "true", "The highlighter is hidden when the rect is incomplete");
+
+  yield highlighter.show(body, {
+    rect: {x: 0, y: 0, width: -Infinity, height: 0}
+  });
+  hidden = yield getAttribute(highlighter, "hidden");
+  is(hidden, "true", "The highlighter is hidden when the rect is invalid (1)");
+
+  yield highlighter.show(body, {
+    rect: {x: 0, y: 0, width: 5, height: -45}
+  });
+  hidden = yield getAttribute(highlighter, "hidden");
+  is(hidden, "true", "The highlighter is hidden when the rect is invalid (2)");
+
+  yield highlighter.show(body, {
+    rect: {x: "test", y: 0, width: 5, height: 5}
+  });
+  hidden = yield getAttribute(highlighter, "hidden");
+  is(hidden, "true", "The highlighter is hidden when the rect is invalid (3)");
+
+  info("Check that the highlighter is displayed when valid options are passed");
+
+  yield highlighter.show(body, {
+    rect: {x: 5, y: 5, width: 50, height: 50}
+  });
+  hidden = yield getAttribute(highlighter, "hidden");
+  ok(!hidden, "The highlighter is displayed");
+  let style = yield getAttribute(highlighter, "style");
+  is(style, "left:5px;top:5px;width:50px;height:50px;",
+    "The highlighter is positioned correctly");
+
+  info("Check that the highlighter can be displayed at x=0 y=0");
+
+  yield highlighter.show(body, {
+    rect: {x: 0, y: 0, width: 50, height: 50}
+  });
+  hidden = yield getAttribute(highlighter, "hidden");
+  ok(!hidden, "The highlighter is displayed when x=0 and y=0");
+  style = yield getAttribute(highlighter, "style");
+  is(style, "left:0px;top:0px;width:50px;height:50px;",
+    "The highlighter is positioned correctly");
+
+  info("Check that the highlighter is hidden when dimensions are 0");
+
+  yield highlighter.show(body, {
+    rect: {x: 0, y: 0, width: 0, height: 0}
+  });
+  hidden = yield getAttribute(highlighter, "hidden");
+  is(hidden, "true", "The highlighter is hidden width and height are 0");
+
+  info("Check that a fill color can be passed");
+
+  yield highlighter.show(body, {
+    rect: {x: 100, y: 200, width: 500, height: 200},
+    fill: "red"
+  });
+  hidden = yield getAttribute(highlighter, "hidden");
+  ok(!hidden, "The highlighter is displayed");
+  style = yield getAttribute(highlighter, "style");
+  is(style, "left:100px;top:200px;width:500px;height:200px;background:red;",
+    "The highlighter has the right background color");
+
+  yield highlighter.hide();
+  yield highlighter.finalize();
+  gBrowser.removeCurrentTab();
+});
+
+function* getAttribute(highlighter, name) {
+  let {data: value} = yield executeInContent("Test:GetHighlighterAttribute", {
+    nodeID: "highlighted-rect",
+    name: name,
+    actorID: highlighter.actorID
+  });
+  return value;
+}
new file mode 100644
--- /dev/null
+++ b/browser/devtools/inspector/test/browser_inspector_highlighter-rect_02.js
@@ -0,0 +1,46 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+// Test that the custom rect highlighter positions the rectangle relative to the
+// viewport of the context node we pass to it.
+
+const TEST_URL = TEST_URL_ROOT + "doc_inspector_highlighter_rect.html";
+
+add_task(function*() {
+  let {inspector, toolbox} = yield openInspectorForURL(TEST_URL);
+  let front = inspector.inspector;
+  let highlighter = yield front.getHighlighterByType("RectHighlighter");
+
+  info("Showing the rect highlighter in the context of the iframe");
+
+  // Get the reference to a context node inside the iframe
+  let childBody = yield getNodeFrontInFrame("body", "iframe", inspector);
+  yield highlighter.show(childBody, {
+    rect: {x: 50, y: 50, width: 100, height: 100}
+  });
+
+  let style = yield getAttribute(highlighter, "style");
+
+  // The parent body has margin=50px and border=10px
+  // The parent iframe also has margin=50px and border=10px
+  // = 50 + 10 + 50 + 10 = 120px
+  // The rect is aat x=50 and y=50, so left and top should be 170px
+  is(style, "left:170px;top:170px;width:100px;height:100px;",
+    "The highlighter is correctly positioned");
+
+  yield highlighter.hide();
+  yield highlighter.finalize();
+  gBrowser.removeCurrentTab();
+});
+
+function* getAttribute(highlighter, name) {
+  let {data: value} = yield executeInContent("Test:GetHighlighterAttribute", {
+    nodeID: "highlighted-rect",
+    name: name,
+    actorID: highlighter.actorID
+  });
+  return value;
+}
new file mode 100644
--- /dev/null
+++ b/browser/devtools/inspector/test/doc_inspector_highlighter_rect.html
@@ -0,0 +1,22 @@
+<!doctype html>
+<html lang="en">
+<head>
+  <meta charset="utf-8">
+  <title>rect highlighter parent test page</title>
+  <style type="text/css">
+    body {
+      margin: 50px;
+      border: 10px solid red;
+    }
+
+    iframe {
+      border: 10px solid yellow;
+      padding: 0;
+      margin: 50px;
+    }
+  </style>
+</head>
+<body>
+  <iframe src="doc_inspector_highlighter_rect_iframe.html"></iframe>
+</body>
+</html>
new file mode 100644
--- /dev/null
+++ b/browser/devtools/inspector/test/doc_inspector_highlighter_rect_iframe.html
@@ -0,0 +1,15 @@
+<!doctype html>
+<html lang="en">
+<head>
+  <meta charset="utf-8">
+  <title>rect highlighter child test page</title>
+  <style type="text/css">
+    body {
+      margin: 0;
+    }
+  </style>
+</head>
+<body>
+
+</body>
+</html>
--- a/browser/devtools/inspector/test/head.js
+++ b/browser/devtools/inspector/test/head.js
@@ -252,18 +252,18 @@ function getNodeFront(selector, {walker}
  * @param {String|NodeFront} frameSelector A selector that matches the iframe
  * the node is in
  * @param {InspectorPanel} inspector The instance of InspectorPanel currently
  * loaded in the toolbox
  * @param {String} reason Defaults to "test" which instructs the inspector not
  * to highlight the node upon selection
  * @return {Promise} Resolves when the inspector is updated with the new node
  */
-let getNodeFrontInFrame = Task.async(function*(selector, frameSelector, inspector,
-                                             reason="test") {
+let getNodeFrontInFrame = Task.async(function*(selector, frameSelector,
+                                               inspector, reason="test") {
   let iframe = yield getNodeFront(frameSelector, inspector);
   let {nodes} = yield inspector.walker.children(iframe);
   return inspector.walker.querySelector(nodes[0], selector);
 });
 
 /**
  * Get the current rect of the border region of the box-model highlighter
  */
--- a/browser/devtools/webide/modules/runtimes.js
+++ b/browser/devtools/webide/modules/runtimes.js
@@ -354,17 +354,23 @@ EventEmitter.decorate(WiFiScanner);
 WiFiScanner.init();
 
 exports.WiFiScanner = WiFiScanner;
 
 let StaticScanner = {
   enable() {},
   disable() {},
   scan() { return promise.resolve(); },
-  listRuntimes() { return [gRemoteRuntime, gLocalRuntime]; }
+  listRuntimes() {
+    let runtimes = [gRemoteRuntime];
+    if (Services.prefs.getBoolPref("devtools.webide.enableLocalRuntime")) {
+      runtimes.push(gLocalRuntime);
+    }
+    return runtimes;
+  }
 };
 
 EventEmitter.decorate(StaticScanner);
 RuntimeScanners.add(StaticScanner);
 
 /* RUNTIMES */
 
 // These type strings are used for logging events to Telemetry.
--- a/browser/devtools/webide/test/head.js
+++ b/browser/devtools/webide/test/head.js
@@ -17,26 +17,28 @@ const {AppProjects} = require("devtools/
 let TEST_BASE;
 if (window.location === "chrome://browser/content/browser.xul") {
   TEST_BASE = "chrome://mochitests/content/browser/browser/devtools/webide/test/";
 } else {
   TEST_BASE = "chrome://mochitests/content/chrome/browser/devtools/webide/test/";
 }
 
 Services.prefs.setBoolPref("devtools.webide.enabled", true);
+Services.prefs.setBoolPref("devtools.webide.enableLocalRuntime", true);
 
 Services.prefs.setCharPref("devtools.webide.addonsURL", TEST_BASE + "addons/simulators.json");
 Services.prefs.setCharPref("devtools.webide.simulatorAddonsURL", TEST_BASE + "addons/fxos_#SLASHED_VERSION#_simulator-#OS#.xpi");
 Services.prefs.setCharPref("devtools.webide.adbAddonURL", TEST_BASE + "addons/adbhelper-#OS#.xpi");
 Services.prefs.setCharPref("devtools.webide.adaptersAddonURL", TEST_BASE + "addons/fxdt-adapters-#OS#.xpi");
 Services.prefs.setCharPref("devtools.webide.templatesURL", TEST_BASE + "templates.json");
 
 
 SimpleTest.registerCleanupFunction(() => {
   Services.prefs.clearUserPref("devtools.webide.enabled");
+  Services.prefs.clearUserPref("devtools.webide.enableLocalRuntime");
   Services.prefs.clearUserPref("devtools.webide.autoinstallADBHelper");
   Services.prefs.clearUserPref("devtools.webide.autoinstallFxdtAdapters");
 });
 
 function openWebIDE(autoInstallAddons) {
   info("opening WebIDE");
 
   Services.prefs.setBoolPref("devtools.webide.autoinstallADBHelper", !!autoInstallAddons);
--- a/browser/devtools/webide/webide-prefs.js
+++ b/browser/devtools/webide/webide-prefs.js
@@ -7,16 +7,17 @@ pref("devtools.webide.showProjectEditor"
 pref("devtools.webide.templatesURL", "https://code.cdn.mozilla.net/templates/list.json");
 pref("devtools.webide.autoinstallADBHelper", true);
 #ifdef MOZ_DEV_EDITION
 pref("devtools.webide.autoinstallFxdtAdapters", true);
 #else
 pref("devtools.webide.autoinstallFxdtAdapters", false);
 #endif
 pref("devtools.webide.restoreLastProject", true);
+pref("devtools.webide.enableLocalRuntime", false);
 pref("devtools.webide.addonsURL", "https://ftp.mozilla.org/pub/mozilla.org/labs/fxos-simulator/index.json");
 pref("devtools.webide.simulatorAddonsURL", "https://ftp.mozilla.org/pub/mozilla.org/labs/fxos-simulator/#VERSION#/#OS#/fxos_#SLASHED_VERSION#_simulator-#OS#-latest.xpi");
 pref("devtools.webide.simulatorAddonID", "fxos_#SLASHED_VERSION#_simulator@mozilla.org");
 pref("devtools.webide.adbAddonURL", "https://ftp.mozilla.org/pub/mozilla.org/labs/fxos-simulator/adb-helper/#OS#/adbhelper-#OS#-latest.xpi");
 pref("devtools.webide.adbAddonID", "adbhelper@mozilla.org");
 pref("devtools.webide.adaptersAddonURL", "https://ftp.mozilla.org/pub/mozilla.org/labs/fxdt-adapters/#OS#/fxdt-adapters-#OS#-latest.xpi");
 pref("devtools.webide.adaptersAddonID", "fxdevtools-adapters@mozilla.org");
 pref("devtools.webide.monitorWebSocketURL", "ws://localhost:9000");
--- a/build/annotationProcessors/Makefile.in
+++ b/build/annotationProcessors/Makefile.in
@@ -1,12 +1,12 @@
 # This Source Code Form is subject to the terms of the Mozilla Public
 # License, v. 2.0. If a copy of the MPL was not distributed with this
 # file, You can obtain one at http://mozilla.org/MPL/2.0/.
 
 include $(topsrcdir)/config/rules.mk
 
-JAVA_CLASSPATH := $(ANDROID_SDK)/android.jar:$(ANDROID_SDK)/../../tools/lib/lint.jar:$(ANDROID_SDK)/../../tools/lib/lint-checks.jar
+JAVA_CLASSPATH := $(ANDROID_SDK)/android.jar:$(ANDROID_TOOLS)/lib/lint.jar:$(ANDROID_TOOLS)/lib/lint-checks.jar
 
 # Include Android specific java flags, instead of what's in rules.mk.
 include $(topsrcdir)/config/android-common.mk
 
 export:: annotationProcessors.jar
--- a/build/annotationProcessors/SDKProcessor.java
+++ b/build/annotationProcessors/SDKProcessor.java
@@ -53,20 +53,16 @@ public class SDKProcessor {
         System.out.println("Processing platform bindings...");
 
         String sdkJar = args[0];
         Vector classes = getClassList(args[1]);
         String outdir = args[2];
         String generatedFilePrefix = args[3];
         sMaxSdkVersion = Integer.parseInt(args[4]);
 
-        Properties props = System.getProperties();
-        props.setProperty("com.android.tools.lint.bindir",
-            new File(new File(sdkJar).getParentFile(), "../../tools").toString());
-
         LintCliClient lintClient = new LintCliClient();
         sApiLookup = ApiLookup.get(lintClient);
 
         // Start the clock!
         long s = System.currentTimeMillis();
 
         // Get an iterator over the classes in the jar files given...
         // Iterator<ClassWithOptions> jarClassIterator = IterableJarLoadingURLClassLoader.getIteratorOverJars(args);
@@ -247,9 +243,9 @@ public class SDKProcessor {
                     headerStream.close();
                 } catch (IOException e) {
                     System.err.println("Unable to close headerStream due to "+e);
                     e.printStackTrace(System.err);
                 }
             }
         }
     }
-}
\ No newline at end of file
+}
--- a/build/autoconf/android.m4
+++ b/build/autoconf/android.m4
@@ -371,16 +371,19 @@ case "$target" in
     fi
     AC_MSG_RESULT([$ANDROID_COMPAT_DIR_BASE])
 
     ANDROID_TOOLS="${android_tools}"
     ANDROID_PLATFORM_TOOLS="${android_platform_tools}"
     ANDROID_BUILD_TOOLS="${android_build_tools}"
     AC_SUBST(ANDROID_SDK_ROOT)
     AC_SUBST(ANDROID_SDK)
+    AC_SUBST(ANDROID_TOOLS)
+    AC_SUBST(ANDROID_PLATFORM_TOOLS)
+    AC_SUBST(ANDROID_BUILD_TOOLS)
 
     ANDROID_COMPAT_LIB=$ANDROID_COMPAT_DIR_BASE/v4/android-support-v4.jar
     AC_MSG_CHECKING([for v4 compat library])
     AC_SUBST(ANDROID_COMPAT_LIB)
     if ! test -e $ANDROID_COMPAT_LIB ; then
         AC_MSG_ERROR([You must download the Android v4 support library when targeting Android.  Run the Android SDK tool and install Android Support Library under Extras.  See https://developer.android.com/tools/extras/support-library.html for more info. (looked for $ANDROID_COMPAT_LIB)])
     fi
     AC_MSG_RESULT([$ANDROID_COMPAT_LIB])
--- a/dom/ipc/ContentChild.cpp
+++ b/dom/ipc/ContentChild.cpp
@@ -11,16 +11,17 @@
 #ifdef MOZ_WIDGET_QT
 #include "nsQAppInstance.h"
 #endif
 
 #include "ContentChild.h"
 
 #include "BlobChild.h"
 #include "CrashReporterChild.h"
+#include "GeckoProfiler.h"
 #include "TabChild.h"
 
 #include "mozilla/Attributes.h"
 #ifdef ACCESSIBILITY
 #include "mozilla/a11y/DocAccessibleChild.h"
 #endif
 #include "mozilla/Preferences.h"
 #include "mozilla/docshell/OfflineCacheUpdateChild.h"
@@ -2380,16 +2381,58 @@ ContentChild::RecvOnAppThemeChanged()
 {
     nsCOMPtr<nsIObserverService> os = services::GetObserverService();
     if (os) {
         os->NotifyObservers(nullptr, "app-theme-changed", nullptr);
     }
     return true;
 }
 
+bool
+ContentChild::RecvStartProfiler(const uint32_t& aEntries,
+                                const double& aInterval,
+                                const nsTArray<nsCString>& aFeatures,
+                                const nsTArray<nsCString>& aThreadNameFilters)
+{
+    nsTArray<const char*> featureArray;
+    for (size_t i = 0; i < aFeatures.Length(); ++i) {
+        featureArray.AppendElement(aFeatures[i].get());
+    }
+
+    nsTArray<const char*> threadNameFilterArray;
+    for (size_t i = 0; i < aThreadNameFilters.Length(); ++i) {
+        threadNameFilterArray.AppendElement(aThreadNameFilters[i].get());
+    }
+
+    profiler_start(aEntries, aInterval, featureArray.Elements(), featureArray.Length(),
+                   threadNameFilterArray.Elements(), threadNameFilterArray.Length());
+
+    return true;
+}
+
+bool
+ContentChild::RecvStopProfiler()
+{
+    profiler_stop();
+    return true;
+}
+
+bool
+ContentChild::AnswerGetProfile(nsCString* aProfile)
+{
+    char* profile = profiler_get_profile();
+    if (profile) {
+        *aProfile = nsCString(profile, strlen(profile));
+        free(profile);
+    } else {
+        *aProfile = EmptyCString();
+    }
+    return true;
+}
+
 PBrowserOrId
 ContentChild::GetBrowserOrId(TabChild* aTabChild)
 {
     if (!aTabChild ||
         this == aTabChild->Manager()) {
         return PBrowserOrId(aTabChild);
     }
     else {
--- a/dom/ipc/ContentChild.h
+++ b/dom/ipc/ContentChild.h
@@ -353,16 +353,23 @@ public:
     void AddIdleObserver(nsIObserver* aObserver, uint32_t aIdleTimeInS);
     void RemoveIdleObserver(nsIObserver* aObserver, uint32_t aIdleTimeInS);
     virtual bool RecvNotifyIdleObserver(const uint64_t& aObserver,
                                         const nsCString& aTopic,
                                         const nsString& aData) MOZ_OVERRIDE;
 
     virtual bool RecvOnAppThemeChanged() MOZ_OVERRIDE;
 
+    virtual bool RecvStartProfiler(const uint32_t& aEntries,
+                                   const double& aInterval,
+                                   const nsTArray<nsCString>& aFeatures,
+                                   const nsTArray<nsCString>& aThreadNameFilters) MOZ_OVERRIDE;
+    virtual bool RecvStopProfiler() MOZ_OVERRIDE;
+    virtual bool AnswerGetProfile(nsCString* aProfile) MOZ_OVERRIDE;
+
 #ifdef ANDROID
     gfxIntSize GetScreenSize() { return mScreenSize; }
 #endif
 
     // Get the directory for IndexedDB files. We query the parent for this and
     // cache the value
     nsString &GetIndexedDBPath();
 
--- a/dom/ipc/ContentParent.cpp
+++ b/dom/ipc/ContentParent.cpp
@@ -196,16 +196,21 @@ using namespace mozilla::system;
 #if defined(MOZ_CONTENT_SANDBOX) && defined(XP_LINUX)
 #include "mozilla/Sandbox.h"
 #endif
 
 #ifdef MOZ_TOOLKIT_SEARCH
 #include "nsIBrowserSearchService.h"
 #endif
 
+#ifdef MOZ_ENABLE_PROFILER_SPS
+#include "nsIProfiler.h"
+#include "nsIProfileSaveEvent.h"
+#endif
+
 static NS_DEFINE_CID(kCClipboardCID, NS_CLIPBOARD_CID);
 static const char* sClipboardTextFlavors[] = { kUnicodeMime };
 
 using base::ChildPrivileges;
 using base::KillProcess;
 using namespace mozilla::dom::bluetooth;
 using namespace mozilla::dom::cellbroadcast;
 using namespace mozilla::dom::devicestorage;
@@ -585,16 +590,21 @@ static const char* sObserverTopics[] = {
 #ifdef MOZ_WIDGET_GONK
     NS_VOLUME_STATE_CHANGED,
     "phone-state-changed",
 #endif
 #ifdef ACCESSIBILITY
     "a11y-init-or-shutdown",
 #endif
     "app-theme-changed",
+#ifdef MOZ_ENABLE_PROFILER_SPS
+    "profiler-started",
+    "profiler-stopped",
+    "profiler-subprocess",
+#endif
 };
 
 /* static */ already_AddRefed<ContentParent>
 ContentParent::RunNuwaProcess()
 {
     MOZ_ASSERT(NS_IsMainThread());
     nsRefPtr<ContentParent> nuwaProcess =
         new ContentParent(/* aApp = */ nullptr,
@@ -2790,16 +2800,41 @@ ContentParent::Observe(nsISupports* aSub
     else if (aData && (*aData == '1') &&
              !strcmp(aTopic, "a11y-init-or-shutdown")) {
         unused << SendActivateA11y();
     }
 #endif
     else if (!strcmp(aTopic, "app-theme-changed")) {
         unused << SendOnAppThemeChanged();
     }
+#ifdef MOZ_ENABLE_PROFILER_SPS
+    else if (!strcmp(aTopic, "profiler-started")) {
+        nsCOMPtr<nsIProfilerStartParams> params(do_QueryInterface(aSubject));
+        uint32_t entries;
+        double interval;
+        params->GetEntries(&entries);
+        params->GetInterval(&interval);
+        const nsTArray<nsCString>& features = params->GetFeatures();
+        const nsTArray<nsCString>& threadFilterNames = params->GetThreadFilterNames();
+        unused << SendStartProfiler(entries, interval, features, threadFilterNames);
+    }
+    else if (!strcmp(aTopic, "profiler-stopped")) {
+        unused << SendStopProfiler();
+    }
+    else if (!strcmp(aTopic, "profiler-subprocess")) {
+        nsCOMPtr<nsIProfileSaveEvent> pse = do_QueryInterface(aSubject);
+        if (pse) {
+            nsCString result;
+            unused << CallGetProfile(&result);
+            if (!result.IsEmpty()) {
+                pse->AddSubProfile(result.get());
+            }
+        }
+    }
+#endif
     return NS_OK;
 }
 
   a11y::PDocAccessibleParent*
 ContentParent::AllocPDocAccessibleParent(PDocAccessibleParent* aParent, const uint64_t&)
 {
 #ifdef ACCESSIBILITY
   return new a11y::DocAccessibleParent();
--- a/dom/ipc/PContent.ipdl
+++ b/dom/ipc/PContent.ipdl
@@ -504,16 +504,25 @@ child:
      */
     NotifyIdleObserver(uint64_t observerId, nsCString topic, nsString str);
 
     /**
      * Notify windows in the child to apply a new app style.
      */
     OnAppThemeChanged();
 
+    /**
+     * Control the Gecko Profiler in the child process.
+     */
+    async StartProfiler(uint32_t aEntries, double aInterval, nsCString[] aFeatures,
+                        nsCString[] aThreadNameFilters);
+    async StopProfiler();
+    intr GetProfile()
+      returns (nsCString aProfile);
+
 parent:
     /**
      * Tell the parent process a new accessible document has been created.
      * aParentDoc is the accessible document it was created in if any, and
      * aParentAcc is the id of the accessible in that document the new document
      * is a child of.
      */
     PDocAccessible(nullable PDocAccessible aParentDoc, uint64_t aParentAcc);
--- a/dom/plugins/ipc/PPluginModule.ipdl
+++ b/dom/plugins/ipc/PPluginModule.ipdl
@@ -76,17 +76,23 @@ child:
                             nsString aDisplayName,
                             nsString aIconPath);
 
   async SetParentHangTimeout(uint32_t seconds);
 
   intr PCrashReporter()
     returns (NativeThreadId tid, uint32_t processType);
 
-  intr GeckoGetProfile()
+  /**
+   * Control the Gecko Profiler in the plugin process.
+   */
+  async StartProfiler(uint32_t aEntries, double aInterval, nsCString[] aFeatures,
+                      nsCString[] aThreadNameFilters);
+  async StopProfiler();
+  intr GetProfile()
     returns (nsCString aProfile);
 
   async SettingChanged(PluginSettings settings);
 
 parent:
   /**
    * This message is only used on X11 platforms.
    *
--- a/dom/plugins/ipc/PluginModuleChild.cpp
+++ b/dom/plugins/ipc/PluginModuleChild.cpp
@@ -2365,19 +2365,48 @@ PluginModuleChild::RecvProcessNativeEven
 #ifdef MOZ_WIDGET_COCOA
 void
 PluginModuleChild::ProcessNativeEvents() {
     CallProcessSomeEvents();    
 }
 #endif
 
 bool
-PluginModuleChild::AnswerGeckoGetProfile(nsCString* aProfile) {
+PluginModuleChild::RecvStartProfiler(const uint32_t& aEntries,
+                                     const double& aInterval,
+                                     const nsTArray<nsCString>& aFeatures,
+                                     const nsTArray<nsCString>& aThreadNameFilters)
+{
+    nsTArray<const char*> featureArray;
+    for (size_t i = 0; i < aFeatures.Length(); ++i) {
+        featureArray.AppendElement(aFeatures[i].get());
+    }
+
+    nsTArray<const char*> threadNameFilterArray;
+    for (size_t i = 0; i < aThreadNameFilters.Length(); ++i) {
+        threadNameFilterArray.AppendElement(aThreadNameFilters[i].get());
+    }
+
+    profiler_start(aEntries, aInterval, featureArray.Elements(), featureArray.Length(),
+                   threadNameFilterArray.Elements(), threadNameFilterArray.Length());
+
+    return true;
+}
+
+bool
+PluginModuleChild::RecvStopProfiler()
+{
+    profiler_stop();
+    return true;
+}
+
+bool
+PluginModuleChild::AnswerGetProfile(nsCString* aProfile)
+{
     char* profile = profiler_get_profile();
     if (profile != nullptr) {
         *aProfile = nsCString(profile, strlen(profile));
         free(profile);
     } else {
         *aProfile = nsCString("", 0);
     }
     return true;
 }
-
--- a/dom/plugins/ipc/PluginModuleChild.h
+++ b/dom/plugins/ipc/PluginModuleChild.h
@@ -137,18 +137,22 @@ protected:
     virtual void
     ActorDestroy(ActorDestroyReason why) MOZ_OVERRIDE;
 
     MOZ_NORETURN void QuickExit();
 
     virtual bool
     RecvProcessNativeEventsInInterruptCall() MOZ_OVERRIDE;
 
-    virtual bool
-    AnswerGeckoGetProfile(nsCString* aProfile) MOZ_OVERRIDE;
+    virtual bool RecvStartProfiler(const uint32_t& aEntries,
+                                   const double& aInterval,
+                                   const nsTArray<nsCString>& aFeatures,
+                                   const nsTArray<nsCString>& aThreadNameFilters) MOZ_OVERRIDE;
+    virtual bool RecvStopProfiler() MOZ_OVERRIDE;
+    virtual bool AnswerGetProfile(nsCString* aProfile) MOZ_OVERRIDE;
 
 public:
     PluginModuleChild(bool aIsChrome);
     virtual ~PluginModuleChild();
 
     bool CommonInit(base::ProcessHandle aParentProcessHandle,
                     MessageLoop* aIOLoop,
                     IPC::Channel* aChannel);
--- a/dom/plugins/ipc/PluginModuleParent.cpp
+++ b/dom/plugins/ipc/PluginModuleParent.cpp
@@ -35,16 +35,17 @@
 #include "GeckoProfiler.h"
 
 #ifdef XP_WIN
 #include "PluginHangUIParent.h"
 #include "mozilla/widget/AudioSession.h"
 #endif
 
 #ifdef MOZ_ENABLE_PROFILER_SPS
+#include "nsIProfiler.h"
 #include "nsIProfileSaveEvent.h"
 #endif
 
 #ifdef MOZ_WIDGET_GTK
 #include <glib.h>
 #elif XP_MACOSX
 #include "PluginInterposeOSX.h"
 #include "PluginUtilsOSX.h"
@@ -1963,38 +1964,55 @@ private:
 
 NS_IMPL_ISUPPORTS(PluginProfilerObserver, nsIObserver, nsISupportsWeakReference)
 
 NS_IMETHODIMP
 PluginProfilerObserver::Observe(nsISupports *aSubject,
                                 const char *aTopic,
                                 const char16_t *aData)
 {
-    nsCOMPtr<nsIProfileSaveEvent> pse = do_QueryInterface(aSubject);
-    if (pse) {
-      nsCString result;
-      bool success = mPmp->CallGeckoGetProfile(&result);
-      if (success && !result.IsEmpty()) {
-          pse->AddSubProfile(result.get());
-      }
+    if (!strcmp(aTopic, "profiler-started")) {
+        nsCOMPtr<nsIProfilerStartParams> params(do_QueryInterface(aSubject));
+        uint32_t entries;
+        double interval;
+        params->GetEntries(&entries);
+        params->GetInterval(&interval);
+        const nsTArray<nsCString>& features = params->GetFeatures();
+        const nsTArray<nsCString>& threadFilterNames = params->GetThreadFilterNames();
+        unused << mPmp->SendStartProfiler(entries, interval, features, threadFilterNames);
+    } else if (!strcmp(aTopic, "profiler-stopped")) {
+        unused << mPmp->SendStopProfiler();
+    } else if (!strcmp(aTopic, "profiler-subprocess")) {
+        nsCOMPtr<nsIProfileSaveEvent> pse = do_QueryInterface(aSubject);
+        if (pse) {
+            nsCString result;
+            bool success = mPmp->CallGetProfile(&result);
+            if (success && !result.IsEmpty()) {
+                pse->AddSubProfile(result.get());
+            }
+        }
     }
     return NS_OK;
 }
 
 void
 PluginModuleChromeParent::InitPluginProfiling()
 {
     nsCOMPtr<nsIObserverService> observerService = mozilla::services::GetObserverService();
     if (observerService) {
         mProfilerObserver = new PluginProfilerObserver(this);
+        observerService->AddObserver(mProfilerObserver, "profiler-started", false);
+        observerService->AddObserver(mProfilerObserver, "profiler-stopped", false);
         observerService->AddObserver(mProfilerObserver, "profiler-subprocess", false);
     }
 }
 
 void
 PluginModuleChromeParent::ShutdownPluginProfiling()
 {
     nsCOMPtr<nsIObserverService> observerService = mozilla::services::GetObserverService();
     if (observerService) {
+        observerService->RemoveObserver(mProfilerObserver, "profiler-started");
+        observerService->RemoveObserver(mProfilerObserver, "profiler-stopped");
         observerService->RemoveObserver(mProfilerObserver, "profiler-subprocess");
     }
 }
 #endif
--- a/embedding/android/geckoview_example/Makefile.in
+++ b/embedding/android/geckoview_example/Makefile.in
@@ -20,17 +20,17 @@ GARBAGE_DIRS = \
 	gen \
 	bin \
 	libs \
 	res \
 	src \
 	binaries \
 	$(NULL)
 
-ANDROID=$(ANDROID_SDK)/../../tools/android
+ANDROID=$(ANDROID_TOOLS)/android
 
 TARGET="android-$(ANDROID_TARGET_SDK)"
 
 PACKAGE_DEPS = \
 	assets/libxul.so \
 	build.xml \
 	src/org/mozilla/geckoviewexample/GeckoViewExample.java \
 	$(CURDIR)/res/layout/main.xml \
--- a/mobile/android/components/build/nsAndroidHistory.cpp
+++ b/mobile/android/components/build/nsAndroidHistory.cpp
@@ -1,32 +1,36 @@
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 #include "nsThreadUtils.h"
 #include "nsAndroidHistory.h"
+#include "nsComponentManagerUtils.h"
 #include "AndroidBridge.h"
 #include "Link.h"
 #include "nsIURI.h"
-#include "mozilla/Services.h"
 #include "nsIObserverService.h"
 
+#include "mozilla/Services.h"
 #include "mozilla/Preferences.h"
 
 #define NS_LINK_VISITED_EVENT_TOPIC "link-visited"
 
 // We copy Places here.
 // Note that we don't yet observe this pref at runtime.
 #define PREF_HISTORY_ENABLED "places.history.enabled"
 
+// Time we wait to see if a pending visit is really a redirect
+#define PENDING_REDIRECT_TIMEOUT 3000
+
 using namespace mozilla;
 using mozilla::dom::Link;
 
-NS_IMPL_ISUPPORTS(nsAndroidHistory, IHistory, nsIRunnable)
+NS_IMPL_ISUPPORTS(nsAndroidHistory, IHistory, nsIRunnable, nsITimerCallback)
 
 nsAndroidHistory* nsAndroidHistory::sHistory = nullptr;
 
 /*static*/
 nsAndroidHistory*
 nsAndroidHistory::GetSingleton()
 {
   if (!sHistory) {
@@ -37,16 +41,18 @@ nsAndroidHistory::GetSingleton()
   NS_ADDREF(sHistory);
   return sHistory;
 }
 
 nsAndroidHistory::nsAndroidHistory()
   : mHistoryEnabled(true)
 {
   LoadPrefs();
+
+  mTimer = do_CreateInstance(NS_TIMER_CONTRACTID);
 }
 
 NS_IMETHODIMP
 nsAndroidHistory::RegisterVisitedCallback(nsIURI *aURI, Link *aContent)
 {
   if (!aContent || !aURI)
     return NS_OK;
 
@@ -153,21 +159,86 @@ nsAndroidHistory::IsEmbedURI(nsIURI* aUR
   EmbedArray::index_type i;
   EmbedArray::size_type length = mEmbedURIs.Length();
   for (i = 0; i < length && !equals; ++i) {
     aURI->Equals(mEmbedURIs.ElementAt(i), &equals);
   }
   return equals;
 }
 
+inline bool
+nsAndroidHistory::RemovePendingVisitURI(nsIURI* aURI) {
+  // Remove the first pending URI that matches. Return a boolean to
+  // let the caller know if we removed a URI or not.
+  bool equals = false;
+  PendingVisitArray::index_type i;
+  for (i = 0; i < mPendingVisitURIs.Length(); ++i) {
+    aURI->Equals(mPendingVisitURIs.ElementAt(i), &equals);
+    if (equals) {
+      mPendingVisitURIs.RemoveElementAt(i);
+      return true;
+    }
+  }
+  return false;
+}
+
+NS_IMETHODIMP
+nsAndroidHistory::Notify(nsITimer *timer)
+{
+  // Any pending visits left in the queue have exceeded our threshold for
+  // redirects, so save them
+  PendingVisitArray::index_type i;
+  for (i = 0; i < mPendingVisitURIs.Length(); ++i) {
+    SaveVisitURI(mPendingVisitURIs.ElementAt(i));
+  }
+  mPendingVisitURIs.Clear();
+
+  return NS_OK;
+}
+
+void
+nsAndroidHistory::SaveVisitURI(nsIURI* aURI) {
+  // Add the URI to our cache so we can take a fast path later
+  AppendToRecentlyVisitedURIs(aURI);
+
+  if (AndroidBridge::HasEnv()) {
+    // Save this URI in our history
+    nsAutoCString spec;
+    (void)aURI->GetSpec(spec);
+    mozilla::widget::android::GeckoAppShell::MarkURIVisited(NS_ConvertUTF8toUTF16(spec));
+  }
+
+  // Finally, notify that we've been visited.
+  nsCOMPtr<nsIObserverService> obsService = mozilla::services::GetObserverService();
+  if (obsService) {
+    obsService->NotifyObservers(aURI, NS_LINK_VISITED_EVENT_TOPIC, nullptr);
+  }
+}
+
 NS_IMETHODIMP
 nsAndroidHistory::VisitURI(nsIURI *aURI, nsIURI *aLastVisitedURI, uint32_t aFlags)
 {
-  if (!aURI)
+  if (!aURI) {
+    return NS_OK;
+  }
+
+  if (!(aFlags & VisitFlags::TOP_LEVEL)) {
     return NS_OK;
+  }
+
+  if (aFlags & VisitFlags::UNRECOVERABLE_ERROR) {
+    return NS_OK;
+  }
+
+  if (aFlags & VisitFlags::REDIRECT_SOURCE || aFlags & VisitFlags::REDIRECT_PERMANENT || aFlags & VisitFlags::REDIRECT_TEMPORARY) {
+    // aLastVisitedURI redirected to aURI. We want to ignore aLastVisitedURI,
+    // so remove the pending visit. We want to give aURI a chance to be saved,
+    // so don't return early.
+    RemovePendingVisitURI(aLastVisitedURI);
+  }
 
   // Silently return if URI is something we shouldn't add to DB.
   bool canAdd;
   nsresult rv = CanAddURI(aURI, &canAdd);
   NS_ENSURE_SUCCESS(rv, rv);
   if (!canAdd) {
     return NS_OK;
   }
@@ -175,45 +246,27 @@ nsAndroidHistory::VisitURI(nsIURI *aURI,
   if (aLastVisitedURI) {
     bool same;
     rv = aURI->Equals(aLastVisitedURI, &same);
     NS_ENSURE_SUCCESS(rv, rv);
     if (same && IsRecentlyVisitedURI(aURI)) {
       // Do not save refresh visits if we have visited this URI recently.
       return NS_OK;
     }
-  }
 
-  if (!(aFlags & VisitFlags::TOP_LEVEL)) {
-    AppendToEmbedURIs(aURI);
-    return NS_OK;
+    // Since we have a last visited URI and we were not redirected, it is
+    // safe to save the visit if it's still pending.
+    if (RemovePendingVisitURI(aLastVisitedURI)) {
+      SaveVisitURI(aLastVisitedURI);
+    }
   }
 
-  if (aFlags & VisitFlags::REDIRECT_SOURCE)
-    return NS_OK;
-
-  if (aFlags & VisitFlags::UNRECOVERABLE_ERROR)
-    return NS_OK;
-
-  if (AndroidBridge::HasEnv()) {
-    nsAutoCString uri;
-    rv = aURI->GetSpec(uri);
-    if (NS_FAILED(rv)) return rv;
-    NS_ConvertUTF8toUTF16 uriString(uri);
-    mozilla::widget::android::GeckoAppShell::MarkURIVisited(uriString);
-  }
-
-  AppendToRecentlyVisitedURIs(aURI);
-
-  // Finally, notify that we've been visited.
-  nsCOMPtr<nsIObserverService> obsService =
-    mozilla::services::GetObserverService();
-  if (obsService) {
-    obsService->NotifyObservers(aURI, NS_LINK_VISITED_EVENT_TOPIC, nullptr);
-  }
+  // Let's wait and see if this visit is not a redirect.
+  mPendingVisitURIs.AppendElement(aURI);
+  mTimer->InitWithCallback(this, PENDING_REDIRECT_TIMEOUT, nsITimer::TYPE_ONE_SHOT);
 
   return NS_OK;
 }
 
 NS_IMETHODIMP
 nsAndroidHistory::SetURITitle(nsIURI *aURI, const nsAString& aTitle)
 {
   // Silently return if URI is something we shouldn't add to DB.
@@ -239,27 +292,27 @@ nsAndroidHistory::SetURITitle(nsIURI *aU
 }
 
 NS_IMETHODIMP
 nsAndroidHistory::NotifyVisited(nsIURI *aURI)
 {
   if (aURI && sHistory) {
     nsAutoCString spec;
     (void)aURI->GetSpec(spec);
-    sHistory->mPendingURIs.Push(NS_ConvertUTF8toUTF16(spec));
+    sHistory->mPendingLinkURIs.Push(NS_ConvertUTF8toUTF16(spec));
     NS_DispatchToMainThread(sHistory);
   }
   return NS_OK;
 }
 
 NS_IMETHODIMP
 nsAndroidHistory::Run()
 {
-  while (! mPendingURIs.IsEmpty()) {
-    nsString uriString = mPendingURIs.Pop();
+  while (! mPendingLinkURIs.IsEmpty()) {
+    nsString uriString = mPendingLinkURIs.Pop();
     nsTArray<Link*>* list = sHistory->mListeners.Get(uriString);
     if (list) {
       for (unsigned int i = 0; i < list->Length(); i++) {
         list->ElementAt(i)->SetLinkState(eLinkState_Visited);
       }
       // as per the IHistory interface contract, remove the
       // Link pointers once they have been notified
       mListeners.Remove(uriString);
--- a/mobile/android/components/build/nsAndroidHistory.h
+++ b/mobile/android/components/build/nsAndroidHistory.h
@@ -6,32 +6,37 @@
 #ifndef NS_ANDROIDHISTORY_H
 #define NS_ANDROIDHISTORY_H
 
 #include "IHistory.h"
 #include "nsDataHashtable.h"
 #include "nsTPriorityQueue.h"
 #include "nsIRunnable.h"
 #include "nsIURI.h"
+#include "nsITimer.h"
+
 
 #define NS_ANDROIDHISTORY_CID \
     {0xCCAA4880, 0x44DD, 0x40A7, {0xA1, 0x3F, 0x61, 0x56, 0xFC, 0x88, 0x2C, 0x0B}}
 
 // Max size of History::mRecentlyVisitedURIs
 #define RECENTLY_VISITED_URI_SIZE 8
 
 // Max size of History::mEmbedURIs
 #define EMBED_URI_SIZE 128
 
-class nsAndroidHistory MOZ_FINAL : public mozilla::IHistory, public nsIRunnable
+class nsAndroidHistory MOZ_FINAL : public mozilla::IHistory,
+                                   public nsIRunnable,
+                                   public nsITimerCallback
 {
 public:
   NS_DECL_ISUPPORTS
   NS_DECL_IHISTORY
   NS_DECL_NSIRUNNABLE
+  NS_DECL_NSITIMERCALLBACK
 
   /**
    * Obtains a pointer that has had AddRef called on it.  Used by the service
    * manager only.
    */
   static nsAndroidHistory* GetSingleton();
 
   nsAndroidHistory();
@@ -39,29 +44,43 @@ public:
 private:
   ~nsAndroidHistory() {}
 
   static nsAndroidHistory* sHistory;
 
   // Will mimic the value of the places.history.enabled preference.
   bool mHistoryEnabled;
 
-  nsDataHashtable<nsStringHashKey, nsTArray<mozilla::dom::Link *> *> mListeners;
-  nsTPriorityQueue<nsString> mPendingURIs;
-
   void LoadPrefs();
   bool ShouldRecordHistory();
   nsresult CanAddURI(nsIURI* aURI, bool* canAdd);
 
   /**
+   * We need to manage data used to determine a:visited status.
+   */
+  nsDataHashtable<nsStringHashKey, nsTArray<mozilla::dom::Link *> *> mListeners;
+  nsTPriorityQueue<nsString> mPendingLinkURIs;
+
+  /**
+   * Redirection (temporary and permanent) flags are sent with the redirected
+   * URI, not the original URI. Since we want to ignore the original URI, we
+   * need to cache the pending visit and make sure it doesn't redirect.
+   */
+  nsRefPtr<nsITimer> mTimer;
+  typedef nsAutoTArray<nsCOMPtr<nsIURI>, RECENTLY_VISITED_URI_SIZE> PendingVisitArray;
+  PendingVisitArray mPendingVisitURIs;
+
+  bool RemovePendingVisitURI(nsIURI* aURI);
+  void SaveVisitURI(nsIURI* aURI);
+
+  /**
    * mRecentlyVisitedURIs remembers URIs which are recently added to the DB,
    * to avoid saving these locations repeatedly in a short period.
    */
-  typedef nsAutoTArray<nsCOMPtr<nsIURI>, RECENTLY_VISITED_URI_SIZE>
-          RecentlyVisitedArray;
+  typedef nsAutoTArray<nsCOMPtr<nsIURI>, RECENTLY_VISITED_URI_SIZE> RecentlyVisitedArray;
   RecentlyVisitedArray mRecentlyVisitedURIs;
   RecentlyVisitedArray::index_type mRecentlyVisitedURIsNextIndex;
 
   void AppendToRecentlyVisitedURIs(nsIURI* aURI);
   bool IsRecentlyVisitedURI(nsIURI* aURI);
 
   /**
    * mEmbedURIs remembers URIs which are explicitly not added to the DB,
--- a/mobile/android/geckoview_library/Makefile.in
+++ b/mobile/android/geckoview_library/Makefile.in
@@ -5,23 +5,23 @@
 INSTALL_TARGETS += GECKOVIEW_LIBRARY
 GECKOVIEW_LIBRARY_DEST = $(CURDIR)
 GECKOVIEW_LIBRARY_FILES := \
   .classpath \
   .project \
   build.xml \
   $(NULL)
 
-PP_TARGETS = properties manifest project
-
-properties = local.properties.in
-project = project.properties.in
-manifest = AndroidManifest.xml.in
-
-GARBAGE = $(GECKOVIEW_LIBRARY_FILES) local.properties project.properties AndroidManifest.xml
+PP_TARGETS += gen
+gen := \
+  local.properties.in \
+  project.properties.in \
+  AndroidManifest.xml.in \
+  $(NULL)
+gen_FLAGS += -DANDROID_SDK_ROOT=$(ANDROID_SDK_ROOT)
 
 GARBAGE_DIRS = \
   bin \
   libs \
   src \
   .deps \
   gen  \
   res \
--- a/mobile/android/geckoview_library/local.properties.in
+++ b/mobile/android/geckoview_library/local.properties.in
@@ -3,9 +3,9 @@
 # Do not modify this file -- YOUR CHANGES WILL BE ERASED!
 #
 # This file must *NOT* be checked into Version Control Systems,
 # as it contains information specific to your local configuration.
 
 # location of the SDK. This is only used by Ant
 # For customization when using a Version Control System, please read the
 # header note.
-sdk.dir=@ANDROID_SDK@/../../
+sdk.dir=@ANDROID_SDK_ROOT@
--- a/toolkit/devtools/server/actors/highlighter.css
+++ b/toolkit/devtools/server/actors/highlighter.css
@@ -165,8 +165,16 @@
 
 :-moz-native-anonymous .css-transform-transformed,
 :-moz-native-anonymous .css-transform-untransformed,
 :-moz-native-anonymous .css-transform-line {
   stroke: #08C;
   stroke-dasharray: 5 3;
   stroke-width: 2;
 }
+
+/* Rect highlighter */
+
+:-moz-native-anonymous .highlighted-rect {
+  position: absolute;
+  background: #80d4ff;
+  opacity: 0.8;
+}
--- a/toolkit/devtools/server/actors/highlighter.js
+++ b/toolkit/devtools/server/actors/highlighter.js
@@ -43,17 +43,18 @@ const SIMPLE_OUTLINE_SHEET = ".__fx-devt
                              "  outline: 2px dashed #F06!important;" +
                              "  outline-offset: -2px!important;" +
                              "}";
 
 // All possible highlighter classes
 let HIGHLIGHTER_CLASSES = exports.HIGHLIGHTER_CLASSES = {
   "BoxModelHighlighter": BoxModelHighlighter,
   "CssTransformHighlighter": CssTransformHighlighter,
-  "SelectorHighlighter": SelectorHighlighter
+  "SelectorHighlighter": SelectorHighlighter,
+  "RectHighlighter": RectHighlighter
 };
 
 /**
  * The Highlighter is the server-side entry points for any tool that wishes to
  * highlight elements in some way in the content document.
  *
  * A little bit of vocabulary:
  * - <something>HighlighterActor classes are the actors that can be used from
@@ -338,17 +339,27 @@ let CustomHighlighterActor = exports.Cus
   get conn() this._inspector && this._inspector.conn,
 
   destroy: function() {
     protocol.Actor.prototype.destroy.call(this);
     this.finalize();
   },
 
   /**
-   * Display the highlighter on a given NodeActor.
+   * Show the highlighter.
+   * This calls through to the highlighter instance's |show(node, options)|
+   * method.
+   *
+   * Most custom highlighters are made to highlight DOM nodes, hence the first
+   * NodeActor argument (NodeActor as in toolkit/devtools/server/actor/inspector).
+   * Note however that some highlighters use this argument merely as a context
+   * node: the RectHighlighter for instance uses it to calculate the absolute
+   * position of the provided rect. The SelectHighlighter uses it as a base node
+   * to run the provided CSS selector on.
+   *
    * @param NodeActor The node to be highlighted
    * @param Object Options for the custom highlighter
    */
   show: method(function(node, options) {
     if (!node || !isNodeValid(node.rawNode) || !this._highlighter) {
       return;
     }
 
@@ -1468,16 +1479,96 @@ SelectorHighlighter.prototype = {
 
   destroy: function() {
     this.hide();
     this.tabActor = null;
   }
 };
 
 /**
+ * The RectHighlighter is a class that draws a rectangle highlighter at specific
+ * coordinates.
+ * It does *not* highlight DOM nodes, but rects.
+ * It also does *not* update dynamically, it only highlights a rect and remains
+ * there as long as it is shown.
+ */
+function RectHighlighter(tabActor) {
+  this.win = tabActor.window;
+  this.layoutHelpers = new LayoutHelpers(this.win);
+  this.markup = new CanvasFrameAnonymousContentHelper(tabActor,
+    this._buildMarkup.bind(this));
+}
+
+RectHighlighter.prototype = {
+  _buildMarkup: function() {
+    let doc = this.win.document;
+
+    let container = doc.createElement("div");
+    container.className = "highlighter-container";
+    container.innerHTML = '<div id="highlighted-rect" ' +
+                          'class="highlighted-rect" hidden="true">';
+
+    return container;
+  },
+
+  destroy: function() {
+    this.win = null;
+    this.layoutHelpers = null;
+    this.markup.destroy();
+  },
+
+  _hasValidOptions: function(options) {
+    let isValidNb = n => typeof n === "number" && n >= 0 && isFinite(n);
+    return options && options.rect &&
+           isValidNb(options.rect.x) &&
+           isValidNb(options.rect.y) &&
+           options.rect.width && isValidNb(options.rect.width) &&
+           options.rect.height && isValidNb(options.rect.height);
+  },
+
+  /**
+   * @param {DOMNode} node The highlighter rect is relatively positioned to the
+   * viewport this node is in. Using the provided node, the highligther will get
+   * the parent documentElement and use it as context to position the
+   * highlighter correctly.
+   * @param {Object} options Accepts the following options:
+   * - rect: mandatory object that should have the x, y, width, height properties
+   * - fill: optional fill color for the rect
+   */
+  show: function(node, options) {
+    if (!this._hasValidOptions(options) || !node || !node.ownerDocument) {
+      this.hide();
+      return;
+    }
+
+    let contextNode = node.ownerDocument.documentElement;
+
+    // Caculate the absolute rect based on the context node's adjusted quads.
+    let {bounds} = this.layoutHelpers.getAdjustedQuads(contextNode);
+    let x = "left:" + (bounds.x + options.rect.x) + "px;";
+    let y = "top:" + (bounds.y + options.rect.y) + "px;";
+    let width = "width:" + options.rect.width + "px;";
+    let height = "height:" + options.rect.height + "px;";
+
+    let style = x + y + width + height;
+    if (options.fill) {
+      style += "background:" + options.fill + ";";
+    }
+
+    // Set the coordinates of the highlighter and show it
+    this.markup.setAttributeForElement("highlighted-rect", "style", style);
+    this.markup.removeAttributeForElement("highlighted-rect", "hidden");
+  },
+
+  hide: function() {
+    this.markup.setAttributeForElement("highlighted-rect", "hidden", "true");
+  }
+};
+
+/**
  * The SimpleOutlineHighlighter is a class that has the same API than the
  * BoxModelHighlighter, but adds a pseudo-class on the target element itself
  * to draw a simple css outline around the element.
  * It is used by the HighlighterActor when canvasframe-based highlighters can't
  * be used. This is the case for XUL windows.
  */
 function SimpleOutlineHighlighter(tabActor) {
   this.chromeDoc = tabActor.window.document;
--- a/toolkit/devtools/server/actors/root.js
+++ b/toolkit/devtools/server/actors/root.js
@@ -123,17 +123,18 @@ RootActor.prototype = {
     // Whether the server-side highlighter actor exists and can be used to
     // remotely highlight nodes (see server/actors/highlighter.js)
     highlightable: true,
     // Which custom highlighter does the server-side highlighter actor supports?
     // (see server/actors/highlighter.js)
     customHighlighters: [
       "BoxModelHighlighter",
       "CssTransformHighlighter",
-      "SelectorHighlighter"
+      "SelectorHighlighter",
+      "RectHighlighter"
     ],
     // Whether the inspector actor implements the getImageDataFromURL
     // method that returns data-uris for image URLs. This is used for image
     // tooltips for instance
     urlToImageDataResolver: true,
     networkMonitor: true,
     // Whether the storage inspector actor to inspect cookies, etc.
     storageInspector: true,
--- a/tools/profiler/ProfileEntry.cpp
+++ b/tools/profiler/ProfileEntry.cpp
@@ -318,16 +318,20 @@ void ThreadProfile::ToStreamAsJSON(std::
 
 void ThreadProfile::StreamJSObject(JSStreamWriter& b)
 {
   b.BeginObject();
     // Thread meta data
     if (XRE_GetProcessType() == GeckoProcessType_Plugin) {
       // TODO Add the proper plugin name
       b.NameValue("name", "Plugin");
+    } else if (XRE_GetProcessType() == GeckoProcessType_Content) {
+      // This isn't going to really help once we have multiple content
+      // processes, but it'll do for now.
+      b.NameValue("name", "Content");
     } else {
       b.NameValue("name", Name());
     }
     b.NameValue("tid", static_cast<int>(mThreadId));
 
     b.Name("samples");
     b.BeginArray();
 
--- a/tools/profiler/moz.build
+++ b/tools/profiler/moz.build
@@ -23,16 +23,17 @@ if CONFIG['MOZ_ENABLE_PROFILER_SPS']:
     EXTRA_JS_MODULES += [
         'Profiler.jsm',
     ]
     UNIFIED_SOURCES += [
         'BreakpadSampler.cpp',
         'JSStreamWriter.cpp',
         'nsProfiler.cpp',
         'nsProfilerFactory.cpp',
+        'nsProfilerStartParams.cpp',
         'platform.cpp',
         'ProfileEntry.cpp',
         'ProfilerBacktrace.cpp',
         'ProfilerIOInterposeObserver.cpp',
         'ProfilerMarkers.cpp',
         'SaveProfileTask.cpp',
         'SyncProfile.cpp',
         'TableTicker.cpp',
--- a/tools/profiler/nsIProfiler.idl
+++ b/tools/profiler/nsIProfiler.idl
@@ -1,15 +1,22 @@
 /* -*- Mode: IDL; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
  
 #include "nsISupports.idl"
 
+%{C++
+template<class T> class nsTArray;
+class nsCString;
+%}
+
+[ref] native StringArrayRef(const nsTArray<nsCString>);
+
 [scriptable, uuid(f7f3709c-04a9-45c6-8e34-40f762654a78)]
 interface nsIProfiler : nsISupports
 {
   void StartProfiler(in uint32_t aEntries, in double aInterval,
                       [array, size_is(aFeatureCount)] in string aFeatures,
                       in uint32_t aFeatureCount,
                       [array, size_is(aFilterCount), optional] in string aThreadNameFilters,
                       [optional] in uint32_t aFilterCount);
@@ -32,8 +39,22 @@ interface nsIProfiler : nsISupports
    *
    * On Windows profiling builds, the shared library objects will have
    * additional pdbSignature and pdbAge properties for uniquely identifying
    * shared library versions for stack symbolication.
    */
   AString getSharedLibraryInformation();
 };
 
+/**
+ * Start-up parameters for subprocesses are passed through nsIObserverService,
+ * which, unfortunately, means we need to implement nsISupports in order to
+ * go through it.
+ */
+[uuid(0a175ba7-8fcf-4ce9-9c4b-ccc6272f4425)]
+interface nsIProfilerStartParams : nsISupports
+{
+  attribute uint32_t entries;
+  attribute double interval;
+
+  [noscript, notxpcom, nostdcall] StringArrayRef getFeatures();
+  [noscript, notxpcom, nostdcall] StringArrayRef getThreadFilterNames();
+};
new file mode 100644
--- /dev/null
+++ b/tools/profiler/nsProfilerStartParams.cpp
@@ -0,0 +1,67 @@
+/* -*- Mode: C++; tab-width: 20; indent-tabs-mode: nil; c-basic-offset: 4 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "nsProfilerStartParams.h"
+
+NS_IMPL_ISUPPORTS(nsProfilerStartParams, nsIProfilerStartParams)
+
+nsProfilerStartParams::nsProfilerStartParams(uint32_t aEntries,
+                                             double aInterval,
+                                             const nsTArray<nsCString>& aFeatures,
+                                             const nsTArray<nsCString>& aThreadFilterNames) :
+  mEntries(aEntries),
+  mInterval(aInterval),
+  mFeatures(aFeatures),
+  mThreadFilterNames(aThreadFilterNames)
+{
+}
+
+nsProfilerStartParams::~nsProfilerStartParams()
+{
+}
+
+NS_IMETHODIMP
+nsProfilerStartParams::GetEntries(uint32_t* aEntries)
+{
+  NS_ENSURE_ARG_POINTER(aEntries);
+  *aEntries = mEntries;
+  return NS_OK;
+}
+
+NS_IMETHODIMP
+nsProfilerStartParams::SetEntries(uint32_t aEntries)
+{
+  NS_ENSURE_ARG(aEntries);
+  mEntries = aEntries;
+  return NS_OK;
+}
+
+NS_IMETHODIMP
+nsProfilerStartParams::GetInterval(double* aInterval)
+{
+  NS_ENSURE_ARG_POINTER(aInterval);
+  *aInterval = mInterval;
+  return NS_OK;
+}
+
+NS_IMETHODIMP
+nsProfilerStartParams::SetInterval(double aInterval)
+{
+  NS_ENSURE_ARG(aInterval);
+  mInterval = aInterval;
+  return NS_OK;
+}
+
+const nsTArray<nsCString>&
+nsProfilerStartParams::GetFeatures()
+{
+  return mFeatures;
+}
+
+const nsTArray<nsCString>&
+nsProfilerStartParams::GetThreadFilterNames()
+{
+  return mThreadFilterNames;
+}
new file mode 100644
--- /dev/null
+++ b/tools/profiler/nsProfilerStartParams.h
@@ -0,0 +1,32 @@
+/* -*- Mode: C++; tab-width: 20; indent-tabs-mode: nil; c-basic-offset: 4 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef _NSPROFILERSTARTPARAMS_H_
+#define _NSPROFILERSTARTPARAMS_H_
+
+#include "nsIProfiler.h"
+#include "nsString.h"
+#include "nsTArray.h"
+
+class nsProfilerStartParams : public nsIProfilerStartParams
+{
+public:
+  NS_DECL_ISUPPORTS
+  NS_DECL_NSIPROFILERSTARTPARAMS
+
+  nsProfilerStartParams(uint32_t aEntries,
+                        double aInterval,
+                        const nsTArray<nsCString>& aFeatures,
+                        const nsTArray<nsCString>& aThreadFilterNames);
+
+private:
+  virtual ~nsProfilerStartParams();
+  uint32_t mEntries;
+  double mInterval;
+  nsTArray<nsCString> mFeatures;
+  nsTArray<nsCString> mThreadFilterNames;
+};
+
+#endif
--- a/tools/profiler/platform.cpp
+++ b/tools/profiler/platform.cpp
@@ -14,16 +14,17 @@
 #include "mozilla/StaticPtr.h"
 #include "mozilla/ThreadLocal.h"
 #include "PseudoStack.h"
 #include "TableTicker.h"
 #include "UnwinderThread2.h"
 #include "nsIObserverService.h"
 #include "nsDirectoryServiceUtils.h"
 #include "nsDirectoryServiceDefs.h"
+#include "nsProfilerStartParams.h"
 #include "mozilla/Services.h"
 #include "nsThreadUtils.h"
 #include "ProfilerMarkers.h"
 #include "nsXULAppAPI.h"
 
 #if defined(SPS_OS_android) && !defined(MOZ_WIDGET_GONK)
   #include "AndroidBridge.h"
   using namespace mozilla::widget::android;
@@ -775,18 +776,34 @@ void mozilla_sampler_start(int aProfileE
                                     sInterposeObserver);
   }
 
   sIsProfiling = true;
   sIsGPUProfiling = t->ProfileGPU();
 
   if (Sampler::CanNotifyObservers()) {
     nsCOMPtr<nsIObserverService> os = mozilla::services::GetObserverService();
-    if (os)
-      os->NotifyObservers(nullptr, "profiler-started", nullptr);
+    if (os) {
+      nsTArray<nsCString> featuresArray;
+      nsTArray<nsCString> threadNameFiltersArray;
+
+      for (size_t i = 0; i < aFeatureCount; ++i) {
+        featuresArray.AppendElement(aFeatures[i]);
+      }
+
+      for (size_t i = 0; i < aFilterCount; ++i) {
+        threadNameFiltersArray.AppendElement(aThreadNameFilters[i]);
+      }
+
+      nsCOMPtr<nsIProfilerStartParams> params =
+        new nsProfilerStartParams(aProfileEntries, aInterval, featuresArray,
+                                  threadNameFiltersArray);
+
+      os->NotifyObservers(params, "profiler-started", nullptr);
+    }
   }
 
   LOG("END   mozilla_sampler_start");
 }
 
 void mozilla_sampler_stop()
 {
   LOG("BEGIN mozilla_sampler_stop");
--- a/widget/android/bindings/Makefile.in
+++ b/widget/android/bindings/Makefile.in
@@ -1,23 +1,27 @@
 # This Source Code Form is subject to the terms of the Mozilla Public
 # License, v. 2.0. If a copy of the MPL was not distributed with this
 # file, You can obtain one at http://mozilla.org/MPL/2.0/.
 
-annotation_processor_jar_files := $(DEPTH)/build/annotationProcessors/annotationProcessors.jar:$(ANDROID_SDK)/../../tools/lib/lint.jar:$(ANDROID_SDK)/../../tools/lib/lint-checks.jar
+annotation_processor_jar_files := $(DEPTH)/build/annotationProcessors/annotationProcessors.jar:$(ANDROID_TOOLS)/lib/lint.jar:$(ANDROID_TOOLS)/lib/lint-checks.jar
 
-MediaCodec.cpp: $(ANDROID_SDK)/android.jar mediacodec-classes.txt
-	$(JAVA) -classpath $(annotation_processor_jar_files) org.mozilla.gecko.annotationProcessors.SDKProcessor $(ANDROID_SDK)/android.jar $(srcdir)/mediacodec-classes.txt $(CURDIR) MediaCodec 16
+sdk_processor := \
+  $(JAVA) \
+  -Dcom.android.tools.lint.bindir='$(ANDROID_TOOLS)' \
+  -classpath $(annotation_processor_jar_files) \
+  org.mozilla.gecko.annotationProcessors.SDKProcessor
 
-MediaCodec.h: MediaCodec.cpp
+# For the benefit of readers: the following pattern rule says that,
+# for example, MediaCodec.cpp and MediaCodec.h can be produced from
+# MediaCodec-classes.txt.  This formulation invokes the SDK processor
+# at most once.
 
-SurfaceTexture.cpp: $(ANDROID_SDK)/android.jar surfacetexture-classes.txt
-	$(JAVA) -classpath $(annotation_processor_jar_files) org.mozilla.gecko.annotationProcessors.SDKProcessor $(ANDROID_SDK)/android.jar $(srcdir)/surfacetexture-classes.txt $(CURDIR) SurfaceTexture 16
-
-SurfaceTexture.h: SurfaceTexture.cpp
+%.cpp %.h: $(ANDROID_SDK)/android.jar %-classes.txt
+	$(sdk_processor) $(ANDROID_SDK)/android.jar $(srcdir)/$*-classes.txt $(CURDIR) $* 16
 
 # We'd like these to be defined in a future GENERATED_EXPORTS list.
 bindings_exports_FILES := \
   MediaCodec.h \
   SurfaceTexture.h \
   $(NULL)
 bindings_exports_DEST = $(DIST)/include
 bindings_exports_TARGET := export
rename from widget/android/bindings/mediacodec-classes.txt
rename to widget/android/bindings/MediaCodec-classes.txt
rename from widget/android/bindings/surfacetexture-classes.txt
rename to widget/android/bindings/SurfaceTexture-classes.txt
--- a/widget/android/bindings/moz.build
+++ b/widget/android/bindings/moz.build
@@ -1,24 +1,35 @@
 # -*- Mode: python; c-basic-offset: 4; indent-tabs-mode: nil; tab-width: 40 -*-
 # vim: set filetype=python:
 # 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/.
 
-GENERATED_SOURCES += [
-    'MediaCodec.cpp',
-    'SurfaceTexture.cpp',
+# List of stems to generate .cpp and .h files for.  To add a stem, add it to
+# this list and ensure that $(stem)-classes.txt exists in this directory.
+generated = [
+    'MediaCodec',
+    'SurfaceTexture',
 ]
 
+GENERATED_SOURCES += [stem + '.cpp' for stem in generated]
+
 # We'd like to add these to a future GENERATED_EXPORTS list, but for now we mark
 # them as generated here and manually install them in Makefile.in.
-GENERATED_FILES += [
-    'MediaCodec.h',
-    'SurfaceTexture.h',
-]
+GENERATED_FILES += [stem + '.h' for stem in generated]
+
+# There is an unfortunate race condition when using GENERATED_SOURCES and
+# pattern rules (see Makefile.in) that manifests itself as a VPATH resolution
+# conflict: MediaCodec.o looks for MediaCodec.cpp and $(CURDIR)/MediaCodec.cpp,
+# and the pattern rule is matched but doesn't resolve both sources, causing a
+# failure.  Adding the GENERATED_SOURCES to GENERATED_FILES causes the sources
+# to be built at export time, which is before MediaCodec.o needs them; and by
+# the time MediaCodec.o is built, the source is in place and the VPATH
+# resolution works as expected.
+GENERATED_FILES += GENERATED_SOURCES
 
 FAIL_ON_WARNINGS = True
 FINAL_LIBRARY = 'xul'
 
 LOCAL_INCLUDES += [
     '/widget/android',
 ]