Merge m-c to inbound a=merge CLOSED TREE
authorWes Kocher <wkocher@mozilla.com>
Thu, 09 Oct 2014 17:08:37 -0700
changeset 209716 1da6afd3818adfb57ad92157add1324453efdf08
parent 209715 9d88161338f34effcf3d92f90e002bf4c460854d (current diff)
parent 209672 95d1486223f7c222b70f6878348ea0be3a66b821 (diff)
child 209717 50b689feab5f519b102141d629137cfd40bcd60b
child 209737 fdd14734b0364cae8a74eb086354f52e6c46c5ea
push id1
push userroot
push dateMon, 20 Oct 2014 17:29:22 +0000
reviewersmerge
milestone35.0a1
Merge m-c to inbound a=merge CLOSED TREE
mobile/android/search/res/color/facet_button_text_color.xml
mobile/android/search/res/drawable-hdpi/ic_action_settings.png
mobile/android/search/res/drawable-hdpi/ic_widget_new_tab.png
mobile/android/search/res/drawable-hdpi/ic_widget_search.png
mobile/android/search/res/drawable-hdpi/network_error.png
mobile/android/search/res/drawable-hdpi/search_clear.png
mobile/android/search/res/drawable-hdpi/search_fox.png
mobile/android/search/res/drawable-hdpi/search_history.png
mobile/android/search/res/drawable-hdpi/search_icon_active.png
mobile/android/search/res/drawable-hdpi/search_icon_inactive.png
mobile/android/search/res/drawable-hdpi/search_launcher.png
mobile/android/search/res/drawable-hdpi/search_plus.png
mobile/android/search/res/drawable-hdpi/widget_bg.9.png
mobile/android/search/res/drawable-mdpi/ic_action_settings.png
mobile/android/search/res/drawable-mdpi/ic_widget_new_tab.png
mobile/android/search/res/drawable-mdpi/ic_widget_search.png
mobile/android/search/res/drawable-mdpi/network_error.png
mobile/android/search/res/drawable-mdpi/search_clear.png
mobile/android/search/res/drawable-mdpi/search_fox.png
mobile/android/search/res/drawable-mdpi/search_history.png
mobile/android/search/res/drawable-mdpi/search_icon_active.png
mobile/android/search/res/drawable-mdpi/search_icon_inactive.png
mobile/android/search/res/drawable-mdpi/search_launcher.png
mobile/android/search/res/drawable-mdpi/search_plus.png
mobile/android/search/res/drawable-mdpi/widget_bg.9.png
mobile/android/search/res/drawable-xhdpi/ic_action_settings.png
mobile/android/search/res/drawable-xhdpi/ic_widget_new_tab.png
mobile/android/search/res/drawable-xhdpi/ic_widget_search.png
mobile/android/search/res/drawable-xhdpi/network_error.png
mobile/android/search/res/drawable-xhdpi/search_clear.png
mobile/android/search/res/drawable-xhdpi/search_fox.png
mobile/android/search/res/drawable-xhdpi/search_history.png
mobile/android/search/res/drawable-xhdpi/search_icon_active.png
mobile/android/search/res/drawable-xhdpi/search_icon_inactive.png
mobile/android/search/res/drawable-xhdpi/search_launcher.png
mobile/android/search/res/drawable-xhdpi/search_plus.png
mobile/android/search/res/drawable-xhdpi/widget_bg.9.png
mobile/android/search/res/drawable-xxhdpi/ic_action_settings.png
mobile/android/search/res/drawable-xxhdpi/ic_widget_new_tab.png
mobile/android/search/res/drawable-xxhdpi/ic_widget_search.png
mobile/android/search/res/drawable-xxhdpi/network_error.png
mobile/android/search/res/drawable-xxhdpi/search_clear.png
mobile/android/search/res/drawable-xxhdpi/search_fox.png
mobile/android/search/res/drawable-xxhdpi/search_history.png
mobile/android/search/res/drawable-xxhdpi/search_icon_active.png
mobile/android/search/res/drawable-xxhdpi/search_icon_inactive.png
mobile/android/search/res/drawable-xxhdpi/search_launcher.png
mobile/android/search/res/drawable-xxhdpi/search_plus.png
mobile/android/search/res/drawable-xxxhdpi/search_launcher.png
mobile/android/search/res/drawable/edit_text_default.xml
mobile/android/search/res/drawable/edit_text_focused.xml
mobile/android/search/res/drawable/facet_button_background.xml
mobile/android/search/res/drawable/facet_button_background_default.xml
mobile/android/search/res/drawable/facet_button_background_pressed.xml
mobile/android/search/res/drawable/progressbar.xml
mobile/android/search/res/drawable/search_row_background.xml
mobile/android/search/res/drawable/widget_button_left.xml
mobile/android/search/res/drawable/widget_button_left_default.xml
mobile/android/search/res/drawable/widget_button_left_pressed.xml
mobile/android/search/res/drawable/widget_button_middle.xml
mobile/android/search/res/drawable/widget_button_middle_pressed.xml
mobile/android/search/res/drawable/widget_button_right.xml
mobile/android/search/res/drawable/widget_button_right_pressed.xml
mobile/android/search/res/layout/keyguard_widget.xml
mobile/android/search/res/layout/search_activity_main.xml
mobile/android/search/res/layout/search_bar.xml
mobile/android/search/res/layout/search_empty.xml
mobile/android/search/res/layout/search_fragment_post_search.xml
mobile/android/search/res/layout/search_fragment_pre_search.xml
mobile/android/search/res/layout/search_history_row.xml
mobile/android/search/res/layout/search_sugestions.xml
mobile/android/search/res/layout/search_suggestions_row.xml
mobile/android/search/res/layout/search_widget.xml
mobile/android/search/res/values-v13/search_styles.xml
mobile/android/search/res/values-v16/search_styles.xml
mobile/android/search/res/values/search_attrs.xml
mobile/android/search/res/values/search_colors.xml
mobile/android/search/res/values/search_dimens.xml
mobile/android/search/res/values/search_styles.xml
mobile/android/search/res/xml/search_preferences.xml
mobile/android/search/res/xml/search_widget_info.xml
--- a/b2g/config/dolphin/sources.xml
+++ b/b2g/config/dolphin/sources.xml
@@ -10,19 +10,19 @@
   <!--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="3a2947df41a480de1457a6dcdbf46ad0af70d8e0">
     <copyfile dest="Makefile" src="core/root.mk"/>
   </project>
-  <project name="gaia" path="gaia" remote="mozillaorg" revision="7b92615bdc97e5c675cd385ec68bc5e47e0c5288"/>
+  <project name="gaia" path="gaia" remote="mozillaorg" revision="55766f4d12f57b5e7289d068a81a3dc501cf10db"/>
   <project name="fake-libdvm" path="dalvik" remote="b2g" revision="d50ae982b19f42f0b66d08b9eb306be81687869f"/>
-  <project name="gonk-misc" path="gonk-misc" remote="b2g" revision="a42f56ffac1c52e5bbc82143a949feeed359d528"/>
+  <project name="gonk-misc" path="gonk-misc" remote="b2g" revision="cc1f362ce43dce92ac786187ff4abf39060094bd"/>
   <project name="librecovery" path="librecovery" remote="b2g" revision="891e5069c0ad330d8191bf8c7b879c814258c89f"/>
   <project name="moztt" path="external/moztt" remote="b2g" revision="562d357b72279a9e35d4af5aeecc8e1ffa2f44f1"/>
   <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="6ca2008ac50b163d31244ef9f036cb224f4f229b"/>
   <!-- Stock Android things -->
   <project groups="linux" name="platform/prebuilts/gcc/linux-x86/host/i686-linux-glibc2.7-4.6" path="prebuilts/gcc/linux-x86/host/i686-linux-glibc2.7-4.6" revision="95bb5b66b3ec5769c3de8d3f25d681787418e7d2"/>
--- a/b2g/config/emulator-ics/sources.xml
+++ b/b2g/config/emulator-ics/sources.xml
@@ -14,18 +14,18 @@
   <!--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="84923f1940625c47ff4c1fdf01b10fde3b7d909e">
     <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="7b92615bdc97e5c675cd385ec68bc5e47e0c5288"/>
-  <project name="gonk-misc" path="gonk-misc" remote="b2g" revision="a42f56ffac1c52e5bbc82143a949feeed359d528"/>
+  <project name="gaia.git" path="gaia" remote="mozillaorg" revision="55766f4d12f57b5e7289d068a81a3dc501cf10db"/>
+  <project name="gonk-misc" path="gonk-misc" remote="b2g" revision="cc1f362ce43dce92ac786187ff4abf39060094bd"/>
   <project name="rilproxy" path="rilproxy" remote="b2g" revision="827214fcf38d6569aeb5c6d6f31cb296d1f09272"/>
   <project name="platform_hardware_ril" path="hardware/ril" remote="b2g" revision="cd88d860656c31c7da7bb310d6a160d0011b0961"/>
   <project name="platform_external_qemu" path="external/qemu" remote="b2g" revision="c058843242068d0df7c107e09da31b53d2e08fa6"/>
   <project name="moztt" path="external/moztt" remote="b2g" revision="562d357b72279a9e35d4af5aeecc8e1ffa2f44f1"/>
   <project name="apitrace" path="external/apitrace" remote="apitrace" revision="6ca2008ac50b163d31244ef9f036cb224f4f229b"/>
   <!-- Stock Android things -->
   <project name="platform/abi/cpp" path="abi/cpp" revision="dd924f92906085b831bf1cbbc7484d3c043d613c"/>
   <project name="platform/bionic" path="bionic" revision="c72b8f6359de7ed17c11ddc9dfdde3f615d188a9"/>
--- a/b2g/config/emulator-jb/sources.xml
+++ b/b2g/config/emulator-jb/sources.xml
@@ -12,18 +12,18 @@
   <!--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="8986df0f82e15ac2798df0b6c2ee3435400677ac">
     <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="7b92615bdc97e5c675cd385ec68bc5e47e0c5288"/>
-  <project name="gonk-misc" path="gonk-misc" remote="b2g" revision="a42f56ffac1c52e5bbc82143a949feeed359d528"/>
+  <project name="gaia" path="gaia" remote="mozillaorg" revision="55766f4d12f57b5e7289d068a81a3dc501cf10db"/>
+  <project name="gonk-misc" path="gonk-misc" remote="b2g" revision="cc1f362ce43dce92ac786187ff4abf39060094bd"/>
   <project name="moztt" path="external/moztt" remote="b2g" revision="562d357b72279a9e35d4af5aeecc8e1ffa2f44f1"/>
   <project name="apitrace" path="external/apitrace" remote="apitrace" revision="6ca2008ac50b163d31244ef9f036cb224f4f229b"/>
   <project name="valgrind" path="external/valgrind" remote="b2g" revision="daa61633c32b9606f58799a3186395fd2bbb8d8c"/>
   <project name="vex" path="external/VEX" remote="b2g" revision="47f031c320888fe9f3e656602588565b52d43010"/>
   <!-- Stock Android things -->
   <project groups="linux" name="platform/prebuilts/clang/linux-x86/3.1" path="prebuilts/clang/linux-x86/3.1" revision="5c45f43419d5582949284eee9cef0c43d866e03b"/>
   <project groups="linux" name="platform/prebuilts/clang/linux-x86/3.2" path="prebuilts/clang/linux-x86/3.2" revision="3748b4168e7bd8d46457d4b6786003bc6a5223ce"/>
   <project groups="linux" name="platform/prebuilts/gcc/linux-x86/host/i686-linux-glibc2.7-4.6" path="prebuilts/gcc/linux-x86/host/i686-linux-glibc2.7-4.6" revision="9025e50b9d29b3cabbbb21e1dd94d0d13121a17e"/>
--- a/b2g/config/emulator-kk/sources.xml
+++ b/b2g/config/emulator-kk/sources.xml
@@ -10,19 +10,19 @@
   <!--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="3a2947df41a480de1457a6dcdbf46ad0af70d8e0">
     <copyfile dest="Makefile" src="core/root.mk"/>
   </project>
-  <project name="gaia" path="gaia" remote="mozillaorg" revision="7b92615bdc97e5c675cd385ec68bc5e47e0c5288"/>
+  <project name="gaia" path="gaia" remote="mozillaorg" revision="55766f4d12f57b5e7289d068a81a3dc501cf10db"/>
   <project name="fake-libdvm" path="dalvik" remote="b2g" revision="d50ae982b19f42f0b66d08b9eb306be81687869f"/>
-  <project name="gonk-misc" path="gonk-misc" remote="b2g" revision="a42f56ffac1c52e5bbc82143a949feeed359d528"/>
+  <project name="gonk-misc" path="gonk-misc" remote="b2g" revision="cc1f362ce43dce92ac786187ff4abf39060094bd"/>
   <project name="librecovery" path="librecovery" remote="b2g" revision="891e5069c0ad330d8191bf8c7b879c814258c89f"/>
   <project name="moztt" path="external/moztt" remote="b2g" revision="562d357b72279a9e35d4af5aeecc8e1ffa2f44f1"/>
   <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="6ca2008ac50b163d31244ef9f036cb224f4f229b"/>
   <!-- Stock Android things -->
   <project groups="linux" name="platform/prebuilts/gcc/linux-x86/host/i686-linux-glibc2.7-4.6" path="prebuilts/gcc/linux-x86/host/i686-linux-glibc2.7-4.6" revision="f92a936f2aa97526d4593386754bdbf02db07a12"/>
--- a/b2g/config/emulator/sources.xml
+++ b/b2g/config/emulator/sources.xml
@@ -14,18 +14,18 @@
   <!--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="84923f1940625c47ff4c1fdf01b10fde3b7d909e">
     <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="7b92615bdc97e5c675cd385ec68bc5e47e0c5288"/>
-  <project name="gonk-misc" path="gonk-misc" remote="b2g" revision="a42f56ffac1c52e5bbc82143a949feeed359d528"/>
+  <project name="gaia.git" path="gaia" remote="mozillaorg" revision="55766f4d12f57b5e7289d068a81a3dc501cf10db"/>
+  <project name="gonk-misc" path="gonk-misc" remote="b2g" revision="cc1f362ce43dce92ac786187ff4abf39060094bd"/>
   <project name="rilproxy" path="rilproxy" remote="b2g" revision="827214fcf38d6569aeb5c6d6f31cb296d1f09272"/>
   <project name="platform_hardware_ril" path="hardware/ril" remote="b2g" revision="cd88d860656c31c7da7bb310d6a160d0011b0961"/>
   <project name="platform_external_qemu" path="external/qemu" remote="b2g" revision="c058843242068d0df7c107e09da31b53d2e08fa6"/>
   <project name="moztt" path="external/moztt" remote="b2g" revision="562d357b72279a9e35d4af5aeecc8e1ffa2f44f1"/>
   <project name="apitrace" path="external/apitrace" remote="apitrace" revision="6ca2008ac50b163d31244ef9f036cb224f4f229b"/>
   <!-- Stock Android things -->
   <project name="platform/abi/cpp" path="abi/cpp" revision="dd924f92906085b831bf1cbbc7484d3c043d613c"/>
   <project name="platform/bionic" path="bionic" revision="c72b8f6359de7ed17c11ddc9dfdde3f615d188a9"/>
--- a/b2g/config/flame-kk/sources.xml
+++ b/b2g/config/flame-kk/sources.xml
@@ -10,19 +10,19 @@
   <!--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="3a2947df41a480de1457a6dcdbf46ad0af70d8e0">
     <copyfile dest="Makefile" src="core/root.mk"/>
   </project>
-  <project name="gaia" path="gaia" remote="mozillaorg" revision="7b92615bdc97e5c675cd385ec68bc5e47e0c5288"/>
+  <project name="gaia" path="gaia" remote="mozillaorg" revision="55766f4d12f57b5e7289d068a81a3dc501cf10db"/>
   <project name="fake-libdvm" path="dalvik" remote="b2g" revision="d50ae982b19f42f0b66d08b9eb306be81687869f"/>
-  <project name="gonk-misc" path="gonk-misc" remote="b2g" revision="a42f56ffac1c52e5bbc82143a949feeed359d528"/>
+  <project name="gonk-misc" path="gonk-misc" remote="b2g" revision="cc1f362ce43dce92ac786187ff4abf39060094bd"/>
   <project name="librecovery" path="librecovery" remote="b2g" revision="891e5069c0ad330d8191bf8c7b879c814258c89f"/>
   <project name="moztt" path="external/moztt" remote="b2g" revision="562d357b72279a9e35d4af5aeecc8e1ffa2f44f1"/>
   <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="6ca2008ac50b163d31244ef9f036cb224f4f229b"/>
   <!-- Stock Android things -->
   <project groups="linux" name="platform/prebuilts/gcc/linux-x86/host/i686-linux-glibc2.7-4.6" path="prebuilts/gcc/linux-x86/host/i686-linux-glibc2.7-4.6" revision="95bb5b66b3ec5769c3de8d3f25d681787418e7d2"/>
--- a/b2g/config/flame/sources.xml
+++ b/b2g/config/flame/sources.xml
@@ -12,18 +12,18 @@
   <!--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="8986df0f82e15ac2798df0b6c2ee3435400677ac">
     <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="7b92615bdc97e5c675cd385ec68bc5e47e0c5288"/>
-  <project name="gonk-misc" path="gonk-misc" remote="b2g" revision="a42f56ffac1c52e5bbc82143a949feeed359d528"/>
+  <project name="gaia" path="gaia" remote="mozillaorg" revision="55766f4d12f57b5e7289d068a81a3dc501cf10db"/>
+  <project name="gonk-misc" path="gonk-misc" remote="b2g" revision="cc1f362ce43dce92ac786187ff4abf39060094bd"/>
   <project name="moztt" path="external/moztt" remote="b2g" revision="562d357b72279a9e35d4af5aeecc8e1ffa2f44f1"/>
   <project name="apitrace" path="external/apitrace" remote="apitrace" revision="6ca2008ac50b163d31244ef9f036cb224f4f229b"/>
   <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"/>
   <project groups="linux" name="platform/prebuilts/gcc/linux-x86/host/i686-linux-glibc2.7-4.6" path="prebuilts/gcc/linux-x86/host/i686-linux-glibc2.7-4.6" revision="95bb5b66b3ec5769c3de8d3f25d681787418e7d2"/>
--- a/b2g/config/gaia.json
+++ b/b2g/config/gaia.json
@@ -1,9 +1,9 @@
 {
     "git": {
         "git_revision": "", 
         "remote": "", 
         "branch": ""
     }, 
-    "revision": "9d8c254abad2ceaad8f1ae22a18e0f36af5860c4", 
+    "revision": "ce7bf759bbf4ecdfa46b64cdccc16ccf7531178d", 
     "repo_path": "/integration/gaia-central"
 }
--- a/b2g/config/hamachi/sources.xml
+++ b/b2g/config/hamachi/sources.xml
@@ -12,18 +12,18 @@
   <!--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="84923f1940625c47ff4c1fdf01b10fde3b7d909e">
     <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="7b92615bdc97e5c675cd385ec68bc5e47e0c5288"/>
-  <project name="gonk-misc" path="gonk-misc" remote="b2g" revision="a42f56ffac1c52e5bbc82143a949feeed359d528"/>
+  <project name="gaia.git" path="gaia" remote="mozillaorg" revision="55766f4d12f57b5e7289d068a81a3dc501cf10db"/>
+  <project name="gonk-misc" path="gonk-misc" remote="b2g" revision="cc1f362ce43dce92ac786187ff4abf39060094bd"/>
   <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="562d357b72279a9e35d4af5aeecc8e1ffa2f44f1"/>
   <project name="apitrace" path="external/apitrace" remote="apitrace" revision="6ca2008ac50b163d31244ef9f036cb224f4f229b"/>
   <!-- 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,18 +10,18 @@
   <!--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="84923f1940625c47ff4c1fdf01b10fde3b7d909e">
     <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="7b92615bdc97e5c675cd385ec68bc5e47e0c5288"/>
-  <project name="gonk-misc" path="gonk-misc" remote="b2g" revision="a42f56ffac1c52e5bbc82143a949feeed359d528"/>
+  <project name="gaia.git" path="gaia" remote="mozillaorg" revision="55766f4d12f57b5e7289d068a81a3dc501cf10db"/>
+  <project name="gonk-misc" path="gonk-misc" remote="b2g" revision="cc1f362ce43dce92ac786187ff4abf39060094bd"/>
   <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="562d357b72279a9e35d4af5aeecc8e1ffa2f44f1"/>
   <project name="gonk-patches" path="patches" remote="b2g" revision="223a2421006e8f5da33f516f6891c87cae86b0f6"/>
   <!-- Stock Android things -->
   <project name="platform/abi/cpp" path="abi/cpp" revision="6426040f1be4a844082c9769171ce7f5341a5528"/>
   <project name="platform/bionic" path="bionic" revision="d2eb6c7b6e1bc7643c17df2d9d9bcb1704d0b9ab"/>
   <project name="platform/bootable/recovery" path="bootable/recovery" revision="575fdbf046e966a5915b1f1e800e5d6ad0ea14c0"/>
--- a/b2g/config/nexus-4/sources.xml
+++ b/b2g/config/nexus-4/sources.xml
@@ -12,18 +12,18 @@
   <!--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="8986df0f82e15ac2798df0b6c2ee3435400677ac">
     <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="7b92615bdc97e5c675cd385ec68bc5e47e0c5288"/>
-  <project name="gonk-misc" path="gonk-misc" remote="b2g" revision="a42f56ffac1c52e5bbc82143a949feeed359d528"/>
+  <project name="gaia" path="gaia" remote="mozillaorg" revision="55766f4d12f57b5e7289d068a81a3dc501cf10db"/>
+  <project name="gonk-misc" path="gonk-misc" remote="b2g" revision="cc1f362ce43dce92ac786187ff4abf39060094bd"/>
   <project name="moztt" path="external/moztt" remote="b2g" revision="562d357b72279a9e35d4af5aeecc8e1ffa2f44f1"/>
   <project name="apitrace" path="external/apitrace" remote="apitrace" revision="6ca2008ac50b163d31244ef9f036cb224f4f229b"/>
   <project name="valgrind" path="external/valgrind" remote="b2g" revision="daa61633c32b9606f58799a3186395fd2bbb8d8c"/>
   <project name="vex" path="external/VEX" remote="b2g" revision="47f031c320888fe9f3e656602588565b52d43010"/>
   <!-- Stock Android things -->
   <project groups="linux" name="platform/prebuilts/clang/linux-x86/3.1" path="prebuilts/clang/linux-x86/3.1" revision="5c45f43419d5582949284eee9cef0c43d866e03b"/>
   <project groups="linux" name="platform/prebuilts/clang/linux-x86/3.2" path="prebuilts/clang/linux-x86/3.2" revision="3748b4168e7bd8d46457d4b6786003bc6a5223ce"/>
   <project groups="linux" name="platform/prebuilts/gcc/linux-x86/host/i686-linux-glibc2.7-4.6" path="prebuilts/gcc/linux-x86/host/i686-linux-glibc2.7-4.6" revision="9025e50b9d29b3cabbbb21e1dd94d0d13121a17e"/>
--- a/b2g/config/wasabi/sources.xml
+++ b/b2g/config/wasabi/sources.xml
@@ -12,18 +12,18 @@
   <!--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="84923f1940625c47ff4c1fdf01b10fde3b7d909e">
     <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="7b92615bdc97e5c675cd385ec68bc5e47e0c5288"/>
-  <project name="gonk-misc" path="gonk-misc" remote="b2g" revision="a42f56ffac1c52e5bbc82143a949feeed359d528"/>
+  <project name="gaia.git" path="gaia" remote="mozillaorg" revision="55766f4d12f57b5e7289d068a81a3dc501cf10db"/>
+  <project name="gonk-misc" path="gonk-misc" remote="b2g" revision="cc1f362ce43dce92ac786187ff4abf39060094bd"/>
   <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="562d357b72279a9e35d4af5aeecc8e1ffa2f44f1"/>
   <project name="apitrace" path="external/apitrace" remote="apitrace" revision="6ca2008ac50b163d31244ef9f036cb224f4f229b"/>
   <project name="gonk-patches" path="patches" remote="b2g" revision="223a2421006e8f5da33f516f6891c87cae86b0f6"/>
   <!-- Stock Android things -->
   <project name="platform/abi/cpp" path="abi/cpp" revision="6426040f1be4a844082c9769171ce7f5341a5528"/>
   <project name="platform/bionic" path="bionic" revision="cd5dfce80bc3f0139a56b58aca633202ccaee7f8"/>
--- a/browser/app/profile/firefox.js
+++ b/browser/app/profile/firefox.js
@@ -1618,16 +1618,19 @@ pref("loop.debug.websocket", false);
 pref("loop.debug.sdk", false);
 #ifdef DEBUG
 pref("loop.CSP", "default-src 'self' about: file: chrome: http://localhost:*; img-src 'self' data: http://www.gravatar.com/ about: file: chrome:; font-src 'none'; connect-src wss://*.tokbox.com https://*.opentok.com https://*.tokbox.com wss://*.mozilla.com https://*.mozilla.org wss://*.mozaws.net http://localhost:* ws://localhost:*");
 #else
 pref("loop.CSP", "default-src 'self' about: file: chrome:; img-src 'self' data: http://www.gravatar.com/ about: file: chrome:; font-src 'none'; connect-src wss://*.tokbox.com https://*.opentok.com https://*.tokbox.com wss://*.mozilla.com https://*.mozilla.org wss://*.mozaws.net");
 #endif
 pref("loop.oauth.google.redirect_uri", "urn:ietf:wg:oauth:2.0:oob:auto");
 pref("loop.oauth.google.scope", "https://www.google.com/m8/feeds");
+pref("loop.rooms.enabled", false);
+pref("loop.fxa_oauth.tokendata", "");
+pref("loop.fxa_oauth.profile", "");
 
 // serverURL to be assigned by services team
 pref("services.push.serverURL", "wss://push.services.mozilla.com/");
 
 pref("social.sidebar.unload_timeout_ms", 10000);
 
 pref("dom.identity.enabled", false);
 
--- a/browser/base/content/popup-notifications.inc
+++ b/browser/base/content/popup-notifications.inc
@@ -54,14 +54,8 @@
     </popupnotification>
 
     <popupnotification id="pointerLock-notification" hidden="true">
       <popupnotificationcontent orient="vertical" align="start">
         <separator class="thin"/>
         <label id="pointerLock-cancel" value="&pointerLock.notification.message;"/>
       </popupnotificationcontent>
     </popupnotification>
-
-#ifdef E10S_TESTING_ONLY
-    <popupnotification id="enable-e10s-notification" hidden="true">
-      <popupnotificationcontent orient="vertical"/>
-    </popupnotification>
-#endif
--- a/browser/components/loop/GoogleImporter.jsm
+++ b/browser/components/loop/GoogleImporter.jsm
@@ -456,18 +456,20 @@ this.GoogleImporter.prototype = {
       }
     }
 
     let orgNodes = entry.getElementsByTagNameNS(kNS_GD, "organization");
     if (orgNodes.length) {
       contact.org = [];
       contact.jobTitle = [];
       for (let [,orgNode] of Iterator(orgNodes)) {
-        contact.org.push(orgNode.getElementsByTagNameNS(kNS_GD, "orgName")[0].firstChild.nodeValue);
-        contact.jobTitle.push(orgNode.getElementsByTagNameNS(kNS_GD, "orgTitle")[0].firstChild.nodeValue);
+        let orgElement = orgNode.getElementsByTagNameNS(kNS_GD, "orgName")[0];
+        let titleElement = orgNode.getElementsByTagNameNS(kNS_GD, "orgTitle")[0];
+        contact.org.push(orgElement ? orgElement.firstChild.nodeValue : "")
+        contact.jobTitle.push(titleElement ? titleElement.firstChild.nodeValue : "");
       }
     }
 
     contact.category = ["google"];
 
     // Basic sanity checking: make sure the name field isn't empty
     if (!("name" in contact) || contact.name[0].length == 0) {
       if (("familyName" in contact) && ("givenName" in contact)) {
--- a/browser/components/loop/MozLoopService.jsm
+++ b/browser/components/loop/MozLoopService.jsm
@@ -72,29 +72,37 @@ XPCOMUtils.defineLazyGetter(this, "log",
   let ConsoleAPI = Cu.import("resource://gre/modules/devtools/Console.jsm", {}).ConsoleAPI;
   let consoleOptions = {
     maxLogLevel: Services.prefs.getCharPref(PREF_LOG_LEVEL).toLowerCase(),
     prefix: "Loop",
   };
   return new ConsoleAPI(consoleOptions);
 });
 
+function setJSONPref(aName, aValue) {
+  let value = !!aValue ? JSON.stringify(aValue) : "";
+  Services.prefs.setCharPref(aName, value);
+}
+
+function getJSONPref(aName) {
+  let value = Services.prefs.getCharPref(aName);
+  return !!value ? JSON.parse(value) : null;
+}
+
 // The current deferred for the registration process. This is set if in progress
 // or the registration was successful. This is null if a registration attempt was
 // unsuccessful.
 let gRegisteredDeferred = null;
 let gPushHandler = null;
 let gHawkClient = null;
-let gLocalizedStrings =  null;
+let gLocalizedStrings = null;
 let gInitializeTimer = null;
 let gFxAEnabled = true;
 let gFxAOAuthClientPromise = null;
 let gFxAOAuthClient = null;
-let gFxAOAuthTokenData = null;
-let gFxAOAuthProfile = null;
 let gErrors = new Map();
 
  /**
  * Attempts to open a websocket.
  *
  * A new websocket interface is used each time. If an onStop callback
  * was received, calling asyncOpen() on the same interface will
  * trigger a "alreay open socket" exception even though the channel
@@ -302,16 +310,48 @@ let MozLoopServiceInternal = {
   /**
    * Returns true if the expiry time is in the future.
    */
   urlExpiryTimeIsInFuture: function() {
     return this.expiryTimeSeconds * 1000 > Date.now();
   },
 
   /**
+   * Retrieves MozLoopService Firefox Accounts OAuth token.
+   *
+   * @return {Object} OAuth token
+   */
+  get fxAOAuthTokenData() {
+    return getJSONPref("loop.fxa_oauth.tokendata");
+  },
+
+  /**
+   * Sets MozLoopService Firefox Accounts OAuth token.
+   * If the tokenData is being cleared, will also clear the
+   * profile since the profile is dependent on the token data.
+   *
+   * @param {Object} aTokenData OAuth token
+   */
+  set fxAOAuthTokenData(aTokenData) {
+    setJSONPref("loop.fxa_oauth.tokendata", aTokenData);
+    if (!aTokenData) {
+      this.fxAOAuthProfile = null;
+    }
+  },
+
+  /**
+   * Sets MozLoopService Firefox Accounts Profile data.
+   *
+   * @param {Object} aProfileData Profile data
+   */
+  set fxAOAuthProfile(aProfileData) {
+    setJSONPref("loop.fxa_oauth.profile", aProfileData);
+  },
+
+  /**
    * Retrieves MozLoopService "do not disturb" pref value.
    *
    * @return {Boolean} aFlag
    */
   get doNotDisturb() {
     return Services.prefs.getBoolPref("loop.do_not_disturb");
   },
 
@@ -415,19 +455,18 @@ let MozLoopServiceInternal = {
     }
 
     gRegisteredDeferred = Promise.defer();
     // We grab the promise early in case .initialize or its results sets
     // it back to null on error.
     let result = gRegisteredDeferred.promise;
 
     gPushHandler = mockPushHandler || MozLoopPushHandler;
-
     gPushHandler.initialize(this.onPushRegistered.bind(this),
-      this.onHandleNotification.bind(this));
+                            this.onHandleNotification.bind(this));
 
     return result;
   },
 
   /**
    * Performs a hawk based request to the loop server.
    *
    * @param {LOOP_SESSION_TYPE} sessionType The type of session to use for the request.
@@ -570,22 +609,25 @@ let MozLoopServiceInternal = {
       return;
     }
 
     this.registerWithLoopServer(LOOP_SESSION_TYPE.GUEST, pushUrl).then(() => {
       // storeSessionToken could have rejected and nulled the promise if the token was malformed.
       if (!gRegisteredDeferred) {
         return;
       }
-      gRegisteredDeferred.resolve();
+      gRegisteredDeferred.resolve("registered to guest status");
       // No need to clear the promise here, everything was good, so we don't need
       // to re-register.
-    }, (error) => {
+    }, error => {
       log.error("Failed to register with Loop server: ", error);
-      gRegisteredDeferred.reject(error.errno);
+      // registerWithLoopServer may have already made this null.
+      if (gRegisteredDeferred) {
+        gRegisteredDeferred.reject(error);
+      }
       gRegisteredDeferred = null;
     });
   },
 
   /**
    * Registers with the Loop server either as a guest or a FxA user.
    *
    * @param {LOOP_SESSION_TYPE} sessionType The type of session e.g. guest or FxA
@@ -611,16 +653,18 @@ let MozLoopServiceInternal = {
           // Authorization failed, invalid token, we need to try again with a new token.
           if (retry) {
             return this.registerWithLoopServer(sessionType, pushUrl, false);
           }
         }
 
         log.error("Failed to register with the loop server. Error: ", error);
         this.setError("registration", error);
+        gRegisteredDeferred.reject(error);
+        gRegisteredDeferred = null;
         throw error;
       }
     );
   },
 
   /**
    * Unregisters from the Loop server either as a guest or a FxA user.
    *
@@ -1064,66 +1108,101 @@ let MozLoopServiceInternal = {
       deferred.resolve(result);
     } else {
       deferred.reject("Invalid token data");
     }
   },
 };
 Object.freeze(MozLoopServiceInternal);
 
-let gInitializeTimerFunc = () => {
-  // Kick off the push notification service into registering after a timeout
-  // this ensures we're not doing too much straight after the browser's finished
+let gInitializeTimerFunc = (deferredInitialization, mockPushHandler, mockWebSocket) => {
+  // Kick off the push notification service into registering after a timeout.
+  // This ensures we're not doing too much straight after the browser's finished
   // starting up.
   gInitializeTimer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer);
-  gInitializeTimer.initWithCallback(() => {
-    MozLoopService.register();
+  gInitializeTimer.initWithCallback(Task.async(function* initializationCallback() {
+    yield MozLoopService.register(mockPushHandler, mockWebSocket).then(Task.async(function*() {
+      if (!MozLoopServiceInternal.fxAOAuthTokenData) {
+        log.debug("MozLoopService: Initialized without an already logged-in account");
+        deferredInitialization.resolve("initialized to guest status");
+        return;
+      }
+
+      log.debug("MozLoopService: Initializing with already logged-in account");
+      let registeredPromise =
+            MozLoopServiceInternal.registerWithLoopServer(LOOP_SESSION_TYPE.FXA,
+                                                          gPushHandler.pushUrl);
+      registeredPromise.then(() => {
+        deferredInitialization.resolve("initialized to logged-in status");
+      }, error => {
+        log.debug("MozLoopService: error logging in using cached auth token");
+        MozLoopServiceInternal.setError("login", error);
+        deferredInitialization.reject("error logging in using cached auth token");
+      });
+    }), error => {
+      log.debug("MozLoopService: Failure of initial registration", error);
+      deferredInitialization.reject(error);
+    });
     gInitializeTimer = null;
-  },
+  }),
   MozLoopServiceInternal.initialRegistrationDelayMilliseconds, Ci.nsITimer.TYPE_ONE_SHOT);
 };
 
 /**
  * Public API
  */
 this.MozLoopService = {
   _DNSService: gDNSService,
 
   set initializeTimerFunc(value) {
     gInitializeTimerFunc = value;
   },
 
   /**
    * Initialized the loop service, and starts registration with the
    * push and loop servers.
+   *
+   * @return {Promise}
    */
-  initialize: function() {
-
+  initialize: Task.async(function*(mockPushHandler, mockWebSocket) {
     // Do this here, rather than immediately after definition, so that we can
     // stub out API functions for unit testing
     Object.freeze(this);
 
     // Don't do anything if loop is not enabled.
     if (!Services.prefs.getBoolPref("loop.enabled") ||
         Services.prefs.getBoolPref("loop.throttled")) {
-      return;
+      return Promise.reject("loop is not enabled");
     }
 
     if (Services.prefs.getPrefType("loop.fxa.enabled") == Services.prefs.PREF_BOOL) {
       gFxAEnabled = Services.prefs.getBoolPref("loop.fxa.enabled");
       if (!gFxAEnabled) {
-        this.logOutFromFxA();
+        yield this.logOutFromFxA();
       }
     }
 
-    // If expiresTime is in the future then kick-off registration.
-    if (MozLoopServiceInternal.urlExpiryTimeIsInFuture()) {
-      gInitializeTimerFunc();
+    // If expiresTime is not in the future and the user hasn't
+    // previously authenticated then skip registration.
+    if (!MozLoopServiceInternal.urlExpiryTimeIsInFuture() &&
+        !MozLoopServiceInternal.fxAOAuthTokenData) {
+      return Promise.resolve("registration not needed");
     }
-  },
+
+    let deferredInitialization = Promise.defer();
+    gInitializeTimerFunc(deferredInitialization, mockPushHandler, mockWebSocket);
+
+    return deferredInitialization.promise.catch(error => {
+      if (typeof(error) == "object") {
+        // This never gets cleared since there is no UI to recover. Only restarting will work.
+        MozLoopServiceInternal.setError("initialization", error);
+      }
+      throw error;
+    });
+  }),
 
   /**
    * If we're operating the service in "soft start" mode, and this browser
    * isn't already activated, check whether it's time for it to become active.
    * If so, activate the loop service.
    *
    * @param {Object} buttonNode DOM node representing the Loop button -- if we
    *                            change from inactive to active, we need this
@@ -1243,17 +1322,17 @@ this.MozLoopService = {
   },
 
   /**
    * Used to note a call url expiry time. If the time is later than the current
    * latest expiry time, then the stored expiry time is increased. For times
    * sooner, this function is a no-op; this ensures we always have the latest
    * expiry time for a url.
    *
-   * This is used to deterimine whether or not we should be registering with the
+   * This is used to determine whether or not we should be registering with the
    * push server on start.
    *
    * @param {Integer} expiryTimeSeconds The seconds since epoch of the expiry time
    *                                    of the url.
    */
   noteCallUrlExpiry: function(expiryTimeSeconds) {
     MozLoopServiceInternal.expiryTimeSeconds = expiryTimeSeconds;
   },
@@ -1300,18 +1379,26 @@ this.MozLoopService = {
   set doNotDisturb(aFlag) {
     MozLoopServiceInternal.doNotDisturb = aFlag;
   },
 
   get fxAEnabled() {
     return gFxAEnabled;
   },
 
+  /**
+   * Gets the user profile, but only if there is
+   * tokenData present. Without tokenData, the
+   * profile is meaningless.
+   *
+   * @return {Object}
+   */
   get userProfile() {
-    return gFxAOAuthProfile;
+    return getJSONPref("loop.fxa_oauth.tokendata") &&
+           getJSONPref("loop.fxa_oauth.profile");
   },
 
   get errors() {
     return MozLoopServiceInternal.errors;
   },
 
   get log() {
     return log;
@@ -1430,55 +1517,55 @@ this.MozLoopService = {
   /**
    * Start the FxA login flow using the OAuth client and params from the Loop server.
    *
    * The caller should be prepared to handle rejections related to network, server or login errors.
    *
    * @return {Promise} that resolves when the FxA login flow is complete.
    */
   logInToFxA: function() {
-    log.debug("logInToFxA with gFxAOAuthTokenData:", !!gFxAOAuthTokenData);
-    if (gFxAOAuthTokenData) {
-      return Promise.resolve(gFxAOAuthTokenData);
+    log.debug("logInToFxA with fxAOAuthTokenData:", !!MozLoopServiceInternal.fxAOAuthTokenData);
+    if (MozLoopServiceInternal.fxAOAuthTokenData) {
+      return Promise.resolve(MozLoopServiceInternal.fxAOAuthTokenData);
     }
 
     return MozLoopServiceInternal.promiseFxAOAuthAuthorization().then(response => {
       return MozLoopServiceInternal.promiseFxAOAuthToken(response.code, response.state);
     }).then(tokenData => {
-      gFxAOAuthTokenData = tokenData;
+      MozLoopServiceInternal.fxAOAuthTokenData = tokenData;
       return tokenData;
     }).then(tokenData => {
       return gRegisteredDeferred.promise.then(Task.async(function*() {
         if (gPushHandler.pushUrl) {
           yield MozLoopServiceInternal.registerWithLoopServer(LOOP_SESSION_TYPE.FXA, gPushHandler.pushUrl);
         } else {
           throw new Error("No pushUrl for FxA registration");
         }
         MozLoopServiceInternal.clearError("login");
         MozLoopServiceInternal.clearError("profile");
-        return gFxAOAuthTokenData;
+        return MozLoopServiceInternal.fxAOAuthTokenData;
       }));
     }).then(tokenData => {
       let client = new FxAccountsProfileClient({
         serverURL: gFxAOAuthClient.parameters.profile_uri,
         token: tokenData.access_token
       });
       client.fetchProfile().then(result => {
-        gFxAOAuthProfile = result;
+        MozLoopServiceInternal.fxAOAuthProfile = result;
         MozLoopServiceInternal.notifyStatusChanged("login");
       }, error => {
         log.error("Failed to retrieve profile", error);
         this.setError("profile", error);
-        gFxAOAuthProfile = null;
+        MozLoopServiceInternal.fxAOAuthProfile = null;
         MozLoopServiceInternal.notifyStatusChanged();
       });
       return tokenData;
     }).catch(error => {
-      gFxAOAuthTokenData = null;
-      gFxAOAuthProfile = null;
+      MozLoopServiceInternal.fxAOAuthTokenData = null;
+      MozLoopServiceInternal.fxAOAuthProfile = null;
       throw error;
     }).catch((error) => {
       MozLoopServiceInternal.setError("login", error);
       // Re-throw for testing
       throw error;
     });
   },
 
@@ -1493,18 +1580,18 @@ this.MozLoopService = {
     log.debug("logOutFromFxA");
     if (gPushHandler && gPushHandler.pushUrl) {
       yield MozLoopServiceInternal.unregisterFromLoopServer(LOOP_SESSION_TYPE.FXA,
                                                             gPushHandler.pushUrl);
     } else {
       MozLoopServiceInternal.clearSessionToken(LOOP_SESSION_TYPE.FXA);
     }
 
-    gFxAOAuthTokenData = null;
-    gFxAOAuthProfile = null;
+    MozLoopServiceInternal.fxAOAuthTokenData = null;
+    MozLoopServiceInternal.fxAOAuthProfile = null;
 
     // Reset the client since the initial promiseFxAOAuthParameters() call is
     // what creates a new session.
     gFxAOAuthClient = null;
     gFxAOAuthClientPromise = null;
 
     // clearError calls notifyStatusChanged so should be done last when the
     // state is clean.
--- a/browser/components/loop/content/js/panel.js
+++ b/browser/components/loop/content/js/panel.js
@@ -9,39 +9,55 @@
 
 var loop = loop || {};
 loop.panel = (function(_, mozL10n) {
   "use strict";
 
   var sharedViews = loop.shared.views;
   var sharedModels = loop.shared.models;
   var sharedMixins = loop.shared.mixins;
+  var sharedActions = loop.shared.actions;
   var Button = sharedViews.Button;
   var ButtonGroup = sharedViews.ButtonGroup;
   var ContactsList = loop.contacts.ContactsList;
   var ContactDetailsForm = loop.contacts.ContactDetailsForm;
   var __ = mozL10n.get; // aliasing translation function as __ for concision
 
   var TabView = React.createClass({displayName: 'TabView',
-    getInitialState: function() {
+    propTypes: {
+      buttonsHidden: React.PropTypes.bool,
+      // The selectedTab prop is used by the UI showcase.
+      selectedTab: React.PropTypes.string
+    },
+
+    getDefaultProps: function() {
       return {
+        buttonsHidden: false,
         selectedTab: "call"
       };
     },
 
+    getInitialState: function() {
+      return {selectedTab: this.props.selectedTab};
+    },
+
     handleSelectTab: function(event) {
       var tabName = event.target.dataset.tabName;
       this.setState({selectedTab: tabName});
     },
 
     render: function() {
       var cx = React.addons.classSet;
       var tabButtons = [];
       var tabs = [];
       React.Children.forEach(this.props.children, function(tab, i) {
+        // Filter out null tabs (eg. rooms when the feature is disabled)
+        if (!tab) {
+          return;
+        }
         var tabName = tab.props.name;
         var isSelected = (this.state.selectedTab == tabName);
         if (!tab.props.hidden) {
           tabButtons.push(
             React.DOM.li({className: cx({selected: isSelected}), 
                 key: i, 
                 'data-tab-name': tabName, 
                 onClick: this.handleSelectTab})
@@ -438,26 +454,145 @@ loop.panel = (function(_, mozL10n) {
         React.DOM.p({className: "user-identity"}, 
           this.props.displayName
         )
       );
     }
   });
 
   /**
+   * Room list entry.
+   */
+  var RoomEntry = React.createClass({displayName: 'RoomEntry',
+    propTypes: {
+      openRoom: React.PropTypes.func.isRequired,
+      room:     React.PropTypes.instanceOf(loop.store.Room).isRequired
+    },
+
+    shouldComponentUpdate: function(nextProps, nextState) {
+      return nextProps.room.ctime > this.props.room.ctime;
+    },
+
+    handleClickRoom: function(event) {
+      event.preventDefault();
+      this.props.openRoom(this.props.room);
+    },
+
+    _isActive: function() {
+      // XXX bug 1074679 will implement this properly
+      return this.props.room.currSize > 0;
+    },
+
+    render: function() {
+      var room = this.props.room;
+      var roomClasses = React.addons.classSet({
+        "room-entry": true,
+        "room-active": this._isActive()
+      });
+
+      return (
+        React.DOM.div({className: roomClasses}, 
+          React.DOM.h2(null, 
+            React.DOM.span({className: "room-notification"}), 
+            room.roomName
+          ), 
+          React.DOM.p(null, 
+            React.DOM.a({ref: "room", href: "#", onClick: this.handleClickRoom}, 
+              room.roomUrl
+            )
+          )
+        )
+      );
+    }
+  });
+
+  /**
+   * Room list.
+   */
+  var RoomList = React.createClass({displayName: 'RoomList',
+    mixins: [Backbone.Events],
+
+    propTypes: {
+      store: React.PropTypes.instanceOf(loop.store.RoomListStore).isRequired,
+      dispatcher: React.PropTypes.instanceOf(loop.Dispatcher).isRequired,
+      rooms: React.PropTypes.array
+    },
+
+    getInitialState: function() {
+      var storeState = this.props.store.getStoreState();
+      return {
+        error: this.props.error || storeState.error,
+        rooms: this.props.rooms || storeState.rooms,
+      };
+    },
+
+    componentWillMount: function() {
+      this.listenTo(this.props.store, "change", this._onRoomListChanged);
+
+      this.props.dispatcher.dispatch(new sharedActions.GetAllRooms());
+    },
+
+    componentWillUnmount: function() {
+      this.stopListening(this.props.store);
+    },
+
+    _onRoomListChanged: function() {
+      var storeState = this.props.store.getStoreState();
+      this.setState({
+        error: storeState.error,
+        rooms: storeState.rooms
+      });
+    },
+
+    _getListHeading: function() {
+      var numRooms = this.state.rooms.length;
+      if (numRooms === 0) {
+        return mozL10n.get("rooms_list_no_current_conversations");
+      }
+      return mozL10n.get("rooms_list_current_conversations", {num: numRooms});
+    },
+
+    openRoom: function(room) {
+      // XXX implement me; see bug 1074678
+    },
+
+    render: function() {
+      if (this.state.error) {
+        // XXX Better end user reporting of errors.
+        console.error(this.state.error);
+      }
+
+      return (
+        React.DOM.div({className: "room-list"}, 
+          React.DOM.h1(null, this._getListHeading()), 
+          
+            this.state.rooms.map(function(room, i) {
+              return RoomEntry({key: i, room: room, openRoom: this.openRoom});
+            }, this)
+          
+        )
+      );
+    }
+  });
+
+  /**
    * Panel view.
    */
   var PanelView = React.createClass({displayName: 'PanelView',
     propTypes: {
       notifications: React.PropTypes.object.isRequired,
       client: React.PropTypes.object.isRequired,
       // Mostly used for UI components showcase and unit tests
       callUrl: React.PropTypes.string,
       userProfile: React.PropTypes.object,
       showTabButtons: React.PropTypes.bool,
+      selectedTab: React.PropTypes.string,
+      dispatcher: React.PropTypes.instanceOf(loop.Dispatcher).isRequired,
+      roomListStore:
+        React.PropTypes.instanceOf(loop.store.RoomListStore).isRequired
     },
 
     getInitialState: function() {
       return {
         userProfile: this.props.userProfile || navigator.mozLoop.userProfile,
       };
     },
 
@@ -493,16 +628,32 @@ loop.panel = (function(_, mozL10n) {
       if (profile != this.state.userProfile) {
         // On profile change (login, logout), switch back to the default tab.
         this.selectTab("call");
       }
       this.setState({userProfile: profile});
       this.updateServiceErrors();
     },
 
+    /**
+     * The rooms feature is hidden by default for now. Once it gets mainstream,
+     * this method can be safely removed.
+     */
+    _renderRoomsTab: function() {
+      if (!navigator.mozLoop.getLoopBoolPref("rooms.enabled")) {
+        return null;
+      }
+      return (
+        Tab({name: "rooms"}, 
+          RoomList({dispatcher: this.props.dispatcher, 
+                    store: this.props.roomListStore})
+        )
+      );
+    },
+
     startForm: function(name, contact) {
       this.refs[name].initForm(contact);
       this.selectTab(name);
     },
 
     selectTab: function(name) {
       this.refs.tabView.setState({ selectedTab: name });
     },
@@ -522,25 +673,27 @@ loop.panel = (function(_, mozL10n) {
     render: function() {
       var NotificationListView = sharedViews.NotificationListView;
       var displayName = this.state.userProfile && this.state.userProfile.email ||
                         __("display_name_guest");
       return (
         React.DOM.div(null, 
           NotificationListView({notifications: this.props.notifications, 
                                 clearOnDocumentHidden: true}), 
-          TabView({ref: "tabView", buttonsHidden: !this.state.userProfile && !this.props.showTabButtons}, 
+          TabView({ref: "tabView", selectedTab: this.props.selectedTab, 
+            buttonsHidden: !this.state.userProfile && !this.props.showTabButtons}, 
             Tab({name: "call"}, 
               React.DOM.div({className: "content-area"}, 
                 CallUrlResult({client: this.props.client, 
                                notifications: this.props.notifications, 
                                callUrl: this.props.callUrl}), 
                 ToSView(null)
               )
             ), 
+            this._renderRoomsTab(), 
             Tab({name: "contacts"}, 
               ContactsList({selectTab: this.selectTab, 
                             startForm: this.startForm})
             ), 
             Tab({name: "contacts_add", hidden: true}, 
               ContactDetailsForm({ref: "contacts_add", mode: "add", 
                                   selectTab: this.selectTab})
             ), 
@@ -570,21 +723,29 @@ loop.panel = (function(_, mozL10n) {
    * Panel initialisation.
    */
   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 notifications = new sharedModels.NotificationCollection();
+    var dispatcher = new loop.Dispatcher();
+    var roomListStore = new loop.store.RoomListStore({
+      mozLoop: navigator.mozLoop,
+      dispatcher: dispatcher
+    });
 
     React.renderComponent(PanelView({
       client: client, 
-      notifications: notifications}), document.querySelector("#main"));
+      notifications: notifications, 
+      roomListStore: roomListStore, 
+      dispatcher: dispatcher}
+    ), document.querySelector("#main"));
 
     document.body.classList.add(loop.shared.utils.getTargetPlatform());
     document.body.setAttribute("dir", mozL10n.getDirection());
 
     // Notify the window that we've finished initalization and initial layout
     var evtObject = document.createEvent('Event');
     evtObject.initEvent('loopPanelInitialized', true, false);
     window.dispatchEvent(evtObject);
@@ -592,14 +753,15 @@ loop.panel = (function(_, mozL10n) {
 
   return {
     init: init,
     UserIdentity: UserIdentity,
     AuthLink: AuthLink,
     AvailabilityDropdown: AvailabilityDropdown,
     CallUrlResult: CallUrlResult,
     PanelView: PanelView,
+    RoomList: RoomList,
     SettingsDropdown: SettingsDropdown,
     ToSView: ToSView
   };
 })(_, document.mozL10n);
 
 document.addEventListener('DOMContentLoaded', loop.panel.init);
--- a/browser/components/loop/content/js/panel.jsx
+++ b/browser/components/loop/content/js/panel.jsx
@@ -9,39 +9,55 @@
 
 var loop = loop || {};
 loop.panel = (function(_, mozL10n) {
   "use strict";
 
   var sharedViews = loop.shared.views;
   var sharedModels = loop.shared.models;
   var sharedMixins = loop.shared.mixins;
+  var sharedActions = loop.shared.actions;
   var Button = sharedViews.Button;
   var ButtonGroup = sharedViews.ButtonGroup;
   var ContactsList = loop.contacts.ContactsList;
   var ContactDetailsForm = loop.contacts.ContactDetailsForm;
   var __ = mozL10n.get; // aliasing translation function as __ for concision
 
   var TabView = React.createClass({
-    getInitialState: function() {
+    propTypes: {
+      buttonsHidden: React.PropTypes.bool,
+      // The selectedTab prop is used by the UI showcase.
+      selectedTab: React.PropTypes.string
+    },
+
+    getDefaultProps: function() {
       return {
+        buttonsHidden: false,
         selectedTab: "call"
       };
     },
 
+    getInitialState: function() {
+      return {selectedTab: this.props.selectedTab};
+    },
+
     handleSelectTab: function(event) {
       var tabName = event.target.dataset.tabName;
       this.setState({selectedTab: tabName});
     },
 
     render: function() {
       var cx = React.addons.classSet;
       var tabButtons = [];
       var tabs = [];
       React.Children.forEach(this.props.children, function(tab, i) {
+        // Filter out null tabs (eg. rooms when the feature is disabled)
+        if (!tab) {
+          return;
+        }
         var tabName = tab.props.name;
         var isSelected = (this.state.selectedTab == tabName);
         if (!tab.props.hidden) {
           tabButtons.push(
             <li className={cx({selected: isSelected})}
                 key={i}
                 data-tab-name={tabName}
                 onClick={this.handleSelectTab} />
@@ -438,26 +454,145 @@ loop.panel = (function(_, mozL10n) {
         <p className="user-identity">
           {this.props.displayName}
         </p>
       );
     }
   });
 
   /**
+   * Room list entry.
+   */
+  var RoomEntry = React.createClass({
+    propTypes: {
+      openRoom: React.PropTypes.func.isRequired,
+      room:     React.PropTypes.instanceOf(loop.store.Room).isRequired
+    },
+
+    shouldComponentUpdate: function(nextProps, nextState) {
+      return nextProps.room.ctime > this.props.room.ctime;
+    },
+
+    handleClickRoom: function(event) {
+      event.preventDefault();
+      this.props.openRoom(this.props.room);
+    },
+
+    _isActive: function() {
+      // XXX bug 1074679 will implement this properly
+      return this.props.room.currSize > 0;
+    },
+
+    render: function() {
+      var room = this.props.room;
+      var roomClasses = React.addons.classSet({
+        "room-entry": true,
+        "room-active": this._isActive()
+      });
+
+      return (
+        <div className={roomClasses}>
+          <h2>
+            <span className="room-notification" />
+            {room.roomName}
+          </h2>
+          <p>
+            <a ref="room" href="#" onClick={this.handleClickRoom}>
+              {room.roomUrl}
+            </a>
+          </p>
+        </div>
+      );
+    }
+  });
+
+  /**
+   * Room list.
+   */
+  var RoomList = React.createClass({
+    mixins: [Backbone.Events],
+
+    propTypes: {
+      store: React.PropTypes.instanceOf(loop.store.RoomListStore).isRequired,
+      dispatcher: React.PropTypes.instanceOf(loop.Dispatcher).isRequired,
+      rooms: React.PropTypes.array
+    },
+
+    getInitialState: function() {
+      var storeState = this.props.store.getStoreState();
+      return {
+        error: this.props.error || storeState.error,
+        rooms: this.props.rooms || storeState.rooms,
+      };
+    },
+
+    componentWillMount: function() {
+      this.listenTo(this.props.store, "change", this._onRoomListChanged);
+
+      this.props.dispatcher.dispatch(new sharedActions.GetAllRooms());
+    },
+
+    componentWillUnmount: function() {
+      this.stopListening(this.props.store);
+    },
+
+    _onRoomListChanged: function() {
+      var storeState = this.props.store.getStoreState();
+      this.setState({
+        error: storeState.error,
+        rooms: storeState.rooms
+      });
+    },
+
+    _getListHeading: function() {
+      var numRooms = this.state.rooms.length;
+      if (numRooms === 0) {
+        return mozL10n.get("rooms_list_no_current_conversations");
+      }
+      return mozL10n.get("rooms_list_current_conversations", {num: numRooms});
+    },
+
+    openRoom: function(room) {
+      // XXX implement me; see bug 1074678
+    },
+
+    render: function() {
+      if (this.state.error) {
+        // XXX Better end user reporting of errors.
+        console.error(this.state.error);
+      }
+
+      return (
+        <div className="room-list">
+          <h1>{this._getListHeading()}</h1>
+          {
+            this.state.rooms.map(function(room, i) {
+              return <RoomEntry key={i} room={room} openRoom={this.openRoom} />;
+            }, this)
+          }
+        </div>
+      );
+    }
+  });
+
+  /**
    * Panel view.
    */
   var PanelView = React.createClass({
     propTypes: {
       notifications: React.PropTypes.object.isRequired,
       client: React.PropTypes.object.isRequired,
       // Mostly used for UI components showcase and unit tests
       callUrl: React.PropTypes.string,
       userProfile: React.PropTypes.object,
       showTabButtons: React.PropTypes.bool,
+      selectedTab: React.PropTypes.string,
+      dispatcher: React.PropTypes.instanceOf(loop.Dispatcher).isRequired,
+      roomListStore:
+        React.PropTypes.instanceOf(loop.store.RoomListStore).isRequired
     },
 
     getInitialState: function() {
       return {
         userProfile: this.props.userProfile || navigator.mozLoop.userProfile,
       };
     },
 
@@ -493,16 +628,32 @@ loop.panel = (function(_, mozL10n) {
       if (profile != this.state.userProfile) {
         // On profile change (login, logout), switch back to the default tab.
         this.selectTab("call");
       }
       this.setState({userProfile: profile});
       this.updateServiceErrors();
     },
 
+    /**
+     * The rooms feature is hidden by default for now. Once it gets mainstream,
+     * this method can be safely removed.
+     */
+    _renderRoomsTab: function() {
+      if (!navigator.mozLoop.getLoopBoolPref("rooms.enabled")) {
+        return null;
+      }
+      return (
+        <Tab name="rooms">
+          <RoomList dispatcher={this.props.dispatcher}
+                    store={this.props.roomListStore} />
+        </Tab>
+      );
+    },
+
     startForm: function(name, contact) {
       this.refs[name].initForm(contact);
       this.selectTab(name);
     },
 
     selectTab: function(name) {
       this.refs.tabView.setState({ selectedTab: name });
     },
@@ -522,25 +673,27 @@ loop.panel = (function(_, mozL10n) {
     render: function() {
       var NotificationListView = sharedViews.NotificationListView;
       var displayName = this.state.userProfile && this.state.userProfile.email ||
                         __("display_name_guest");
       return (
         <div>
           <NotificationListView notifications={this.props.notifications}
                                 clearOnDocumentHidden={true} />
-          <TabView ref="tabView" buttonsHidden={!this.state.userProfile && !this.props.showTabButtons}>
+          <TabView ref="tabView" selectedTab={this.props.selectedTab}
+            buttonsHidden={!this.state.userProfile && !this.props.showTabButtons}>
             <Tab name="call">
               <div className="content-area">
                 <CallUrlResult client={this.props.client}
                                notifications={this.props.notifications}
                                callUrl={this.props.callUrl} />
                 <ToSView />
               </div>
             </Tab>
+            {this._renderRoomsTab()}
             <Tab name="contacts">
               <ContactsList selectTab={this.selectTab}
                             startForm={this.startForm} />
             </Tab>
             <Tab name="contacts_add" hidden={true}>
               <ContactDetailsForm ref="contacts_add" mode="add"
                                   selectTab={this.selectTab} />
             </Tab>
@@ -570,21 +723,29 @@ loop.panel = (function(_, mozL10n) {
    * Panel initialisation.
    */
   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 notifications = new sharedModels.NotificationCollection();
+    var dispatcher = new loop.Dispatcher();
+    var roomListStore = new loop.store.RoomListStore({
+      mozLoop: navigator.mozLoop,
+      dispatcher: dispatcher
+    });
 
     React.renderComponent(<PanelView
       client={client}
-      notifications={notifications} />, document.querySelector("#main"));
+      notifications={notifications}
+      roomListStore={roomListStore}
+      dispatcher={dispatcher}
+    />, document.querySelector("#main"));
 
     document.body.classList.add(loop.shared.utils.getTargetPlatform());
     document.body.setAttribute("dir", mozL10n.getDirection());
 
     // Notify the window that we've finished initalization and initial layout
     var evtObject = document.createEvent('Event');
     evtObject.initEvent('loopPanelInitialized', true, false);
     window.dispatchEvent(evtObject);
@@ -592,14 +753,15 @@ loop.panel = (function(_, mozL10n) {
 
   return {
     init: init,
     UserIdentity: UserIdentity,
     AuthLink: AuthLink,
     AvailabilityDropdown: AvailabilityDropdown,
     CallUrlResult: CallUrlResult,
     PanelView: PanelView,
+    RoomList: RoomList,
     SettingsDropdown: SettingsDropdown,
     ToSView: ToSView
   };
 })(_, document.mozL10n);
 
 document.addEventListener('DOMContentLoaded', loop.panel.init);
--- a/browser/components/loop/content/panel.html
+++ b/browser/components/loop/content/panel.html
@@ -20,13 +20,17 @@
     <script type="text/javascript" src="loop/shared/libs/jquery-2.1.0.js"></script>
     <script type="text/javascript" src="loop/shared/libs/lodash-2.4.1.js"></script>
     <script type="text/javascript" src="loop/shared/libs/backbone-1.1.2.js"></script>
 
     <script type="text/javascript" src="loop/shared/js/utils.js"></script>
     <script type="text/javascript" src="loop/shared/js/models.js"></script>
     <script type="text/javascript" src="loop/shared/js/mixins.js"></script>
     <script type="text/javascript" src="loop/shared/js/views.js"></script>
+    <script type="text/javascript" src="loop/shared/js/validate.js"></script>
+    <script type="text/javascript" src="loop/shared/js/actions.js"></script>
+    <script type="text/javascript" src="loop/shared/js/dispatcher.js"></script>
+    <script type="text/javascript" src="loop/shared/js/roomListStore.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/css/panel.css
+++ b/browser/components/loop/content/shared/css/panel.css
@@ -1,12 +1,16 @@
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
+body {
+  background: none;
+}
+
 /* Panel styles */
 
 .panel {
   /* hide the extra margin space that the panel resizer now wants to show */
   overflow: hidden;
 }
 
 /* Notifications displayed over tabs */
@@ -21,17 +25,16 @@
 
 /* Tabs and tab selection buttons */
 
 .tab-view {
   display: flex;
   flex-direction: row;
   padding: 10px;
   border-bottom: 1px solid #ccc;
-  background-color: #fbfbfb;
   color: #000;
   border-top-right-radius: 2px;
   border-top-left-radius: 2px;
   list-style: none;
 }
 
 .tab-view > li {
   flex: 1;
@@ -115,16 +118,80 @@
   box-shadow: none;
 }
 
 .content-area input:not(.pristine):invalid {
   border-color: #d74345;
   box-shadow: 0 0 4px #c43c3e;
 }
 
+/* Rooms */
+.room-list {
+  background: #f5f5f5;
+}
+
+.room-list > h1 {
+  font-weight: bold;
+  color: #999;
+  padding: .5rem 1rem;
+  border-bottom: 1px solid #ddd;
+}
+
+.room-list > .room-entry {
+  padding: 1rem 1rem 0 .5rem;
+}
+
+.room-list > .room-entry > h2 {
+  font-size: .85rem;
+  color: #777;
+}
+
+.room-list > .room-entry.room-active > h2 {
+  font-weight: bold;
+  color: #000;
+}
+
+.room-list > .room-entry > h2 > .room-notification {
+  display: inline-block;
+  background: transparent;
+  width: 8px;
+  height: 8px;
+  border-radius: 50%;
+  margin-right: .3rem;
+}
+
+.room-list > .room-entry.room-active > h2 > .room-notification {
+  background-color: #00a0ec;
+}
+
+.room-list > .room-entry:hover {
+  background: #f1f1f1;
+}
+
+.room-list > .room-entry:not(:last-child) {
+  border-bottom: 1px solid #ddd;
+}
+
+.room-list > .room-entry > p {
+  margin: 0;
+  padding: .2em 0 1rem .8rem;
+}
+
+.room-list > .room-entry > p > a {
+  color: #777;
+  opacity: .5;
+  transition: opacity .1s ease-in-out 0s;
+  text-decoration: none;
+}
+
+.room-list > .room-entry > p > a:hover {
+  opacity: 1;
+  text-decoration: underline;
+}
+
 /* Buttons */
 
 .button-group {
   display: flex;
   flex-direction: row;
   width: 100%;
 }
 
--- a/browser/components/loop/content/shared/js/actions.js
+++ b/browser/components/loop/content/shared/js/actions.js
@@ -113,11 +113,18 @@ loop.shared.actions = (function() {
     /**
      * Used to mute or unmute a stream
      */
     SetMute: Action.define("setMute", {
       // The part of the stream to enable, e.g. "audio" or "video"
       type: String,
       // Whether or not to enable the stream.
       enabled: Boolean
+    }),
+
+    /**
+     * Retrieves room list.
+     * XXX: should move to some roomActions module - refs bug 1079284
+     */
+    GetAllRooms: Action.define("getAllRooms", {
     })
   };
 })();
--- a/browser/components/loop/content/shared/js/conversationStore.js
+++ b/browser/components/loop/content/shared/js/conversationStore.js
@@ -1,42 +1,43 @@
 /* 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 = (function() {
+loop.store = loop.store || {};
 
+loop.store.ConversationStore = (function() {
   var sharedActions = loop.shared.actions;
   var CALL_TYPES = loop.shared.utils.CALL_TYPES;
 
   /**
    * Websocket states taken from:
    * https://docs.services.mozilla.com/loop/apis.html#call-progress-state-change-progress
    */
-  var WS_STATES = {
+  var WS_STATES = loop.store.WS_STATES = {
     // The call is starting, and the remote party is not yet being alerted.
     INIT: "init",
     // The called party is being alerted.
     ALERTING: "alerting",
     // The call is no longer being set up and has been aborted for some reason.
     TERMINATED: "terminated",
     // The called party has indicated that he has answered the call,
     // but the media is not yet confirmed.
     CONNECTING: "connecting",
     // One of the two parties has indicated successful media set up,
     // but the other has not yet.
     HALF_CONNECTED: "half-connected",
     // Both endpoints have reported successfully establishing media.
     CONNECTED: "connected"
   };
 
-  var CALL_STATES = {
+  var CALL_STATES = loop.store.CALL_STATES = {
     // The initial state of the view.
     INIT: "cs-init",
     // The store is gathering the call data from the server.
     GATHER: "cs-gather",
     // The initial data has been gathered, the websocket is connecting, or has
     // connected, and waiting for the other side to connect to the server.
     CONNECTING: "cs-connecting",
     // The websocket has received information that we're now alerting
@@ -47,17 +48,16 @@ loop.store = (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"
   };
 
-
   var ConversationStore = Backbone.Model.extend({
     defaults: {
       // The current state of the call
       callState: CALL_STATES.INIT,
       // The reason if a call was terminated
       callStateReason: undefined,
       // The error information, if there was a failure
       error: undefined,
@@ -397,14 +397,10 @@ loop.store = (function() {
           break;
         }
       }
 
       this.dispatcher.dispatch(action);
     }
   });
 
-  return {
-    CALL_STATES: CALL_STATES,
-    ConversationStore: ConversationStore,
-    WS_STATES: WS_STATES
-  };
+  return ConversationStore;
 })();
new file mode 100644
--- /dev/null
+++ b/browser/components/loop/content/shared/js/roomListStore.js
@@ -0,0 +1,171 @@
+/* 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 || {};
+
+(function() {
+  "use strict";
+
+  /**
+   * Room validation schema. See validate.js.
+   * @type {Object}
+   */
+  var roomSchema = {
+    roomToken: String,
+    roomUrl:   String,
+    roomName:  String,
+    maxSize:   Number,
+    currSize:  Number,
+    ctime:     Number
+  };
+
+  /**
+   * Temporary sample raw room list data.
+   * XXX Should be removed when we plug the real mozLoop API for rooms.
+   *     See bug 1074664.
+   * @type {Array}
+   */
+  var temporaryRawRoomList = [{
+    roomToken: "_nxD4V4FflQ",
+    roomUrl: "http://sample/_nxD4V4FflQ",
+    roomName: "First Room Name",
+    maxSize: 2,
+    currSize: 0,
+    ctime: 1405517546
+  }, {
+    roomToken: "QzBbvGmIZWU",
+    roomUrl: "http://sample/QzBbvGmIZWU",
+    roomName: "Second Room Name",
+    maxSize: 2,
+    currSize: 0,
+    ctime: 1405517418
+  }, {
+    roomToken: "3jKS_Els9IU",
+    roomUrl: "http://sample/3jKS_Els9IU",
+    roomName: "Third Room Name",
+    maxSize: 3,
+    clientMaxSize: 2,
+    currSize: 1,
+    ctime: 1405518241
+  }];
+
+  /**
+   * Room type. Basically acts as a typed object constructor.
+   *
+   * @param {Object} values Room property values.
+   */
+  function Room(values) {
+    var validatedData = new loop.validate.Validator(roomSchema || {})
+                                         .validate(values || {});
+    for (var prop in validatedData) {
+      this[prop] = validatedData[prop];
+    }
+  }
+
+  loop.store.Room = Room;
+
+  /**
+   * Room store.
+   *
+   * Options:
+   * - {loop.Dispatcher} dispatcher The dispatcher for dispatching actions and
+   *                                registering to consume actions.
+   * - {mozLoop}         mozLoop    The MozLoop API object.
+   *
+   * @extends {Backbone.Events}
+   * @param {Object} options Options object.
+   */
+  function RoomListStore(options) {
+    options = options || {};
+    this.storeState = {error: null, rooms: []};
+
+    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;
+
+    this.dispatcher.register(this, [
+      "getAllRooms",
+      "openRoom"
+    ]);
+  }
+
+  RoomListStore.prototype = _.extend({
+    /**
+     * Retrieves current store state.
+     *
+     * @return {Object}
+     */
+    getStoreState: function() {
+      return this.storeState;
+    },
+
+    /**
+     * Updates store states and trigger a "change" event.
+     *
+     * @param {Object} state The new store state.
+     */
+    setStoreState: function(state) {
+      this.storeState = state;
+      this.trigger("change");
+    },
+
+    /**
+     * Proxy to navigator.mozLoop.rooms.getAll.
+     * XXX Could probably be removed when bug 1074664 lands.
+     *
+     * @param  {Function} cb Callback(error, roomList)
+     */
+    _fetchRoomList: function(cb) {
+      // Faking this.mozLoop.rooms until it's available; bug 1074664.
+      if (!this.mozLoop.hasOwnProperty("rooms")) {
+        cb(null, temporaryRawRoomList);
+        return;
+      }
+      this.mozLoop.rooms.getAll(cb);
+    },
+
+    /**
+     * Maps and sorts the raw room list received from the mozLoop API.
+     *
+     * @param  {Array} rawRoomList Raw room list.
+     * @return {Array}
+     */
+    _processRawRoomList: function(rawRoomList) {
+      if (!rawRoomList) {
+        return [];
+      }
+      return rawRoomList
+        .map(function(rawRoom) {
+          return new Room(rawRoom);
+        })
+        .slice()
+        .sort(function(a, b) {
+          return b.ctime - a.ctime;
+        });
+    },
+
+    /**
+     * Gather the list of all available rooms from the MozLoop API.
+     */
+    getAllRooms: function() {
+      this._fetchRoomList(function(err, rawRoomList) {
+        this.setStoreState({
+          error: err,
+          rooms: this._processRawRoomList(rawRoomList)
+        });
+      }.bind(this));
+    }
+  }, Backbone.Events);
+
+  loop.store.RoomListStore = RoomListStore;
+})();
--- a/browser/components/loop/jar.mn
+++ b/browser/components/loop/jar.mn
@@ -50,16 +50,17 @@ browser.jar:
   content/browser/loop/shared/img/audio-call-avatar.svg         (content/shared/img/audio-call-avatar.svg)
   content/browser/loop/shared/img/icons-10x10.svg               (content/shared/img/icons-10x10.svg)
   content/browser/loop/shared/img/icons-14x14.svg               (content/shared/img/icons-14x14.svg)
   content/browser/loop/shared/img/icons-16x16.svg               (content/shared/img/icons-16x16.svg)
 
   # Shared scripts
   content/browser/loop/shared/js/actions.js           (content/shared/js/actions.js)
   content/browser/loop/shared/js/conversationStore.js (content/shared/js/conversationStore.js)
+  content/browser/loop/shared/js/roomListStore.js     (content/shared/js/roomListStore.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)
   content/browser/loop/shared/js/utils.js             (content/shared/js/utils.js)
   content/browser/loop/shared/js/validate.js          (content/shared/js/validate.js)
--- 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/roomListStore.js"></script>
   <script src="../../content/js/client.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>
   <script src="../../content/js/panel.js"></script>
 
   <!-- Test scripts -->
   <script src="client_test.js"></script>
--- a/browser/components/loop/test/desktop-local/panel_test.js
+++ b/browser/components/loop/test/desktop-local/panel_test.js
@@ -2,23 +2,24 @@
  * 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/. */
 
 /*jshint newcap:false*/
 /*global loop, sinon */
 
 var expect = chai.expect;
 var TestUtils = React.addons.TestUtils;
+var sharedActions = loop.shared.actions;
 
 describe("loop.panel", function() {
   "use strict";
 
   var sandbox, notifications, fakeXHR, requests = [];
 
-  beforeEach(function() {
+  beforeEach(function(done) {
     sandbox = sinon.sandbox.create();
     fakeXHR = sandbox.useFakeXMLHttpRequest();
     requests = [];
     // https://github.com/cjohansen/Sinon.JS/issues/393
     fakeXHR.xhr.onCreate = function (xhr) {
       requests.push(xhr);
     };
     notifications = new loop.shared.models.NotificationCollection();
@@ -27,31 +28,37 @@ describe("loop.panel", function() {
       doNotDisturb: true,
       fxAEnabled: true,
       getStrings: function() {
         return JSON.stringify({textContent: "fakeText"});
       },
       get locale() {
         return "en-US";
       },
+      getLoopBoolPref: sandbox.stub(),
       setLoopCharPref: sandbox.stub(),
       getLoopCharPref: sandbox.stub().returns("unseen"),
+      getPluralForm: function() {
+        return "fakeText";
+      },
       copyString: sandbox.stub(),
       noteCallUrlExpiry: sinon.spy(),
       composeEmail: sinon.spy(),
       telemetryAdd: sinon.spy(),
       contacts: {
         getAll: function(callback) {
           callback(null, []);
         },
         on: sandbox.stub()
       }
     };
 
     document.mozL10n.initialize(navigator.mozLoop);
+    // XXX prevent a race whenever mozL10n hasn't been initialized yet
+    setTimeout(done, 0);
   });
 
   afterEach(function() {
     delete navigator.mozLoop;
     sandbox.restore();
   });
 
   describe("#init", function() {
@@ -121,64 +128,129 @@ describe("loop.panel", function() {
         TestUtils.Simulate.click(availableMenuOption);
 
         expect(view.state.showMenu).eql(true);
       });
     });
   });
 
   describe("loop.panel.PanelView", function() {
-    var fakeClient, callUrlData, view, callTab, contactsTab;
+    var fakeClient, dispatcher, roomListStore, callUrlData;
 
     beforeEach(function() {
       callUrlData = {
         callUrl: "http://call.invalid/",
         expiresAt: 1000
       };
 
       fakeClient = {
         requestCallUrl: function(_, cb) {
           cb(null, callUrlData);
         }
       };
 
-      view = TestUtils.renderIntoDocument(loop.panel.PanelView({
+      dispatcher = new loop.Dispatcher();
+      roomListStore = new loop.store.RoomListStore({
+        dispatcher: dispatcher,
+        mozLoop: navigator.mozLoop
+      });
+    });
+
+    function createTestPanelView() {
+      return TestUtils.renderIntoDocument(loop.panel.PanelView({
         notifications: notifications,
         client: fakeClient,
         showTabButtons: true,
+        dispatcher: dispatcher,
+        roomListStore: roomListStore
       }));
-
-      [callTab, contactsTab] =
-        TestUtils.scryRenderedDOMComponentsWithClass(view, "tab");
-    });
+    }
 
     describe('TabView', function() {
-      it("should select contacts tab when clicking tab button", function() {
-        TestUtils.Simulate.click(
-          view.getDOMNode().querySelector('li[data-tab-name="contacts"]'));
+      var view, callTab, roomsTab, contactsTab;
+
+      describe("loop.rooms.enabled on", function() {
+        beforeEach(function() {
+          navigator.mozLoop.getLoopBoolPref = function(pref) {
+            if (pref === "rooms.enabled") {
+              return true;
+            }
+          };
+
+          view = createTestPanelView();
+
+          [callTab, roomsTab, contactsTab] =
+            TestUtils.scryRenderedDOMComponentsWithClass(view, "tab");
+        });
+
+        it("should select contacts tab when clicking tab button", function() {
+          TestUtils.Simulate.click(
+            view.getDOMNode().querySelector("li[data-tab-name=\"contacts\"]"));
 
-        expect(contactsTab.getDOMNode().classList.contains("selected"))
-          .to.be.true;
+          expect(contactsTab.getDOMNode().classList.contains("selected"))
+            .to.be.true;
+        });
+
+        it("should select rooms tab when clicking tab button", function() {
+          TestUtils.Simulate.click(
+            view.getDOMNode().querySelector("li[data-tab-name=\"rooms\"]"));
+
+          expect(roomsTab.getDOMNode().classList.contains("selected"))
+            .to.be.true;
+        });
+
+        it("should select call tab when clicking tab button", function() {
+          TestUtils.Simulate.click(
+            view.getDOMNode().querySelector("li[data-tab-name=\"call\"]"));
+
+          expect(callTab.getDOMNode().classList.contains("selected"))
+            .to.be.true;
+        });
       });
 
-      it("should select call tab when clicking tab button", function() {
-        TestUtils.Simulate.click(
-          view.getDOMNode().querySelector('li[data-tab-name="call"]'));
+      describe("loop.rooms.enabled off", function() {
+        beforeEach(function() {
+          navigator.mozLoop.getLoopBoolPref = function(pref) {
+            if (pref === "rooms.enabled") {
+              return false;
+            }
+          };
+
+          view = createTestPanelView();
+
+          [callTab, contactsTab] =
+            TestUtils.scryRenderedDOMComponentsWithClass(view, "tab");
+        });
 
-        expect(callTab.getDOMNode().classList.contains("selected"))
-          .to.be.true;
+        it("should select contacts tab when clicking tab button", function() {
+          TestUtils.Simulate.click(
+            view.getDOMNode().querySelector("li[data-tab-name=\"contacts\"]"));
+
+          expect(contactsTab.getDOMNode().classList.contains("selected"))
+            .to.be.true;
+        });
+
+        it("should select call tab when clicking tab button", function() {
+          TestUtils.Simulate.click(
+            view.getDOMNode().querySelector("li[data-tab-name=\"call\"]"));
+
+          expect(callTab.getDOMNode().classList.contains("selected"))
+            .to.be.true;
+        });
       });
     });
 
     describe("AuthLink", function() {
       it("should trigger the FxA sign in/up process when clicking the link",
         function() {
           navigator.mozLoop.loggedInToFxA = false;
           navigator.mozLoop.logInToFxA = sandbox.stub();
 
+          var view = createTestPanelView();
+
           TestUtils.Simulate.click(
             view.getDOMNode().querySelector(".signin-link a"));
 
           sinon.assert.calledOnce(navigator.mozLoop.logInToFxA);
         });
 
       it("should be hidden if FxA is not enabled",
         function() {
@@ -188,18 +260,16 @@ describe("loop.panel", function() {
       });
 
       afterEach(function() {
         navigator.mozLoop.fxAEnabled = true;
       });
     });
 
     describe("SettingsDropdown", function() {
-      var view;
-
       beforeEach(function() {
         navigator.mozLoop.logInToFxA = sandbox.stub();
         navigator.mozLoop.logOutFromFxA = sandbox.stub();
         navigator.mozLoop.openFxASettings = sandbox.stub();
       });
 
       afterEach(function() {
         navigator.mozLoop.fxAEnabled = true;
@@ -283,16 +353,18 @@ describe("loop.panel", function() {
           view.getDOMNode().querySelector(".icon-signout"));
 
         sinon.assert.calledOnce(navigator.mozLoop.logOutFromFxA);
       });
     });
 
     describe("#render", function() {
       it("should render a ToSView", function() {
+        var view = createTestPanelView();
+
         TestUtils.findRenderedComponentWithType(view, loop.panel.ToSView);
       });
     });
   });
 
   describe("loop.panel.CallUrlResult", function() {
     var fakeClient, callUrlData, view;
 
@@ -545,16 +617,44 @@ describe("loop.panel", function() {
 
         sinon.assert.calledOnce(notifications.errorL10n);
         sinon.assert.calledWithExactly(notifications.errorL10n,
                                        "unable_retrieve_url");
       });
     });
   });
 
+  describe("loop.panel.RoomList", function() {
+    var roomListStore, dispatcher;
+
+    beforeEach(function() {
+      dispatcher = new loop.Dispatcher();
+      roomListStore = new loop.store.RoomListStore({
+        dispatcher: dispatcher,
+        mozLoop: navigator.mozLoop
+      });
+    });
+
+    function createTestComponent() {
+      return TestUtils.renderIntoDocument(loop.panel.RoomList({
+        store: roomListStore,
+        dispatcher: dispatcher
+      }));
+    }
+
+    it("should dispatch a GetAllRooms action on mount", function() {
+      var dispatch = sandbox.stub(dispatcher, "dispatch");
+
+      createTestComponent();
+
+      sinon.assert.calledOnce(dispatch);
+      sinon.assert.calledWithExactly(dispatch, new sharedActions.GetAllRooms());
+    });
+  });
+
   describe('loop.panel.ToSView', 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");
     });
 
--- a/browser/components/loop/test/functional/test_1_browser_call.py
+++ b/browser/components/loop/test/functional/test_1_browser_call.py
@@ -126,22 +126,22 @@ class Test1BrowserCall(MarionetteTestCas
         # expect a video container on desktop side
         video = self.wait_for_element_displayed(By.CLASS_NAME, "media")
         self.assertEqual(video.tag_name, "div", "expect a video container")
 
     def hangup_call_and_verify_feedback(self):
         self.marionette.set_context("chrome")
         button = self.marionette.find_element(By.CLASS_NAME, "btn-hangup")
 
-        # XXX For whatever reason, the click doesn't take effect unless we
-        # wait for a bit (even if we wait for the element to actually be
-        # displayed first, which we're not currently bothering with).  It's
-        # not entirely clear whether the click is being delivered in this case,
-        # or whether there's a Marionette bug here.
-        sleep(2)
+        # XXX bug 1080095 For whatever reason, the click doesn't take effect
+        # unless we wait for a bit (even if we wait for the element to
+        # actually be displayed first, which we're not currently bothering
+        # with).  It's not entirely clear whether the click is being
+        # delivered in this case, or whether there's a Marionette bug here.
+        sleep(5)
         button.click()
 
         # check that the feedback form is displayed
         feedback_form = self.wait_for_element_displayed(By.CLASS_NAME, "faces")
         self.assertEqual(feedback_form.tag_name, "div", "expect feedback form")
 
     def test_1_browser_call(self):
         self.switch_to_panel()
--- a/browser/components/loop/test/mochitest/browser_fxa_login.js
+++ b/browser/components/loop/test/mochitest/browser_fxa_login.js
@@ -2,21 +2,16 @@
    http://creativecommons.org/publicdomain/zero/1.0/ */
 
 /**
  * Test FxA logins with Loop.
  */
 
 "use strict";
 
-const {
-  gFxAOAuthTokenData,
-  gFxAOAuthProfile,
-} = Cu.import("resource:///modules/loop/MozLoopService.jsm", {});
-
 const BASE_URL = "http://mochi.test:8888/browser/browser/components/loop/test/mochitest/loop_fxa.sjs?";
 
 function* checkFxA401() {
   let err = MozLoopService.errors.get("login");
   ise(err.code, 401, "Check error code");
   ise(err.friendlyMessage, getLoopString("could_not_authenticate"),
       "Check friendlyMessage");
   ise(err.friendlyDetails, getLoopString("password_changed_question"),
@@ -206,16 +201,18 @@ add_task(function* registrationWithInval
   Services.prefs.setCharPref(fxASessionPref, "X".repeat(HAWK_TOKEN_LENGTH));
 
   let tokenPromise = MozLoopServiceInternal.promiseFxAOAuthToken("code1", "state");
   yield tokenPromise.then(body => {
     ok(false, "Promise should have rejected");
   },
   error => {
     is(error.code, 400, "Check error code");
+    checkFxAOAuthTokenData(null);
+    is(MozLoopService.userProfile, null, "Profile should be empty after invalid login");
   });
 });
 
 add_task(function* registrationWith401() {
   yield resetFxA();
   let params = {
     client_id: "client_id",
     content_uri: BASE_URL + "/content",
@@ -227,16 +224,18 @@ add_task(function* registrationWith401()
   yield promiseOAuthParamsSetup(BASE_URL, params);
 
   let tokenPromise = MozLoopServiceInternal.promiseFxAOAuthToken("code1", "state");
   yield tokenPromise.then(body => {
     ok(false, "Promise should have rejected");
   },
   error => {
     is(error.code, 401, "Check error code");
+    checkFxAOAuthTokenData(null);
+    is(MozLoopService.userProfile, null, "Profile should be empty after invalid login");
   });
 
   yield checkFxA401();
 });
 
 add_task(function* basicAuthorizationAndRegistration() {
   yield resetFxA();
   let params = {
@@ -316,17 +315,17 @@ add_task(function* loginWithParams401() 
   yield MozLoopService.register(mockPushHandler);
 
   let loginPromise = MozLoopService.logInToFxA();
   yield loginPromise.then(tokenData => {
     ok(false, "Promise should have rejected");
   },
   error => {
     ise(error.code, 401, "Check error code");
-    ise(gFxAOAuthTokenData, null, "Check there is no saved token data");
+    checkFxAOAuthTokenData(null);
   });
 
   yield checkFxA401();
 });
 
 add_task(function* logoutWithIncorrectPushURL() {
   yield resetFxA();
   let pushURL = "http://www.example.com/";
@@ -382,13 +381,13 @@ add_task(function* loginWithRegistration
   yield promiseOAuthParamsSetup(BASE_URL, params);
 
   let loginPromise = MozLoopService.logInToFxA();
   yield loginPromise.then(tokenData => {
     ok(false, "Promise should have rejected");
   },
   error => {
     ise(error.code, 401, "Check error code");
-    ise(gFxAOAuthTokenData, null, "Check there is no saved token data");
+    checkFxAOAuthTokenData(null);
   });
 
   yield checkFxA401();
 });
--- a/browser/components/loop/test/mochitest/browser_toolbarbutton.js
+++ b/browser/components/loop/test/mochitest/browser_toolbarbutton.js
@@ -4,74 +4,76 @@
 /**
  * Test the toolbar button states.
  */
 
 "use strict";
 
 registerCleanupFunction(function*() {
   MozLoopService.doNotDisturb = false;
-  setInternalLoopGlobal("gFxAOAuthProfile", null);
+  MozLoopServiceInternal.fxAOAuthProfile = null;
   yield MozLoopServiceInternal.clearError("testing");
 });
 
 add_task(function* test_doNotDisturb() {
   Assert.strictEqual(LoopUI.toolbarButton.node.getAttribute("state"), "", "Check button is in default state");
   yield MozLoopService.doNotDisturb = true;
   Assert.strictEqual(LoopUI.toolbarButton.node.getAttribute("state"), "disabled", "Check button is in disabled state");
   yield MozLoopService.doNotDisturb = false;
   Assert.notStrictEqual(LoopUI.toolbarButton.node.getAttribute("state"), "disabled", "Check button is not in disabled state");
 });
 
 add_task(function* test_doNotDisturb_with_login() {
   Assert.strictEqual(LoopUI.toolbarButton.node.getAttribute("state"), "", "Check button is in default state");
   yield MozLoopService.doNotDisturb = true;
   Assert.strictEqual(LoopUI.toolbarButton.node.getAttribute("state"), "disabled", "Check button is in disabled state");
-  setInternalLoopGlobal("gFxAOAuthProfile", {email: "test@example.com", uid: "abcd1234"});
+  MozLoopServiceInternal.fxAOAuthTokenData = {token_type:"bearer",access_token:"1bad3e44b12f77a88fe09f016f6a37c42e40f974bc7a8b432bb0d2f0e37e1752",scope:"profile"};
+  MozLoopServiceInternal.fxAOAuthProfile = {email: "test@example.com", uid: "abcd1234"};
   yield MozLoopServiceInternal.notifyStatusChanged("login");
   Assert.strictEqual(LoopUI.toolbarButton.node.getAttribute("state"), "active", "Check button is in active state");
   yield loadLoopPanel();
   Assert.strictEqual(LoopUI.toolbarButton.node.getAttribute("state"), "disabled", "Check button is in disabled state after opening panel");
   let loopPanel = document.getElementById("loop-notification-panel");
   loopPanel.hidePopup();
   yield MozLoopService.doNotDisturb = false;
   Assert.strictEqual(LoopUI.toolbarButton.node.getAttribute("state"), "", "Check button is in default state");
-  setInternalLoopGlobal("gFxAOAuthProfile", null);
+  MozLoopServiceInternal.fxAOAuthTokenData = null;
   yield MozLoopServiceInternal.notifyStatusChanged();
   Assert.strictEqual(LoopUI.toolbarButton.node.getAttribute("state"), "", "Check button is in default state");
 });
 
 add_task(function* test_error() {
   Assert.strictEqual(LoopUI.toolbarButton.node.getAttribute("state"), "", "Check button is in default state");
   yield MozLoopServiceInternal.setError("testing", {});
   Assert.strictEqual(LoopUI.toolbarButton.node.getAttribute("state"), "error", "Check button is in error state");
   yield MozLoopServiceInternal.clearError("testing");
   Assert.notStrictEqual(LoopUI.toolbarButton.node.getAttribute("state"), "error", "Check button is not in error state");
 });
 
 add_task(function* test_error_with_login() {
   Assert.strictEqual(LoopUI.toolbarButton.node.getAttribute("state"), "", "Check button is in default state");
   yield MozLoopServiceInternal.setError("testing", {});
   Assert.strictEqual(LoopUI.toolbarButton.node.getAttribute("state"), "error", "Check button is in error state");
-  setInternalLoopGlobal("gFxAOAuthProfile", {email: "test@example.com", uid: "abcd1234"});
+  MozLoopServiceInternal.fxAOAuthProfile = {email: "test@example.com", uid: "abcd1234"};
   MozLoopServiceInternal.notifyStatusChanged("login");
   Assert.strictEqual(LoopUI.toolbarButton.node.getAttribute("state"), "error", "Check button is in error state");
   yield MozLoopServiceInternal.clearError("testing");
   Assert.strictEqual(LoopUI.toolbarButton.node.getAttribute("state"), "", "Check button is in default state");
-  setInternalLoopGlobal("gFxAOAuthProfile", null);
+  MozLoopServiceInternal.fxAOAuthProfile = null;
   MozLoopServiceInternal.notifyStatusChanged();
   Assert.strictEqual(LoopUI.toolbarButton.node.getAttribute("state"), "", "Check button is in default state");
 });
 
 add_task(function* test_active() {
   Assert.strictEqual(LoopUI.toolbarButton.node.getAttribute("state"), "", "Check button is in default state");
-  setInternalLoopGlobal("gFxAOAuthProfile", {email: "test@example.com", uid: "abcd1234"});
+  MozLoopServiceInternal.fxAOAuthTokenData = {token_type:"bearer",access_token:"1bad3e44b12f77a88fe09f016f6a37c42e40f974bc7a8b432bb0d2f0e37e1752",scope:"profile"};
+  MozLoopServiceInternal.fxAOAuthProfile = {email: "test@example.com", uid: "abcd1234"};
   yield MozLoopServiceInternal.notifyStatusChanged("login");
   Assert.strictEqual(LoopUI.toolbarButton.node.getAttribute("state"), "active", "Check button is in active state");
   yield loadLoopPanel();
   Assert.strictEqual(LoopUI.toolbarButton.node.getAttribute("state"), "", "Check button is in default state after opening panel");
   let loopPanel = document.getElementById("loop-notification-panel");
   loopPanel.hidePopup();
-  setInternalLoopGlobal("gFxAOAuthProfile", null);
+  MozLoopServiceInternal.fxAOAuthTokenData = null;
   MozLoopServiceInternal.notifyStatusChanged();
   Assert.strictEqual(LoopUI.toolbarButton.node.getAttribute("state"), "", "Check button is in default state");
 });
 
--- a/browser/components/loop/test/mochitest/head.js
+++ b/browser/components/loop/test/mochitest/head.js
@@ -115,37 +115,36 @@ function promiseOAuthParamsSetup(baseURL
   return deferred.promise;
 }
 
 function* resetFxA() {
   let global = Cu.import("resource:///modules/loop/MozLoopService.jsm", {});
   global.gHawkClient = null;
   global.gFxAOAuthClientPromise = null;
   global.gFxAOAuthClient = null;
-  global.gFxAOAuthTokenData = null;
-  global.gFxAOAuthProfile = null;
+  MozLoopServiceInternal.fxAOAuthProfile = null;
+  MozLoopServiceInternal.fxAOAuthTokenData = null;
   const fxASessionPref = MozLoopServiceInternal.getSessionTokenPrefName(LOOP_SESSION_TYPE.FXA);
   Services.prefs.clearUserPref(fxASessionPref);
   MozLoopService.errors.clear();
   let notified = promiseObserverNotified("loop-status-changed");
   MozLoopServiceInternal.notifyStatusChanged();
   yield notified;
 }
 
-function setInternalLoopGlobal(aName, aValue) {
-  let global = Cu.import("resource:///modules/loop/MozLoopService.jsm", {});
-  global[aName] = aValue;
+function checkFxAOAuthTokenData(aValue) {
+  ise(MozLoopServiceInternal.fxAOAuthTokenData, aValue, "fxAOAuthTokenData should be " + aValue);
 }
 
 function checkLoggedOutState() {
   let global = Cu.import("resource:///modules/loop/MozLoopService.jsm", {});
   ise(global.gFxAOAuthClientPromise, null, "gFxAOAuthClientPromise should be cleared");
-  ise(global.gFxAOAuthProfile, null, "gFxAOAuthProfile should be cleared");
+  ise(MozLoopService.userProfile, null, "fxAOAuthProfile should be cleared");
   ise(global.gFxAOAuthClient, null, "gFxAOAuthClient should be cleared");
-  ise(global.gFxAOAuthTokenData, null, "gFxAOAuthTokenData should be cleared");
+  checkFxAOAuthTokenData(null);
   const fxASessionPref = MozLoopServiceInternal.getSessionTokenPrefName(LOOP_SESSION_TYPE.FXA);
   ise(Services.prefs.getPrefType(fxASessionPref), Services.prefs.PREF_INVALID,
       "FxA hawk session should be cleared anyways");
 }
 
 function promiseDeletedOAuthParams(baseURL) {
   let deferred = Promise.defer();
   let xhr = Cc["@mozilla.org/xmlextras/xmlhttprequest;1"].
--- a/browser/components/loop/test/shared/index.html
+++ b/browser/components/loop/test/shared/index.html
@@ -39,27 +39,29 @@
   <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/conversationStore.js"></script>
+  <script src="../../content/shared/js/roomListStore.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="conversationStore_test.js"></script>
   <script src="otSdkDriver_test.js"></script>
+  <script src="roomListStore_test.js"></script>
   <script>
     mocha.run(function () {
       $("#mocha").append("<p id='complete'>Complete.</p>");
     });
   </script>
 </body>
 </html>
new file mode 100644
--- /dev/null
+++ b/browser/components/loop/test/shared/roomListStore_test.js
@@ -0,0 +1,130 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+var expect = chai.expect;
+
+describe("loop.store.Room", function () {
+  "use strict";
+  describe("#constructor", function() {
+    it("should validate room values", function() {
+      expect(function() {
+        new loop.store.Room();
+      }).to.Throw(Error, /missing required/);
+    });
+  });
+});
+
+describe("loop.store.RoomListStore", function () {
+  "use strict";
+
+  var sharedActions = loop.shared.actions;
+  var sandbox, dispatcher;
+
+  beforeEach(function() {
+    sandbox = sinon.sandbox.create();
+    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.RoomListStore({mozLoop: {}});
+      }).to.Throw(/dispatcher/);
+    });
+
+    it("should throw an error if mozLoop is missing", function() {
+      expect(function() {
+        new loop.store.RoomListStore({dispatcher: dispatcher});
+      }).to.Throw(/mozLoop/);
+    });
+  });
+
+  describe("#getAllRooms", function() {
+    var store, fakeMozLoop;
+    var fakeRoomList = [{
+      roomToken: "_nxD4V4FflQ",
+      roomUrl: "http://sample/_nxD4V4FflQ",
+      roomName: "First Room Name",
+      maxSize: 2,
+      currSize: 0,
+      ctime: 1405517546
+    }, {
+      roomToken: "QzBbvGmIZWU",
+      roomUrl: "http://sample/QzBbvGmIZWU",
+      roomName: "Second Room Name",
+      maxSize: 2,
+      currSize: 0,
+      ctime: 1405517418
+    }, {
+      roomToken: "3jKS_Els9IU",
+      roomUrl: "http://sample/3jKS_Els9IU",
+      roomName: "Third Room Name",
+      maxSize: 3,
+      clientMaxSize: 2,
+      currSize: 1,
+      ctime: 1405518241
+    }];
+
+    beforeEach(function() {
+      fakeMozLoop = {
+        rooms: {
+          getAll: function(cb) {
+            cb(null, fakeRoomList);
+          }
+        }
+      };
+      store = new loop.store.RoomListStore({
+        dispatcher: dispatcher,
+        mozLoop: fakeMozLoop
+      });
+    });
+
+    it("should trigger a list:changed event", function(done) {
+      store.on("change", function() {
+        done();
+      });
+
+      dispatcher.dispatch(new sharedActions.GetAllRooms());
+    });
+
+    it("should fetch the room list from the mozLoop API", function(done) {
+      store.once("change", function() {
+        expect(store.getStoreState().error).to.be.a.null;
+        expect(store.getStoreState().rooms).to.have.length.of(3);
+        done();
+      });
+
+      dispatcher.dispatch(new sharedActions.GetAllRooms());
+    });
+
+    it("should order the room list using ctime desc", function(done) {
+      store.once("change", function() {
+        var storeState = store.getStoreState();
+        expect(storeState.error).to.be.a.null;
+        expect(storeState.rooms[0].ctime).eql(1405518241);
+        expect(storeState.rooms[1].ctime).eql(1405517546);
+        expect(storeState.rooms[2].ctime).eql(1405517418);
+        done();
+      });
+
+      dispatcher.dispatch(new sharedActions.GetAllRooms());
+    });
+
+    it("should report an error", function() {
+      fakeMozLoop.rooms.getAll = function(cb) {
+        cb("fakeError");
+      };
+
+      store.once("change", function() {
+        var storeState = store.getStoreState();
+        expect(storeState.error).eql("fakeError");
+      });
+
+      dispatcher.dispatch(new sharedActions.GetAllRooms());
+    });
+  });
+});
--- a/browser/components/loop/test/xpcshell/test_loopservice_busy.js
+++ b/browser/components/loop/test/xpcshell/test_loopservice_busy.js
@@ -47,19 +47,20 @@ function test_send_busy_on_call() {
 
 add_test(test_send_busy_on_call); //FXA call accepted, Guest call rejected
 add_test(test_send_busy_on_call); //No FXA call, first Guest call accepted, second rejected
 
 function run_test()
 {
   setupFakeLoopServer();
 
-  // Setup fake login (profile) state so we get FxA requests.
-  const serviceGlobal = Cu.import("resource:///modules/loop/MozLoopService.jsm", {});
-  serviceGlobal.gFxAOAuthProfile = {email: "test@example.com", uid: "abcd1234"};
+  // Setup fake login state so we get FxA requests.
+  const MozLoopServiceInternal = Cu.import("resource:///modules/loop/MozLoopService.jsm", {}).MozLoopServiceInternal;
+  MozLoopServiceInternal.fxAOAuthTokenData = {token_type:"bearer",access_token:"1bad3e44b12f77a88fe09f016f6a37c42e40f974bc7a8b432bb0d2f0e37e1752",scope:"profile"};
+  MozLoopServiceInternal.fxAOAuthProfile = {email: "test@example.com", uid: "abcd1234"};
 
   // For each notification received from the PushServer, MozLoopService will first query
   // for any pending calls on the FxA hawk session and then again using the guest session.
   // A pair of response objects in the callsResponses array will be consumed for each
   // notification. The even calls object is for the FxA session, the odd the Guest session.
 
   let callsRespCount = 0;
   let callsResponses = [
@@ -97,16 +98,16 @@ function run_test()
     response.finish();
   });
 
   do_register_cleanup(function() {
     // Revert original Chat.open implementation
     Chat.open = openChatOrig;
 
     // Revert fake login state
-    serviceGlobal.gFxAOAuthProfile = null;
+    MozLoopServiceInternal.fxAOAuthTokenData = null;
 
     // clear test pref
     Services.prefs.clearUserPref("loop.seenToS");
   });
 
   run_next_test();
 }
--- a/browser/components/loop/test/xpcshell/test_loopservice_initialize.js
+++ b/browser/components/loop/test/xpcshell/test_loopservice_initialize.js
@@ -5,18 +5,19 @@ var startTimerCalled = false;
 
 /**
  * Tests that registration doesn't happen when the expiry time is
  * not set.
  */
 add_task(function test_initialize_no_expiry() {
   startTimerCalled = false;
 
-  MozLoopService.initialize();
-
+  let initializedPromise = yield MozLoopService.initialize();
+  Assert.equal(initializedPromise, "registration not needed",
+               "Promise should be fulfilled");
   Assert.equal(startTimerCalled, false,
     "should not register when no expiry time is set");
 });
 
 /**
  * Tests that registration doesn't happen when the expiry time is
  * in the past.
  */
--- a/browser/components/loop/test/xpcshell/test_loopservice_registration.js
+++ b/browser/components/loop/test/xpcshell/test_loopservice_registration.js
@@ -35,17 +35,17 @@ add_test(function test_register_offline(
 add_test(function test_register_websocket_success_loop_server_fail() {
   mockPushHandler.registrationResult = null;
 
   MozLoopService.register(mockPushHandler).then(() => {
     do_throw("should not succeed when loop server registration fails");
   }, err => {
     // 404 is an expected failure indicated by the lack of route being set
     // up on the Loop server mock. This is added in the next test.
-    Assert.equal(err, 404, "Expected no errors in websocket registration");
+    Assert.equal(err.errno, 404, "Expected no errors in websocket registration");
 
     run_next_test();
   });
 });
 
 /**
  * Tests that we get a success response when both websocket and Loop server
  * registration are complete.
new file mode 100644
--- /dev/null
+++ b/browser/components/loop/test/xpcshell/test_loopservice_restart.js
@@ -0,0 +1,111 @@
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+
+const FAKE_FXA_TOKEN_DATA = JSON.stringify({
+  "token_type": "bearer",
+  "access_token": "1bad3e44b12f77a88fe09f016f6a37c42e40f974bc7a8b432bb0d2f0e37e1752",
+  "scope": "profile"
+});
+const FAKE_FXA_PROFILE = JSON.stringify({
+  "email": "test@example.com",
+  "uid": "999999994d9f4b08a2cbfc0999999999",
+  "avatar": null
+});
+const LOOP_FXA_TOKEN_PREF = "loop.fxa_oauth.tokendata";
+const LOOP_FXA_PROFILE_PREF = "loop.fxa_oauth.profile";
+const LOOP_URL_EXPIRY_PREF = "loop.urlsExpiryTimeSeconds";
+const LOOP_INITIAL_DELAY_PREF = "loop.initialDelay";
+
+/**
+ * This file is to test restart+reauth.
+ */
+
+add_task(function test_initialize_with_expired_urls_and_no_auth_token() {
+  // Set time to be 2 seconds in the past.
+  var nowSeconds = Date.now() / 1000;
+  Services.prefs.setIntPref(LOOP_URL_EXPIRY_PREF, nowSeconds - 2);
+  Services.prefs.clearUserPref(LOOP_FXA_TOKEN_PREF);
+
+  yield MozLoopService.initialize(mockPushHandler).then((msg) => {
+    Assert.equal(msg, "registration not needed", "Initialize should not register when the " +
+                                                 "URLs are expired and there are no auth tokens");
+  }, (error) => {
+    Assert.ok(false, error, "should have resolved the promise that initialize returned");
+  });
+});
+
+add_task(function test_initialize_with_urls_and_no_auth_token() {
+  Services.prefs.setIntPref(LOOP_URL_EXPIRY_PREF, Date.now() / 1000 + 10);
+  Services.prefs.clearUserPref(LOOP_FXA_TOKEN_PREF);
+
+  loopServer.registerPathHandler("/registration", (request, response) => {
+    response.setStatusLine(null, 200, "OK");
+  });
+
+  yield MozLoopService.initialize(mockPushHandler).then((msg) => {
+    Assert.equal(msg, "initialized to guest status", "Initialize should register as a " +
+                                                     "guest when no auth tokens but expired URLs");
+  }, (error) => {
+    Assert.ok(false, error, "should have resolved the promise that initialize returned");
+  });
+});
+
+add_task(function test_initialize_with_invalid_fxa_token() {
+  Services.prefs.setCharPref(LOOP_FXA_PROFILE_PREF, FAKE_FXA_PROFILE);
+  Services.prefs.setCharPref(LOOP_FXA_TOKEN_PREF, FAKE_FXA_TOKEN_DATA);
+
+  // Only need to implement the FxA registration because the previous
+  // test registered as a guest.
+  loopServer.registerPathHandler("/registration", (request, response) => {
+    response.setStatusLine(null, 401, "Unauthorized");
+    response.write(JSON.stringify({
+      code: 401,
+      errno: 110,
+      error: "Unauthorized",
+      message: "Unknown credentials",
+    }));
+  });
+
+  yield MozLoopService.initialize(mockPushHandler).then(() => {
+    Assert.ok(false, "Initializing with an invalid token should reject the promise");
+  },
+  (error) => {
+    let pushHandler = Cu.import("resource:///modules/loop/MozLoopService.jsm", {}).gPushHandler;
+    Assert.equal(pushHandler.pushUrl, kEndPointUrl, "Push URL should match");
+    Assert.equal(Services.prefs.getCharPref(LOOP_FXA_TOKEN_PREF), "",
+                 "FXA pref should be cleared if token was invalid");
+    Assert.equal(Services.prefs.getCharPref(LOOP_FXA_PROFILE_PREF), "",
+                 "FXA profile pref should be cleared if token was invalid");
+  });
+});
+
+add_task(function test_initialize_with_fxa_token() {
+  Services.prefs.setCharPref(LOOP_FXA_PROFILE_PREF, FAKE_FXA_PROFILE);
+  Services.prefs.setCharPref(LOOP_FXA_TOKEN_PREF, FAKE_FXA_TOKEN_DATA);
+  loopServer.registerPathHandler("/registration", (request, response) => {
+    response.setStatusLine(null, 200, "OK");
+  });
+
+  yield MozLoopService.initialize(mockPushHandler).then(() => {
+    Assert.equal(Services.prefs.getCharPref(LOOP_FXA_TOKEN_PREF), FAKE_FXA_TOKEN_DATA,
+                 "FXA pref should still be set after initialization");
+    Assert.equal(Services.prefs.getCharPref(LOOP_FXA_PROFILE_PREF), FAKE_FXA_PROFILE,
+                 "FXA profile should still be set after initialization");
+  });
+});
+
+function run_test() {
+  setupFakeLoopServer();
+  // Note, this is just used to speed up the test.
+  Services.prefs.setIntPref(LOOP_INITIAL_DELAY_PREF, 0);
+  mockPushHandler.pushUrl = kEndPointUrl;
+
+  do_register_cleanup(function() {
+    Services.prefs.clearUserPref(LOOP_INITIAL_DELAY_PREF);
+    Services.prefs.clearUserPref(LOOP_FXA_TOKEN_PREF);
+    Services.prefs.clearUserPref(LOOP_FXA_PROFILE_PREF);
+    Services.prefs.clearUserPref(LOOP_URL_EXPIRY_PREF);
+  });
+
+  run_next_test();
+};
--- a/browser/components/loop/test/xpcshell/test_loopservice_token_invalid.js
+++ b/browser/components/loop/test/xpcshell/test_loopservice_token_invalid.js
@@ -13,21 +13,18 @@ add_test(function test_registration_inva
     // Due to the way the time stamp checking code works in hawkclient, we expect a couple
     // of authorization requests before we reset the token.
     if (request.hasHeader("Authorization")) {
       authorizationAttempts++;
       response.setStatusLine(null, 401, "Unauthorized");
       response.write(JSON.stringify({
         code: 401,
         errno: 110,
-        error: {
-          error: "Unauthorized",
-          message: "Unknown credentials",
-          statusCode: 401
-        }
+        error: "Unauthorized",
+        message: "Unknown credentials",
       }));
     } else {
       // We didn't have an authorization header, so check the pref has been cleared.
       Assert.equal(Services.prefs.prefHasUserValue(LOOP_HAWK_PREF), false);
       response.setStatusLine(null, 200, "OK");
       response.setHeader("Hawk-Session-Token", fakeSessionToken2, false);
     }
     response.processAsync();
--- a/browser/components/loop/test/xpcshell/xpcshell.ini
+++ b/browser/components/loop/test/xpcshell/xpcshell.ini
@@ -10,13 +10,14 @@ skip-if = toolkit == 'gonk'
 [test_loopservice_dnd.js]
 [test_loopservice_expiry.js]
 [test_loopservice_hawk_errors.js]
 [test_loopservice_loop_prefs.js]
 [test_loopservice_initialize.js]
 [test_loopservice_locales.js]
 [test_loopservice_notification.js]
 [test_loopservice_registration.js]
+[test_loopservice_restart.js]
 [test_loopservice_token_invalid.js]
 [test_loopservice_token_save.js]
 [test_loopservice_token_send.js]
 [test_loopservice_token_validation.js]
 [test_loopservice_busy.js]
--- a/browser/components/loop/ui/fake-mozLoop.js
+++ b/browser/components/loop/ui/fake-mozLoop.js
@@ -4,17 +4,22 @@
 
 /**
  * Faking the mozLoop object which doesn't exist in regular web pages.
  * @type {Object}
  */
 navigator.mozLoop = {
   ensureRegistered: function() {},
   getLoopCharPref: function() {},
-  getLoopBoolPref: function() {},
+  getLoopBoolPref: function(pref) {
+    // Ensure UI for rooms is displayed in the showcase.
+    if (pref === "rooms.enabled") {
+      return true;
+    }
+  },
   releaseCallData: function() {},
   contacts: {
     getAll: function(callback) {
       callback(null, []);
     },
     on: function() {}
   }
 };
--- a/browser/components/loop/ui/index.html
+++ b/browser/components/loop/ui/index.html
@@ -33,17 +33,20 @@
     <script src="../content/shared/libs/backbone-1.1.2.js"></script>
     <script src="../content/shared/js/feedbackApiClient.js"></script>
     <script src="../content/shared/js/actions.js"></script>
     <script src="../content/shared/js/utils.js"></script>
     <script src="../content/shared/js/models.js"></script>
     <script src="../content/shared/js/mixins.js"></script>
     <script src="../content/shared/js/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/conversationStore.js"></script>
+    <script src="../content/shared/js/roomListStore.js"></script>
     <script src="../content/js/conversationViews.js"></script>
     <script src="../content/js/client.js"></script>
     <script src="../standalone/content/js/webapp.js"></script>
     <script type="text/javascript;version=1.8" src="../content/js/contacts.js"></script>
     <script>
       if (!loop.contacts) {
         // For browsers that don't support ES6 without special flags (all but Fx
         // at the moment), we shim the contacts namespace with its most barebone
--- a/browser/components/loop/ui/ui-showcase.css
+++ b/browser/components/loop/ui/ui-showcase.css
@@ -64,19 +64,26 @@
   margin: 1.5em 0;
 }
 
 .showcase > section .example > h3 {
   font-size: 1.2em;
   font-weight: bold;
   border-bottom: 1px dashed #aaa;
   margin: 1em 0;
+  margin-top: -14em;
+  padding-top: 14em;
   text-align: left;
 }
 
+.showcase > section .example > h3 a {
+  text-decoration: none;
+  color: #555;
+}
+
 .showcase p.note {
   margin: 0;
   padding: 0;
   color: #666;
   font-style: italic;
 }
 
 .override-position * {
--- a/browser/components/loop/ui/ui-showcase.js
+++ b/browser/components/loop/ui/ui-showcase.js
@@ -51,16 +51,22 @@
   // Feedback API client configured to send data to the stage input server,
   // 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 roomListStore = new loop.store.RoomListStore({
+    dispatcher: dispatcher,
+    mozLoop: {}
+  });
+
   // Local mocks
 
   var mockContact = {
     name: ["Mr Smith"],
     email: [{
       value: "smith@invalid.com"
     }]
   };
@@ -88,21 +94,28 @@
   errNotifications.add({
     level: "error",
     message: "Could Not Authenticate",
     details: "Did you change your password?",
     detailsButtonLabel: "Retry",
   });
 
   var Example = React.createClass({displayName: 'Example',
+    makeId: function(prefix) {
+      return (prefix || "") + this.props.summary.toLowerCase().replace(/\s/g, "-");
+    },
+
     render: function() {
       var cx = React.addons.classSet;
       return (
         React.DOM.div({className: "example"}, 
-          React.DOM.h3(null, this.props.summary), 
+          React.DOM.h3({id: this.makeId()}, 
+            this.props.summary, 
+            React.DOM.a({href: this.makeId("#")}, " ¶")
+          ), 
           React.DOM.div({className: cx({comp: true, dashed: this.props.dashed}), 
                style: this.props.style || {}}, 
             this.props.children
           )
         )
       );
     }
   });
@@ -145,36 +158,55 @@
       return (
         ShowCase(null, 
           Section({name: "PanelView"}, 
             React.DOM.p({className: "note"}, 
               React.DOM.strong(null, "Note:"), " 332px wide."
             ), 
             Example({summary: "Call URL retrieved", dashed: "true", style: {width: "332px"}}, 
               PanelView({client: mockClient, notifications: notifications, 
-                         callUrl: "http://invalid.example.url/"})
+                         callUrl: "http://invalid.example.url/", 
+                         dispatcher: dispatcher, 
+                         roomListStore: roomListStore})
             ), 
             Example({summary: "Call URL retrieved - authenticated", dashed: "true", style: {width: "332px"}}, 
               PanelView({client: mockClient, notifications: notifications, 
                          callUrl: "http://invalid.example.url/", 
-                         userProfile: {email: "test@example.com"}})
+                         userProfile: {email: "test@example.com"}, 
+                         dispatcher: dispatcher, 
+                         roomListStore: roomListStore})
             ), 
             Example({summary: "Pending call url retrieval", dashed: "true", style: {width: "332px"}}, 
-              PanelView({client: mockClient, notifications: notifications})
+              PanelView({client: mockClient, notifications: notifications, 
+                         dispatcher: dispatcher, 
+                         roomListStore: roomListStore})
             ), 
             Example({summary: "Pending call url retrieval - authenticated", dashed: "true", style: {width: "332px"}}, 
               PanelView({client: mockClient, notifications: notifications, 
-                         userProfile: {email: "test@example.com"}})
+                         userProfile: {email: "test@example.com"}, 
+                         dispatcher: dispatcher, 
+                         roomListStore: roomListStore})
             ), 
             Example({summary: "Error Notification", dashed: "true", style: {width: "332px"}}, 
-              PanelView({client: mockClient, notifications: errNotifications})
+              PanelView({client: mockClient, notifications: errNotifications, 
+                         dispatcher: dispatcher, 
+                         roomListStore: roomListStore})
             ), 
             Example({summary: "Error Notification - authenticated", dashed: "true", style: {width: "332px"}}, 
               PanelView({client: mockClient, notifications: errNotifications, 
-                         userProfile: {email: "test@example.com"}})
+                         userProfile: {email: "test@example.com"}, 
+                         dispatcher: dispatcher, 
+                         roomListStore: roomListStore})
+            ), 
+            Example({summary: "Room list tab", dashed: "true", style: {width: "332px"}}, 
+              PanelView({client: mockClient, notifications: notifications, 
+                         userProfile: {email: "test@example.com"}, 
+                         dispatcher: dispatcher, 
+                         roomListStore: roomListStore, 
+                         selectedTab: "rooms"})
             )
           ), 
 
           Section({name: "IncomingCallView"}, 
             Example({summary: "Default / incoming video call", dashed: "true", style: {width: "260px", height: "254px"}}, 
               React.DOM.div({className: "fx-embedded"}, 
                 IncomingCallView({model: mockConversationModel, 
                                   video: true})
@@ -242,41 +274,45 @@
                                      publishStream: noop})
               )
             )
           ), 
 
           Section({name: "PendingConversationView"}, 
             Example({summary: "Pending conversation view (connecting)", dashed: "true"}, 
               React.DOM.div({className: "standalone"}, 
-                PendingConversationView({websocket: mockWebSocket})
+                PendingConversationView({websocket: mockWebSocket, 
+                                         dispatcher: dispatcher})
               )
             ), 
             Example({summary: "Pending conversation view (ringing)", dashed: "true"}, 
               React.DOM.div({className: "standalone"}, 
-                PendingConversationView({websocket: mockWebSocket, callState: "ringing"})
+                PendingConversationView({websocket: mockWebSocket, 
+                                         dispatcher: dispatcher, 
+                                         callState: "ringing"})
               )
             )
           ), 
 
           Section({name: "PendingConversationView (Desktop)"}, 
             Example({summary: "Connecting", dashed: "true", 
                      style: {width: "260px", height: "265px"}}, 
               React.DOM.div({className: "fx-embedded"}, 
                 DesktopPendingConversationView({callState: "gather", 
-                                                contact: mockContact})
+                                                contact: mockContact, 
+                                                dispatcher: dispatcher})
               )
             )
           ), 
 
           Section({name: "CallFailedView"}, 
             Example({summary: "Call Failed", dashed: "true", 
                      style: {width: "260px", height: "265px"}}, 
               React.DOM.div({className: "fx-embedded"}, 
-                CallFailedView(null)
+                CallFailedView({dispatcher: dispatcher})
               )
             )
           ), 
 
           Section({name: "StartConversationView"}, 
             Example({summary: "Start conversation view", dashed: "true"}, 
               React.DOM.div({className: "standalone"}, 
                 StartConversationView({conversation: mockConversationModel, 
--- a/browser/components/loop/ui/ui-showcase.jsx
+++ b/browser/components/loop/ui/ui-showcase.jsx
@@ -51,16 +51,22 @@
   // Feedback API client configured to send data to the stage input server,
   // 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 roomListStore = new loop.store.RoomListStore({
+    dispatcher: dispatcher,
+    mozLoop: {}
+  });
+
   // Local mocks
 
   var mockContact = {
     name: ["Mr Smith"],
     email: [{
       value: "smith@invalid.com"
     }]
   };
@@ -88,21 +94,28 @@
   errNotifications.add({
     level: "error",
     message: "Could Not Authenticate",
     details: "Did you change your password?",
     detailsButtonLabel: "Retry",
   });
 
   var Example = React.createClass({
+    makeId: function(prefix) {
+      return (prefix || "") + this.props.summary.toLowerCase().replace(/\s/g, "-");
+    },
+
     render: function() {
       var cx = React.addons.classSet;
       return (
         <div className="example">
-          <h3>{this.props.summary}</h3>
+          <h3 id={this.makeId()}>
+            {this.props.summary}
+            <a href={this.makeId("#")}>&nbsp;¶</a>
+          </h3>
           <div className={cx({comp: true, dashed: this.props.dashed})}
                style={this.props.style || {}}>
             {this.props.children}
           </div>
         </div>
       );
     }
   });
@@ -145,36 +158,55 @@
       return (
         <ShowCase>
           <Section name="PanelView">
             <p className="note">
               <strong>Note:</strong> 332px wide.
             </p>
             <Example summary="Call URL retrieved" dashed="true" style={{width: "332px"}}>
               <PanelView client={mockClient} notifications={notifications}
-                         callUrl="http://invalid.example.url/" />
+                         callUrl="http://invalid.example.url/"
+                         dispatcher={dispatcher}
+                         roomListStore={roomListStore} />
             </Example>
             <Example summary="Call URL retrieved - authenticated" dashed="true" style={{width: "332px"}}>
               <PanelView client={mockClient} notifications={notifications}
                          callUrl="http://invalid.example.url/"
-                         userProfile={{email: "test@example.com"}} />
+                         userProfile={{email: "test@example.com"}}
+                         dispatcher={dispatcher}
+                         roomListStore={roomListStore} />
             </Example>
             <Example summary="Pending call url retrieval" dashed="true" style={{width: "332px"}}>
-              <PanelView client={mockClient} notifications={notifications} />
+              <PanelView client={mockClient} notifications={notifications}
+                         dispatcher={dispatcher}
+                         roomListStore={roomListStore} />
             </Example>
             <Example summary="Pending call url retrieval - authenticated" dashed="true" style={{width: "332px"}}>
               <PanelView client={mockClient} notifications={notifications}
-                         userProfile={{email: "test@example.com"}} />
+                         userProfile={{email: "test@example.com"}}
+                         dispatcher={dispatcher}
+                         roomListStore={roomListStore} />
             </Example>
             <Example summary="Error Notification" dashed="true" style={{width: "332px"}}>
-              <PanelView client={mockClient} notifications={errNotifications}/>
+              <PanelView client={mockClient} notifications={errNotifications}
+                         dispatcher={dispatcher}
+                         roomListStore={roomListStore} />
             </Example>
             <Example summary="Error Notification - authenticated" dashed="true" style={{width: "332px"}}>
               <PanelView client={mockClient} notifications={errNotifications}
-                         userProfile={{email: "test@example.com"}} />
+                         userProfile={{email: "test@example.com"}}
+                         dispatcher={dispatcher}
+                         roomListStore={roomListStore} />
+            </Example>
+            <Example summary="Room list tab" dashed="true" style={{width: "332px"}}>
+              <PanelView client={mockClient} notifications={notifications}
+                         userProfile={{email: "test@example.com"}}
+                         dispatcher={dispatcher}
+                         roomListStore={roomListStore}
+                         selectedTab="rooms" />
             </Example>
           </Section>
 
           <Section name="IncomingCallView">
             <Example summary="Default / incoming video call" dashed="true" style={{width: "260px", height: "254px"}}>
               <div className="fx-embedded">
                 <IncomingCallView model={mockConversationModel}
                                   video={true} />
@@ -242,41 +274,45 @@
                                      publishStream={noop} />
               </Example>
             </div>
           </Section>
 
           <Section name="PendingConversationView">
             <Example summary="Pending conversation view (connecting)" dashed="true">
               <div className="standalone">
-                <PendingConversationView websocket={mockWebSocket}/>
+                <PendingConversationView websocket={mockWebSocket}
+                                         dispatcher={dispatcher} />
               </div>
             </Example>
             <Example summary="Pending conversation view (ringing)" dashed="true">
               <div className="standalone">
-                <PendingConversationView websocket={mockWebSocket} callState="ringing"/>
+                <PendingConversationView websocket={mockWebSocket}
+                                         dispatcher={dispatcher}
+                                         callState="ringing"/>
               </div>
             </Example>
           </Section>
 
           <Section name="PendingConversationView (Desktop)">
             <Example summary="Connecting" dashed="true"
                      style={{width: "260px", height: "265px"}}>
               <div className="fx-embedded">
                 <DesktopPendingConversationView callState={"gather"}
-                                                contact={mockContact} />
+                                                contact={mockContact}
+                                                dispatcher={dispatcher} />
               </div>
             </Example>
           </Section>
 
           <Section name="CallFailedView">
             <Example summary="Call Failed" dashed="true"
                      style={{width: "260px", height: "265px"}}>
               <div className="fx-embedded">
-                <CallFailedView />
+                <CallFailedView dispatcher={dispatcher} />
               </div>
             </Example>
           </Section>
 
           <Section name="StartConversationView">
             <Example summary="Start conversation view" dashed="true">
               <div className="standalone">
                 <StartConversationView conversation={mockConversationModel}
--- a/browser/components/nsBrowserGlue.js
+++ b/browser/components/nsBrowserGlue.js
@@ -102,21 +102,16 @@ XPCOMUtils.defineLazyModuleGetter(this, 
 #ifdef NIGHTLY_BUILD
 XPCOMUtils.defineLazyModuleGetter(this, "SignInToWebsiteUX",
                                   "resource:///modules/SignInToWebsite.jsm");
 #endif
 
 XPCOMUtils.defineLazyModuleGetter(this, "ContentSearch",
                                   "resource:///modules/ContentSearch.jsm");
 
-#ifdef E10S_TESTING_ONLY
-XPCOMUtils.defineLazyModuleGetter(this, "UpdateChannel",
-                                  "resource://gre/modules/UpdateChannel.jsm");
-#endif
-
 XPCOMUtils.defineLazyGetter(this, "ShellService", function() {
   try {
     return Cc["@mozilla.org/browser/shell-service;1"].
            getService(Ci.nsIShellService);
   }
   catch(ex) {
     return null;
   }
@@ -743,17 +738,20 @@ BrowserGlue.prototype = {
       SignInToWebsiteUX.uninit();
     }
 #endif
     webrtcUI.uninit();
     FormValidationHandler.uninit();
 
     // XXX: Temporary hack to allow Loop FxA login after a restart to work.
     // Remove this once bug 1071247 is deployed.
-    Services.prefs.clearUserPref("loop.hawk-session-token.fxa");
+    if (Services.prefs.getPrefType("loop.autologin-after-restart") != Ci.nsIPrefBranch.PREF_BOOL ||
+        !Services.prefs.getBoolPref("loop.autologin-after-restart")) {
+      Services.prefs.clearUserPref("loop.hawk-session-token.fxa");
+    }
   },
 
   // All initial windows have opened.
   _onWindowsRestored: function BG__onWindowsRestored() {
     // Show update notification, if needed.
     if (Services.prefs.prefHasUserValue("app.update.postupdate"))
       this._showUpdateNotification();
 
@@ -2265,25 +2263,22 @@ let DefaultBrowserCheck = {
     }
   },
 };
 
 #ifdef E10S_TESTING_ONLY
 let E10SUINotification = {
   // Increase this number each time we want to roll out an
   // e10s testing period to Nightly users.
-  CURRENT_NOTICE_COUNT: 1,
-  CURRENT_PROMPT_PREF: "browser.displayedE10SPrompt.1",
-  PREVIOUS_PROMPT_PREF: "browser.displayedE10SPrompt",
+  CURRENT_NOTICE_COUNT: 0,
 
   checkStatus: function() {
     let skipE10sChecks = false;
     try {
-      skipE10sChecks = (UpdateChannel.get() != "nightly") ||
-                       Services.prefs.getBoolPref("browser.tabs.remote.autostart.disabled-because-using-a11y");
+      skipE10sChecks = Services.prefs.getBoolPref("browser.tabs.remote.autostart.disabled-because-using-a11y");
     } catch(e) {}
 
     if (skipE10sChecks) {
       return;
     }
 
     if (Services.appinfo.browserTabsRemoteAutostart) {
       let notice = 0;
@@ -2319,36 +2314,27 @@ let E10SUINotification = {
         url += "?utm_source=tab&utm_campaign=e10sfeedback";
 
         win.openUILinkIn(url, "tab");
         return;
       }
 
       let e10sPromptShownCount = 0;
       try {
-        e10sPromptShownCount = Services.prefs.getIntPref(this.CURRENT_PROMPT_PREF);
+        e10sPromptShownCount = Services.prefs.getIntPref("browser.displayedE10SPrompt");
       } catch(e) {}
 
-      let isHardwareAccelerated = true;
-      try {
-        let win = RecentWindow.getMostRecentBrowserWindow();
-        let winutils = win.QueryInterface(Ci.nsIInterfaceRequestor).getInterface(Ci.nsIDOMWindowUtils);
-        isHardwareAccelerated = winutils.layerManagerType != "Basic";
-      } catch (e) {}
-
       if (!Services.appinfo.inSafeMode &&
           !Services.appinfo.accessibilityEnabled &&
           !Services.appinfo.keyboardMayHaveIME &&
-          isHardwareAccelerated &&
           e10sPromptShownCount < 5) {
         Services.tm.mainThread.dispatch(() => {
           try {
             this._showE10SPrompt();
-            Services.prefs.setIntPref(this.CURRENT_PROMPT_PREF, e10sPromptShownCount + 1);
-            Services.prefs.clearUserPref(this.PREVIOUS_PROMPT_PREF);
+            Services.prefs.setIntPref("browser.displayedE10SPrompt", e10sPromptShownCount + 1);
           } catch (ex) {
             Cu.reportError("Failed to show e10s prompt: " + ex);
           }
         }, Ci.nsIThread.DISPATCH_NORMAL);
       }
     }
   },
 
@@ -2385,17 +2371,17 @@ let E10SUINotification = {
 
   _showE10SPrompt: function BG__showE10SPrompt() {
     let win = RecentWindow.getMostRecentBrowserWindow();
     if (!win)
       return;
 
     let browser = win.gBrowser.selectedBrowser;
 
-    let promptMessage = "Would you like to help us test multiprocess Nightly (e10s)? You can also enable e10s in Nightly preferences. Notable fixes:";
+    let promptMessage = "Would you like to help us test multiprocess Nightly (e10s)? You can also enable e10s in Nightly preferences.";
     let mainAction = {
       label: "Enable and Restart",
       accessKey: "E",
       callback: function () {
         Services.prefs.setBoolPref("browser.tabs.remote.autostart", true);
         Services.prefs.setBoolPref("browser.enabledE10SFromPrompt", true);
         // Restart the app
         let cancelQuit = Cc["@mozilla.org/supports-PRBool;1"].createInstance(Ci.nsISupportsPRBool);
@@ -2405,41 +2391,27 @@ let E10SUINotification = {
         Services.startup.quit(Services.startup.eAttemptQuit | Services.startup.eRestart);
       }
     };
     let secondaryActions = [
       {
         label: "No thanks",
         accessKey: "N",
         callback: function () {
-          Services.prefs.setIntPref(E10SUINotification.CURRENT_PROMPT_PREF, 5);
+          Services.prefs.setIntPref("browser.displayedE10SPrompt", 5);
         }
       }
     ];
     let options = {
       popupIconURL: "chrome://browser/skin/e10s-64@2x.png",
       learnMoreURL: "https://wiki.mozilla.org/Electrolysis",
       persistWhileVisible: true
     };
 
-    win.PopupNotifications.show(browser, "enable-e10s", promptMessage, null, mainAction, secondaryActions, options);
-
-    let highlights = [
-      "Less crashing!",
-      "Improved add-on compatibility and DevTools",
-      "PDF.js, Web Console, Spellchecking, WebRTC now work"
-    ];
-
-    let doorhangerExtraContent = win.document.getElementById("enable-e10s-notification")
-                                             .querySelector("popupnotificationcontent");
-    for (let highlight of highlights) {
-      let highlightLabel = win.document.createElement("label");
-      highlightLabel.setAttribute("value", highlight);
-      doorhangerExtraContent.appendChild(highlightLabel);
-    }
+    win.PopupNotifications.show(browser, "enable_e10s", promptMessage, null, mainAction, secondaryActions, options);
   },
 
   _warnedAboutAccessibility: false,
 
   _showE10sAccessibilityWarning: function() {
     Services.prefs.setBoolPref("browser.tabs.remote.autostart.disabled-because-using-a11y", true);
 
     if (this._warnedAboutAccessibility) {
--- a/browser/devtools/framework/gDevTools.jsm
+++ b/browser/devtools/framework/gDevTools.jsm
@@ -401,16 +401,20 @@ DevTools.prototype = {
       });
     }
     else {
       // No toolbox for target, create one
       toolbox = new devtools.Toolbox(target, toolId, hostType, hostOptions);
 
       this._toolboxes.set(target, toolbox);
 
+      toolbox.once("destroy", () => {
+        this.emit("toolbox-destroy", target);
+      });
+
       toolbox.once("destroyed", () => {
         this._toolboxes.delete(target);
         this.emit("toolbox-destroyed", target);
       });
 
       // If toolId was passed in, it will already be selected before the
       // open promise resolves.
       toolbox.open().then(() => {
@@ -424,28 +428,28 @@ DevTools.prototype = {
 
   /**
    * Return the toolbox for a given target.
    *
    * @param  {object} target
    *         Target value e.g. the target that owns this toolbox
    *
    * @return {Toolbox} toolbox
-   *         The toobox that is debugging the given target
+   *         The toolbox that is debugging the given target
    */
   getToolbox: function DT_getToolbox(target) {
     return this._toolboxes.get(target);
   },
 
   /**
    * Close the toolbox for a given target
    *
    * @return promise
    *         This promise will resolve to false if no toolbox was found
-   *         associated to the target. true, if the toolbox was successfuly
+   *         associated to the target. true, if the toolbox was successfully
    *         closed.
    */
   closeToolbox: function DT_closeToolbox(target) {
     let toolbox = this._toolboxes.get(target);
     if (toolbox == null) {
       return promise.resolve(false);
     }
     return toolbox.destroy().then(() => true);
@@ -601,21 +605,21 @@ let gDevToolsBrowser = {
 
   /**
    * This function is for the benefit of Tools:{toolId} commands,
    * triggered from the WebDeveloper menu and keyboard shortcuts.
    *
    * selectToolCommand's behavior:
    * - if the toolbox is closed,
    *   we open the toolbox and select the tool
-   * - if the toolbox is open, and the targetted tool is not selected,
+   * - if the toolbox is open, and the targeted tool is not selected,
    *   we select it
-   * - if the toolbox is open, and the targetted tool is selected,
+   * - if the toolbox is open, and the targeted tool is selected,
    *   and the host is NOT a window, we close the toolbox
-   * - if the toolbox is open, and the targetted tool is selected,
+   * - if the toolbox is open, and the targeted tool is selected,
    *   and the host is a window, we raise the toolbox window
    */
   selectToolCommand: function(gBrowser, toolId) {
     let target = devtools.TargetFactory.forTab(gBrowser.selectedTab);
     let toolbox = gDevTools.getToolbox(target);
     let toolDefinition = gDevTools.getToolDefinition(toolId);
 
     if (toolbox &&
--- a/browser/devtools/framework/test/browser.ini
+++ b/browser/devtools/framework/test/browser.ini
@@ -3,16 +3,17 @@ subsuite = devtools
 support-files =
   browser_toolbox_options_disable_js.html
   browser_toolbox_options_disable_js_iframe.html
   browser_toolbox_options_disable_cache.sjs
   head.js
   doc_theme.css
 
 [browser_devtools_api.js]
+[browser_devtools_api_destroy.js]
 skip-if = e10s # Bug 1070837 - devtools/framework/toolbox.js |doc| getter not e10s friendly
 [browser_dynamic_tool_enabling.js]
 [browser_keybindings.js]
 [browser_new_activation_workflow.js]
 [browser_target_events.js]
 [browser_target_remote.js]
 [browser_two_tabs.js]
 [browser_toolbox_dynamic_registration.js]
new file mode 100644
--- /dev/null
+++ b/browser/devtools/framework/test/browser_devtools_api_destroy.js
@@ -0,0 +1,71 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// Tests devtools API
+
+const Cu = Components.utils;
+
+function test() {
+  addTab("about:blank").then(runTests);
+}
+
+function runTests(aTab) {
+  let toolDefinition = {
+    id: "testTool",
+    visibilityswitch: "devtools.testTool.enabled",
+    isTargetSupported: function() true,
+    url: "about:blank",
+    label: "someLabel",
+    build: function(iframeWindow, toolbox) {
+      let deferred = promise.defer();
+      executeSoon(() => {
+        deferred.resolve({
+          target: toolbox.target,
+          toolbox: toolbox,
+          isReady: true,
+          destroy: function(){},
+        });
+      });
+      return deferred.promise;
+    },
+  };
+
+  gDevTools.registerTool(toolDefinition);
+
+  let collectedEvents = [];
+
+  let target = TargetFactory.forTab(aTab);
+  gDevTools.showToolbox(target, toolDefinition.id).then(function(toolbox) {
+    let panel = toolbox.getPanel(toolDefinition.id);
+    ok(panel, "Tool open");
+
+    gDevTools.once("toolbox-destroy", (event, toolbox, iframe) => {
+      collectedEvents.push(event);
+    });
+
+    gDevTools.once(toolDefinition.id + "-destroy", (event, toolbox, iframe) => {
+      collectedEvents.push("gDevTools-" + event);
+    });
+
+    toolbox.once("destroy", (event) => {
+      collectedEvents.push(event);
+    });
+
+    toolbox.once(toolDefinition.id + "-destroy", (event) => {
+      collectedEvents.push("toolbox-" + event);
+    });
+
+    toolbox.destroy().then(function() {
+      is(collectedEvents.join(":"),
+        "toolbox-destroy:destroy:gDevTools-testTool-destroy:toolbox-testTool-destroy",
+        "Found the right amount of collected events.");
+
+      gDevTools.unregisterTool(toolDefinition.id);
+      gBrowser.removeCurrentTab();
+
+      executeSoon(function() {
+        finish();
+      });
+    });
+  });
+}
--- a/browser/devtools/framework/toolbox.js
+++ b/browser/devtools/framework/toolbox.js
@@ -1511,16 +1511,18 @@ Toolbox.prototype = {
    */
   destroy: function() {
     // If several things call destroy then we give them all the same
     // destruction promise so we're sure to destroy only once
     if (this._destroyer) {
       return this._destroyer;
     }
 
+    this.emit("destroy");
+
     this._target.off("navigate", this._refreshHostTitle);
     this._target.off("frame-update", this._updateFrames);
     this.off("select", this._refreshHostTitle);
     this.off("host-changed", this._refreshHostTitle);
 
     gDevTools.off("tool-registered", this._toolRegistered);
     gDevTools.off("tool-unregistered", this._toolUnregistered);
 
@@ -1532,16 +1534,19 @@ Toolbox.prototype = {
       this.webconsolePanel.removeEventListener("resize",
         this._saveSplitConsoleHeight);
     }
     this.closeButton.removeEventListener("command", this.destroy, true);
 
     let outstanding = [];
     for (let [id, panel] of this._toolPanels) {
       try {
+        gDevTools.emit(id + "-destroy", this, panel);
+        this.emit(id + "-destroy", panel);
+
         outstanding.push(panel.destroy());
       } catch (e) {
         // We don't want to stop here if any panel fail to close.
         console.error("Panel " + id + ":", e);
       }
     }
 
     // Now that we are closing the toolbox we can re-enable JavaScript for the
--- a/browser/devtools/inspector/test/browser_inspector_menu.js
+++ b/browser/devtools/inspector/test/browser_inspector_menu.js
@@ -180,29 +180,32 @@ let test = asyncTest(function* () {
     info("Testing that 'Paste Outer HTML' menu item works.");
     clipboard.set("this was pasted");
 
     let node = getNode("h1");
     yield selectNode(node, inspector);
 
     contextMenuClick(getContainerForRawNode(inspector.markup, node).tagLine);
 
+    let onNodeReselected = inspector.markup.once("reselectedonremoved");
     let menu = inspector.panelDoc.getElementById("node-menu-pasteouterhtml");
     dispatchCommandEvent(menu);
 
     info("Waiting for inspector selection to update");
-    yield inspector.selection.once("new-node");
+    yield onNodeReselected;
 
     ok(content.document.body.outerHTML.contains(clipboard.get()),
        "Clipboard content was pasted into the node's outer HTML.");
     ok(!getNode("h1", { expectNoMatch: true }), "The original node was removed.");
   }
 
   function* testDeleteNode() {
     info("Testing 'Delete Node' menu item for normal elements.");
+
+    yield selectNode("p", inspector);
     let deleteNode = inspector.panelDoc.getElementById("node-menu-delete");
     ok(deleteNode, "the popup menu has a delete menu item");
 
     let updated = inspector.once("inspector-updated");
 
     info("Triggering 'Delete Node' and waiting for inspector to update");
     dispatchCommandEvent(deleteNode);
     yield updated;
--- a/browser/devtools/markupview/markup-view.js
+++ b/browser/devtools/markupview/markup-view.js
@@ -615,18 +615,16 @@ MarkupView.prototype = {
     return container;
   },
 
   /**
    * Mutation observer used for included nodes.
    */
   _mutationObserver: function(aMutations) {
     let requiresLayoutChange = false;
-    let reselectParent;
-    let reselectChildIndex;
 
     for (let mutation of aMutations) {
       let type = mutation.type;
       let target = mutation.target;
 
       if (mutation.type === "documentUnload") {
         // Treat this as a childList change of the child (maybe the protocol
         // should do this).
@@ -646,61 +644,32 @@ MarkupView.prototype = {
       if (type === "attributes" || type === "characterData") {
         container.update();
 
         // Auto refresh style properties on selected node when they change.
         if (type === "attributes" && container.selected) {
           requiresLayoutChange = true;
         }
       } else if (type === "childList") {
-        let isFromOuterHTML = mutation.removed.some((n) => {
-          return n === this._outerHTMLNode;
-        });
-
-        // Keep track of which node should be reselected after mutations.
-        if (isFromOuterHTML) {
-          reselectParent = target;
-          reselectChildIndex = this._outerHTMLChildIndex;
-
-          delete this._outerHTMLNode;
-          delete this._outerHTMLChildIndex;
-        }
-
         container.childrenDirty = true;
         // Update the children to take care of changes in the markup view DOM.
-        this._updateChildren(container, {flash: !isFromOuterHTML});
+        this._updateChildren(container, {flash: true});
       }
     }
 
     if (requiresLayoutChange) {
       this._inspector.immediateLayoutChange();
     }
     this._waitForChildren().then((nodes) => {
       this._flashMutatedNodes(aMutations);
       this._inspector.emit("markupmutation", aMutations);
 
       // Since the htmlEditor is absolutely positioned, a mutation may change
       // the location in which it should be shown.
       this.htmlEditor.refresh();
-
-      // If a node has had its outerHTML set, the parent node will be selected.
-      // Reselect the original node immediately.
-      if (this._inspector.selection.nodeFront === reselectParent) {
-        this.walker.children(reselectParent).then((o) => {
-          let node = o.nodes[reselectChildIndex];
-          let container = this.getContainer(node);
-          if (node && container) {
-            this.markNodeAsSelected(node, "outerhtml");
-            if (container.hasChildren) {
-              this.expandNode(node);
-            }
-          }
-        });
-
-      }
     });
   },
 
   /**
    * React to display-change events from the walker
    * @param {Array} nodes An array of nodeFronts
    */
   _onDisplayChange: function(nodes) {
@@ -842,66 +811,102 @@ MarkupView.prototype = {
         longstr.release().then(null, console.error);
         def.resolve(outerHTML);
       });
     });
     return def.promise;
   },
 
   /**
-   * Retrieve the index of a child within its parent's children list.
-   * @param aNode The NodeFront to find the index of.
-   * @returns A promise that will be resolved with the integer index.
-   *          If the child cannot be found, returns -1
+   * Listen to mutations, expect a given node to be removed and try and select
+   * the node that sits at the same place instead.
+   * This is useful when changing the outerHTML or the tag name so that the
+   * newly inserted node gets selected instead of the one that just got removed.
    */
-  getNodeChildIndex: function(aNode) {
-    let def = promise.defer();
-    let parentNode = aNode.parentNode();
+  reselectOnRemoved: function(removedNode, reason) {
+    // Only allow one removed node reselection at a time, so that when there are
+    // more than 1 request in parallel, the last one wins.
+    this.cancelReselectOnRemoved();
+
+    // Get the removedNode index in its parent node to reselect the right node.
+    let isHTMLTag = removedNode.tagName.toLowerCase() === "html";
+    let oldContainer = this.getContainer(removedNode);
+    let parentContainer = this.getContainer(removedNode.parentNode());
+    let childIndex = parentContainer.getChildContainers().indexOf(oldContainer);
+
+    let onMutations = this._removedNodeObserver = (e, mutations) => {
+      let isNodeRemovalMutation = false;
+      for (let mutation of mutations) {
+        let containsRemovedNode = mutation.removed &&
+                                  mutation.removed.some(n => n === removedNode);
+        if (mutation.type === "childList" && (containsRemovedNode || isHTMLTag)) {
+          isNodeRemovalMutation = true;
+          break;
+        }
+      }
+      if (!isNodeRemovalMutation) {
+        return;
+      }
 
-    // Node may have been removed from the DOM, instead of throwing an error,
-    // return -1 indicating that it isn't inside of its parent children list.
-    if (!parentNode) {
-      def.resolve(-1);
-    } else {
-      this.walker.children(parentNode).then(children => {
-        def.resolve(children.nodes.indexOf(aNode));
-      });
+      this._inspector.off("markupmutation", onMutations);
+      this._removedNodeObserver = null;
+
+      // Don't select the new node if the user has already changed the current
+      // selection.
+      if (this._inspector.selection.nodeFront === parentContainer.node ||
+          (this._inspector.selection.nodeFront === removedNode && isHTMLTag)) {
+        let childContainers = parentContainer.getChildContainers();
+        if (childContainers && childContainers[childIndex]) {
+          this.markNodeAsSelected(childContainers[childIndex].node, reason);
+          if (childContainers[childIndex].hasChildren) {
+            this.expandNode(childContainers[childIndex].node);
+          }
+          this.emit("reselectedonremoved");
+        }
+      }
+    };
+
+    // Start listening for mutations until we find a childList change that has
+    // removedNode removed.
+    this._inspector.on("markupmutation", onMutations);
+  },
+
+  /**
+   * Make sure to stop listening for node removal markupmutations and not
+   * reselect the corresponding node when that happens.
+   * Useful when the outerHTML/tagname edition failed.
+   */
+  cancelReselectOnRemoved: function() {
+    if (this._removedNodeObserver) {
+      this._inspector.off("markupmutation", this._removedNodeObserver);
+      this._removedNodeObserver = null;
+      this.emit("canceledreselectonremoved");
     }
-
-    return def.promise;
   },
 
   /**
    * Replace the outerHTML of any node displayed in the inspector with
    * some other HTML code
    * @param aNode node which outerHTML will be replaced.
    * @param newValue The new outerHTML to set on the node.
    * @param oldValue The old outerHTML that will be used if the user undos the update.
    * @returns A promise that will resolve when the outer HTML has been updated.
    */
   updateNodeOuterHTML: function(aNode, newValue, oldValue) {
     let container = this._containers.get(aNode);
     if (!container) {
       return promise.reject();
     }
 
-    let def = promise.defer();
-
-    this.getNodeChildIndex(aNode).then((i) => {
-      this._outerHTMLChildIndex = i;
-      this._outerHTMLNode = aNode;
-
-      container.undo.do(() => {
-        this.walker.setOuterHTML(aNode, newValue).then(def.resolve, def.reject);
-      }, () => {
-        this.walker.setOuterHTML(aNode, oldValue).then(def.resolve, def.reject);
-      });
+    // Changing the outerHTML removes the node which outerHTML was changed.
+    // Listen to this removal to reselect the right node afterwards.
+    this.reselectOnRemoved(aNode, "outerhtml");
+    return this.walker.setOuterHTML(aNode, newValue).then(null, () => {
+      this.cancelReselectOnRemoved();
     });
-
-    return def.promise;
   },
 
   /**
    * Open an editor in the UI to allow editing of a node's outerHTML.
    * @param aNode The NodeFront to edit.
    */
   beginEditingOuterHTML: function(aNode) {
     this.getNodeOuterHTML(aNode).then((oldValue)=> {
@@ -1422,16 +1427,28 @@ MarkupContainer.prototype = {
     if (aValue) {
       this.expander.style.visibility = "visible";
     } else {
       this.expander.style.visibility = "hidden";
     }
   },
 
   /**
+   * If the node has children, return the list of containers for all these
+   * children.
+   */
+  getChildContainers: function() {
+    if (!this.hasChildren) {
+      return null;
+    }
+
+    return [...this.children.children].map(node => node.container);
+  },
+
+  /**
    * True if the node has been visually expanded in the tree.
    */
   get expanded() {
     return !this.elt.classList.contains("collapsed");
   },
 
   set expanded(aValue) {
     if (!this.expander) {
@@ -1823,17 +1840,25 @@ function RootContainer(aMarkupView, aNod
   this.node = aNode;
   this.toString = () => "[root container]";
 }
 
 RootContainer.prototype = {
   hasChildren: true,
   expanded: true,
   update: function() {},
-  destroy: function() {}
+  destroy: function() {},
+
+  /**
+   * If the node has children, return the list of containers for all these
+   * children.
+   */
+  getChildContainers: function() {
+    return [...this.children.children].map(node => node.container);
+  }
 };
 
 /**
  * Creates an editor for non-editable nodes.
  */
 function GenericEditor(aContainer, aNode) {
   this.container = aContainer;
   this.markup = this.container.markup;
@@ -1964,23 +1989,19 @@ function ElementEditor(aContainer, aNode
   this.closeTag = null;
   this.attrList = null;
   this.newAttr = null;
   this.closeElt = null;
 
   // Create the main editor
   this.template("element", this);
 
-  if (aNode.isLocal_toBeDeprecated()) {
-    this.rawNode = aNode.rawNode();
-  }
-
   // Make the tag name editable (unless this is a remote node or
   // a document element)
-  if (this.rawNode && !aNode.isDocumentElement) {
+  if (!aNode.isDocumentElement) {
     this.tag.setAttribute("tabindex", "0");
     editableField({
       element: this.tag,
       trigger: "dblclick",
       stopOnReturn: true,
       done: this.onTagEdit.bind(this),
     });
   }
@@ -2202,67 +2223,29 @@ ElementEditor.prototype = {
     } else {
       aUndoMods.removeAttribute(aName);
     }
   },
 
   /**
    * Called when the tag name editor has is done editing.
    */
-  onTagEdit: function(aVal, aCommit) {
-    if (!aCommit || aVal == this.rawNode.tagName) {
-      return;
-    }
-
-    // Create a new element with the same attributes as the
-    // current element and prepare to replace the current node
-    // with it.
-    try {
-      var newElt = nodeDocument(this.rawNode).createElement(aVal);
-    } catch(x) {
-      // Failed to create a new element with that tag name, ignore
-      // the change.
+  onTagEdit: function(newTagName, isCommit) {
+    if (!isCommit || newTagName == this.node.tagName ||
+        !("editTagName" in this.markup.walker)) {
       return;
     }
 
-    let attrs = this.rawNode.attributes;
-
-    for (let i = 0 ; i < attrs.length; i++) {
-      newElt.setAttribute(attrs[i].name, attrs[i].value);
-    }
-    let newFront = this.markup.walker.frontForRawNode(newElt);
-    let newContainer = this.markup.importNode(newFront);
-
-    // Retain the two nodes we care about here so we can undo.
-    let walker = this.markup.walker;
-    promise.all([
-      walker.retainNode(newFront), walker.retainNode(this.node)
-    ]).then(() => {
-      function swapNodes(aOld, aNew) {
-        aOld.parentNode.insertBefore(aNew, aOld);
-        while (aOld.firstChild) {
-          aNew.appendChild(aOld.firstChild);
-        }
-        aOld.parentNode.removeChild(aOld);
-      }
-
-      this.container.undo.do(() => {
-        swapNodes(this.rawNode, newElt);
-        this.markup.setNodeExpanded(newFront, this.container.expanded);
-        if (this.container.selected) {
-          this.markup.navigate(newContainer);
-        }
-      }, () => {
-        swapNodes(newElt, this.rawNode);
-        this.markup.setNodeExpanded(this.node, newContainer.expanded);
-        if (newContainer.selected) {
-          this.markup.navigate(this.container);
-        }
-      });
-    }).then(null, console.error);
+    // Changing the tagName removes the node. Make sure the replacing node gets
+    // selected afterwards.
+    this.markup.reselectOnRemoved(this.node, "edittagname");
+    this.markup.walker.editTagName(this.node, newTagName).then(null, () => {
+      // Failed to edit the tag name, cancel the reselection.
+      this.markup.cancelReselectOnRemoved();
+    });
   },
 
   destroy: function() {}
 };
 
 function nodeDocument(node) {
   return node.ownerDocument ||
     (node.nodeType == Ci.nsIDOMNode.DOCUMENT_NODE ? node : null);
--- a/browser/devtools/markupview/test/browser.ini
+++ b/browser/devtools/markupview/test/browser.ini
@@ -75,19 +75,19 @@ skip-if = e10s # Bug 1040751 - CodeMirro
 [browser_markupview_node_not_displayed_02.js]
 [browser_markupview_pagesize_01.js]
 [browser_markupview_pagesize_02.js]
 skip-if = e10s # Bug 1036409 - The last selected node isn't reselected
 [browser_markupview_search_01.js]
 [browser_markupview_tag_edit_01.js]
 [browser_markupview_tag_edit_02.js]
 [browser_markupview_tag_edit_03.js]
-skip-if = e10s # Bug 1036421 - Tag editing isn't remote-safe
 [browser_markupview_tag_edit_04.js]
 [browser_markupview_tag_edit_05.js]
 [browser_markupview_tag_edit_06.js]
 [browser_markupview_tag_edit_07.js]
 [browser_markupview_tag_edit_08.js]
 [browser_markupview_tag_edit_09.js]
+[browser_markupview_tag_edit_10.js]
 [browser_markupview_textcontent_edit_01.js]
 [browser_markupview_toggle_01.js]
 [browser_markupview_toggle_02.js]
 [browser_markupview_toggle_03.js]
--- a/browser/devtools/markupview/test/browser_markupview_html_edit_03.js
+++ b/browser/devtools/markupview/test/browser_markupview_html_edit_03.js
@@ -102,56 +102,55 @@ function testF2Commits(inspector) {
 }
 
 function* testBody(inspector) {
   let body = getNode("body");
   let bodyHTML = '<body id="updated"><p></p></body>';
   let bodyFront = yield getNodeFront("body", inspector);
   let doc = content.document;
 
-  let mutated = inspector.once("markupmutation");
-  inspector.markup.updateNodeOuterHTML(bodyFront, bodyHTML, body.outerHTML);
-
-  let mutations = yield mutated;
+  let onReselected = inspector.markup.once("reselectedonremoved");
+  yield inspector.markup.updateNodeOuterHTML(bodyFront, bodyHTML, body.outerHTML);
+  yield onReselected;
 
   is(getNode("body").outerHTML, bodyHTML, "<body> HTML has been updated");
   is(doc.querySelectorAll("head").length, 1, "no extra <head>s have been added");
 
   yield inspector.once("inspector-updated");
 }
 
 function* testHead(inspector) {
   let head = getNode("head");
+  yield selectNode("head", inspector);
+
   let headHTML = '<head id="updated"><title>New Title</title><script>window.foo="bar";</script></head>';
   let headFront = yield getNodeFront("head", inspector);
   let doc = content.document;
 
-  let mutated = inspector.once("markupmutation");
-  inspector.markup.updateNodeOuterHTML(headFront, headHTML, head.outerHTML);
-
-  let mutations = yield mutated;
+  let onReselected = inspector.markup.once("reselectedonremoved");
+  yield inspector.markup.updateNodeOuterHTML(headFront, headHTML, head.outerHTML);
+  yield onReselected;
 
   is(doc.title, "New Title", "New title has been added");
   is(doc.defaultView.foo, undefined, "Script has not been executed");
   is(doc.querySelector("head").outerHTML, headHTML, "<head> HTML has been updated");
   is(doc.querySelectorAll("body").length, 1, "no extra <body>s have been added");
 
   yield inspector.once("inspector-updated");
 }
 
 function* testDocumentElement(inspector) {
   let doc = content.document;
   let docElement = doc.documentElement;
   let docElementHTML = '<html id="updated" foo="bar"><head><title>Updated from document element</title><script>window.foo="bar";</script></head><body><p>Hello</p></body></html>';
   let docElementFront = yield inspector.markup.walker.documentElement();
 
-  let mutated = inspector.once("markupmutation");
-  inspector.markup.updateNodeOuterHTML(docElementFront, docElementHTML, docElement.outerHTML);
-
-  let mutations = yield mutated;
+  let onReselected = inspector.markup.once("reselectedonremoved");
+  yield inspector.markup.updateNodeOuterHTML(docElementFront, docElementHTML, docElement.outerHTML);
+  yield onReselected;
 
   is(doc.title, "Updated from document element", "New title has been added");
   is(doc.defaultView.foo, undefined, "Script has not been executed");
   is(doc.documentElement.id, "updated", "<html> ID has been updated");
   is(doc.documentElement.className, "", "<html> class has been updated");
   is(doc.documentElement.getAttribute("foo"), "bar", "<html> attribute has been updated");
   is(doc.documentElement.outerHTML, docElementHTML, "<html> HTML has been updated");
   is(doc.querySelectorAll("head").length, 1, "no extra <head>s have been added");
@@ -160,20 +159,19 @@ function* testDocumentElement(inspector)
 }
 
 function* testDocumentElement2(inspector) {
   let doc = content.document;
   let docElement = doc.documentElement;
   let docElementHTML = '<html class="updated" id="somethingelse"><head><title>Updated again from document element</title><script>window.foo="bar";</script></head><body><p>Hello again</p></body></html>';
   let docElementFront = yield inspector.markup.walker.documentElement();
 
-  let mutated = inspector.once("markupmutation");
+  let onReselected = inspector.markup.once("reselectedonremoved");
   inspector.markup.updateNodeOuterHTML(docElementFront, docElementHTML, docElement.outerHTML);
-
-  let mutations = yield mutated;
+  yield onReselected;
 
   is(doc.title, "Updated again from document element", "New title has been added");
   is(doc.defaultView.foo, undefined, "Script has not been executed");
   is(doc.documentElement.id, "somethingelse", "<html> ID has been updated");
   is(doc.documentElement.className, "updated", "<html> class has been updated");
   is(doc.documentElement.getAttribute("foo"), null, "<html> attribute has been removed");
   is(doc.documentElement.outerHTML, docElementHTML, "<html> HTML has been updated");
   is(doc.querySelectorAll("head").length, 1, "no extra <head>s have been added");
--- a/browser/devtools/markupview/test/browser_markupview_tag_edit_03.js
+++ b/browser/devtools/markupview/test/browser_markupview_tag_edit_03.js
@@ -1,17 +1,17 @@
 /* vim: set ts=2 et sw=2 tw=80: */
 /* Any copyright is dedicated to the Public Domain.
  http://creativecommons.org/publicdomain/zero/1.0/ */
 
 "use strict";
 
 // Tests that a node's tagname can be edited in the markup-view
 
-const TEST_URL = "data:text/html,<div id='retag-me'><div id='retag-me-2'></div></div>";
+const TEST_URL = "data:text/html;charset=utf-8,<div id='retag-me'><div id='retag-me-2'></div></div>";
 
 let test = asyncTest(function*() {
   let {toolbox, inspector} = yield addTab(TEST_URL).then(openInspector);
 
   yield inspector.markup.expandAll();
 
   info("Selecting the test node");
   let node = content.document.querySelector("#retag-me");
new file mode 100644
--- /dev/null
+++ b/browser/devtools/markupview/test/browser_markupview_tag_edit_10.js
@@ -0,0 +1,33 @@
+/* vim: set ts=2 et sw=2 tw=80: */
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Tests that invalid tagname updates are handled correctly
+
+const TEST_URL = "data:text/html;charset=utf-8,<div></div>";
+
+let test = asyncTest(function*() {
+  let {toolbox, inspector} = yield addTab(TEST_URL).then(openInspector);
+  yield inspector.markup.expandAll();
+  yield selectNode("div", inspector);
+
+  info("Updating the DIV tagname to an invalid value");
+  let container = yield getContainerForSelector("div", inspector);
+  let onCancelReselect = inspector.markup.once("canceledreselectonremoved");
+  let tagEditor = container.editor.tag;
+  setEditableFieldValue(tagEditor, "<<<", inspector);
+  yield onCancelReselect;
+  ok(true, "The markup-view emitted the canceledreselectonremoved event");
+  is(inspector.selection.nodeFront, container.node, "The test DIV is still selected");
+
+  info("Updating the DIV tagname to a valid value this time");
+  let onReselect = inspector.markup.once("reselectedonremoved");
+  setEditableFieldValue(tagEditor, "span", inspector);
+  yield onReselect;
+  ok(true, "The markup-view emitted the reselectedonremoved event");
+
+  let spanFront = yield getNodeFront("span", inspector);
+  is(inspector.selection.nodeFront, spanFront, "The seelected node is now the SPAN");
+});
--- a/browser/devtools/markupview/test/helper_outerhtml_test_runner.js
+++ b/browser/devtools/markupview/test/helper_outerhtml_test_runner.js
@@ -39,35 +39,21 @@ function runEditOuterHTMLTests(tests, in
  */
 function* runEditOuterHTMLTest(test, inspector) {
   info("Running an edit outerHTML test on '" + test.selector + "'");
   yield selectNode(test.selector, inspector);
   let oldNodeFront = inspector.selection.nodeFront;
 
   let onUpdated = inspector.once("inspector-updated");
 
-  info("Listening for the markupmutation event");
-  // This event fires once the outerHTML is set, with a target as the parent node and a type of "childList".
-  let mutated = inspector.once("markupmutation");
-  info("Editing the outerHTML");
-  inspector.markup.updateNodeOuterHTML(inspector.selection.nodeFront, test.newHTML, test.oldHTML);
-  let mutations = yield mutated;
-  ok(true, "The markupmutation event has fired, mutation done");
-
-  info("Check to make the sure the correct mutation event was fired, and that the parent is selected");
-  let nodeFront = inspector.selection.nodeFront;
-  let mutation = mutations[0];
-  let isFromOuterHTML = mutation.removed.some(n => n === oldNodeFront);
-
-  ok(isFromOuterHTML, "The node is in the 'removed' list of the mutation");
-  is(mutation.type, "childList", "Mutation is a childList after updating outerHTML");
-  is(mutation.target, nodeFront, "Parent node is selected immediately after setting outerHTML");
-
-  // Wait for node to be reselected after outerHTML has been set
-  yield inspector.selection.once("new-node-front");
+  info("Listen for reselectedonremoved and edit the outerHTML");
+  let onReselected = inspector.markup.once("reselectedonremoved");
+  yield inspector.markup.updateNodeOuterHTML(inspector.selection.nodeFront,
+                                             test.newHTML, test.oldHTML);
+  yield onReselected;
 
   // Typically selectedNode will === pageNode, but if a new element has been injected in front
   // of it, this will not be the case.  If this happens.
   let selectedNodeFront = inspector.selection.nodeFront;
   let pageNodeFront = yield inspector.walker.querySelector(inspector.walker.rootNode, test.selector);
   let pageNode = getNode(test.selector);
 
   if (test.validate) {
--- a/browser/devtools/projecteditor/lib/tree.js
+++ b/browser/devtools/projecteditor/lib/tree.js
@@ -64,34 +64,40 @@ var ResourceContainer = Class({
     }, false);
 
     this.children = doc.createElementNS(HTML_NS, "ul");
     this.children.classList.add("children");
 
     this.elt.appendChild(this.children);
 
     this.line.addEventListener("click", (evt) => {
-      if (!this.selected) {
-        this.select();
-        this.expanded = true;
-        evt.stopPropagation();
-      }
+      this.select();
+      this.toggleExpansion();
+      evt.stopPropagation();
     }, false);
     this.expander.addEventListener("click", (evt) => {
-      this.expanded = !this.expanded;
+      this.toggleExpansion();
       this.select();
       evt.stopPropagation();
     }, true);
 
     if (!this.resource.isRoot) {
       this.expanded = false;
     }
     this.update();
   },
 
+  toggleExpansion: function() {
+    if (!this.resource.isRoot) {
+      this.expanded = !this.expanded;
+    } else {
+      this.expanded = true;
+    }
+  },
+
   destroy: function() {
     this.elt.remove();
     this.expander.remove();
     this.highlighter.remove();
     this.children.remove();
     this.label.remove();
     this.elt = this.expander = this.highlighter = this.children = this.label = null;
   },
--- a/browser/devtools/projecteditor/test/browser_projecteditor_tree_selection_01.js
+++ b/browser/devtools/projecteditor/test/browser_projecteditor_tree_selection_01.js
@@ -34,20 +34,26 @@ let test = asyncTest(function*() {
 
 function selectFileFirstLoad(projecteditor, resource) {
   ok (resource && resource.path, "A valid resource has been passed in for selection " + (resource && resource.path));
   projecteditor.projectTree.selectResource(resource);
   let container = projecteditor.projectTree.getViewContainer(resource);
 
   if (resource.isRoot) {
     ok (container.expanded, "The root directory is expanded by default.");
+    container.line.click();
+    ok (container.expanded, "Clicking on the line does not toggles expansion.");
     return;
   }
   if (resource.isDir) {
     ok (!container.expanded, "A directory is not expanded by default.");
+    container.line.click();
+    ok (container.expanded, "Clicking on the line toggles expansion.");
+    container.line.click();
+    ok (!container.expanded, "Clicking on the line toggles expansion.");
     return;
   }
 
   let [editorCreated, editorLoaded, editorActivated] = yield promise.all([
     onceEditorCreated(projecteditor),
     onceEditorLoad(projecteditor),
     onceEditorActivated(projecteditor)
   ]);
--- a/browser/locales/en-US/chrome/browser/loop/loop.properties
+++ b/browser/locales/en-US/chrome/browser/loop/loop.properties
@@ -267,8 +267,13 @@ feedback_back_button=Back
 feedback_window_will_close_in2=This window will close in {{countdown}} second;This window will close in {{countdown}} seconds
 ## LOCALIZATION_NOTE (feedback_rejoin_button): Displayed on the feedback form after
 ## a signed-in to signed-in user call.
 ## https://people.mozilla.org/~dhenein/labs/loop-mvp-spec/#feedback
 feedback_rejoin_button=Rejoin
 ## LOCALIZATION NOTE (feedback_report_user_button): Used to report a user in the case of
 ## an abusive user.
 feedback_report_user_button=Report User
+
+## LOCALIZATION NOTE (rooms_list_current_conversations): We prefer to have no
+## number in the string, but if you need it for your language please use {{num}}.
+rooms_list_current_conversations=Current conversation;Current conversations
+rooms_list_no_current_conversations=No current conversations
--- a/dom/bluetooth/bluedroid/BluetoothSocket.cpp
+++ b/dom/bluetooth/bluedroid/BluetoothSocket.cpp
@@ -363,24 +363,37 @@ public:
   {
     MOZ_ASSERT(NS_IsMainThread());
 
     if (mImpl->IsShutdownOnMainThread()) {
       BT_LOGD("mConsumer is null, aborting receive!");
       return;
     }
 
+    if (aConnectionStatus != 0) {
+      mImpl->mConsumer->NotifyError();
+      return;
+    }
+
     mImpl->mConsumer->SetAddress(aBdAddress);
     XRE_GetIOMessageLoop()->PostTask(FROM_HERE, new AcceptTask(mImpl, aFd));
   }
 
   void OnError(BluetoothStatus aStatus) MOZ_OVERRIDE
   {
     MOZ_ASSERT(NS_IsMainThread());
     BT_LOGR("BluetoothSocketInterface::Accept failed: %d", (int)aStatus);
+
+    if (!mImpl->IsShutdownOnMainThread()) {
+      // Instead of NotifyError(), call NotifyDisconnect() to trigger
+      // BluetoothOppManager::OnSocketDisconnect() as
+      // DroidSocketImpl::OnFileCanReadWithoutBlocking() in Firefox OS 2.0 in
+      // order to keep the same behavior and reduce regression risk.
+      mImpl->mConsumer->NotifyDisconnect();
+    }
   }
 
 private:
   DroidSocketImpl* mImpl;
 };
 
 class AcceptRunnable MOZ_FINAL : public SocketIORunnable<DroidSocketImpl>
 {
@@ -499,27 +512,43 @@ public:
     MOZ_ASSERT(mImpl);
   }
 
   void Connect(int aFd, const nsAString& aBdAddress,
                int aConnectionStatus) MOZ_OVERRIDE
   {
     MOZ_ASSERT(NS_IsMainThread());
 
-    if (!mImpl->IsShutdownOnMainThread()) {
-      mImpl->mConsumer->SetAddress(aBdAddress);
+    if (mImpl->IsShutdownOnMainThread()) {
+      BT_LOGD("mConsumer is null, aborting send!");
+      return;
     }
+
+    if (aConnectionStatus != 0) {
+      mImpl->mConsumer->NotifyError();
+      return;
+    }
+
+    mImpl->mConsumer->SetAddress(aBdAddress);
     XRE_GetIOMessageLoop()->PostTask(FROM_HERE,
                                      new SocketConnectTask(mImpl, aFd));
   }
 
   void OnError(BluetoothStatus aStatus) MOZ_OVERRIDE
   {
     MOZ_ASSERT(NS_IsMainThread());
     BT_WARNING("Connect failed: %d", (int)aStatus);
+
+    if (!mImpl->IsShutdownOnMainThread()) {
+      // Instead of NotifyError(), call NotifyDisconnect() to trigger
+      // BluetoothOppManager::OnSocketDisconnect() as
+      // DroidSocketImpl::OnFileCanReadWithoutBlocking() in Firefox OS 2.0 in
+      // order to keep the same behavior and reduce regression risk.
+      mImpl->mConsumer->NotifyDisconnect();
+    }
   }
 
 private:
   DroidSocketImpl* mImpl;
 };
 
 bool
 BluetoothSocket::ConnectSocket(const nsAString& aDeviceAddress, int aChannel)
--- a/dom/bluetooth/bluedroid/BluetoothSocketHALInterface.cpp
+++ b/dom/bluetooth/bluedroid/BluetoothSocketHALInterface.cpp
@@ -281,17 +281,17 @@ private:
     iv.iov_len = MSG1_SIZE;
 
     struct msghdr msg;
     memset(&msg, 0, sizeof(msg));
     msg.msg_iov = &iv;
     msg.msg_iovlen = 1;
 
     ssize_t res = TEMP_FAILURE_RETRY(recvmsg(mFd, &msg, MSG_NOSIGNAL));
-    if (res < 0) {
+    if (res <= 0) {
       return STATUS_FAIL;
     }
 
     mLen += res;
 
     return STATUS_SUCCESS;
   }
 
@@ -306,17 +306,17 @@ private:
     struct cmsghdr cmsgbuf[2 * sizeof(cmsghdr) + 0x100];
     memset(&msg, 0, sizeof(msg));
     msg.msg_iov = &iv;
     msg.msg_iovlen = 1;
     msg.msg_control = cmsgbuf;
     msg.msg_controllen = sizeof(cmsgbuf);
 
     ssize_t res = TEMP_FAILURE_RETRY(recvmsg(mFd, &msg, MSG_NOSIGNAL));
-    if (res < 0) {
+    if (res <= 0) {
       return STATUS_FAIL;
     }
 
     mLen += res;
 
     if (msg.msg_flags & (MSG_CTRUNC | MSG_OOB | MSG_ERRQUEUE)) {
       return STATUS_FAIL;
     }
--- a/dom/cellbroadcast/gonk/CellBroadcastService.js
+++ b/dom/cellbroadcast/gonk/CellBroadcastService.js
@@ -11,20 +11,32 @@ Cu.import("resource://gre/modules/XPCOMU
 Cu.import("resource://gre/modules/Services.jsm");
 
 XPCOMUtils.defineLazyGetter(this, "RIL", function () {
   let obj = {};
   Cu.import("resource://gre/modules/ril_consts.js", obj);
   return obj;
 });
 
+const kMozSettingsChangedObserverTopic   = "mozsettings-changed";
+const kSettingsCellBroadcastDisabled = "ril.cellbroadcast.disabled";
+const kSettingsCellBroadcastSearchList = "ril.cellbroadcast.searchlist";
+
 XPCOMUtils.defineLazyServiceGetter(this, "gSystemMessenger",
                                    "@mozilla.org/system-message-internal;1",
                                    "nsISystemMessagesInternal");
 
+XPCOMUtils.defineLazyServiceGetter(this, "gSettingsService",
+                                   "@mozilla.org/settingsService;1",
+                                   "nsISettingsService");
+
+XPCOMUtils.defineLazyServiceGetter(this, "gRadioInterfaceLayer",
+                                   "@mozilla.org/ril;1",
+                                   "nsIRadioInterfaceLayer");
+
 const GONK_CELLBROADCAST_SERVICE_CONTRACTID =
   "@mozilla.org/cellbroadcast/gonkservice;1";
 const GONK_CELLBROADCAST_SERVICE_CID =
   Components.ID("{7ba407ce-21fd-11e4-a836-1bfdee377e5c}");
 const CELLBROADCASTMESSAGE_CID =
   Components.ID("{29474c96-3099-486f-bb4a-3c9a1da834e4}");
 const CELLBROADCASTETWSINFO_CID =
   Components.ID("{59f176ee-9dcd-4005-9d47-f6be0cd08e17}");
@@ -36,35 +48,68 @@ function debug(s) {
   dump("CellBroadcastService: " + s);
 }
 
 function CellBroadcastService() {
   this._listeners = [];
 
   this._updateDebugFlag();
 
+  let lock = gSettingsService.createLock();
+
+  /**
+  * Read the settings of the toggle of Cellbroadcast Service:
+  *
+  * Simple Format: Boolean
+  *   true if CBS is disabled. The value is applied to all RadioInterfaces.
+  * Enhanced Format: Array of Boolean
+  *   Each element represents the toggle of CBS per RadioInterface.
+  */
+  lock.get(kSettingsCellBroadcastDisabled, this);
+
+  /**
+   * Read the Cell Broadcast Search List setting to set listening channels:
+   *
+   * Simple Format:
+   *   String of integers or integer ranges separated by comma.
+   *   For example, "1, 2, 4-6"
+   * Enhanced Format:
+   *   Array of Objects with search lists specified in gsm/cdma network.
+   *   For example, [{'gsm' : "1, 2, 4-6", 'cdma' : "1, 50, 99"},
+   *                 {'cdma' : "3, 6, 8-9"}]
+   *   This provides the possibility to
+   *   1. set gsm/cdma search list individually for CDMA+LTE device.
+   *   2. set search list per RadioInterface.
+   */
+  lock.get(kSettingsCellBroadcastSearchList, this);
+
   Services.obs.addObserver(this, NS_XPCOM_SHUTDOWN_OBSERVER_ID, false);
+  Services.obs.addObserver(this, kMozSettingsChangedObserverTopic, false);
 }
 CellBroadcastService.prototype = {
   classID: GONK_CELLBROADCAST_SERVICE_CID,
 
   classInfo: XPCOMUtils.generateCI({classID: GONK_CELLBROADCAST_SERVICE_CID,
                                     contractID: GONK_CELLBROADCAST_SERVICE_CONTRACTID,
                                     classDescription: "CellBroadcastService",
                                     interfaces: [Ci.nsICellBroadcastService,
                                                  Ci.nsIGonkCellBroadcastService],
                                     flags: Ci.nsIClassInfo.SINGLETON}),
 
   QueryInterface: XPCOMUtils.generateQI([Ci.nsICellBroadcastService,
                                          Ci.nsIGonkCellBroadcastService,
+                                         Ci.nsISettingsServiceCallback,
                                          Ci.nsIObserver]),
 
   // An array of nsICellBroadcastListener instances.
   _listeners: null,
 
+  // Setting values of Cell Broadcast SearchList.
+  _cellBroadcastSearchList: null,
+
   _updateDebugFlag: function() {
     try {
       DEBUG = RIL.DEBUG_RIL ||
               Services.prefs.getBoolPref(kPrefRilDebuggingEnabled);
     } catch (e) {}
   },
 
   _convertCbGsmGeographicalScope: function(aGeographicalScope) {
@@ -80,16 +125,84 @@ CellBroadcastService.prototype = {
   },
 
   _convertCbEtwsWarningType: function(aWarningType) {
     return (aWarningType >= Ci.nsICellBroadcastService.GSM_ETWS_WARNING_INVALID)
       ? null
       : RIL.CB_ETWS_WARNING_TYPE_NAMES[aWarningType];
   },
 
+  _retrieveSettingValueByClient: function(aClientId, aSettings) {
+    return Array.isArray(aSettings) ? aSettings[aClientId] : aSettings;
+  },
+
+  /**
+   * Helper function to set CellBroadcastDisabled to each RadioInterface.
+   */
+  setCellBroadcastDisabled: function(aSettings) {
+    let numOfRilClients = gRadioInterfaceLayer.numRadioInterfaces;
+    let responses = [];
+    for (let clientId = 0; clientId < numOfRilClients; clientId++) {
+      gRadioInterfaceLayer
+        .getRadioInterface(clientId)
+        .sendWorkerMessage("setCellBroadcastDisabled",
+                           { disabled: this._retrieveSettingValueByClient(clientId, aSettings) });
+    }
+  },
+
+  /**
+   * Helper function to set CellBroadcastSearchList to each RadioInterface.
+   */
+  setCellBroadcastSearchList: function(aSettings) {
+    let numOfRilClients = gRadioInterfaceLayer.numRadioInterfaces;
+    let responses = [];
+    for (let clientId = 0; clientId < numOfRilClients; clientId++) {
+      let newSearchList = this._retrieveSettingValueByClient(clientId, aSettings);
+      let oldSearchList = this._retrieveSettingValueByClient(clientId,
+                                                          this._cellBroadcastSearchList);
+
+      if ((newSearchList == oldSearchList) ||
+          (newSearchList && oldSearchList &&
+           newSearchList.gsm == oldSearchList.gsm &&
+           newSearchList.cdma == oldSearchList.cdma)) {
+        return;
+      }
+
+      gRadioInterfaceLayer
+        .getRadioInterface(clientId).sendWorkerMessage("setCellBroadcastSearchList",
+                                                       { searchList: newSearchList },
+                                                       (function callback(aResponse) {
+        if (DEBUG && !aResponse.success) {
+          debug("Failed to set new search list: " + newSearchList +
+                " to client id: " + clientId);
+        }
+
+        responses.push(aResponse);
+        if (responses.length == numOfRilClients) {
+          let successCount = 0;
+          for (let i = 0; i < responses.length; i++) {
+            if (responses[i].success) {
+              successCount++;
+            }
+          }
+          if (successCount == numOfRilClients) {
+            this._cellBroadcastSearchList = aSettings;
+          } else {
+            // Rollback the change when failure.
+            let lock = gSettingsService.createLock();
+            lock.set(kSettingsCellBroadcastSearchList,
+                     this._cellBroadcastSearchList, null);
+          }
+        }
+
+        return false;
+      }).bind(this));
+    }
+  },
+
   /**
    * nsICellBroadcastService interface
    */
   registerListener: function(aListener) {
     if (this._listeners.indexOf(aListener) >= 0) {
       throw Cr.NS_ERROR_UNEXPECTED;
     }
 
@@ -174,22 +287,53 @@ CellBroadcastService.prototype = {
                                        aEtwsPopup);
       } catch (e) {
         debug("listener threw an exception: " + e);
       }
     }
   },
 
   /**
+   * nsISettingsServiceCallback interface.
+   */
+  handle: function(aName, aResult) {
+    switch (aName) {
+      case kSettingsCellBroadcastSearchList:
+        if (DEBUG) {
+          debug("'" + kSettingsCellBroadcastSearchList +
+                "' is now " + JSON.stringify(aResult));
+        }
+
+        this.setCellBroadcastSearchList(aResult);
+        break;
+      case kSettingsCellBroadcastDisabled:
+        if (DEBUG) {
+          debug("'" + kSettingsCellBroadcastDisabled +
+                "' is now " + JSON.stringify(aResult));
+        }
+
+        this.setCellBroadcastDisabled(aResult);
+        break;
+    }
+  },
+
+  /**
    * nsIObserver interface.
    */
   observe: function(aSubject, aTopic, aData) {
     switch (aTopic) {
+      case kMozSettingsChangedObserverTopic:
+        if ("wrappedJSObject" in aSubject) {
+          aSubject = aSubject.wrappedJSObject;
+        }
+        this.handle(aSubject.key, aSubject.value);
+        break;
       case NS_XPCOM_SHUTDOWN_OBSERVER_ID:
         Services.obs.removeObserver(this, NS_XPCOM_SHUTDOWN_OBSERVER_ID);
+        Services.obs.removeObserver(this, kMozSettingsChangedObserverTopic);
 
         // Remove all listeners.
         this._listeners = [];
         break;
     }
   }
 };
 
--- a/dom/cellbroadcast/gonk/CellBroadcastService.manifest
+++ b/dom/cellbroadcast/gonk/CellBroadcastService.manifest
@@ -1,6 +1,7 @@
 # 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/.
 
 component {7ba407ce-21fd-11e4-a836-1bfdee377e5c} CellBroadcastService.js
 contract @mozilla.org/cellbroadcast/gonkservice;1 {7ba407ce-21fd-11e4-a836-1bfdee377e5c}
+category profile-after-change CellBroadcastService @mozilla.org/cellbroadcast/gonkservice;1
--- a/dom/settings/SettingsRequestManager.jsm
+++ b/dom/settings/SettingsRequestManager.jsm
@@ -738,30 +738,43 @@ let SettingsRequestManager = {
     // If index is 0, the lock we just removed was at the head of
     // the queue, so possibly queue the next lock if it's
     // consumable.
     if (index == 0) {
       this.queueConsume();
     }
   },
 
-  removeMessageManager: function(aMsgMgr){
+  removeMessageManager: function(aMsgMgr, aPrincipal) {
     if (DEBUG) debug("Removing message manager");
     this.removeObserver(aMsgMgr);
     let closedLockIDs = [];
     let lockIDs = Object.keys(this.lockInfo);
     for (let i in lockIDs) {
-      if (this.lockInfo[lockIDs[i]]._mm == aMsgMgr) {
-      	if (DEBUG) debug("Removing lock " + lockIDs[i] + " due to process close/crash");
-        closedLockIDs.push(lockIDs[i]);
+      let lock = this.lockInfo[lockIDs[i]];
+      if (lock._mm == aMsgMgr) {
+        let is_finalizing = false;
+        for (let task_index in lock.tasks) {
+          if (lock.tasks[task_index].operation === "finalize") {
+            is_finalizing = true;
+            break;
+          }
+        }
+        if (!is_finalizing) {
+          this.queueTask("finalize", {lockID: lockIDs[i]}, aPrincipal).then(
+            function() {
+              if (DEBUG) debug("Lock " + lockIDs[i] + " with dead message manager finalized");
+            },
+            function(error) {
+              if (DEBUG) debug("Lock " + lockIDs[i] + " with dead message manager NOT FINALIZED due to error: " + error);
+            }
+          );
+        }
       }
     }
-    for (let i in closedLockIDs) {
-      this.removeLock(closedLockIDs[i]);
-    }
   },
 
   receiveMessage: function(aMessage) {
     if (DEBUG) debug("receiveMessage " + aMessage.name);
 
     let msg = aMessage.data;
     let mm = aMessage.target;
 
@@ -807,17 +820,17 @@ let SettingsRequestManager = {
         }
       default:
       break;
     }
 
     switch (aMessage.name) {
       case "child-process-shutdown":
         if (DEBUG) debug("Child process shutdown received.");
-        this.removeMessageManager(mm);
+        this.removeMessageManager(mm, aMessage.principal);
         break;
       case "Settings:RegisterForMessages":
         if (!SettingsPermissions.hasSomeReadPermission(aMessage.principal)) {
           Cu.reportError("Settings message " + aMessage.name +
                          " from a content process with no 'settings-api-read' privileges.");
           aMessage.target.assertPermission("message-manager-no-read-kill");
           return;
         }
--- a/dom/system/gonk/MozMtpDatabase.cpp
+++ b/dom/system/gonk/MozMtpDatabase.cpp
@@ -692,22 +692,28 @@ MozMtpDatabase::getNumObjects(MtpStorage
   MTP_LOG("  returning %d items", count);
   return count;
 }
 
 //virtual
 MtpObjectFormatList*
 MozMtpDatabase::getSupportedPlaybackFormats()
 {
-  static const uint16_t init_data[] = {MTP_FORMAT_UNDEFINED, MTP_FORMAT_ASSOCIATION, MTP_FORMAT_PNG};
+  static const uint16_t init_data[] = {MTP_FORMAT_UNDEFINED, MTP_FORMAT_ASSOCIATION,
+                                       MTP_FORMAT_TEXT, MTP_FORMAT_HTML, MTP_FORMAT_WAV,
+                                       MTP_FORMAT_MP3, MTP_FORMAT_MPEG, MTP_FORMAT_EXIF_JPEG,
+                                       MTP_FORMAT_TIFF_EP, MTP_FORMAT_BMP, MTP_FORMAT_GIF,
+                                       MTP_FORMAT_PNG, MTP_FORMAT_TIFF, MTP_FORMAT_WMA,
+                                       MTP_FORMAT_OGG, MTP_FORMAT_AAC, MTP_FORMAT_MP4_CONTAINER,
+                                       MTP_FORMAT_MP2, MTP_FORMAT_3GP_CONTAINER, MTP_FORMAT_FLAC};
 
   MtpObjectFormatList *list = new MtpObjectFormatList();
   list->appendArray(init_data, MOZ_ARRAY_LENGTH(init_data));
 
-  MTP_LOG("returning MTP_FORMAT_UNDEFINED, MTP_FORMAT_ASSOCIATION, MTP_FORMAT_PNG");
+  MTP_LOG("returning Supported Playback Formats");
   return list;
 }
 
 //virtual
 MtpObjectFormatList*
 MozMtpDatabase::getSupportedCaptureFormats()
 {
   static const uint16_t init_data[] = {MTP_FORMAT_ASSOCIATION, MTP_FORMAT_PNG};
--- a/dom/system/gonk/RadioInterfaceLayer.js
+++ b/dom/system/gonk/RadioInterfaceLayer.js
@@ -73,18 +73,16 @@ const kSmsSentObserverTopic             
 const kSmsFailedObserverTopic            = "sms-failed";
 const kSmsDeliverySuccessObserverTopic   = "sms-delivery-success";
 const kSmsDeliveryErrorObserverTopic     = "sms-delivery-error";
 const kMozSettingsChangedObserverTopic   = "mozsettings-changed";
 const kSysMsgListenerReadyObserverTopic  = "system-message-listener-ready";
 const kSysClockChangeObserverTopic       = "system-clock-change";
 const kScreenStateChangedTopic           = "screen-state-changed";
 
-const kSettingsCellBroadcastDisabled = "ril.cellbroadcast.disabled";
-const kSettingsCellBroadcastSearchList = "ril.cellbroadcast.searchlist";
 const kSettingsClockAutoUpdateEnabled = "time.clock.automatic-update.enabled";
 const kSettingsClockAutoUpdateAvailable = "time.clock.automatic-update.available";
 const kSettingsTimezoneAutoUpdateEnabled = "time.timezone.automatic-update.enabled";
 const kSettingsTimezoneAutoUpdateAvailable = "time.timezone.automatic-update.available";
 
 const NS_PREFBRANCH_PREFCHANGE_TOPIC_ID = "nsPref:changed";
 
 const kPrefRilNumRadioInterfaces = "ril.numRadioInterfaces";
@@ -1818,42 +1816,16 @@ function RadioInterface(aClientId, aWork
   lock.get(kSettingsTimezoneAutoUpdateEnabled, this);
 
   // Set "time.clock.automatic-update.available" to false when starting up.
   this.setClockAutoUpdateAvailable(false);
 
   // Set "time.timezone.automatic-update.available" to false when starting up.
   this.setTimezoneAutoUpdateAvailable(false);
 
-  /**
-  * Read the settings of the toggle of Cellbroadcast Service:
-  *
-  * Simple Format: Boolean
-  *   true if CBS is disabled. The value is applied to all RadioInterfaces.
-  * Enhanced Format: Array of Boolean
-  *   Each element represents the toggle of CBS per RadioInterface.
-  */
-  lock.get(kSettingsCellBroadcastDisabled, this);
-
-  /**
-   * Read the Cell Broadcast Search List setting to set listening channels:
-   *
-   * Simple Format:
-   *   String of integers or integer ranges separated by comma.
-   *   For example, "1, 2, 4-6"
-   * Enhanced Format:
-   *   Array of Objects with search lists specified in gsm/cdma network.
-   *   For example, [{'gsm' : "1, 2, 4-6", 'cdma' : "1, 50, 99"},
-   *                 {'cdma' : "3, 6, 8-9"}]
-   *   This provides the possibility to
-   *   1. set gsm/cdma search list individually for CDMA+LTE device.
-   *   2. set search list per RadioInterface.
-   */
-  lock.get(kSettingsCellBroadcastSearchList, this);
-
   Services.obs.addObserver(this, kMozSettingsChangedObserverTopic, false);
   Services.obs.addObserver(this, kSysClockChangeObserverTopic, false);
   Services.obs.addObserver(this, kScreenStateChangedTopic, false);
 
   Services.obs.addObserver(this, kNetworkConnStateChangedTopic, false);
 
   this.portAddressedSmsApps = {};
   this.portAddressedSmsApps[WAP.WDP_PORT_PUSH] = this.handleSmsWdpPortPush.bind(this);
@@ -2236,46 +2208,16 @@ RadioInterface.prototype = {
     }
 
     target.sendAsyncMessage("RIL:MatchMvno", {
       clientId: this.clientId,
       data: message
     });
   },
 
-  setCellBroadcastSearchList: function(settings) {
-    let newSearchList =
-      Array.isArray(settings) ? settings[this.clientId] : settings;
-    let oldSearchList =
-      Array.isArray(this._cellBroadcastSearchList) ?
-        this._cellBroadcastSearchList[this.clientId] :
-        this._cellBroadcastSearchList;
-
-    if ((newSearchList == oldSearchList) ||
-          (newSearchList && oldSearchList &&
-            newSearchList.gsm == oldSearchList.gsm &&
-            newSearchList.cdma == oldSearchList.cdma)) {
-      return;
-    }
-
-    this.workerMessenger.send("setCellBroadcastSearchList",
-                              { searchList: newSearchList },
-                              (function callback(response) {
-      if (!response.success) {
-        let lock = gSettingsService.createLock();
-        lock.set(kSettingsCellBroadcastSearchList,
-                 this._cellBroadcastSearchList, null);
-      } else {
-        this._cellBroadcastSearchList = settings;
-      }
-
-      return false;
-    }).bind(this));
-  },
-
   setDataRegistration: function(attach) {
     let deferred = Promise.defer();
     this.workerMessenger.send("setDataRegistration",
                               {attach: attach},
                               (function(response) {
       // Always resolve to proceed with the following steps.
       deferred.resolve(response.errorMsg ? response.errorMsg : null);
     }).bind(this));
@@ -3100,35 +3042,16 @@ RadioInterface.prototype = {
 
         if (this._timezoneAutoUpdateEnabled) {
           // Apply the latest cached NITZ for timezone if it's available.
           if (this._timezoneAutoUpdateEnabled && this._lastNitzMessage) {
             this.setTimezoneByNitz(this._lastNitzMessage);
           }
         }
         break;
-      case kSettingsCellBroadcastSearchList:
-        if (DEBUG) {
-          this.debug("'" + kSettingsCellBroadcastSearchList +
-            "' is now " + JSON.stringify(aResult));
-        }
-
-        this.setCellBroadcastSearchList(aResult);
-        break;
-      case kSettingsCellBroadcastDisabled:
-        if (DEBUG) {
-          this.debug("'" + kSettingsCellBroadcastDisabled +
-            "' is now " + JSON.stringify(aResult));
-        }
-
-        let setCbsDisabled =
-          Array.isArray(aResult) ? aResult[this.clientId] : aResult;
-        this.workerMessenger.send("setCellBroadcastDisabled",
-                                  { disabled: setCbsDisabled });
-        break;
     }
   },
 
   handleError: function(aErrorMessage) {
     if (DEBUG) {
       this.debug("There was an error while reading RIL settings.");
     }
   },
--- a/dom/wifi/DOMWifiManager.js
+++ b/dom/wifi/DOMWifiManager.js
@@ -178,48 +178,16 @@ DOMWifiManager.prototype = {
   },
 
   _convertWifiCapabilities: function(aCapabilities) {
     let capabilities = aCapabilities ?
                          new MozWifiCapabilities(aCapabilities) : null;
     return capabilities;
   },
 
-  _genReadonlyPropDesc: function(value) {
-    return {
-      enumerable: true, configurable: false, writable: false, value: value
-    };
-  },
-
-  _convertWifiCertificateInfo: function(aInfo) {
-    let propList = {};
-    for (let k in aInfo) {
-      propList[k] = this._genReadonlyPropDesc(aInfo[k]);
-    }
-
-    let info = Cu.createObjectIn(this._window);
-    Object.defineProperties(info, propList);
-    Cu.makeObjectPropsNormal(info);
-
-    return info;
-  },
-
-  _convertWifiCertificateList: function(aList) {
-    let propList = {};
-    for (let k in aList) {
-      propList[k] = this._genReadonlyPropDesc(aList[k]);
-    }
-
-    let list = Cu.createObjectIn(this._window);
-    Object.defineProperties(list, propList);
-    Cu.makeObjectPropsNormal(list);
-
-    return list;
-  },
-
   _sendMessageForRequest: function(name, data, request) {
     let id = this.getRequestId(request);
     this._mm.sendAsyncMessage(name, { data: data, rid: id, mid: this._id });
   },
 
   receiveMessage: function(aMessage) {
     let msg = aMessage.json;
     if (msg.mid && msg.mid != this._id)
@@ -302,25 +270,25 @@ DOMWifiManager.prototype = {
         Services.DOMRequest.fireSuccess(request, msg.data);
         break;
 
       case "WifiManager:setStaticIpMode:Return:NO":
         Services.DOMRequest.fireError(request, msg.data);
         break;
 
       case "WifiManager:importCert:Return:OK":
-        Services.DOMRequest.fireSuccess(request, this._convertWifiCertificateInfo(msg.data));
+        Services.DOMRequest.fireSuccess(request, Cu.cloneInto(msg.data, this._window));
         break;
 
       case "WifiManager:importCert:Return:NO":
         Services.DOMRequest.fireError(request, msg.data);
         break;
 
       case "WifiManager:getImportedCerts:Return:OK":
-        Services.DOMRequest.fireSuccess(request, this._convertWifiCertificateList(msg.data));
+        Services.DOMRequest.fireSuccess(request, Cu.cloneInto(msg.data, this._window));
         break;
 
       case "WifiManager:getImportedCerts:Return:NO":
         Services.DOMRequest.fireError(request, msg.data);
         break;
 
       case "WifiManager:deleteCert:Return:OK":
         Services.DOMRequest.fireSuccess(request, msg.data);
--- a/mobile/android/base/BrowserApp.java
+++ b/mobile/android/base/BrowserApp.java
@@ -80,17 +80,16 @@ import org.mozilla.gecko.widget.GeckoAct
 import android.app.Activity;
 import android.app.AlertDialog;
 import android.app.KeyguardManager;
 import android.content.BroadcastReceiver;
 import android.content.ContentValues;
 import android.content.Context;
 import android.content.DialogInterface;
 import android.content.Intent;
-import android.content.IntentFilter;
 import android.content.SharedPreferences;
 import android.content.pm.PackageManager;
 import android.content.pm.ResolveInfo;
 import android.content.res.Configuration;
 import android.content.res.Resources;
 import android.database.Cursor;
 import android.graphics.Bitmap;
 import android.graphics.Rect;
@@ -155,17 +154,18 @@ public class BrowserApp extends GeckoApp
     public static final String GUEST_BROWSING_ARG = "--guest";
 
     private static final String STATE_ABOUT_HOME_TOP_PADDING = "abouthome_top_padding";
 
     private static final String BROWSER_SEARCH_TAG = "browser_search";
 
     // Request ID for startActivityForResult.
     private static final int ACTIVITY_REQUEST_PREFERENCES = 1001;
-    public static final String ACTION_NEW_PROFILE = "org.mozilla.gecko.NEW_PROFILE";
+
+    public static final String PREF_STARTPANE_ENABLED = "startpane_enabled";
 
     private BrowserSearch mBrowserSearch;
     private View mBrowserSearchContainer;
 
     public ViewGroup mBrowserChrome;
     public ViewFlipper mActionBarFlipper;
     public ActionModeCompatView mActionBar;
     private BrowserToolbar mBrowserToolbar;
@@ -616,18 +616,16 @@ public class BrowserApp extends GeckoApp
             "Reader:ListStatusRequest",
             "Reader:Removed",
             "Reader:Share",
             "Settings:Show",
             "Telemetry:Gather",
             "Updater:Launch",
             "BrowserToolbar:Visibility");
 
-        registerOnboardingReceiver(this);
-
         Distribution distribution = Distribution.init(this);
 
         // Init suggested sites engine in BrowserDB.
         final SuggestedSites suggestedSites = new SuggestedSites(appContext, distribution);
         BrowserDB.setSuggestedSites(suggestedSites);
 
         JavaAddonManager.getInstance().init(appContext);
         mSharedPreferencesHelper = new SharedPreferencesHelper(appContext);
@@ -683,34 +681,41 @@ public class BrowserApp extends GeckoApp
 
         getWindow().addFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS);
 
         SystemBarTintManager tintManager = new SystemBarTintManager(this);
         tintManager.setTintColor(getResources().getColor(R.color.background_tabs));
         tintManager.setStatusBarTintEnabled(true);
     }
 
-    private void registerOnboardingReceiver(Context context) {
-        final LocalBroadcastManager lbm = LocalBroadcastManager.getInstance(this);
-
-        // Receiver for launching first run start pane on new profile creation.
-        mOnboardingReceiver = new BroadcastReceiver() {
-            @Override
-            public void onReceive(Context context, Intent intent) {
-                launchStartPane(BrowserApp.this);
+    /**
+     * Check and show Onboarding start pane if Firefox has never been launched and
+     * is not opening an external link from another application.
+     *
+     * @param context Context of application; used to show Start Pane if appropriate
+     * @param intentAction Intent that launched this activity
+     */
+    private void checkStartPane(Context context, String intentAction) {
+        final StrictMode.ThreadPolicy savedPolicy = StrictMode.allowThreadDiskReads();
+
+        try {
+            final SharedPreferences prefs = GeckoSharedPrefs.forProfile(this);
+
+            if (prefs.getBoolean(PREF_STARTPANE_ENABLED, false)) {
+                if (!Intent.ACTION_VIEW.equals(intentAction)) {
+                    final Intent startIntent = new Intent(this, StartPane.class);
+                    context.startActivity(startIntent);
+                }
+                // Don't bother trying again to show the v1 minimal first run.
+                prefs.edit().putBoolean(PREF_STARTPANE_ENABLED, false).apply();
             }
-        };
-
-        lbm.registerReceiver(mOnboardingReceiver, new IntentFilter(ACTION_NEW_PROFILE));
-    }
-
-    private void launchStartPane(Context context) {
-         final Intent startIntent = new Intent(context, StartPane.class);
-         context.startActivity(startIntent);
-     }
+        } finally {
+            StrictMode.setThreadPolicy(savedPolicy);
+        }
+      }
 
     private Class<?> getMediaPlayerManager() {
         if (AppConstants.MOZ_MEDIA_PLAYER) {
             try {
                 return Class.forName("org.mozilla.gecko.MediaPlayerManager");
             } catch(Exception ex) {
                 // Ignore failures
                 Log.e(LOGTAG, "No native casting support", ex);
@@ -736,16 +741,22 @@ public class BrowserApp extends GeckoApp
             endActionModeCompat();
             return;
         }
 
         super.onBackPressed();
     }
 
     @Override
+    public void onAttachedToWindow() {
+        // We can't show Onboarding until Gecko has finished initialization (bug 1077583).
+        checkStartPane(this, getIntent().getAction());
+    }
+
+    @Override
     public void onResume() {
         super.onResume();
 
         final String args = StringUtils.getStringExtra(getIntent(), "args");
         // If an external intent tries to start Fennec in guest mode, and it's not already
         // in guest mode, this will change modes before opening the url.
         // NOTE: OnResume is called twice sometimes when showing on the lock screen.
         final boolean enableGuestSession = GuestSession.shouldUse(this, args);
@@ -2663,17 +2674,16 @@ public class BrowserApp extends GeckoApp
         MenuItem desktopMode = aMenu.findItem(R.id.desktop_mode);
         MenuItem enterGuestMode = aMenu.findItem(R.id.new_guest_session);
         MenuItem exitGuestMode = aMenu.findItem(R.id.exit_guest_session);
 
         // Only show the "Quit" menu item on pre-ICS, television devices,
         // or if the user has explicitly enabled the clear on shutdown pref.
         // (We check the pref last to save the pref read.)
         // In ICS+, it's easy to kill an app through the task switcher.
-        final SharedPreferences prefs = GeckoSharedPrefs.forProfile(this);
         final boolean visible = Versions.preICS ||
                                 HardwareUtils.isTelevision() ||
                                 !PrefUtils.getStringSet(GeckoSharedPrefs.forProfile(this),
                                                         ClearOnShutdownPref.PREF,
                                                         new HashSet<String>()).isEmpty();
         aMenu.findItem(R.id.quit).setVisible(visible);
 
         if (tab == null || tab.getURL() == null) {
--- a/mobile/android/base/ChromeCast.java
+++ b/mobile/android/base/ChromeCast.java
@@ -32,17 +32,17 @@ import android.content.Context;
 import android.os.Bundle;
 import android.support.v7.media.MediaRouter.RouteInfo;
 import android.util.Log;
 
 /* Implementation of GeckoMediaPlayer for talking to ChromeCast devices */
 class ChromeCast implements GeckoMediaPlayer {
     private static final boolean SHOW_DEBUG = false;
 
-    static final String MIRROR_RECIEVER_APP_ID = "D40D28D6";
+    static final String MIRROR_RECIEVER_APP_ID = "5F72F863";
 
     private final Context context;
     private final RouteInfo route;
     private GoogleApiClient apiClient;
     private RemoteMediaPlayer remoteMediaPlayer;
     private boolean canMirror;
     private String mSessionId;
     private MirrorChannel mMirrorChannel;
--- a/mobile/android/base/GeckoProfile.java
+++ b/mobile/android/base/GeckoProfile.java
@@ -22,16 +22,17 @@ import org.mozilla.gecko.distribution.Di
 import org.mozilla.gecko.mozglue.RobocopTarget;
 import org.mozilla.gecko.util.INIParser;
 import org.mozilla.gecko.util.INISection;
 
 import android.app.Activity;
 import android.content.ContentResolver;
 import android.content.Context;
 import android.content.Intent;
+import android.content.SharedPreferences;
 import android.support.v4.content.LocalBroadcastManager;
 import android.text.TextUtils;
 import android.util.Log;
 
 public final class GeckoProfile {
     private static final String LOGTAG = "GeckoProfile";
 
     // Used to "lock" the guest profile, so that we'll always restart in it
@@ -687,19 +688,19 @@ public final class GeckoProfile {
             } finally {
                 writer.close();
             }
         } catch (Exception e) {
             // Best-effort.
             Log.w(LOGTAG, "Couldn't write times.json.", e);
         }
 
-        LocalBroadcastManager lbm = LocalBroadcastManager.getInstance(mApplicationContext);
-        final Intent intent = new Intent(BrowserApp.ACTION_NEW_PROFILE);
-        lbm.sendBroadcast(intent);
+        // Initialize pref flag for displaying the start pane for a new profile.
+        final SharedPreferences prefs = GeckoSharedPrefs.forProfile(mApplicationContext);
+        prefs.edit().putBoolean(BrowserApp.PREF_STARTPANE_ENABLED, true).apply();
 
         return profileDir;
     }
 
     /**
      * This method is called once, immediately before creation of the profile
      * directory completes.
      *
--- a/mobile/android/base/Makefile.in
+++ b/mobile/android/base/Makefile.in
@@ -87,19 +87,17 @@ ALL_JARS = \
   sync-thirdparty.jar \
   $(NULL)
 
 ifdef MOZ_WEBRTC
 ALL_JARS += webrtc.jar
 endif
 
 ifdef MOZ_ANDROID_SEARCH_ACTIVITY
-extra_packages += org.mozilla.search
 ALL_JARS += search-activity.jar
-generated/org/mozilla/search/R.java: .aapt.deps ;
 endif
 
 ifdef MOZ_ANDROID_MLS_STUMBLER
 extra_packages += org.mozilla.mozstumbler
 ALL_JARS += ../stumbler/stumbler.jar
 generated/org/mozilla/mozstumbler/R.java: .aapt.deps ;
 endif
 
--- a/mobile/android/base/db/LocalBrowserDB.java
+++ b/mobile/android/base/db/LocalBrowserDB.java
@@ -1008,17 +1008,24 @@ public class LocalBrowserDB {
         Cursor c = cr.query(mHistoryUriWithProfile,
                             new String[] { History.FAVICON_URL },
                             Combined.URL + " = ?",
                             new String[] { uri },
                             null);
 
         try {
             if (c.moveToFirst()) {
-                return c.getString(c.getColumnIndexOrThrow(History.FAVICON_URL));
+                // Interrupted page loads can leave History items without a valid favicon_id.
+                final int columnIndex = c.getColumnIndexOrThrow(History.FAVICON_URL);
+                if (!c.isNull(columnIndex)) {
+                    final String faviconURL = c.getString(columnIndex);
+                    if (faviconURL != null) {
+                        return faviconURL;
+                    }
+                }
             }
         } finally {
             c.close();
         }
 
         // If that fails, check in the bookmarks table.
         c = cr.query(mBookmarksUriWithProfile,
                      new String[] { Bookmarks.FAVICON_URL },
--- a/mobile/android/base/moz.build
+++ b/mobile/android/base/moz.build
@@ -654,19 +654,16 @@ else:
 
 if CONFIG['MOZ_ANDROID_SEARCH_ACTIVITY']:
     # The Search Activity is mostly independent of Fennec proper, but
     # it does depend on Geckoview.  Therefore, we build it as a jar
     # that depends on the Geckoview jars.
     search_source_dir = SRCDIR + '/../search'
     include('../search/search_activity_sources.mozbuild')
 
-    ANDROID_RES_DIRS += [search_source_dir + '/res']
-    resjar.generated_sources += ['org/mozilla/search/R.java']
-
     search_activity = add_java_jar('search-activity')
     search_activity.sources += [search_source_dir + '/' + f for f in search_activity_sources]
     search_activity.javac_flags += ['-Xlint:all']
     search_activity.extra_jars = [
         'gecko-R.jar',
         'gecko-browser.jar',
         'gecko-mozglue.jar',
         'gecko-thirdparty.jar',
@@ -826,22 +823,10 @@ if CONFIG['MOZ_CRASHREPORTER']:
     resources.referenced_projects += ['../' + crashreporter.name]
 
 if CONFIG['MOZ_ANDROID_MLS_STUMBLER']:
     main.included_projects += ['../FennecStumbler']
     main.referenced_projects += ['../FennecStumbler']
     DEFINES['MOZ_STUMBLER_API_KEY'] = CONFIG['MOZ_MOZILLA_API_KEY']
 
 if CONFIG['MOZ_ANDROID_SEARCH_ACTIVITY']:
-    searchres = add_android_eclipse_library_project('FennecResourcesSearch')
-    # Eclipse generates org.mozilla.search.R for this project, which is
-    # referenced by the search/**/*.java code.
-    searchres.package_name = 'org.mozilla.search'
-    searchres.res = SRCDIR + '/../search/res'
-
-    searchres.included_projects += ['../' + static.name, '../' + generated.name, '../' + branding.name]
-    searchres.referenced_projects += ['../' + static.name, '../' + generated.name, '../' + branding.name]
-
-    resources.included_projects += ['../' + searchres.name]
-    resources.referenced_projects += ['../' + searchres.name]
-
     # The Search Activity code is built as part of Fennec, so we follow suit in Eclipse.
     main.add_classpathentry('search', TOPSRCDIR + '/mobile/android/search/java', dstdir='search')
--- a/mobile/android/base/newtablet/res/layout-large-v11/new_tablet_browser_toolbar.xml
+++ b/mobile/android/base/newtablet/res/layout-large-v11/new_tablet_browser_toolbar.xml
@@ -38,22 +38,22 @@
                   style="@style/UrlBar.Button"
                   android:paddingLeft="12dp"
                   android:paddingRight="12dp"
                   android:visibility="gone"
                   android:orientation="horizontal"
                   android:layout_toRightOf="@id/back"
                   android:layout_toLeftOf="@id/menu_items"/>
 
-    <!-- Values of marginLeft are used to animate the forward button so don't change its value. -->
+    <!-- Note: * Values of marginLeft are used to animate the forward button so don't change its value.
+               * We set the padding on the site security icon to increase its tappable area. -->
     <org.mozilla.gecko.toolbar.ToolbarDisplayLayout android:id="@+id/display_layout"
                   style="@style/UrlBar.Button.Container"
                   android:layout_toRightOf="@id/back"
                   android:layout_toLeftOf="@id/menu_items"
-                  android:paddingLeft="6dip"
                   android:paddingRight="4dip"/>
 
     <LinearLayout android:id="@+id/menu_items"
                   android:layout_width="wrap_content"
                   android:layout_height="match_parent"
                   android:gravity="center_vertical"
                   android:layout_marginLeft="6dp"
                   android:orientation="horizontal"
rename from mobile/android/search/res/color/facet_button_text_color.xml
rename to mobile/android/base/resources/color/facet_button_text_color.xml
rename from mobile/android/search/res/drawable-hdpi/ic_action_settings.png
rename to mobile/android/base/resources/drawable-hdpi/ic_action_settings.png
rename from mobile/android/search/res/drawable-hdpi/ic_widget_new_tab.png
rename to mobile/android/base/resources/drawable-hdpi/ic_widget_new_tab.png
rename from mobile/android/search/res/drawable-hdpi/ic_widget_search.png
rename to mobile/android/base/resources/drawable-hdpi/ic_widget_search.png
index fb59a8fed97c35fe529f089794a4880206838e99..781682895789077a55dec2cbf05b3f48d6ce2e93
GIT binary patch
literal 622
zc%17D@N?(olHy`uVBq!ia0vp^LO?9e!3-n~@4mkVq*&4&eH|GXHuiJ>Nn{1`6_P!I
zd>I(3)EF2VS{N990fib~Fff!FFfhDIU|_JC0n}eGXIG#NP-1d`Plzj!o^UP*j{469
zf*5B5C!7V5{bvIE&tjl6Ak=pnhE9RV-cw+75{fzx1%S{&5CR$n)CE-olsysHb3Cy7
zSYY?jz%C#<63_`mhoS2K|NsA}@Be<FvlL5${DK)c^H2Tyd|Ks($kQe7(pPHfH&^VH
zQ@G8@_{QSujo0rY6YNFi@BtMwCV9KN7_7Ua`vl10%y)Au2GVRm%*>!3$0h(0EGuwK
z2hwt2{KNmDEs!zA)5S4F;&Sepn?g+n0xlQXT*Vw&O|-O5`4q(e`~QB5z}lvddzQRj
zCbZz;j^>5tMY4Z>IHt1v+ujnG>blT3d65O@VHLGmH_~11ZILwYKf$xaC9-;^;L@}w
zXFi+h&L}n(+5ETU<eD=bTef8`{ram|WYxO>R<qLcV(yIU596)4cW~_B+QGSlyMm*F
z>&92_&f1@^H2CvZ$LTi;F3y|`bf0R8YeY#(Vo9o1a#1RfVlXl=G|@G%&^0m*F*LI>
tHL)@<(={-+GB9`};xG+GLvDUbW?Cg~4RbwzXaO}ac)I$ztaD0e0svfF?n?jw
index be7d5b1a2860850d66e738dd0cee81788c2984b5..d1e8ed7dc32294378eaddfe02a8022c14748019d
GIT binary patch
literal 532
zc%17D@N?(olHy`uVBq!ia0vp^LO?9e!3-n~@4mkVq*&4&eH|GXHuiJ>Nn{1`6_P!I
zd>I(3)EF2VS{N990fib~Fff!FFfhDIU|_JC0n}eGXIG#NP$D+KC&U#<$0ujd0{#F0
z|4+K=X`ltNB|(0{44nC=|9n=tc2nf>^7rXl`pxB4yX6%2Gd_Q7aVLh?c{<3Oj7i?^
zE^<X&3fe#pXTF<TF_2~hVrB;QI5q*0U|E4<I*^tF;~)MHZGnsuPZ!4!iOW}eqlKCk
zco-ftvmVWQD_r`%{%qEgxnBMk{wAi+R8f+SjGMCTgz?XSs~X#?#IFlv#`x&|IJfhh
zv-EMVGntd`HFWV5+_QI=2+=FJdzts0me`|9$E!k~{+xU0mya>~GtVrcH6IS!FP$K!
z?|tdeC#J8JO0gfWs&NDDS1oakC`m~yNwrEYN(E93Mh1o^x&{`yMy4T#W>%&qRt9Fe
n2If`<25&?hrlDxa%}>cptHiBguICRepaup{S3j3^P6<r_o_Vjg
rename from mobile/android/search/res/drawable-hdpi/network_error.png
rename to mobile/android/base/resources/drawable-hdpi/network_error.png
rename from mobile/android/search/res/drawable-hdpi/search_clear.png
rename to mobile/android/base/resources/drawable-hdpi/search_clear.png
rename from mobile/android/search/res/drawable-hdpi/search_fox.png
rename to mobile/android/base/resources/drawable-hdpi/search_fox.png
rename from mobile/android/search/res/drawable-hdpi/search_history.png
rename to mobile/android/base/resources/drawable-hdpi/search_history.png
rename from mobile/android/search/res/drawable-hdpi/search_icon_active.png
rename to mobile/android/base/resources/drawable-hdpi/search_icon_active.png
rename from mobile/android/search/res/drawable-hdpi/search_icon_inactive.png
rename to mobile/android/base/resources/drawable-hdpi/search_icon_inactive.png
rename from mobile/android/search/res/drawable-hdpi/search_launcher.png
rename to mobile/android/base/resources/drawable-hdpi/search_launcher.png
rename from mobile/android/search/res/drawable-hdpi/search_plus.png
rename to mobile/android/base/resources/drawable-hdpi/search_plus.png
index cdaf60fdc749f2d67a004127fa7ae9d5529a3ee4..a278aab4658236fd07cb25d8dcca83f3edaf26b7
GIT binary patch
literal 740
zc%17D@N?(olHy`uVBq!ia0vp^B0wy`!3-qNPSte*QY`6?zK#qG8~eHcB(ehe3dtTp
zz6=aiY77hwEes65fI<x~7#K<o7#Ln9FfdrnU|<k0n6oR;1}HHtz$e5NNXI8<3@Y^h
z|9^3Tb)7($8I%P11vBV$U1tn9W`2fi(!U#3j+eZ|^45N+7x~U|-}cGw715t_P53yL
zJlJ@-N&Q8c{M{0x(=SRv-eyelc6Twnd0Vy!$l=U)b1MeYpdeyqSa_;rA4ssQz%d<2
z%YpF^|A)3f#t%;y#}J9ju@m1`H5u@<24`sp{46*yqf6<EWohvFzwvjS1jEbgmb|?E
zY>U5>i5E){Ta(S%DQ%O(Hg}$Nwh=ui+m&>9w(F#2C!2(Y-E-6|Pk(wEebB7(n6&1K
zCx>6zu4`QQX7Qs<lfqV9tunsR%fCWO`%=a=(|-!47q+zCIoK`~&2?d!{svcv>II(@
z`QLc{ZDtD4%dt5zoh7n)$;Sky<WBpl4%><Y>c^G$mC2nccv8`189%Qev*1x{ePqW6
z#>Gq?m0Ryj@~PR*Y*Rel>vsm@t5@m@(Y!OI_U_gekI&nF@aHYx&o5#>$KI~aFDW;-
ztX&@dH0pWqKW<M)4dx3EIaGjQpjzS@QIe8al4_M)lnSI6j0_A-bPX(YjZ8xf&8$pK
rtPIR_4a}_!4Bm)1OheI-o1c=IR*74~T+bg`Kn)C@u6{1-oD!M<h6@v4
index 87f44af2e000422843a00adc28f536ef3946c5bf..39d879880419aed270dc1fb92e854f318b4cf4dc
GIT binary patch
literal 676
zc%17D@N?(olHy`uVBq!ia0vp^Vn8g)!3-oz|Girdq*&4&eH|GXHuiJ>Nn{1`6_P!I
zd>I(3)EF2VS{N990fib~096|>FuY1&V6d9Oz#v{QXIG#NP@*fqC&U#<|6F4@;OPJV
z|4i$C_W)g^QxfDC%n<P;T=UO*^M8jv9`s(y#(XIux48L1$Ap>F`CqTxDe?8>R<$+r
zgdd&%W^JP#ACVUW)W?|Q?e3y`_Lie9ki(hp=2i>}9xztDZg>YISXSVe4y5J4_=o>P
zkjfLDE{-7*ms2O)6l*dNa9-S`r1gMlk-}>0pa0(%?|Kk;!h7=VmnBgPuBTYdO;kCq
z8k^qoT;yzf$wH02M;Y`z@2iLZFY(Q5a*$eEpD8ghI>FuTA^$zE`?duO?!6T~GD-M>
zhPO=3$2i}KOzN|O-Jbg#mASFP)H-_3i<$Zk>&_m$u;|K4rmWmkPLD&_(>#Tw%Y(KQ
zT28plk(ayUUf1faYyV74e-k@T{FdIQ<gIHvLJqg{n$EjcssG?^d~hB2k2AOTweXi*
zt(dgxH$&EWjo4#1=KTTsNwvf^q9i4;B-JXpC>2OC7#SFv=o(n)8kvR|npv5eSQ(h<
o8kk!d7`zd2n1-SuH$NpatrE9}xt>3?fEpM)UHx3vIVCg!07}vq9{>OV
rename from mobile/android/search/res/drawable-hdpi/widget_bg.9.png
rename to mobile/android/base/resources/drawable-hdpi/widget_bg.9.png
new file mode 100644
--- /dev/null
+++ b/mobile/android/base/resources/drawable-large-v11/new_tablet_site_security_level.xml
@@ -0,0 +1,14 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+   - License, v. 2.0. If a copy of the MPL was not distributed with this
+   - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<level-list xmlns:android="http://schemas.android.com/apk/res/android">
+
+    <item android:maxLevel="0" android:drawable="@drawable/new_tablet_site_security_unknown"/>
+    <item android:maxLevel="1" android:drawable="@drawable/lock_identified"/>
+    <item android:maxLevel="2" android:drawable="@drawable/lock_verified"/>
+    <item android:maxLevel="3" android:drawable="@drawable/shield"/>
+    <item android:maxLevel="4" android:drawable="@drawable/warning"/>
+
+</level-list>
new file mode 100644
--- /dev/null
+++ b/mobile/android/base/resources/drawable-large-v11/new_tablet_site_security_unknown.xml
@@ -0,0 +1,12 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+   - License, v. 2.0. If a copy of the MPL was not distributed with this
+   - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<!-- The favicon drawable is not the same dimensions as the site security
+     lock icons so we offset it using this drawable to compensate. -->
+<inset
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    android:drawable="@drawable/favicon"
+    android:insetTop="@dimen/new_tablet_site_security_unknown_inset_top"
+    android:insetBottom="@dimen/new_tablet_site_security_unknown_inset_bottom"/>
rename from mobile/android/search/res/drawable-mdpi/ic_action_settings.png
rename to mobile/android/base/resources/drawable-mdpi/ic_action_settings.png
rename from mobile/android/search/res/drawable-mdpi/ic_widget_new_tab.png
rename to mobile/android/base/resources/drawable-mdpi/ic_widget_new_tab.png
rename from mobile/android/search/res/drawable-mdpi/ic_widget_search.png
rename to mobile/android/base/resources/drawable-mdpi/ic_widget_search.png
index a5de9125e80252f9b4f1b5657f371b9bc11f72af..a464ac1605a80bad5b8d5243b4a3f73d0f524a05
GIT binary patch
literal 533
zc%17D@N?(olHy`uVBq!ia0vp^JV4CP!3-py+uy4OQY`6?zK#qG8~eHcB(ehe3dtTp
zz6=aiY77hwEes65fI<x~7#K<o7#Ln9FfdrnU|<k0n6oR;1}IS!;1l8sq$ivULZGvO
z6V8IjeoS-*iuz7NQST`*ItfCZhXNpI!da*qpkUAO!0uy#T}K1Ejs$cb2Fd>a|DT=D
zn+a&EbV-n3FoX7nPdesbtXS@TpEi%x`&VGg`sJN-{`CJ_vFhcNiq}A8j7i?^E~<wa
zPx1pfocV5U#UNh;F%v^^@3bh8U|E4<I*=9t<Hgom7lDi<PZ!4!j_bas!uc8uc$fn=
zFYGZotudwYzwsm`mP+|)vaAd;*FVR|_bXgc>B;FA*1hEOLS=d2nMr+(bG)Ul2HkE?
z%ha?^7GC=jsA;8K{PNbR2ahGB7Z_Mb@?1|p@MHZ;w$7~r1`@L@o&#-DEpd$~Nl7e8
zwMs5Z1yT$~28JfO1{S(TrXhxAR;DIa24=bj=2iv<Z$uoXp=ij>PsvQH#I0ej=MOEQ
O1_n=8KbLh*2~7aag}kl+
index f88b451598e482676516c7e7139b7d255545d8bf..e729acf01f9e9b8bbae2c4e88b228cfb065bc2ff
GIT binary patch
literal 476
zc%17D@N?(olHy`uVBq!ia0vp^JV4CP!3-py+uy4OQY`6?zK#qG8~eHcB(ehe3dtTp
zz6=aiY77hwEes65fI<x~7#K<o7#Ln9FfdrnU|<k0n6oR;1}Nbj;1l8sq~nt_C`bSQ
z|5q|7lmQwiUJ~RN%%Hvg^9CLBFIFsfe@vUh>is*gWkqNIzlf=do<JpxN#5=*60<W~
z<$xT{d^fjZkXL}1iJ`c6S`<jItiUlHNQ;2+Vr#97Kt_nCi(?4K^|@V+d<_Z$N8AEJ
z1RNco{kJbwbhEkbY<yRTfye80q0`|EuFpZ=vtF9L&nWLapyB?w!edUzS4G3_T4STV
z8lM#7Dz=OKco;rqA9v9D66OiFxSxluzFGz}U$w+Fq9i4;B-JXpC>2OC7#SFv=o(n)
z8kvR|npv5eSQ(h<8kk!d7`zd2n1-SuH$NpatrE9}xt>3?fEpM)UHx3vIVCg!0A03|
AN&o-=
rename from mobile/android/search/res/drawable-mdpi/network_error.png
rename to mobile/android/base/resources/drawable-mdpi/network_error.png
rename from mobile/android/search/res/drawable-mdpi/search_clear.png
rename to mobile/android/base/resources/drawable-mdpi/search_clear.png
rename from mobile/android/search/res/drawable-mdpi/search_fox.png
rename to mobile/android/base/resources/drawable-mdpi/search_fox.png
rename from mobile/android/search/res/drawable-mdpi/search_history.png
rename to mobile/android/base/resources/drawable-mdpi/search_history.png
rename from mobile/android/search/res/drawable-mdpi/search_icon_active.png
rename to mobile/android/base/resources/drawable-mdpi/search_icon_active.png
rename from mobile/android/search/res/drawable-mdpi/search_icon_inactive.png
rename to mobile/android/base/resources/drawable-mdpi/search_icon_inactive.png
rename from mobile/android/search/res/drawable-mdpi/search_launcher.png
rename to mobile/android/base/resources/drawable-mdpi/search_launcher.png
rename from mobile/android/search/res/drawable-mdpi/search_plus.png
rename to mobile/android/base/resources/drawable-mdpi/search_plus.png
index 564d4b51466bb725efe00a71fe949aa3a6e284d5..e2d7c59ea2f0a3f17dcf08c992df9ca8d3be4fdb
GIT binary patch
literal 640
zc%17D@N?(olHy`uVBq!ia0vp^d_XL~!3-n?z4mbcDVB6cUq=Rpjs4tz5?O(Kg=CK)
zUj~LMH3o);76yi2K%s^g3=E|P3=FRl7#OT(FffQ0%-I!a1C*E$;1l8sq~nt_1_}EA
z|NoohIUs-Nl?3?(Gbps(*4*@uv*YK(Do>M!%ir^&PO)70;<och`LZIhWV4pnyc-z{
ze_hW?FgWCQR3xm;38<4X$=lsUU-^oHAdthE@8(tvq(LFZ#E{pq>=8(?tiUlHNQ;2+
zVr#97K*k(T7sn8e>#pZt3N;z<usnFWOd&`>Fkt7CfD5(%k8f7s`>kwzUnO%{L)S9t
zwsjB1FK=3z6uqqJF;~?;MT;ttb<Gouy8Kxtd3@s0-<y(Ea+52Ff2Yn4L8IeSx7)wp
zbLg(*1=pR%X`NlAn>VJlUOOizZM>V+NpOC6m{Q2q<sZ7)PWoO5fAg#H@n_Sb7rVdy
zGSSaJfBBng$$S5QanbP$t_XhhpDcO+=uXuV*NBpo#FA92<f2p{#b9J$XrgOip=)Fs
xVrXV%YGP$zrfXnsWnl0|#9<nWhTQy=%(P0}8s>Wb&;n{;@O1TaS?83{1OSH+;n@HH
index a8ac4535e10800712df6a5b2dca3500f3e3cc0ac..d6ea70657b9fdb22a9007703d0259193073167ce
GIT binary patch
literal 533
zc%17D@N?(olHy`uVBq!ia0vp^d?3uh3?wzC-F*zCSkfJR9T^xl_H+M9WCijSl0AZa
z85pY67#JE_7#My5g&JNkFq9fFFuY1&V6d9Oz#v{QXIG#NP$D6~C&U#<|6F58BlQ3O
zf2M1DuLJFnD+%%oW{|ozU+wp@%tD=iENl6N*BHuewy-td^=R*$kIzk-_h@gA0;*(8
z@^*KTXXO>x3*>O-ySWtuX=X4kpFDjrNU*HHF&#*Yfbn8$t&2cLp{I*u2*-8TzG%K?
z10ELMBU6^FchC&bDE^-;blK@!@^R&xlU#LNBgG~KJ5D{Txx#wuvr7T;&zet7eel1k
z`8V@C`)<ZtVorCqGVk%eI&<$Ohxcpmd$OP7QQBKrl6Pl%iqVqkcE>!^4u9(HE}Rzn
zdF?uBQw8n~)3$|u23oFK;u=wsl30>zm0Xkxq!^403{7+mEOd=bLk!KVOiio|%ybRR
ltqcs_h&W6`(U6;;l9^VCTf<z>A6h^S44$rjF6*2UngESH#F_vA
rename from mobile/android/search/res/drawable-mdpi/widget_bg.9.png
rename to mobile/android/base/resources/drawable-mdpi/widget_bg.9.png
rename from mobile/android/search/res/drawable-xhdpi/ic_action_settings.png
rename to mobile/android/base/resources/drawable-xhdpi/ic_action_settings.png
rename from mobile/android/search/res/drawable-xhdpi/ic_widget_new_tab.png
rename to mobile/android/base/resources/drawable-xhdpi/ic_widget_new_tab.png
rename from mobile/android/search/res/drawable-xhdpi/ic_widget_search.png
rename to mobile/android/base/resources/drawable-xhdpi/ic_widget_search.png
index 6988dc33508d5442538519266ed0c9c2b72858df..6b0fec93855e8fcacd12b0edee2fbb4c7b4b4fc5
GIT binary patch
literal 686
zc%17D@N?(olHy`uVBq!ia0vp^5<o1=!3-qZQ(`KC6id3JuOkD)#(wTUiL5}rLb6AY
zF9SoB8UsT^3j@P1pisjL28L1t28LG&3=CE?7#PG0=Ijcz0ZJ?i@Ck7R(i6@F!BPLY
zKoH|>Q2*(`{<9ziWKTE?Wh0}$GlBhJ)CWPQL8$jsVDCvJbRw|xPyiGi3;-E>7N!NR
z_(Wh25FHQfJ{H(@6o>*kj|6lchHCl$|NqZ@66b)fR4EDa3udsHckj<sGqII&H<h_A
z1`7RN$Hv~>o|G)fXynAd^w*z1M^CF?xp>tasGc#&+ug<V-nN_BKn`cVn_DrE<^Tm5
zLy+jl<siYb0>^Y9tpUc13#R-4G7fpVIEHAPFYUW7)?^^S8qTQM)DZFF63fxJfB$d$
zsB~5&Jb!s_?&g~e25P~c(^OO0{;!BSdg02y7;~*glmBPW-rKW^CzvDEe}mYzy&E!u
zjyUsWzMJHBUiIR|nGsh@&wlr9o?<HOcG*OF-#*o&uNIXn{D}FnrSdMP6tf=FtzF6w
zCo0rQTo9{Ez9N*Qmvl=>YSI?j32KHJZ#ATrEDtOzaBJXZiV-X*bUtuYmQjvlM}^V>
zZpKX?E7^YT&{%M0^Y;mh{xERbMP;?kbKVQ|jcSQ&L`h0wNvc(HQ7VvPFfuSS(KWEp
zH8Kq`G_x`_u`)2zH88g_FnA;4FbzdRZhlH;S|x4`b3K1(0W~mqy85}Sb4q9e04Z$@
A)&Kwi
index ae0a96dd083bc1f4e27b87130339b0bad99ccf6d..9fc5798e33d5ca5208629cef2d2e27f70c388c8d
GIT binary patch
literal 570
zc%17D@N?(olHy`uVBq!ia0vp^5<o1=!3-qZQ(`KC6id3JuOkD)#(wTUiL5}rLb6AY
zF9SoB8UsT^3j@P1pisjL28L1t28LG&3=CE?7#PG0=Ijcz0ZPOM_=LCu>G<RfTA=^`
z|Nlu>Jq>bkNswPKgU!5qe|}Fj6I&^FQ;q9FpwN_cY@JDxj7CoUi;o|D_e2k<kTJ>I
z-9@g5OF<jR;mmh)D+baWAa61RiGExT5-cllOb5~$V7$0s$`2r8mZytjh{pM+6C#D0
z6$D(=14CjibX3py9dEp?;UL#Mg}+~`;?4%JPCWg1<`SioMZ1<wc%#%^5EoXXwsQYx
zwHe0YXElvVu9n5Fc3A&;!j0(ljl1p#PVwV8_^+_EfXU|9SHW=ItUGh}%($?nmdT>p
zaPdudF@@M8MGv^{?749v>_GGf|CT!*MJAJ6VwM~+|CwC)J6ugq<f_`X<X8MNl&vDe
zd^TwTU8!2)8c~vxSdwa$T$Bo=7>o=IO>_+`bd5|y49%=eO{@&ebPdd{3=H0gI7~y)
ckei>9nO2Eg!(7iFAR8DwUHx3vIVCg!0CeHP-T(jq
rename from mobile/android/search/res/drawable-xhdpi/network_error.png
rename to mobile/android/base/resources/drawable-xhdpi/network_error.png
rename from mobile/android/search/res/drawable-xhdpi/search_clear.png
rename to mobile/android/base/resources/drawable-xhdpi/search_clear.png
rename from mobile/android/search/res/drawable-xhdpi/search_fox.png
rename to mobile/android/base/resources/drawable-xhdpi/search_fox.png
rename from mobile/android/search/res/drawable-xhdpi/search_history.png
rename to mobile/android/base/resources/drawable-xhdpi/search_history.png
rename from mobile/android/search/res/drawable-xhdpi/search_icon_active.png
rename to mobile/android/base/resources/drawable-xhdpi/search_icon_active.png
rename from mobile/android/search/res/drawable-xhdpi/search_icon_inactive.png
rename to mobile/android/base/resources/drawable-xhdpi/search_icon_inactive.png
rename from mobile/android/search/res/drawable-xhdpi/search_launcher.png
rename to mobile/android/base/resources/drawable-xhdpi/search_launcher.png
rename from mobile/android/search/res/drawable-xhdpi/search_plus.png
rename to mobile/android/base/resources/drawable-xhdpi/search_plus.png
index ad5265cef1afda2c47250a0cf2bb3e1d4475ce2d..8a91ffb5a2335f675de0c504b6c7fcfb43aa3c89
GIT binary patch
literal 926
zc%17D@N?(olHy`uVBq!ia0vp^GC-`r!3-qLvR@PcDVB6cUq=Rpjs4tz5?Mi#$sR$z
z3=CCj3=9n|3=F@3LJcn%7)lKo7+xhXFj&oCU=S~uvn$XBC~-KzC&U#<$0uhDUG)F|
z|D9V8SpvObUlQaO%)sQ_e?z|Kse-}s-}=)3@;)D}_EmW&;qf7XpCj$$9aGj<JI?K0
zaIfY2nYCf1GY@X@n{4&4RXB3$`b|Z=8WC5$$^xGPjbcplc6V|3cQfV&ki(hp=2i@(
z*+9X~kRd<eI!Lgrz%d<2Yk=|Mf+;_My2?CV978nD=T3f^)MUWZ&}<MQqtIpWVS&}$
zbKH(|XZwBrm%mj&<fxwgEtUA&GP#Ugfm*AUtrTTVsl2%QM9|f2{}p*A8z!`U{MF!d
z_|T+?w#x2Q^Wy%?S=*!i_N~h-&bgkr+IgYP{5P}y9pgLB%25&R%)5K<zSf$M6-7*E
z8D{V*CGs0TmC}?lVC_8?_+sDlnRkMC+*2|PmO1|L)o~WslF1`%6{6X-XZrU>i$={y
zJZ(mE6#W*u>%9+dl9ROkklFK0sOQ9ogG&n+dKWmhaW~!Cx<z%zl4Bfx*-yO-T7NhS
zPu*Rny2Gs}Gf1$#>k5mO-%`!hM@!}{I63u+XZ{!U9VfY#?DXF2eMx;rz^1!CFO~CJ
zqneVsZ=~nCpR4$Pb?4z)pI3dMc15$RgkN)A&VA<jxz*5q%He(ga#L^f)il~~=kWY@
zq;Gk=h3wA?AD6&SmfuUtcCRj~tNEB#m^t%|uSR$M&V}9j|5XGH_Obl0-Ml8VZ*%3A
z{|st3Gjf`aR&@iTLbb#-q9i4;B-JXpC>2OC7#SFv=o(n)8kvR|npv5eSQ(h<8kk!d
k7`zd2n1-SuH$NpatrE9}xt>3?fEpM)UHx3vIVCg!03n=dJ^%m!
index 3f12f83c827c6b5395bbf8bad7a18ff1ba5cf52a..0e59c2a1fb1b114172b9eb5f4c3cb8785fd9a754
GIT binary patch
literal 756
zc%17D@N?(olHy`uVBq!ia0vp^G9b*s3?yAI>n{UREa{HEjtmSN`?>!lvI6-E$sR$z
z3=CCj3=9n|3=F@3LJcn%7)lKo7+xhXFj&oCU=S~uvn$XBC^0j@C&U#<|6F4@*wFv~
z|7WghUk`MgVM&l*Fau-x%i=7)ca3#|AG=z`zVuD|t86Ou?H}u8`JZyL)N~iQSZam)
ztVrIFWVo$zufub;`f8>qpk~G-Z+90Xhf6*ufE><zH@9LS%?8A*423KUr-1~^3LMjc
zv<4V2E|~HIsEftZ#W6(VeD1{T>Lvr8*2NXlQ4^aUJQHwzF5?&Z@BjShD*a2JTj%WY
zue!waz(mUZVhT&5??e;c%{OBX{M6wP*Z3-y^fBmQs!W6Sl_ckFegcbqy>_0gVleNp
zVLp*r^7Yg<)}@_I2^(u0HBC>;u2l`X|NBj{{w$Gco$vo`X4l^NoN>W4*Qc9%PRFei
zTXgYEvwP`bVTZ-57AL2hc8E9Vhkk5s4gYI)U+mG<4ZE()<vidzeb2QIX>p~DACh0q
zPTDE?W@V1*p5%b?gHqW)^JV&XZsk4LUivBDAX}cnzkGtv$+`8vIv>eo{JnB$=BYo{
zYgv@?55Bsy?A~6h{KiI!KO6OWthW7T=lSJ#^4ZUZJYc}6mbgZgq$HN4S|t~y0x1R~
z149#C0}EXv(-1>5D^n9I12bI%b1MUbHzE$xP&DM`r(~v8;?^+N^M@8t1B0ilpUXO@
GgeCyTEj*V1
rename from mobile/android/search/res/drawable-xhdpi/widget_bg.9.png
rename to mobile/android/base/resources/drawable-xhdpi/widget_bg.9.png
rename from mobile/android/search/res/drawable-xxhdpi/ic_action_settings.png
rename to mobile/android/base/resources/drawable-xxhdpi/ic_action_settings.png
rename from mobile/android/search/res/drawable-xxhdpi/ic_widget_new_tab.png
rename to mobile/android/base/resources/drawable-xxhdpi/ic_widget_new_tab.png
rename from mobile/android/search/res/drawable-xxhdpi/ic_widget_search.png
rename to mobile/android/base/resources/drawable-xxhdpi/ic_widget_search.png
rename from mobile/android/search/res/drawable-xxhdpi/network_error.png
rename to mobile/android/base/resources/drawable-xxhdpi/network_error.png
rename from mobile/android/search/res/drawable-xxhdpi/search_clear.png
rename to mobile/android/base/resources/drawable-xxhdpi/search_clear.png
rename from mobile/android/search/res/drawable-xxhdpi/search_fox.png
rename to mobile/android/base/resources/drawable-xxhdpi/search_fox.png
rename from mobile/android/search/res/drawable-xxhdpi/search_history.png
rename to mobile/android/base/resources/drawable-xxhdpi/search_history.png
rename from mobile/android/search/res/drawable-xxhdpi/search_icon_active.png
rename to mobile/android/base/resources/drawable-xxhdpi/search_icon_active.png
rename from mobile/android/search/res/drawable-xxhdpi/search_icon_inactive.png
rename to mobile/android/base/resources/drawable-xxhdpi/search_icon_inactive.png
rename from mobile/android/search/res/drawable-xxhdpi/search_launcher.png
rename to mobile/android/base/resources/drawable-xxhdpi/search_launcher.png
rename from mobile/android/search/res/drawable-xxhdpi/search_plus.png
rename to mobile/android/base/resources/drawable-xxhdpi/search_plus.png
rename from mobile/android/search/res/drawable-xxxhdpi/search_launcher.png
rename to mobile/android/base/resources/drawable-xxxhdpi/search_launcher.png
rename from mobile/android/search/res/drawable/edit_text_default.xml
rename to mobile/android/base/resources/drawable/edit_text_default.xml
rename from mobile/android/search/res/drawable/edit_text_focused.xml
rename to mobile/android/base/resources/drawable/edit_text_focused.xml
rename from mobile/android/search/res/drawable/facet_button_background.xml
rename to mobile/android/base/resources/drawable/facet_button_background.xml
rename from mobile/android/search/res/drawable/facet_button_background_default.xml
rename to mobile/android/base/resources/drawable/facet_button_background_default.xml
rename from mobile/android/search/res/drawable/facet_button_background_pressed.xml
rename to mobile/android/base/resources/drawable/facet_button_background_pressed.xml
new file mode 100644
--- /dev/null
+++ b/mobile/android/base/resources/drawable/new_tablet_action_bar_button.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+   - License, v. 2.0. If a copy of the MPL was not distributed with this
+   - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<bitmap xmlns:android="http://schemas.android.com/apk/res/android"
+        android:src="@null"/>
new file mode 100644
--- /dev/null
+++ b/mobile/android/base/resources/drawable/new_tablet_url_bar_nav_button.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+   - License, v. 2.0. If a copy of the MPL was not distributed with this
+   - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<bitmap xmlns:android="http://schemas.android.com/apk/res/android"
+        android:src="@null"/>
rename from mobile/android/search/res/drawable/progressbar.xml
rename to mobile/android/base/resources/drawable/progressbar.xml
rename from mobile/android/search/res/drawable/search_row_background.xml
rename to mobile/android/base/resources/drawable/search_row_background.xml
rename from mobile/android/search/res/drawable/widget_button_left.xml
rename to mobile/android/base/resources/drawable/widget_button_left.xml
rename from mobile/android/search/res/drawable/widget_button_left_default.xml
rename to mobile/android/base/resources/drawable/widget_button_left_default.xml
rename from mobile/android/search/res/drawable/widget_button_left_pressed.xml
rename to mobile/android/base/resources/drawable/widget_button_left_pressed.xml
rename from mobile/android/search/res/drawable/widget_button_middle.xml
rename to mobile/android/base/resources/drawable/widget_button_middle.xml
rename from mobile/android/search/res/drawable/widget_button_middle_pressed.xml
rename to mobile/android/base/resources/drawable/widget_button_middle_pressed.xml
rename from mobile/android/search/res/drawable/widget_button_right.xml
rename to mobile/android/base/resources/drawable/widget_button_right.xml
rename from mobile/android/search/res/drawable/widget_button_right_pressed.xml
rename to mobile/android/base/resources/drawable/widget_button_right_pressed.xml
rename from mobile/android/search/res/layout/keyguard_widget.xml
rename to mobile/android/base/resources/layout/keyguard_widget.xml
rename from mobile/android/search/res/layout/search_activity_main.xml
rename to mobile/android/base/resources/layout/search_activity_main.xml
rename from mobile/android/search/res/layout/search_bar.xml
rename to mobile/android/base/resources/layout/search_bar.xml
rename from mobile/android/search/res/layout/search_empty.xml
rename to mobile/android/base/resources/layout/search_empty.xml
rename from mobile/android/search/res/layout/search_fragment_post_search.xml
rename to mobile/android/base/resources/layout/search_fragment_post_search.xml
rename from mobile/android/search/res/layout/search_fragment_pre_search.xml
rename to mobile/android/base/resources/layout/search_fragment_pre_search.xml
rename from mobile/android/search/res/layout/search_history_row.xml
rename to mobile/android/base/resources/layout/search_history_row.xml
rename from mobile/android/search/res/layout/search_sugestions.xml
rename to mobile/android/base/resources/layout/search_sugestions.xml
rename from mobile/android/search/res/layout/search_suggestions_row.xml
rename to mobile/android/base/resources/layout/search_suggestions_row.xml
rename from mobile/android/search/res/layout/search_widget.xml
rename to mobile/android/base/resources/layout/search_widget.xml
--- a/mobile/android/base/resources/layout/toolbar_display_layout.xml
+++ b/mobile/android/base/resources/layout/toolbar_display_layout.xml
@@ -8,21 +8,24 @@
 
     <ImageButton android:id="@+id/favicon"
                  style="@style/UrlBar.ImageButton"
                  android:layout_width="@dimen/browser_toolbar_favicon_size"
                  android:scaleType="fitCenter"
                  android:paddingRight="4dip"
                  android:layout_gravity="center_vertical"/>
 
+    <!-- The site security icon is misaligned with the page title so
+         we add a bottom margin to align their bottoms. -->
     <ImageButton android:id="@+id/site_security"
                  style="@style/UrlBar.ImageButton"
-                 android:layout_width="@dimen/browser_toolbar_lock_width"
+                 android:layout_width="@dimen/browser_toolbar_site_security_width"
                  android:scaleType="fitCenter"
-                 android:layout_marginLeft="-4dip"
+                 android:layout_marginRight="4dip"
+                 android:layout_marginBottom="@dimen/site_security_bottom_margin"
                  android:src="@drawable/site_security_level"
                  android:contentDescription="@string/site_security"
                  android:visibility="gone"/>
 
     <org.mozilla.gecko.widget.FadedTextView android:id="@+id/url_bar_title"
                                             style="@style/UrlBar.Title"
                                             android:layout_width="match_parent"
                                             android:layout_height="match_parent"
rename from mobile/android/search/res/values-v13/search_styles.xml
rename to mobile/android/base/resources/values-v13/search_styles.xml
rename from mobile/android/search/res/values-v16/search_styles.xml
rename to mobile/android/base/resources/values-v16/search_styles.xml
--- a/mobile/android/base/resources/values/dimens.xml
+++ b/mobile/android/base/resources/values/dimens.xml
@@ -6,27 +6,33 @@
 <resources>
 
     <dimen name="autocomplete_min_width">200dp</dimen>
     <dimen name="autocomplete_row_height">32dp</dimen>
 
     <dimen name="browser_toolbar_height">48dp</dimen>
     <dimen name="browser_toolbar_button_padding">12dp</dimen>
     <dimen name="browser_toolbar_icon_width">48dp</dimen>
-    <dimen name="browser_toolbar_lock_width">20dp</dimen>
+    <dimen name="browser_toolbar_site_security_width">12dp</dimen>
     <!-- favicon_size includes 4dp of right padding. We can't use margin (which would allow us to
          specify the actual size) because that would decrease the size of our hit target. -->
     <dimen name="browser_toolbar_favicon_size">21.33dip</dimen>
     <dimen name="browser_toolbar_shadow_size">2dp</dimen>
 
     <dimen name="new_tablet_tab_strip_height">48dp</dimen>
     <dimen name="new_tablet_tab_strip_item_width">250dp</dimen>
     <dimen name="new_tablet_tab_strip_item_margin">-30dp</dimen>
     <dimen name="new_tablet_tab_strip_favicon_size">16dp</dimen>
     <dimen name="new_tablet_forward_default_offset">-6dp</dimen>
+    <dimen name="new_tablet_site_security_height">60dp</dimen>
+    <dimen name="new_tablet_site_security_width">34dp</dimen>
+    <!-- We primarily use padding (instead of margins) to increase the hit area. -->
+    <dimen name="new_tablet_site_security_padding_vertical">21dp</dimen>
+    <dimen name="new_tablet_site_security_padding_horizontal">8dp</dimen>
+    <dimen name="new_tablet_site_security_right_margin">1dp</dimen>
     <dimen name="new_tablet_browser_toolbar_height">60dp</dimen>
     <dimen name="new_tablet_browser_toolbar_menu_item_width">56dp</dimen>
     <!-- Padding combines with an 18dp image to form the menu item width and height. -->
     <dimen name="new_tablet_browser_toolbar_menu_item_padding_horizontal">19dp</dimen>
     <dimen name="new_tablet_browser_toolbar_menu_item_padding_vertical">21dp</dimen>
     <dimen name="new_tablet_browser_toolbar_menu_item_inset_vertical">5dp</dimen>
     <dimen name="new_tablet_browser_toolbar_menu_item_inset_horizontal">3dp</dimen>
     <dimen name="new_tablet_browser_toolbar_menu_item_corner_radius">5dp</dimen>
@@ -40,16 +46,22 @@
     <!-- Set the upper limit on the size of favicon that will be processed. Favicons larger than
          this will be downscaled to this value. If you need to use larger Favicons (Due to a UI
          redesign sometime after this is written) you should increase this value to the largest
          commonly-used size of favicon and, performance permitting, fetch the remainder from the
          database. The largest available size is always stored in the database, regardless of this
          value.-->
     <dimen name="favicon_largest_interesting_size">32dp</dimen>
 
+    <!-- Site security icon -->
+    <!-- If one of these values changes, they all should. -->
+    <dimen name="site_security_bottom_margin">.5dp</dimen>
+    <dimen name="new_tablet_site_security_unknown_inset_top">1dp</dimen>
+    <dimen name="new_tablet_site_security_unknown_inset_bottom">-1dp</dimen>
+
     <!-- Page Row height -->
     <dimen name="page_row_height">64dp</dimen>
 
     <!-- Remote Tabs static view top padding. Less in landscape on phones. -->
     <dimen name="home_remote_tabs_top_padding">48dp</dimen>
 
     <!-- Remote Tabs Hidden devices row height -->
     <dimen name="home_remote_tabs_hidden_footer_height">48dp</dimen>
rename from mobile/android/search/res/values/search_attrs.xml
rename to mobile/android/base/resources/values/search_attrs.xml
rename from mobile/android/search/res/values/search_colors.xml
rename to mobile/android/base/resources/values/search_colors.xml
rename from mobile/android/search/res/values/search_dimens.xml
rename to mobile/android/base/resources/values/search_dimens.xml
rename from mobile/android/search/res/values/search_styles.xml
rename to mobile/android/base/resources/values/search_styles.xml
rename from mobile/android/search/res/xml/search_preferences.xml
rename to mobile/android/base/resources/xml/search_preferences.xml
rename from mobile/android/search/res/xml/search_widget_info.xml
rename to mobile/android/base/resources/xml/search_widget_info.xml
--- a/mobile/android/base/tests/testGeckoProfile.java
+++ b/mobile/android/base/tests/testGeckoProfile.java
@@ -1,27 +1,26 @@
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 package org.mozilla.gecko.tests;
 
-import org.mozilla.gecko.GeckoApp;
-import org.mozilla.gecko.GeckoProfile;
-import org.mozilla.gecko.GeckoProfileDirectories;
-import org.mozilla.gecko.R;
-import org.mozilla.gecko.util.INIParser;
-import org.mozilla.gecko.util.INISection;
-
-import android.view.View;
-
 import java.io.File;
 import java.util.Enumeration;
 import java.util.Hashtable;
 
+import org.mozilla.gecko.GeckoApp;
+import org.mozilla.gecko.GeckoProfile;
+import org.mozilla.gecko.GeckoProfileDirectories;
+import org.mozilla.gecko.GeckoSharedPrefs;
+import org.mozilla.gecko.util.INIParser;
+import org.mozilla.gecko.util.INISection;
+
+import android.content.Context;
 import android.text.TextUtils;
 
 /**
  * This patch tests GeckoProfile. It has unit tests for basic getting and removing of profiles, as well as
  * some guest mode tests. It does not test locking and unlocking profiles yet. It does not test the file management in GeckoProfile.
  */
 
 public class testGeckoProfile extends PixelTest {
@@ -274,9 +273,18 @@ public class testGeckoProfile extends Pi
         for (File dir : dirs) {
             if (dir.getName().endsWith(name)) {
                 return dir;
             }
         }
 
         return null;
     }
+
+    @Override
+    public void tearDown() throws Exception {
+        // Clear SharedPreferences.
+        final Context context = getInstrumentation().getContext();
+        GeckoSharedPrefs.forProfile(context).edit().clear().apply();
+
+        super.tearDown();
+    }
 }
--- a/mobile/android/base/toolbar/ToolbarDisplayLayout.java
+++ b/mobile/android/base/toolbar/ToolbarDisplayLayout.java
@@ -36,16 +36,17 @@ import android.util.AttributeSet;
 import android.util.Log;
 import android.view.LayoutInflater;
 import android.view.View;
 import android.view.animation.AlphaAnimation;
 import android.view.animation.Animation;
 import android.view.animation.TranslateAnimation;
 import android.widget.Button;
 import android.widget.ImageButton;
+import android.widget.LinearLayout;
 
 /**
 * {@code ToolbarDisplayLayout} is the UI for when the toolbar is in
 * display state. It's used to display the state of the currently selected
 * tab. It should always be updated through a single entry point
 * (updateFromTab) and should never track any tab events or gecko messages
 * on its own to keep it as dumb as possible.
 *
@@ -142,28 +143,49 @@ public class ToolbarDisplayLayout extend
         final Resources res = getResources();
 
         mUrlColor = new ForegroundColorSpan(res.getColor(R.color.url_bar_urltext));
         mBlockedColor = new ForegroundColorSpan(res.getColor(R.color.url_bar_blockedtext));
         mDomainColor = new ForegroundColorSpan(res.getColor(R.color.url_bar_domaintext));
         mPrivateDomainColor = new ForegroundColorSpan(res.getColor(R.color.url_bar_domaintext_private));
 
         mFavicon = (ImageButton) findViewById(R.id.favicon);
+        mSiteSecurity = (ImageButton) findViewById(R.id.site_security);
+
         if (NewTabletUI.isEnabled(context)) {
-            // We don't show favicons in the toolbar on new tablet.
-            // TODO: removeView(mFavicon);
-            mFavicon.setVisibility(View.GONE);
+            mSiteSecurity.setVisibility(View.VISIBLE);
+            // TODO: Rename this resource and remove this call when new tablet is default.
+            mSiteSecurity.setImageResource(R.drawable.new_tablet_site_security_level);
+
+            // TODO: This can likely be set statically in resources when new tablet is default.
+            // Dynamically update parameters for new tablet.
+            final LinearLayout.LayoutParams lp =
+                    (LinearLayout.LayoutParams) mSiteSecurity.getLayoutParams();
+            lp.height = res.getDimensionPixelSize(R.dimen.new_tablet_site_security_height);
+            lp.width = res.getDimensionPixelSize(R.dimen.new_tablet_site_security_width);
+            // TODO: Override a common static value when new tablet is standard.
+            lp.rightMargin = res.getDimensionPixelSize(R.dimen.new_tablet_site_security_right_margin);
+            mSiteSecurity.setLayoutParams(lp);
+            final int siteSecurityVerticalPadding =
+                    res.getDimensionPixelSize(R.dimen.new_tablet_site_security_padding_vertical);
+            final int siteSecurityHorizontalPadding =
+                    res.getDimensionPixelSize(R.dimen.new_tablet_site_security_padding_horizontal);
+            mSiteSecurity.setPadding(siteSecurityHorizontalPadding, siteSecurityVerticalPadding,
+                    siteSecurityHorizontalPadding, siteSecurityVerticalPadding);
+
+            // We don't show favicons in the toolbar on new tablet. Note that while we could
+            // null the favicon reference, we don't do so to avoid excessive null-checking.
+            removeView(mFavicon);
         } else {
             if (Versions.feature16Plus) {
                 mFavicon.setImportantForAccessibility(View.IMPORTANT_FOR_ACCESSIBILITY_NO);
             }
             mFaviconSize = Math.round(res.getDimension(R.dimen.browser_toolbar_favicon_size));
         }
 
-        mSiteSecurity = (ImageButton) findViewById(R.id.site_security);
         mSiteSecurityVisible = (mSiteSecurity.getVisibility() == View.VISIBLE);
 
         mSiteIdentityPopup = new SiteIdentityPopup(mActivity);
         mSiteIdentityPopup.setAnchor(mSiteSecurity);
 
         mStop = (ImageButton) findViewById(R.id.stop);
         mPageActionLayout = (PageActionLayout) findViewById(R.id.page_action_layout);
     }
@@ -195,17 +217,17 @@ public class ToolbarDisplayLayout extend
                     final Tab tab = mStopListener.onStop();
                     if (tab != null) {
                         updateUiMode(tab, UIMode.DISPLAY, EnumSet.noneOf(UpdateFlags.class));
                     }
                 }
             }
         });
 
-        float slideWidth = getResources().getDimension(R.dimen.browser_toolbar_lock_width);
+        float slideWidth = getResources().getDimension(R.dimen.browser_toolbar_site_security_width);
 
         LayoutParams siteSecParams = (LayoutParams) mSiteSecurity.getLayoutParams();
         final float scale = getResources().getDisplayMetrics().density;
         slideWidth += (siteSecParams.leftMargin + siteSecParams.rightMargin) * scale + 0.5f;
 
         mLockFadeIn = new AlphaAnimation(0.0f, 1.0f);
         mLockFadeIn.setAnimationListener(this);
 
@@ -451,17 +473,18 @@ public class ToolbarDisplayLayout extend
 
         // We want title to fill the whole space available for it when there are icons
         // being shown on the right side of the toolbar as the icons already have some
         // padding in them. This is just to avoid wasting space when icons are shown.
         mTitle.setPadding(0, 0, (!isShowingProgress ? mTitlePadding : 0), 0);
     }
 
     private void setSiteSecurityVisibility(boolean visible, EnumSet<UpdateFlags> flags) {
-        if (visible == mSiteSecurityVisible) {
+        // We don't hide site security on new tablets.
+        if (visible == mSiteSecurityVisible || NewTabletUI.isEnabled(getContext())) {
             return;
         }
 
         mSiteSecurityVisible = visible;
 
         mTitle.clearAnimation();
         mSiteSecurity.clearAnimation();
 
--- a/mobile/android/modules/TabMirror.jsm
+++ b/mobile/android/modules/TabMirror.jsm
@@ -157,17 +157,17 @@ let TabMirror = function(deviceId, windo
     poll();
   };
 
 
   let config = {
     iceServers: [{ "url": "stun:stun.services.mozilla.com" }]
   };
 
-  let pc = new RTCPeerConnection(config, {});
+  pc = new RTCPeerConnection(config, {});
 
   if (!pc) {
     log("Failure creating Webrtc object");
     return;
   }
 
   pc.onicecandidate = on_ice_candidate;
 
--- a/mobile/android/search/java/org/mozilla/search/PostSearchFragment.java
+++ b/mobile/android/search/java/org/mozilla/search/PostSearchFragment.java
@@ -1,40 +1,40 @@
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 package org.mozilla.search;
 
+import java.net.URISyntaxException;
+
+import org.mozilla.gecko.AppConstants;
+import org.mozilla.gecko.R;
+import org.mozilla.gecko.Telemetry;
+import org.mozilla.gecko.TelemetryContract;
+import org.mozilla.search.providers.SearchEngine;
+
 import android.content.Intent;
 import android.graphics.Bitmap;
-import android.net.Uri;
 import android.os.Bundle;
 import android.provider.Settings;
 import android.support.v4.app.Fragment;
 import android.text.TextUtils;
 import android.util.Log;
 import android.view.LayoutInflater;
 import android.view.View;
 import android.view.ViewGroup;
 import android.view.ViewStub;
 import android.webkit.WebChromeClient;
 import android.webkit.WebView;
 import android.webkit.WebViewClient;
 import android.widget.ImageView;
 import android.widget.ProgressBar;
 import android.widget.TextView;
 
-import org.mozilla.gecko.AppConstants;
-import org.mozilla.gecko.Telemetry;
-import org.mozilla.gecko.TelemetryContract;
-import org.mozilla.search.providers.SearchEngine;
-
-import java.net.URISyntaxException;
-
 public class PostSearchFragment extends Fragment {
 
     private static final String LOG_TAG = "PostSearchFragment";
 
     private SearchEngine engine;
 
     private ProgressBar progressBar;
     private WebView webview;
--- a/mobile/android/search/java/org/mozilla/search/PreSearchFragment.java
+++ b/mobile/android/search/java/org/mozilla/search/PreSearchFragment.java
@@ -21,16 +21,17 @@ import android.view.LayoutInflater;
 import android.view.View;
 import android.view.ViewGroup;
 import android.view.ViewStub;
 import android.widget.AdapterView;
 import android.widget.ImageView;
 import android.widget.ListView;
 import android.widget.TextView;
 
+import org.mozilla.gecko.R;
 import org.mozilla.gecko.Telemetry;
 import org.mozilla.gecko.TelemetryContract;
 import org.mozilla.gecko.db.BrowserContract;
 import org.mozilla.gecko.db.BrowserContract.SearchHistory;
 import org.mozilla.gecko.widget.SwipeDismissListViewTouchListener;
 import org.mozilla.gecko.widget.SwipeDismissListViewTouchListener.OnDismissCallback;
 import org.mozilla.search.AcceptsSearchQuery.SuggestionAnimation;
 
--- a/mobile/android/search/java/org/mozilla/search/SearchActivity.java
+++ b/mobile/android/search/java/org/mozilla/search/SearchActivity.java
@@ -1,15 +1,16 @@
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 package org.mozilla.search;
 
 import org.mozilla.gecko.LocaleAware;
+import org.mozilla.gecko.R;
 import org.mozilla.gecko.Telemetry;
 import org.mozilla.gecko.TelemetryContract;
 import org.mozilla.gecko.db.BrowserContract.SearchHistory;
 import org.mozilla.gecko.health.BrowserHealthRecorder;
 import org.mozilla.search.autocomplete.SearchBar;
 import org.mozilla.search.autocomplete.SuggestionsFragment;
 import org.mozilla.search.providers.SearchEngine;
 import org.mozilla.search.providers.SearchEngineManager;
--- a/mobile/android/search/java/org/mozilla/search/SearchPreferenceActivity.java
+++ b/mobile/android/search/java/org/mozilla/search/SearchPreferenceActivity.java
@@ -1,30 +1,31 @@
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 package org.mozilla.search;
 
+import org.mozilla.gecko.GeckoSharedPrefs;
+import org.mozilla.gecko.LocaleAware;
+import org.mozilla.gecko.R;
+import org.mozilla.gecko.Telemetry;
+import org.mozilla.gecko.TelemetryContract;
+import org.mozilla.gecko.db.BrowserContract;
+
 import android.app.AlertDialog;
 import android.content.DialogInterface;
 import android.os.AsyncTask;
 import android.os.Build;
 import android.os.Bundle;
 import android.preference.Preference;
 import android.preference.PreferenceActivity;
 import android.util.Log;
 import android.widget.Toast;
 
-import org.mozilla.gecko.GeckoSharedPrefs;
-import org.mozilla.gecko.LocaleAware;
-import org.mozilla.gecko.Telemetry;
-import org.mozilla.gecko.TelemetryContract;
-import org.mozilla.gecko.db.BrowserContract;
-
 /**
  * This activity allows users to modify the settings for the search activity.
  *
  * A note on implementation: At the moment, we don't have tablet-specific designs.
  * Therefore, this implementation uses the old-style PreferenceActivity. When
  * we start optimizing for tablets, we can migrate to Fennec's PreferenceFragment
  * implementation.
  *
--- a/mobile/android/search/java/org/mozilla/search/SearchWidget.java
+++ b/mobile/android/search/java/org/mozilla/search/SearchWidget.java
@@ -2,16 +2,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/. */
 
 package org.mozilla.search;
 
 import org.mozilla.gecko.AboutPages;
 import org.mozilla.gecko.AppConstants;
+import org.mozilla.gecko.R;
 import org.mozilla.gecko.Telemetry;
 import org.mozilla.gecko.TelemetryContract;
 
 import android.annotation.SuppressLint;
 import android.app.PendingIntent;
 import android.appwidget.AppWidgetManager;
 import android.appwidget.AppWidgetProvider;
 import android.appwidget.AppWidgetProviderInfo;
--- a/mobile/android/search/java/org/mozilla/search/autocomplete/AutoCompleteAdapter.java
+++ b/mobile/android/search/java/org/mozilla/search/autocomplete/AutoCompleteAdapter.java
@@ -1,27 +1,27 @@
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 package org.mozilla.search.autocomplete;
 
+import java.util.List;
+
+import org.mozilla.gecko.R;
+import org.mozilla.search.AcceptsSearchQuery;
+import org.mozilla.search.autocomplete.SuggestionsFragment.Suggestion;
+
 import android.content.Context;
 import android.view.LayoutInflater;
 import android.view.View;
 import android.view.ViewGroup;
 import android.widget.ArrayAdapter;
 import android.widget.TextView;
 
-import org.mozilla.search.AcceptsSearchQuery;
-import org.mozilla.search.R;
-import org.mozilla.search.autocomplete.SuggestionsFragment.Suggestion;
-
-import java.util.List;
-
 /**
  * The adapter that is used to populate the autocomplete rows.
  */
 class AutoCompleteAdapter extends ArrayAdapter<Suggestion> {
 
     private final AcceptsSearchQuery searchListener;
 
     private final LayoutInflater inflater;
--- a/mobile/android/search/java/org/mozilla/search/autocomplete/SearchBar.java
+++ b/mobile/android/search/java/org/mozilla/search/autocomplete/SearchBar.java
@@ -1,14 +1,20 @@
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 package org.mozilla.search.autocomplete;
 
+import org.mozilla.gecko.R;
+import org.mozilla.gecko.Telemetry;
+import org.mozilla.gecko.TelemetryContract;
+import org.mozilla.gecko.gfx.BitmapUtils;
+import org.mozilla.search.providers.SearchEngine;
+
 import android.content.Context;
 import android.graphics.Bitmap;
 import android.graphics.Color;
 import android.graphics.PorterDuff;
 import android.graphics.PorterDuffColorFilter;
 import android.graphics.drawable.BitmapDrawable;
 import android.graphics.drawable.Drawable;
 import android.text.Editable;
@@ -21,22 +27,16 @@ import android.view.View;
 import android.view.inputmethod.EditorInfo;
 import android.view.inputmethod.InputMethodManager;
 import android.widget.EditText;
 import android.widget.FrameLayout;
 import android.widget.ImageButton;
 import android.widget.ImageView;
 import android.widget.TextView;
 
-import org.mozilla.gecko.Telemetry;
-import org.mozilla.gecko.TelemetryContract;
-import org.mozilla.gecko.gfx.BitmapUtils;
-import org.mozilla.search.R;
-import org.mozilla.search.providers.SearchEngine;
-
 public class SearchBar extends FrameLayout {
 
     private final EditText editText;
     private final ImageButton clearButton;
     private final ImageView engineIcon;
 
     private final Drawable focusedBackground;
     private final Drawable defaultBackgound;
--- a/mobile/android/search/java/org/mozilla/search/autocomplete/SuggestionsFragment.java
+++ b/mobile/android/search/java/org/mozilla/search/autocomplete/SuggestionsFragment.java
@@ -1,14 +1,25 @@
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 package org.mozilla.search.autocomplete;
 
+import java.util.ArrayList;
+import java.util.List;
+
+import org.mozilla.gecko.R;
+import org.mozilla.gecko.SuggestClient;
+import org.mozilla.gecko.Telemetry;
+import org.mozilla.gecko.TelemetryContract;
+import org.mozilla.search.AcceptsSearchQuery;
+import org.mozilla.search.AcceptsSearchQuery.SuggestionAnimation;
+import org.mozilla.search.providers.SearchEngine;
+
 import android.app.Activity;
 import android.content.Context;
 import android.graphics.Rect;
 import android.os.Bundle;
 import android.support.v4.app.Fragment;
 import android.support.v4.app.LoaderManager;
 import android.support.v4.content.AsyncTaskLoader;
 import android.support.v4.content.Loader;
@@ -16,27 +27,16 @@ import android.text.SpannableString;
 import android.text.style.ForegroundColorSpan;
 import android.util.Log;
 import android.view.LayoutInflater;
 import android.view.View;
 import android.view.ViewGroup;
 import android.widget.AdapterView;
 import android.widget.ListView;
 
-import org.mozilla.gecko.SuggestClient;
-import org.mozilla.gecko.Telemetry;
-import org.mozilla.gecko.TelemetryContract;
-import org.mozilla.search.AcceptsSearchQuery;
-import org.mozilla.search.AcceptsSearchQuery.SuggestionAnimation;
-import org.mozilla.search.R;
-import org.mozilla.search.providers.SearchEngine;
-
-import java.util.ArrayList;
-import java.util.List;
-
 /**
  * A fragment to show search suggestions.
  */
 public class SuggestionsFragment extends Fragment {
 
     private static final String LOG_TAG = "SuggestionsFragment";
 
     private static final int LOADER_ID_SUGGESTION = 0;
--- a/mobile/android/search/java/org/mozilla/search/ui/FacetBar.java
+++ b/mobile/android/search/java/org/mozilla/search/ui/FacetBar.java
@@ -1,24 +1,24 @@
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 package org.mozilla.search.ui;
 
+import org.mozilla.gecko.R;
+
 import android.content.Context;
 import android.graphics.Canvas;
 import android.graphics.Color;
 import android.graphics.Paint;
 import android.util.AttributeSet;
 import android.widget.RadioButton;
 import android.widget.RadioGroup;
 
-import org.mozilla.search.R;
-
 public class FacetBar extends RadioGroup {
 
     // Ensure facets have equal width and match the bar's height. Supplying these
     // in styles.xml/FacetButtonStyle does not work. See:
     //   http://stackoverflow.com/questions/24213193/android-ignores-layout-weight-parameter-from-styles-xml
     private static final RadioGroup.LayoutParams FACET_LAYOUT_PARAMS =
             new RadioGroup.LayoutParams(0, LayoutParams.MATCH_PARENT, 1.0f);
 
--- a/testing/marionette/client/marionette/marionette_test.py
+++ b/testing/marionette/client/marionette/marionette_test.py
@@ -474,17 +474,17 @@ if((navigator.mozSettings == undefined) 
 let setReq = navigator.mozSettings.createLock().set({'lockscreen.enabled': false});
 setReq.onsuccess = function() {
     let appsReq = navigator.mozApps.mgmt.getAll();
     appsReq.onsuccess = function() {
         let apps = appsReq.result;
         for (let i = 0; i < apps.length; i++) {
             let app = apps[i];
             if (app.manifest.name === 'Test Container') {
-                let manager = window.wrappedJSObject.AppWindowManager || window.wrappedJSObject.WindowManager;
+                let manager = window.wrappedJSObject.appWindowManager || window.wrappedJSObject.AppWindowManager;
                 if (!manager) {
                     marionetteScriptFinished(false);
                     return;
                 }
                 manager.kill(app.origin);
                 marionetteScriptFinished(true);
                 return;
             }
--- a/testing/profiles/prefs_general.js
+++ b/testing/profiles/prefs_general.js
@@ -258,9 +258,9 @@ user_pref("loop.CSP","default-src 'self'
 
 // Ensure UITour won't hit the network
 user_pref("browser.uitour.pinnedTabUrl", "http://%(server)s/uitour-dummy/pinnedTab");
 user_pref("browser.uitour.url", "http://%(server)s/uitour-dummy/tour");
 
 user_pref("media.eme.enabled", true);
 
 // Don't prompt about e10s
-user_pref("browser.displayedE10SPrompt.1", 5);
+user_pref("browser.displayedE10SPrompt", 5);
--- a/testing/web-platform/harness/wptrunner/browsers/b2g.py
+++ b/testing/web-platform/harness/wptrunner/browsers/b2g.py
@@ -228,17 +228,17 @@ class B2GExecutorBrowser(ExecutorBrowser
     def use_cert_app(self):
         """Start the app used to run the tests"""
         self.executor.logger.info("Homescreen loaded")
         self.gaia_apps.launch("CertTest App")
 
     def wait_for_homescreen(self, timeout):
         self.executor.logger.info("Waiting for homescreen")
         self.marionette.execute_async_script("""
-let manager = window.wrappedJSObject.AppWindowManager || window.wrappedJSObject.WindowManager;
+let manager = window.wrappedJSObject.appWindowManager || window.wrappedJSObject.AppWindowManager;
 let app = null;
 if (manager) {
   app = ('getActiveApp' in manager) ? manager.getActiveApp() : manager.getCurrentDisplayedApp();
 }
 if (app) {
   log('Already loaded home screen');
   marionetteScriptFinished();
 } else {
--- a/toolkit/devtools/server/actors/inspector.js
+++ b/toolkit/devtools/server/actors/inspector.js
@@ -2101,18 +2101,17 @@ var WalkerActor = protocol.ActorClass({
     } else {
       rawNode.outerHTML = value;
     }
   }, {
     request: {
       node: Arg(0, "domnode"),
       value: Arg(1),
     },
-    response: {
-    }
+    response: {}
   }),
 
   /**
    * Removes a node from its parent node.
    *
    * @returns The node's nextSibling before it was removed.
    */
   removeNode: method(function(node) {
@@ -2146,16 +2145,56 @@ var WalkerActor = protocol.ActorClass({
       node: Arg(0, "domnode"),
       parent: Arg(1, "domnode"),
       sibling: Arg(2, "nullable:domnode")
     },
     response: {}
   }),
 
   /**
+   * Editing a node's tagname actually means creating a new node with the same
+   * attributes, removing the node and inserting the new one instead.
+   * This method does not return anything as mutation events are taking care of
+   * informing the consumers about changes.
+   */
+  editTagName: method(function(node, tagName) {
+    let oldNode = node.rawNode;
+
+    // Create a new element with the same attributes as the current element and
+    // prepare to replace the current node with it.
+    let newNode;
+    try {
+      newNode = nodeDocument(oldNode).createElement(tagName);
+    } catch(x) {
+      // Failed to create a new element with that tag name, ignore the change,
+      // and signal the error to the front.
+      return Promise.reject(new Error("Could not change node's tagName to " + tagName));
+    }
+
+    let attrs = oldNode.attributes;
+    for (let i = 0; i < attrs.length; i ++) {
+      newNode.setAttribute(attrs[i].name, attrs[i].value);
+    }
+
+    // Insert the new node, and transfer the old node's children.
+    oldNode.parentNode.insertBefore(newNode, oldNode);
+    while (oldNode.firstChild) {
+      newNode.appendChild(oldNode.firstChild);
+    }
+
+    oldNode.remove();
+  }, {
+    request: {
+      node: Arg(0, "domnode"),
+      tagName: Arg(1, "string")
+    },
+    response: {}
+  }),
+
+  /**
    * Get any pending mutation records.  Must be called by the client after
    * the `new-mutations` notification is received.  Returns an array of
    * mutation records.
    *
    * Mutation records have a basic structure:
    *
    * {
    *   type: attributes|characterData|childList,